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