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 }