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 }