1 module text.json.Decode; 2 3 import funkwerk.stdx.data.json.lexer; 4 import funkwerk.stdx.data.json.parser; 5 import meta.attributesOrNothing; 6 import meta.never; 7 import std.algorithm : canFind, map; 8 import std.conv; 9 import std.format; 10 import std.json : JSONException, JSONValue; 11 import std.traits; 12 import std.typecons; 13 import text.json.Json; 14 import text.json.JsonValueRange; 15 import text.json.ParserMarker; 16 import text.time.Convert; 17 18 /** 19 * This function decodes a JSON string into a given type using introspection. 20 * Throws: JSONException 21 */ 22 public T decode(T, alias transform = never)(string json) 23 { 24 auto stream = parseJSONStream!(LexOptions.noTrackLocation)(json); 25 26 scope(success) 27 { 28 assert(stream.empty); 29 } 30 31 return decodeJson!(T, transform, Yes.logErrors)(stream, T.stringof); 32 } 33 34 /// ditto 35 public T decode(T, alias transform = never)(JSONValue value) 36 { 37 auto jsonStream = JsonValueRange(value); 38 39 return decodeJson!(T, transform, Yes.logErrors)(jsonStream, T.stringof); 40 } 41 42 /// ditto 43 public T decodeJson(T)(JSONValue value) 44 { 45 auto jsonStream = JsonValueRange(value); 46 47 return decodeJson!(T, never, Yes.logErrors)(jsonStream, T.stringof); 48 } 49 50 /// ditto 51 public T decodeJson(T, alias transform, attributes...)(JSONValue value) 52 { 53 auto jsonStream = JsonValueRange(value); 54 55 return decodeJson!(T, transform, Yes.logErrors, attributes)(jsonStream, T.stringof); 56 } 57 58 // This wrapper for decodeJsonInternal uses pragma(msg) to log the type hierarchy that caused an error. 59 public template decodeJson(T, alias transform, Flag!"logErrors" logErrors, attributes...) 60 { 61 T decodeJson(JsonStream)(ref JsonStream jsonStream, lazy string target) 62 { 63 // Don't attempt to speculatively instantiate decoder if logErrors is off anyways. 64 // Avoids combinatorial explosion on deep errors. 65 static if (logErrors == No.logErrors 66 || __traits(compiles, decodeJsonInternal!(T, transform, No.logErrors, attributes)(jsonStream, target))) 67 { 68 return decodeJsonInternal!(T, transform, No.logErrors, attributes)(jsonStream, target); 69 } 70 else 71 { 72 static if (logErrors) 73 { 74 pragma(msg, "Error trying to decode " ~ fullyQualifiedName!T ~ ":"); 75 } 76 return decodeJsonInternal!(T, transform, logErrors, attributes)(jsonStream, target); 77 } 78 } 79 } 80 81 // lazy string target documents the member or array index which is being decoded. 82 public template decodeJsonInternal(T, alias transform, Flag!"logErrors" logErrors, attributes...) 83 { 84 public T decodeJsonInternal(JsonStream)(ref JsonStream jsonStream, lazy string target) 85 in (isJSONParserNodeInputRange!JsonStream) 86 { 87 import boilerplate.util : formatNamed, optionallyRemoveTrailingUnderline, removeTrailingUnderline, udaIndex; 88 import core.exception : AssertError; 89 import meta.SafeUnqual : SafeUnqual; 90 import std.exception : enforce; 91 import std.meta : AliasSeq, anySatisfy, ApplyLeft; 92 import std.range : array, assocArray, ElementType, enumerate; 93 94 static if (is(Unqual!T == JSONValue)) 95 { 96 return decodeJSONValue(jsonStream); 97 } 98 else static if (__traits(compiles, isCallable!(transform!T)) && isCallable!(transform!T)) 99 { 100 static assert(Parameters!(transform!T).length == 1, "`transform` must take one parameter."); 101 102 alias EncodedType = Parameters!(transform!T)[0]; 103 104 static assert(!is(EncodedType == T), 105 "`transform` must not return the same type as it takes (infinite recursion)."); 106 107 return transform!T(.decodeJson!(EncodedType, transform, logErrors, attributes)(jsonStream, target)); 108 } 109 else 110 { 111 alias typeAttributes = attributesOrNothing!T; 112 113 static if (udaIndex!(Json.Decode, attributes) != -1 || udaIndex!(Json.Decode, typeAttributes) != -1) 114 { 115 static if (udaIndex!(Json.Decode, attributes) != -1) 116 { 117 alias decodeFunction = attributes[udaIndex!(Json.Decode, attributes)].DecodeFunction; 118 } 119 else 120 { 121 alias decodeFunction = typeAttributes[udaIndex!(Json.Decode, typeAttributes)].DecodeFunction; 122 } 123 124 JSONValue value = decodeJSONValue(jsonStream); 125 126 static if (__traits(isTemplate, decodeFunction)) 127 { 128 // full meta form 129 static if (__traits(compiles, decodeFunction!(T, transform, attributes)(value, target))) 130 { 131 return decodeFunction!(T, transform, attributes)(value, target); 132 } 133 else 134 { 135 return decodeFunction!T(value); 136 } 137 } 138 else 139 { 140 return decodeFunction(value); 141 } 142 } 143 else static if (__traits(compiles, decodeValue!T(jsonStream, target))) 144 { 145 return decodeValue!T(jsonStream, target); 146 } 147 else static if (is(T == V[K], K, V)) 148 { 149 static assert(is(string: K), "cannot decode associative array with non-string key from json"); 150 151 // decoded separately to handle const values 152 K[] keys; 153 V[] values; 154 155 jsonStream.readObject((string key) @trusted 156 { 157 auto value = .decodeJson!(Unqual!V, transform, logErrors, attributes)( 158 jsonStream, format!`%s[%s]`(target, key)); 159 160 keys ~= key; 161 values ~= value; 162 }); 163 // The is() implconv above may have cast away constness. 164 // But we can rely that nobody but our caller is mutating assocArray anyways. 165 return cast(T) assocArray(keys, values); 166 } 167 else static if (is(T : E[], E)) 168 { 169 Unqual!T result; 170 size_t index; 171 172 enforce!JSONException( 173 jsonStream.front.kind == JSONParserNodeKind.arrayStart, 174 format!"Invalid JSON:%s expected array, but got %s"( 175 target ? (" " ~ target) : null, jsonStream.decodeJSONValue)); 176 177 jsonStream.readArray(() @trusted { 178 result ~= .decodeJson!(E, transform, logErrors, attributes)( 179 jsonStream, format!`%s[%s]`(target, index)); 180 index++; 181 }); 182 return result; 183 } 184 else static if (is(Unqual!T == ParserMarker)) 185 { 186 T marker = T(jsonStream); 187 jsonStream.skipValue; 188 return marker; 189 } 190 else // object 191 { 192 static if (is(T == struct) || is(T == class)) 193 { 194 static assert( 195 __traits(hasMember, T, "ConstructorInfo"), 196 fullyQualifiedName!T ~ " does not have a boilerplate constructor!"); 197 } 198 else 199 { 200 static assert( 201 false, 202 fullyQualifiedName!T ~ " cannot be decoded!"); 203 } 204 205 static if (is(T == class)) 206 { 207 // TODO only do this if we're not @NonNull 208 if (jsonStream.front.kind == JSONParserNodeKind.literal 209 && jsonStream.front.literal.kind == JSONTokenKind.null_) 210 { 211 jsonStream.popFront; 212 return null; 213 } 214 } 215 216 auto builder = T.Builder(); 217 // see doc/why-we-dont-need-save.md 218 auto streamCopy = jsonStream; 219 220 bool[T.ConstructorInfo.fields.length] fieldAssigned; 221 222 enforce!JSONException( 223 jsonStream.front.kind == JSONParserNodeKind.objectStart, 224 format!"Invalid JSON:%s expected object, but got %s"( 225 target ? (" " ~ target) : null, jsonStream.decodeJSONValue)); 226 227 jsonStream.readObject((string key) @trusted 228 { 229 bool keyUsed = false; 230 231 static foreach (fieldIndex, string constructorField; T.ConstructorInfo.fields) 232 {{ 233 enum builderField = optionallyRemoveTrailingUnderline!constructorField; 234 235 alias Type = SafeUnqual!( 236 __traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).Type); 237 alias attributes = AliasSeq!( 238 __traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).attributes); 239 240 static if (is(Type : Nullable!Arg, Arg)) 241 { 242 alias DecodeType = Arg; 243 enum isNullable = true; 244 } 245 else 246 { 247 alias DecodeType = Type; 248 enum isNullable = false; 249 } 250 251 static if (udaIndex!(Json, attributes) != -1) 252 { 253 enum name = attributes[udaIndex!(Json, attributes)].name; 254 } 255 else 256 { 257 enum name = constructorField.removeTrailingUnderline; 258 } 259 260 if (key == name) 261 { 262 static if (isNullable || 263 __traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).useDefault) 264 { 265 const tokenIsNull = jsonStream.front.kind == JSONParserNodeKind.literal 266 && jsonStream.front.literal.kind == JSONTokenKind.null_; 267 268 if (!tokenIsNull) 269 { 270 __traits(getMember, builder, builderField) 271 = .decodeJson!(DecodeType, transform, logErrors, attributes)( 272 jsonStream, fullyQualifiedName!T ~ "." ~ name); 273 274 keyUsed = true; 275 fieldAssigned[fieldIndex] = true; 276 } 277 } 278 else 279 { 280 enum string[] aliasThisMembers = [__traits(getAliasThis, T)]; 281 enum memberIsAliasedToThis = aliasThisMembers 282 .map!removeTrailingUnderline 283 .canFind(constructorField.removeTrailingUnderline); 284 285 static if (!memberIsAliasedToThis) 286 { 287 __traits(getMember, builder, builderField) 288 = .decodeJson!(DecodeType, transform, logErrors, attributes)( 289 jsonStream, target ~ "." ~ name); 290 291 keyUsed = true; 292 fieldAssigned[fieldIndex] = true; 293 } 294 } 295 } 296 }} 297 298 if (!keyUsed) 299 { 300 jsonStream.skipValue; 301 } 302 }); 303 304 // fix up default values and alias this fields 305 static foreach (fieldIndex, const constructorField; T.ConstructorInfo.fields) 306 {{ 307 enum builderField = optionallyRemoveTrailingUnderline!constructorField; 308 alias Type = SafeUnqual!(__traits(getMember, T.ConstructorInfo.FieldInfo, constructorField).Type); 309 310 static if (is(Type : Nullable!Arg, Arg)) 311 { 312 // Nullable types are always treated as optional, so fill in with default value 313 if (!fieldAssigned[fieldIndex]) 314 { 315 __traits(getMember, builder, builderField) = Type(); 316 } 317 } 318 else 319 { 320 enum string[] aliasThisMembers = [__traits(getAliasThis, T)]; 321 enum memberIsAliasedToThis = aliasThisMembers 322 .map!removeTrailingUnderline 323 .canFind(constructorField.removeTrailingUnderline); 324 enum useDefault = __traits(getMember, T.ConstructorInfo.FieldInfo, constructorField) 325 .useDefault; 326 327 static if (memberIsAliasedToThis) 328 { 329 // don't consume streamCopy; we may need it for an error later. 330 auto aliasStream = streamCopy; 331 332 // alias this: decode from the same json value as the whole object 333 __traits(getMember, builder, builderField) 334 = .decodeJson!(Type, transform, logErrors, attributes)( 335 aliasStream, fullyQualifiedName!T ~ "." ~ constructorField); 336 } 337 else static if (!useDefault) 338 { 339 // not alias-this, not nullable, not default - must be set. 340 enforce!JSONException( 341 fieldAssigned[fieldIndex], 342 format!`expected %s.%s, but got %s`( 343 target, builderField, streamCopy.decodeJSONValue)); 344 } 345 } 346 }} 347 348 // catch invariant violations 349 try 350 { 351 return builder.builderValue; 352 } 353 catch (AssertError error) 354 { 355 throw new JSONException(format!`%s:%s - while decoding %s: %s`( 356 error.file, error.line, target, error.msg)); 357 } 358 } 359 } 360 } 361 } 362 363 private template decodeValue(T: bool) 364 if (!is(T == enum)) 365 { 366 private T decodeValue(JsonStream)(ref JsonStream jsonStream, lazy string target) 367 { 368 scope(success) 369 { 370 jsonStream.popFront; 371 } 372 373 if (jsonStream.front.kind == JSONParserNodeKind.literal 374 && jsonStream.front.literal.kind == JSONTokenKind.boolean) 375 { 376 return jsonStream.front.literal.boolean; 377 } 378 throw new JSONException( 379 format!"Invalid JSON:%s expected bool, but got %s"( 380 target ? (" " ~ target) : null, jsonStream.decodeJSONValue)); 381 } 382 } 383 384 private template decodeValue(T: float) 385 if (!is(T == enum)) 386 { 387 private T decodeValue(JsonStream)(ref JsonStream jsonStream, lazy string target) 388 { 389 scope(success) 390 { 391 jsonStream.popFront; 392 } 393 394 if (jsonStream.front.kind == JSONParserNodeKind.literal 395 && jsonStream.front.literal.kind == JSONTokenKind.number) 396 { 397 return jsonStream.front.literal.number.doubleValue.to!T; 398 } 399 throw new JSONException( 400 format!"Invalid JSON:%s expected float, but got %s"( 401 target ? (" " ~ target) : null, jsonStream.decodeJSONValue)); 402 } 403 } 404 405 private template decodeValue(T: int) 406 if (!is(T == enum)) 407 { 408 private T decodeValue(JsonStream)(ref JsonStream jsonStream, lazy string target) 409 { 410 scope(success) 411 { 412 jsonStream.popFront; 413 } 414 415 if (jsonStream.front.kind == JSONParserNodeKind.literal 416 && jsonStream.front.literal.kind == JSONTokenKind.number) 417 { 418 switch (jsonStream.front.literal.number.type) 419 { 420 case JSONNumber.Type.long_: 421 return jsonStream.front.literal.number.longValue.to!int; 422 default: 423 break; 424 } 425 } 426 throw new JSONException( 427 format!"Invalid JSON:%s expected int, but got %s"( 428 target ? (" " ~ target) : null, jsonStream.decodeJSONValue)); 429 } 430 } 431 432 private template decodeValue(T) 433 if (is(T == enum)) 434 { 435 private T decodeValue(JsonStream)(ref JsonStream jsonStream, lazy string target) 436 { 437 scope(success) 438 { 439 jsonStream.popFront; 440 } 441 442 if (jsonStream.front.kind == JSONParserNodeKind.literal 443 && jsonStream.front.literal.kind == JSONTokenKind..string) 444 { 445 string str = jsonStream.front.literal..string; 446 447 try 448 { 449 return parse!(Unqual!T)(str); 450 } 451 catch (ConvException exception) 452 { 453 throw new JSONException( 454 format!"Invalid JSON:%s expected member of %s, but got \"%s\"" 455 (target ? (" " ~ target) : null, T.stringof, str)); 456 } 457 } 458 throw new JSONException( 459 format!"Invalid JSON:%s expected enum string, but got %s"( 460 target ? (" " ~ target) : null, jsonStream.decodeJSONValue)); 461 } 462 } 463 464 private template decodeValue(T: string) 465 if (!is(T == enum)) 466 { 467 private T decodeValue(JsonStream)(ref JsonStream jsonStream, lazy string target) 468 { 469 scope(success) 470 { 471 jsonStream.popFront; 472 } 473 474 if (jsonStream.front.kind == JSONParserNodeKind.literal 475 && jsonStream.front.literal.kind == JSONTokenKind..string) 476 { 477 return jsonStream.front.literal..string; 478 } 479 throw new JSONException( 480 format!"Invalid JSON:%s expected string, but got %s"( 481 target ? (" " ~ target) : null, jsonStream.decodeJSONValue)); 482 } 483 } 484 485 private template decodeValue(T) 486 if (__traits(compiles, Convert.to!T(string.init))) 487 { 488 private T decodeValue(JsonStream)(ref JsonStream jsonStream, lazy string target) 489 { 490 scope(success) 491 { 492 jsonStream.popFront; 493 } 494 495 if (jsonStream.front.kind == JSONParserNodeKind.literal 496 && jsonStream.front.literal.kind == JSONTokenKind..string) 497 { 498 return Convert.to!T(jsonStream.front.literal..string); 499 } 500 throw new JSONException( 501 format!"Invalid JSON:%s expected string, but got %s"( 502 target ? (" " ~ target) : null, jsonStream.decodeJSONValue)); 503 } 504 } 505 506 private JSONValue decodeJSONValue(JsonStream)(ref JsonStream jsonStream) 507 in (isJSONParserNodeInputRange!JsonStream) 508 { 509 with (JSONParserNodeKind) final switch (jsonStream.front.kind) 510 { 511 case arrayStart: 512 JSONValue[] children; 513 jsonStream.readArray(delegate void() @trusted 514 { 515 children ~= .decodeJSONValue(jsonStream); 516 }); 517 return JSONValue(children); 518 case objectStart: 519 JSONValue[string] children; 520 jsonStream.readObject(delegate void(string key) @trusted 521 { 522 children[key] = .decodeJSONValue(jsonStream); 523 }); 524 return JSONValue(children); 525 case literal: 526 with (JSONTokenKind) switch (jsonStream.front.literal.kind) 527 { 528 case null_: 529 jsonStream.popFront; 530 return JSONValue(null); 531 case boolean: return JSONValue(jsonStream.readBool); 532 case string: return JSONValue(jsonStream.readString); 533 case number: 534 { 535 scope(success) 536 { 537 jsonStream.popFront; 538 } 539 540 switch (jsonStream.front.literal.number.type) 541 { 542 case JSONNumber.Type.long_: 543 return JSONValue(jsonStream.front.literal.number.longValue); 544 case JSONNumber.Type.double_: 545 return JSONValue(jsonStream.front.literal.number.doubleValue); 546 default: 547 throw new JSONException(format!"Unexpected number: %s"(jsonStream.front.literal)); 548 } 549 } 550 default: 551 throw new JSONException(format!"Unexpected JSON token: %s"(jsonStream.front)); 552 } 553 case key: 554 throw new JSONException("Unexpected object key"); 555 case arrayEnd: 556 throw new JSONException("Unexpected end of array"); 557 case objectEnd: 558 throw new JSONException("Unexpected end of object"); 559 case none: 560 assert(false); // "never occurs in a node stream" 561 } 562 }