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