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