1 module text.json.Encode; 2 3 import meta.attributesOrNothing; 4 import meta.never; 5 import meta.SafeUnqual; 6 import std.datetime; 7 import std.format; 8 import std.json; 9 import std.range; 10 import std.traits; 11 import std.typecons; 12 import text.json.Json; 13 14 /** 15 * Encodes an arbitrary type as a JSON string using introspection. 16 */ 17 public string encode(T, alias transform = never)(const T value) 18 { 19 auto sink = StringSink(); 20 encodeJsonStream!(T, transform, StringSink)(sink, value); 21 return sink.output[]; 22 } 23 24 /// ditto 25 public JSONValue encodeJson(T)(const T value) 26 { 27 return encodeJson!(T, never)(value); 28 } 29 30 public JSONValue encodeJson(T, alias transform)(const T value) 31 { 32 auto sink = JSONValueSink(); 33 encodeJsonStream!(T, transform, JSONValueSink)(sink, value); 34 return sink.value; 35 } 36 37 // range is an output range over `JSONOutputToken`s. 38 private void encodeJsonStream(T, alias transform, Range, attributes...)(ref Range output, const T parameter) 39 in 40 { 41 static if (is(T == class)) 42 { 43 assert(parameter !is null); 44 } 45 } 46 do 47 { 48 import boilerplate.util : udaIndex; 49 import std.traits : isIterable, Unqual; 50 51 static if (__traits(compiles, transform(parameter))) 52 { 53 auto transformedValue = transform(parameter); 54 static assert(!is(Unqual!(typeof(transformedValue)) == Unqual!T), 55 "transform must not return the same type as it takes!"); 56 57 encodeJsonStream!(typeof(transformedValue), transform, Range)(output, transformedValue); 58 } 59 else 60 { 61 static assert( 62 !__traits(compiles, transform(Unqual!T.init)), 63 "transform() must take its parameter as const!"); 64 65 auto value = parameter; 66 alias Type = T; 67 68 alias typeAttributes = attributesOrNothing!Type; 69 70 static if (udaIndex!(Json.Encode, attributes) != -1) 71 { 72 alias encodeFunction = attributes[udaIndex!(Json.Encode, attributes)].EncodeFunction; 73 enum hasEncodeFunction = true; 74 } 75 else static if (udaIndex!(Json.Encode, typeAttributes) != -1) 76 { 77 alias encodeFunction = typeAttributes[udaIndex!(Json.Encode, typeAttributes)].EncodeFunction; 78 enum hasEncodeFunction = true; 79 } 80 else 81 { 82 enum hasEncodeFunction = false; 83 } 84 85 static if (hasEncodeFunction) 86 { 87 static if (__traits(compiles, encodeFunction!(typeof(value), transform, attributes))) 88 { 89 auto jsonValue = encodeFunction!(typeof(value), transform, attributes)(value); 90 } 91 else 92 { 93 auto jsonValue = encodeFunction(value); 94 } 95 output.put(JSONOutputToken(jsonValue)); 96 } 97 else static if (__traits(compiles, encodeValue(output, value))) 98 { 99 encodeValue(output, value); 100 } 101 else static if (is(Type : V[string], V)) 102 { 103 output.put(JSONOutputToken.objectStart); 104 foreach (key, element; value) 105 { 106 output.put(JSONOutputToken.key(key)); 107 encodeJsonStream!(typeof(element), transform, Range, attributes)(output, element); 108 } 109 output.put(JSONOutputToken.objectEnd); 110 } 111 else static if (isIterable!Type) 112 { 113 output.put(JSONOutputToken.arrayStart); 114 foreach (element; value) 115 { 116 encodeJsonStream!(typeof(element), transform, Range, attributes)(output, element); 117 } 118 output.put(JSONOutputToken.arrayEnd); 119 } 120 else 121 { 122 output.put(JSONOutputToken.objectStart); 123 encodeStruct!(T, transform, Range, attributes)(output, value); 124 output.put(JSONOutputToken.objectEnd); 125 } 126 } 127 } 128 129 private void encodeStruct(Type, alias transform, Range, attributes...)(ref Range output, const Type value) 130 { 131 import boilerplate.util : formatNamed, optionallyRemoveTrailingUnderline, removeTrailingUnderline, udaIndex; 132 import std.meta : AliasSeq, anySatisfy, ApplyLeft; 133 import std.traits : fullyQualifiedName; 134 135 static assert( 136 __traits(hasMember, Type, "ConstructorInfo"), 137 fullyQualifiedName!Type ~ " does not have a boilerplate constructor!"); 138 139 alias Info = Tuple!(string, "builderField", string, "constructorField"); 140 141 static foreach (string constructorField; Type.ConstructorInfo.fields) 142 {{ 143 enum builderField = optionallyRemoveTrailingUnderline!constructorField; 144 145 mixin(formatNamed!q{ 146 alias MemberType = SafeUnqual!(Type.ConstructorInfo.FieldInfo.%(constructorField).Type); 147 148 const MemberType memberValue = value.%(builderField); 149 150 static if (is(MemberType : Nullable!Arg, Arg)) 151 { 152 bool includeMember = !memberValue.isNull; 153 enum getMemberValue = "memberValue.get"; 154 } 155 else 156 { 157 enum includeMember = true; 158 enum getMemberValue = "memberValue"; 159 } 160 161 alias attributes = AliasSeq!(Type.ConstructorInfo.FieldInfo.%(constructorField).attributes); 162 163 if (includeMember) 164 { 165 static if (udaIndex!(Json, attributes) != -1) 166 { 167 enum name = attributes[udaIndex!(Json, attributes)].name; 168 } 169 else 170 { 171 enum name = constructorField.removeTrailingUnderline; 172 } 173 174 auto finalMemberValue = mixin(getMemberValue); 175 176 enum sameField(string lhs, string rhs) 177 = optionallyRemoveTrailingUnderline!lhs== optionallyRemoveTrailingUnderline!rhs; 178 enum memberIsAliasedToThis = anySatisfy!( 179 ApplyLeft!(sameField, constructorField), 180 __traits(getAliasThis, Type)); 181 182 static if (memberIsAliasedToThis) 183 { 184 encodeStruct!(typeof(finalMemberValue), transform, Range, attributes)( 185 output, finalMemberValue); 186 } 187 else 188 { 189 output.put(JSONOutputToken.key(name)); 190 encodeJsonStream!(typeof(finalMemberValue), transform, Range, attributes)( 191 output, finalMemberValue); 192 } 193 } 194 }.values(Info(builderField, constructorField))); 195 }} 196 } 197 198 private void encodeJsonStream(T : JSONValue, alias transform, Range, attributes...)( 199 ref Range output, const T value) 200 { 201 output.put(JSONOutputToken(value)); 202 } 203 204 static if (__traits(compiles, { import std.sumtype; })) 205 { 206 import std.sumtype : match, SumType; 207 208 private void encodeJsonStream(T : SumType!Types, alias transform, Range, Types...)( 209 ref Range output, const T value) 210 { 211 value.match!( 212 staticMap!( 213 a => encodeJsonStream!(typeof(a), transform, Range)(output, a), 214 Types)); 215 } 216 } 217 218 private void encodeValue(T, Range)(ref Range output, T value) 219 if (!is(T: Nullable!Arg, Arg)) 220 { 221 import std.conv : to; 222 import text.xml.Convert : Convert; 223 224 static if (is(T == enum)) 225 { 226 output.put(JSONOutputToken(value.to!string)); 227 } 228 else static if (isBoolean!T || isIntegral!T || isFloatingPoint!T || isSomeString!T) 229 { 230 output.put(JSONOutputToken(value)); 231 } 232 else static if (is(T == typeof(null))) 233 { 234 // FIXME proper null token? 235 output.put(JSONOutputToken(JSONValue(null))); 236 } 237 else static if (is(T : const SysTime)) 238 { 239 // fastpath for SysTime (it's a very common type) 240 SysTime noFractionalSeconds = value; 241 242 noFractionalSeconds.fracSecs = 0.seconds; 243 output.put(JSONOutputToken(noFractionalSeconds)); 244 } 245 else static if (__traits(compiles, Convert.toString(value))) 246 { 247 output.put(JSONOutputToken(Convert.toString(value))); 248 } 249 else 250 { 251 static assert(false, "Cannot encode " ~ T.stringof ~ " as value"); 252 } 253 } 254 255 // An output range over JSONOutputToken that results in a string. 256 private struct StringSink 257 { 258 private Stack!bool comma; 259 260 private Appender!string output; 261 262 static StringSink opCall() 263 { 264 StringSink sink; 265 sink.output = appender!string(); 266 sink.comma.push(false); 267 return sink; 268 } 269 270 public void put(JSONOutputToken token) 271 { 272 import funkwerk.stdx.data.json.generator : escapeString; 273 274 with (JSONOutputToken.Kind) 275 { 276 if (token.kind != arrayEnd && token.kind != objectEnd) 277 { 278 if (this.comma.head) 279 { 280 this.output.put(","); 281 } 282 this.comma.head = true; 283 } 284 final switch (token.kind) 285 { 286 case arrayStart: 287 this.output.put("["); 288 this.comma.push(false); 289 break; 290 case arrayEnd: 291 this.output.put("]"); 292 this.comma.pop; 293 break; 294 case objectStart: 295 this.output.put("{"); 296 this.comma.push(false); 297 break; 298 case objectEnd: 299 this.output.put("}"); 300 this.comma.pop; 301 break; 302 case key: 303 this.output.put("\""); 304 this.output.escapeString(token.key); 305 this.output.put("\":"); 306 // Suppress the next element's comma. 307 this.comma.head = false; 308 break; 309 case bool_: 310 this.output.put(token.bool_ ? "true" : "false"); 311 break; 312 case long_: 313 this.output.formattedWrite("%s", token.long_); 314 break; 315 case double_: 316 this.output.formattedWrite("%s", token.double_); 317 break; 318 case string_: 319 this.output.put("\""); 320 this.output.escapeString(token.string_); 321 this.output.put("\""); 322 break; 323 case sysTime: 324 this.output.put("\""); 325 token.sysTime.toISOExtString(this.output); 326 this.output.put("\""); 327 break; 328 case json: 329 this.output.put(token.json.toJSON); 330 break; 331 } 332 } 333 } 334 } 335 336 // An output range over JSONOutputToken that results in a JSONValue. 337 private struct JSONValueSink 338 { 339 private alias KeyValuePair = Tuple!(string, "key", JSONValue, "value"); 340 341 private Stack!KeyValuePair stack; 342 343 static JSONValueSink opCall() 344 { 345 JSONValueSink sink; 346 // For convenience, wrap the parse stream in []. 347 sink.stack.push(KeyValuePair(string.init, JSONValue(JSONValue[].init))); 348 return sink; 349 } 350 351 public void put(JSONOutputToken token) 352 { 353 with (JSONOutputToken.Kind) 354 { 355 final switch (token.kind) 356 { 357 case arrayStart: 358 this.stack.push(KeyValuePair(string.init, JSONValue(JSONValue[].init))); 359 break; 360 case arrayEnd: 361 assert(head.value.type == JSONType.array); 362 addValue(pop); 363 break; 364 case objectStart: 365 this.stack.push(KeyValuePair(string.init, JSONValue((JSONValue[string]).init))); 366 break; 367 case objectEnd: 368 assert(head.value.type == JSONType.object); 369 addValue(pop); 370 break; 371 case key: 372 assert(head.key.empty); 373 head.key = token.key; 374 break; 375 case bool_: 376 addValue(JSONValue(token.bool_)); 377 break; 378 case long_: 379 addValue(JSONValue(token.long_)); 380 break; 381 case double_: 382 addValue(JSONValue(token.double_)); 383 break; 384 case string_: 385 addValue(JSONValue(token.string_)); 386 break; 387 case sysTime: 388 addValue(JSONValue(token.sysTime.toISOExtString)); 389 break; 390 case json: 391 addValue(token.json); 392 break; 393 } 394 } 395 } 396 397 public JSONValue value() 398 { 399 assert(this.stack.length == 1 && head.value.type == JSONType.array && head.value.array.length == 1); 400 return head.value.array[0]; 401 } 402 403 private ref KeyValuePair head() 404 { 405 return this.stack.head; 406 } 407 408 private JSONValue pop() 409 { 410 assert(head.key.empty); 411 412 return this.stack.pop.value; 413 } 414 415 private void addValue(JSONValue value) 416 { 417 if (head.value.type == JSONType.array) 418 { 419 head.value.array ~= value; 420 } 421 else if (head.value.type == JSONType.object) 422 { 423 assert(!head.key.empty); 424 head.value.object[head.key] = value; 425 head.key = null; 426 } else { 427 assert(false); 428 } 429 } 430 } 431 432 // Why is this not built in, D! 433 private struct Stack(T) 434 { 435 T[] backing; 436 437 size_t length; 438 439 void push(T value) 440 { 441 if (this.length < this.backing.length) 442 { 443 this.backing[this.length++] = value; 444 } 445 else 446 { 447 this.backing ~= value; 448 this.length++; 449 } 450 } 451 452 T pop() 453 in (this.length > 0) 454 { 455 return this.backing[--this.length]; 456 } 457 458 ref T head() 459 in (this.length > 0) 460 { 461 return this.backing[this.length - 1]; 462 } 463 } 464 465 /// 466 @("stack of ints") 467 unittest 468 { 469 import dshould : be, should; 470 471 Stack!int stack; 472 stack.push(2); 473 stack.push(3); 474 stack.push(4); 475 stack.pop.should.be(4); 476 stack.pop.should.be(3); 477 stack.pop.should.be(2); 478 } 479 480 struct JSONOutputToken 481 { 482 enum Kind 483 { 484 objectStart, 485 objectEnd, 486 arrayStart, 487 arrayEnd, 488 key, 489 bool_, 490 long_, 491 double_, 492 string_, 493 sysTime, 494 json, 495 } 496 Kind kind; 497 union 498 { 499 bool bool_; 500 long long_; 501 double double_; 502 string string_; 503 SysTime sysTime; 504 string key_; 505 JSONValue json; 506 } 507 508 this(Kind kind) 509 { 510 this.kind = kind; 511 } 512 513 static foreach (member; ["objectStart", "objectEnd", "arrayStart", "arrayEnd"]) 514 { 515 mixin(format!q{ 516 static JSONOutputToken %s() 517 { 518 return JSONOutputToken(Kind.%s); 519 } 520 }(member, member)); 521 } 522 523 string key() 524 in (this.kind == Kind.key) 525 { 526 return this.key_; 527 } 528 529 static JSONOutputToken key(string key) 530 { 531 auto result = JSONOutputToken(Kind.key); 532 533 result.key_ = key; 534 return result; 535 } 536 537 static foreach (member; ["bool_", "long_", "double_", "string_", "sysTime", "json"]) 538 { 539 mixin(format!q{ 540 this(typeof(this.%s) value) 541 { 542 this.kind = Kind.%s; 543 this.%s = value; 544 } 545 }(member, member, member)); 546 } 547 }