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