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