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