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