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 }