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 }