1 module text.json.Encode;
2 
3 import meta.attributesOrNothing;
4 import meta.never;
5 import meta.SafeUnqual;
6 import std.datetime;
7 import std.format;
8 import std.json;
9 import std.range;
10 import std.traits;
11 import std.typecons;
12 import text.json.Json;
13 
14 /**
15  * Encodes an arbitrary type as a JSON string using introspection.
16  */
17 public string encode(T, alias transform = never)(const T value)
18 {
19     auto sink = StringSink();
20     encodeJsonStream!(T, transform, StringSink)(sink, value);
21     return sink.output[];
22 }
23 
24 /// ditto
25 public JSONValue encodeJson(T)(const T value)
26 {
27     return encodeJson!(T, never)(value);
28 }
29 
30 public JSONValue encodeJson(T, alias transform)(const T value)
31 {
32     auto sink = JSONValueSink();
33     encodeJsonStream!(T, transform, JSONValueSink)(sink, value);
34     return sink.value;
35 }
36 
37 // range is an output range over `JSONOutputToken`s.
38 private void encodeJsonStream(T, alias transform, Range, attributes...)(ref Range output, const T parameter)
39 in
40 {
41     static if (is(T == class))
42     {
43         assert(parameter !is null);
44     }
45 }
46 do
47 {
48     import boilerplate.util : udaIndex;
49     import std.traits : isIterable, Unqual;
50 
51     static if (__traits(compiles, transform(parameter)))
52     {
53         auto transformedValue = transform(parameter);
54         static assert(!is(Unqual!(typeof(transformedValue)) == Unqual!T),
55             "transform must not return the same type as it takes!");
56 
57         encodeJsonStream!(typeof(transformedValue), transform, Range)(output, transformedValue);
58     }
59     else
60     {
61         static assert(
62             !__traits(compiles, transform(Unqual!T.init)),
63             "transform() must take its parameter as const!");
64 
65         auto value = parameter;
66         alias Type = T;
67 
68         alias typeAttributes = attributesOrNothing!Type;
69 
70         static if (udaIndex!(Json.Encode, attributes) != -1)
71         {
72             alias encodeFunction = attributes[udaIndex!(Json.Encode, attributes)].EncodeFunction;
73             enum hasEncodeFunction = true;
74         }
75         else static if (udaIndex!(Json.Encode, typeAttributes) != -1)
76         {
77             alias encodeFunction = typeAttributes[udaIndex!(Json.Encode, typeAttributes)].EncodeFunction;
78             enum hasEncodeFunction = true;
79         }
80         else
81         {
82             enum hasEncodeFunction = false;
83         }
84 
85         static if (hasEncodeFunction)
86         {
87             static if (__traits(compiles, encodeFunction!(typeof(value), transform, attributes)))
88             {
89                 auto jsonValue = encodeFunction!(typeof(value), transform, attributes)(value);
90             }
91             else
92             {
93                 auto jsonValue = encodeFunction(value);
94             }
95             output.put(JSONOutputToken(jsonValue));
96         }
97         else static if (__traits(compiles, encodeValue(output, value)))
98         {
99             encodeValue(output, value);
100         }
101         else static if (is(Type : V[string], V))
102         {
103             output.put(JSONOutputToken.objectStart);
104             foreach (key, element; value)
105             {
106                 output.put(JSONOutputToken.key(key));
107                 encodeJsonStream!(typeof(element), transform, Range, attributes)(output, element);
108             }
109             output.put(JSONOutputToken.objectEnd);
110         }
111         else static if (isIterable!Type)
112         {
113             output.put(JSONOutputToken.arrayStart);
114             foreach (element; value)
115             {
116                 encodeJsonStream!(typeof(element), transform, Range, attributes)(output, element);
117             }
118             output.put(JSONOutputToken.arrayEnd);
119         }
120         else
121         {
122             output.put(JSONOutputToken.objectStart);
123             encodeStruct!(T, transform, Range, attributes)(output, value);
124             output.put(JSONOutputToken.objectEnd);
125         }
126     }
127 }
128 
129 private void encodeStruct(Type, alias transform, Range, attributes...)(ref Range output, const Type value)
130 {
131     import boilerplate.util : formatNamed, optionallyRemoveTrailingUnderline, removeTrailingUnderline, udaIndex;
132     import std.meta : AliasSeq, anySatisfy, ApplyLeft;
133     import std.traits : fullyQualifiedName;
134 
135     static assert(
136         __traits(hasMember, Type, "ConstructorInfo"),
137         fullyQualifiedName!Type ~ " does not have a boilerplate constructor!");
138 
139     alias Info = Tuple!(string, "builderField", string, "constructorField");
140 
141     static foreach (string constructorField; Type.ConstructorInfo.fields)
142     {{
143         enum builderField = optionallyRemoveTrailingUnderline!constructorField;
144 
145         mixin(formatNamed!q{
146             alias MemberType = SafeUnqual!(Type.ConstructorInfo.FieldInfo.%(constructorField).Type);
147 
148             const MemberType memberValue = value.%(builderField);
149 
150             static if (is(MemberType : Nullable!Arg, Arg))
151             {
152                 bool includeMember = !memberValue.isNull;
153                 enum getMemberValue = "memberValue.get";
154             }
155             else
156             {
157                 enum includeMember = true;
158                 enum getMemberValue = "memberValue";
159             }
160 
161             alias attributes = AliasSeq!(Type.ConstructorInfo.FieldInfo.%(constructorField).attributes);
162 
163             if (includeMember)
164             {
165                 static if (udaIndex!(Json, attributes) != -1)
166                 {
167                     enum name = attributes[udaIndex!(Json, attributes)].name;
168                 }
169                 else
170                 {
171                     enum name = constructorField.removeTrailingUnderline;
172                 }
173 
174                 auto finalMemberValue = mixin(getMemberValue);
175 
176                 enum sameField(string lhs, string rhs)
177                     = optionallyRemoveTrailingUnderline!lhs== optionallyRemoveTrailingUnderline!rhs;
178                 enum memberIsAliasedToThis = anySatisfy!(
179                     ApplyLeft!(sameField, constructorField),
180                     __traits(getAliasThis, Type));
181 
182                 static if (memberIsAliasedToThis)
183                 {
184                     encodeStruct!(typeof(finalMemberValue), transform, Range, attributes)(
185                         output, finalMemberValue);
186                 }
187                 else
188                 {
189                     output.put(JSONOutputToken.key(name));
190                     encodeJsonStream!(typeof(finalMemberValue), transform, Range, attributes)(
191                         output, finalMemberValue);
192                 }
193             }
194         }.values(Info(builderField, constructorField)));
195     }}
196 }
197 
198 private void encodeJsonStream(T : JSONValue, alias transform, Range, attributes...)(
199     ref Range output, const T value)
200 {
201     output.put(JSONOutputToken(value));
202 }
203 
204 static if (__traits(compiles, { import std.sumtype; }))
205 {
206     import std.sumtype : match, SumType;
207 
208     private void encodeJsonStream(T : SumType!Types, alias transform, Range, Types...)(
209         ref Range output, const T value)
210     {
211         value.match!(
212             staticMap!(
213                 a => encodeJsonStream!(typeof(a), transform, Range)(output, a),
214                 Types));
215     }
216 }
217 
218 private void encodeValue(T, Range)(ref Range output, T value)
219 if (!is(T: Nullable!Arg, Arg))
220 {
221     import std.conv : to;
222     import text.xml.Convert : Convert;
223 
224     static if (is(T == enum))
225     {
226         output.put(JSONOutputToken(value.to!string));
227     }
228     else static if (isBoolean!T || isIntegral!T || isFloatingPoint!T || isSomeString!T)
229     {
230         output.put(JSONOutputToken(value));
231     }
232     else static if (is(T == typeof(null)))
233     {
234         // FIXME proper null token?
235         output.put(JSONOutputToken(JSONValue(null)));
236     }
237     else static if (is(T : const SysTime))
238     {
239         // fastpath for SysTime (it's a very common type)
240         SysTime noFractionalSeconds = value;
241 
242         noFractionalSeconds.fracSecs = 0.seconds;
243         output.put(JSONOutputToken(noFractionalSeconds));
244     }
245     else static if (__traits(compiles, Convert.toString(value)))
246     {
247         output.put(JSONOutputToken(Convert.toString(value)));
248     }
249     else
250     {
251         static assert(false, "Cannot encode " ~ T.stringof ~ " as value");
252     }
253 }
254 
255 // An output range over JSONOutputToken that results in a string.
256 private struct StringSink
257 {
258     private Stack!bool comma;
259 
260     private Appender!string output;
261 
262     static StringSink opCall()
263     {
264         StringSink sink;
265         sink.output = appender!string();
266         sink.comma.push(false);
267         return sink;
268     }
269 
270     public void put(JSONOutputToken token)
271     {
272         import funkwerk.stdx.data.json.generator : escapeString;
273 
274         with (JSONOutputToken.Kind)
275         {
276             if (token.kind != arrayEnd && token.kind != objectEnd)
277             {
278                 if (this.comma.head)
279                 {
280                     this.output.put(",");
281                 }
282                 this.comma.head = true;
283             }
284             final switch (token.kind)
285             {
286                 case arrayStart:
287                     this.output.put("[");
288                     this.comma.push(false);
289                     break;
290                 case arrayEnd:
291                     this.output.put("]");
292                     this.comma.pop;
293                     break;
294                 case objectStart:
295                     this.output.put("{");
296                     this.comma.push(false);
297                     break;
298                 case objectEnd:
299                     this.output.put("}");
300                     this.comma.pop;
301                     break;
302                 case key:
303                     this.output.put("\"");
304                     this.output.escapeString(token.key);
305                     this.output.put("\":");
306                     // Suppress the next element's comma.
307                     this.comma.head = false;
308                     break;
309                 case bool_:
310                     this.output.put(token.bool_ ? "true" : "false");
311                     break;
312                 case long_:
313                     this.output.formattedWrite("%s", token.long_);
314                     break;
315                 case double_:
316                     this.output.formattedWrite("%s", token.double_);
317                     break;
318                 case string_:
319                     this.output.put("\"");
320                     this.output.escapeString(token.string_);
321                     this.output.put("\"");
322                     break;
323                 case sysTime:
324                     this.output.put("\"");
325                     token.sysTime.toISOExtString(this.output);
326                     this.output.put("\"");
327                     break;
328                 case json:
329                     this.output.put(token.json.toJSON);
330                     break;
331             }
332         }
333     }
334 }
335 
336 // An output range over JSONOutputToken that results in a JSONValue.
337 private struct JSONValueSink
338 {
339     private alias KeyValuePair = Tuple!(string, "key", JSONValue, "value");
340 
341     private Stack!KeyValuePair stack;
342 
343     static JSONValueSink opCall()
344     {
345         JSONValueSink sink;
346         // For convenience, wrap the parse stream in [].
347         sink.stack.push(KeyValuePair(string.init, JSONValue(JSONValue[].init)));
348         return sink;
349     }
350 
351     public void put(JSONOutputToken token)
352     {
353         with (JSONOutputToken.Kind)
354         {
355             final switch (token.kind)
356             {
357                 case arrayStart:
358                     this.stack.push(KeyValuePair(string.init, JSONValue(JSONValue[].init)));
359                     break;
360                 case arrayEnd:
361                     assert(head.value.type == JSONType.array);
362                     addValue(pop);
363                     break;
364                 case objectStart:
365                     this.stack.push(KeyValuePair(string.init, JSONValue((JSONValue[string]).init)));
366                     break;
367                 case objectEnd:
368                     assert(head.value.type == JSONType.object);
369                     addValue(pop);
370                     break;
371                 case key:
372                     assert(head.key.empty);
373                     head.key = token.key;
374                     break;
375                 case bool_:
376                     addValue(JSONValue(token.bool_));
377                     break;
378                 case long_:
379                     addValue(JSONValue(token.long_));
380                     break;
381                 case double_:
382                     addValue(JSONValue(token.double_));
383                     break;
384                 case string_:
385                     addValue(JSONValue(token.string_));
386                     break;
387                 case sysTime:
388                     addValue(JSONValue(token.sysTime.toISOExtString));
389                     break;
390                 case json:
391                     addValue(token.json);
392                     break;
393             }
394         }
395     }
396 
397     public JSONValue value()
398     {
399         assert(this.stack.length == 1 && head.value.type == JSONType.array && head.value.array.length == 1);
400         return head.value.array[0];
401     }
402 
403     private ref KeyValuePair head()
404     {
405         return this.stack.head;
406     }
407 
408     private JSONValue pop()
409     {
410         assert(head.key.empty);
411 
412         return this.stack.pop.value;
413     }
414 
415     private void addValue(JSONValue value)
416     {
417         if (head.value.type == JSONType.array)
418         {
419             head.value.array ~= value;
420         }
421         else if (head.value.type == JSONType.object)
422         {
423             assert(!head.key.empty);
424             head.value.object[head.key] = value;
425             head.key = null;
426         } else {
427             assert(false);
428         }
429     }
430 }
431 
432 // Why is this not built in, D!
433 private struct Stack(T)
434 {
435     T[] backing;
436 
437     size_t length;
438 
439     void push(T value)
440     {
441         if (this.length < this.backing.length)
442         {
443             this.backing[this.length++] = value;
444         }
445         else
446         {
447             this.backing ~= value;
448             this.length++;
449         }
450     }
451 
452     T pop()
453     in (this.length > 0)
454     {
455         return this.backing[--this.length];
456     }
457 
458     ref T head()
459     in (this.length > 0)
460     {
461         return this.backing[this.length - 1];
462     }
463 }
464 
465 ///
466 @("stack of ints")
467 unittest
468 {
469     import dshould : be, should;
470 
471     Stack!int stack;
472     stack.push(2);
473     stack.push(3);
474     stack.push(4);
475     stack.pop.should.be(4);
476     stack.pop.should.be(3);
477     stack.pop.should.be(2);
478 }
479 
480 struct JSONOutputToken
481 {
482     enum Kind
483     {
484         objectStart,
485         objectEnd,
486         arrayStart,
487         arrayEnd,
488         key,
489         bool_,
490         long_,
491         double_,
492         string_,
493         sysTime,
494         json,
495     }
496     Kind kind;
497     union
498     {
499         bool bool_;
500         long long_;
501         double double_;
502         string string_;
503         SysTime sysTime;
504         string key_;
505         JSONValue json;
506     }
507 
508     this(Kind kind)
509     {
510         this.kind = kind;
511     }
512 
513     static foreach (member; ["objectStart", "objectEnd", "arrayStart", "arrayEnd"])
514     {
515         mixin(format!q{
516             static JSONOutputToken %s()
517             {
518                 return JSONOutputToken(Kind.%s);
519             }
520         }(member, member));
521     }
522 
523     string key()
524     in (this.kind == Kind.key)
525     {
526         return this.key_;
527     }
528 
529     static JSONOutputToken key(string key)
530     {
531         auto result = JSONOutputToken(Kind.key);
532 
533         result.key_ = key;
534         return result;
535     }
536 
537     static foreach (member; ["bool_", "long_", "double_", "string_", "sysTime", "json"])
538     {
539         mixin(format!q{
540             this(typeof(this.%s) value)
541             {
542                 this.kind = Kind.%s;
543                 this.%s = value;
544             }
545         }(member, member, member));
546     }
547 }