1 /**
2  * Contains routines for converting JSON values to their string represencation.
3  *
4  * Synopsis:
5  * ---
6  * ...
7  * ---
8  *
9  * Copyright: Copyright 2012 - 2015, Sönke Ludwig.
10  * License:   $(WEB www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
11  * Authors:   Sönke Ludwig
12  * Source:    $(PHOBOSSRC std/data/json/generator.d)
13  */
14 module funkwerk.stdx.data.json.generator;
15 
16 import funkwerk.stdx.data.json.lexer;
17 import funkwerk.stdx.data.json.parser;
18 import funkwerk.stdx.data.json.value;
19 import std.bigint;
20 import std.range;
21 
22 
23 /**
24  * Converts the given JSON document(s) to its string representation.
25  *
26  * The input can be a $(D JSONValue), or an input range of either $(D JSONToken)
27  * or $(D JSONParserNode) elements. By default, the generator will use newlines
28  * and tabs to pretty-print the result. Use the `options` template parameter
29  * to customize this.
30  *
31  * Params:
32  *   value = A single JSON document
33  *   nodes = A set of JSON documents encoded as single parser nodes. The nodes
34  *     must be in valid document order, or the parser result will be undefined.
35  *   tokens = List of JSON tokens to be converted to strings. The tokens may
36  *     occur in any order and are simply appended in order to the final string.
37  *   token = A single token to convert to a string
38  *
39  * Returns:
40  *   Returns a JSON formatted string.
41  *
42  * See_also: $(D writeJSON), $(D toPrettyJSON)
43  */
44 string toJSON(GeneratorOptions options = GeneratorOptions.init)(JSONValue value)
45 {
46     import std.array;
47     auto dst = appender!string();
48     value.writeJSON!options(dst);
49     return dst.data;
50 }
51 /// ditto
52 string toJSON(GeneratorOptions options = GeneratorOptions.init, Input)(Input nodes)
53     if (isJSONParserNodeInputRange!Input)
54 {
55     import std.array;
56     auto dst = appender!string();
57     nodes.writeJSON!options(dst);
58     return dst.data;
59 }
60 /// ditto
61 string toJSON(GeneratorOptions options = GeneratorOptions.init, Input)(Input tokens)
62     if (isJSONTokenInputRange!Input)
63 {
64     import std.array;
65     auto dst = appender!string();
66     tokens.writeJSON!options(dst);
67     return dst.data;
68 }
69 /// ditto
70 string toJSON(GeneratorOptions options = GeneratorOptions.init, String)(JSONToken!String token)
71 {
72     import std.array;
73     auto dst = appender!string();
74     token.writeJSON!options(dst);
75     return dst.data;
76 }
77 
78 ///
79 @safe unittest
80 {
81     JSONValue value = true;
82     assert(value.toJSON() == "true");
83 }
84 
85 ///
86 @safe unittest
87 {
88     auto a = toJSONValue(`{"a": [], "b": [1, {}]}`);
89 
90     // pretty print:
91     // {
92     //     "a": [],
93     //     "b": [
94     //         1,
95     //         {},
96     //     ]
97     // }
98     assert(
99         a.toJSON() == "{\n\t\"a\": [],\n\t\"b\": [\n\t\t1,\n\t\t{}\n\t]\n}" ||
100         a.toJSON() == "{\n\t\"b\": [\n\t\t1,\n\t\t{}\n\t],\n\t\"a\": []\n}"
101     );
102 
103     // write compact JSON (order of object fields is undefined)
104     assert(
105         a.toJSON!(GeneratorOptions.compact)() == `{"a":[],"b":[1,{}]}` ||
106         a.toJSON!(GeneratorOptions.compact)() == `{"b":[1,{}],"a":[]}`
107     );
108 }
109 
110 @safe unittest
111 {
112     auto nodes = parseJSONStream(`{"a": [], "b": [1, {}]}`);
113     assert(nodes.toJSON() == "{\n\t\"a\": [],\n\t\"b\": [\n\t\t1,\n\t\t{}\n\t]\n}");
114     assert(nodes.toJSON!(GeneratorOptions.compact)() == `{"a":[],"b":[1,{}]}`);
115 
116     auto tokens = lexJSON(`{"a": [], "b": [1, {}, null, true, false]}`);
117     assert(tokens.toJSON!(GeneratorOptions.compact)() == `{"a":[],"b":[1,{},null,true,false]}`);
118 
119     JSONToken!string tok;
120     tok..string = "Hello World";
121     assert(tok.toJSON() == `"Hello World"`);
122 }
123 
124 
125 /**
126  * Writes the string representation of the given JSON document(s)/tokens to an
127  * output range.
128  *
129  * See $(D toJSON) for more information.
130  *
131  * Params:
132  *   output = The output range to take the result string in UTF-8 encoding.
133  *   value = A single JSON document
134  *   nodes = A set of JSON documents encoded as single parser nodes. The nodes
135  *     must be in valid document order, or the parser result will be undefined.
136  *   tokens = List of JSON tokens to be converted to strings. The tokens may
137  *     occur in any order and are simply appended in order to the final string.
138  *   token = A single token to convert to a string
139  *
140  * See_also: $(D toJSON), $(D writePrettyJSON)
141  */
142 void writeJSON(GeneratorOptions options = GeneratorOptions.init, Output)(JSONValue value, ref Output output)
143     if (isOutputRange!(Output, char))
144 {
145     writeAsStringImpl!options(value, output);
146 }
147 /// ditto
148 void writeJSON(GeneratorOptions options = GeneratorOptions.init, Output, Input)(Input nodes, ref Output output)
149     if (isOutputRange!(Output, char) && isJSONParserNodeInputRange!Input)
150 {
151     //import std.algorithm.mutation : copy;
152     auto joutput = JSONOutputRange!(Output, options)(output);
153     foreach (n; nodes) joutput.put(n);
154     //copy(nodes, joutput);
155 }
156 /// ditto
157 void writeJSON(GeneratorOptions options = GeneratorOptions.init, Output, Input)(Input tokens, ref Output output)
158     if (isOutputRange!(Output, char) && isJSONTokenInputRange!Input)
159 {
160     while (!tokens.empty)
161     {
162         tokens.front.writeJSON!options(output);
163         tokens.popFront();
164     }
165 }
166 /// ditto
167 void writeJSON(GeneratorOptions options = GeneratorOptions.init, String, Output)(in ref JSONToken!String token, ref Output output)
168     if (isOutputRange!(Output, char))
169 {
170     final switch (token.kind) with (JSONTokenKind)
171     {
172         case none: assert(false);
173         case error: output.put("_error_"); break;
174         case null_: output.put("null"); break;
175         case boolean: output.put(token.boolean ? "true" : "false"); break;
176         case number: output.writeNumber!options(token.number); break;
177         case string: output.put('"'); output.escapeString!(options & GeneratorOptions.escapeUnicode)(token..string); output.put('"'); break;
178         case objectStart: output.put('{'); break;
179         case objectEnd: output.put('}'); break;
180         case arrayStart: output.put('['); break;
181         case arrayEnd: output.put(']'); break;
182         case colon: output.put(':'); break;
183         case comma: output.put(','); break;
184     }
185 }
186 
187 /** Convenience function for creating a `JSONOutputRange` instance using IFTI.
188 */
189 JSONOutputRange!(R, options) jsonOutputRange(GeneratorOptions options = GeneratorOptions.init, R)(R output)
190     if (isOutputRange!(R, char))
191 {
192     return JSONOutputRange!(R, options)(output);
193 }
194 
195 /** Output range that takes JSON primitives and outputs to a character output
196     range.
197 
198     This range provides the underlying functinality for `writeJSON` and
199     `toJSON` and is well suited as a target for serialization frameworks.
200 
201     Note that pretty-printing (`GeneratorOptions.compact` not set) is currently
202     only supported for primitives of type `JSONParserNode`.
203 */
204 struct JSONOutputRange(R, GeneratorOptions options = GeneratorOptions.init)
205     if (isOutputRange!(R, char))
206 {
207     private {
208         R m_output;
209         size_t m_nesting = 0;
210         bool m_first = false;
211         bool m_isObjectField = false;
212     }
213 
214     /** Constructs the range for a given character output range.
215     */
216     this(R output)
217     {
218         m_output = output;
219     }
220 
221     /** Writes a single JSON primitive to the destination character range.
222     */
223     void put(String)(JSONParserNode!String node)
224     {
225         enum pretty_print = (options & GeneratorOptions.compact) == 0;
226 
227         final switch (node.kind) with (JSONParserNodeKind) {
228             case none: assert(false);
229             case key:
230                 if (m_nesting > 0 && !m_first) m_output.put(',');
231                 else m_first = false;
232                 m_isObjectField = true;
233                 static if (pretty_print) indent();
234                 m_output.put('"');
235                 m_output.escapeString!(options & GeneratorOptions.escapeUnicode)(node.key);
236                 m_output.put(pretty_print ? `": ` : `":`);
237                 break;
238             case literal:
239                 preValue();
240                 node.literal.writeJSON!options(m_output);
241                 break;
242             case objectStart:
243                 preValue();
244                 m_output.put('{');
245                 m_nesting++;
246                 m_first = true;
247                 break;
248             case objectEnd:
249                 m_nesting--;
250                 static if (pretty_print)
251                 {
252                     if (!m_first) indent();
253                 }
254                 m_first = false;
255                 m_output.put('}');
256                 break;
257             case arrayStart:
258                 preValue();
259                 m_output.put('[');
260                 m_nesting++;
261                 m_first = true;
262                 m_isObjectField = false;
263                 break;
264             case arrayEnd:
265                 m_nesting--;
266                 static if (pretty_print)
267                 {
268                     if (!m_first) indent();
269                 }
270                 m_first = false;
271                 m_output.put(']');
272                 break;
273         }
274     }
275     /// ditto
276     void put(String)(JSONToken!String token)
277     {
278         final switch (token.kind) with (JSONToken.Kind) {
279             case none: assert(false);
280             case error: m_output.put("_error_"); break;
281             case null_: put(null); break;
282             case boolean: put(token.boolean); break;
283             case number: put(token.number); break;
284             case string: put(token..string); break;
285             case objectStart: m_output.put('{'); break;
286             case objectEnd: m_output.put('}'); break;
287             case arrayStart: m_output.put('['); break;
288             case arrayEnd: m_output.put(']'); break;
289             case colon: m_output.put(':'); break;
290             case comma: m_output.put(','); break;
291         }
292     }
293     /// ditto
294     void put(typeof(null)) { m_output.put("null"); }
295     /// ditto
296     void put(bool value) { m_output.put(value ? "true" : "false"); }
297     /// ditto
298     void put(long value) { m_output.writeNumber(value); }
299     /// ditto
300     void put(BigInt value) { m_output.writeNumber(value); }
301     /// ditto
302     void put(double value) { m_output.writeNumber!options(value); }
303     /// ditto
304     void put(String)(JSONString!String value)
305     {
306         auto s = value.anyValue;
307         if (s[0]) put(s[1]); // decoded string
308         else m_output.put(s[1]); // raw string literal
309     }
310     /// ditto
311     void put(string value)
312     {
313         m_output.put('"');
314         m_output.escapeString!(options & GeneratorOptions.escapeUnicode)(value);
315         m_output.put('"');
316     }
317 
318     private void indent()
319     {
320         m_output.put('\n');
321         foreach (tab; 0 .. m_nesting) m_output.put('\t');
322     }
323 
324     private void preValue()
325     {
326         if (!m_isObjectField)
327         {
328             if (m_nesting > 0 && !m_first) m_output.put(',');
329             else m_first = false;
330             static if (!(options & GeneratorOptions.compact))
331             {
332                 if (m_nesting > 0) indent();
333             }
334         }
335         else m_isObjectField = false;
336     }
337 }
338 
339 @safe unittest {
340     auto app = appender!(char[]);
341     auto dst = jsonOutputRange(app);
342     dst.put(true);
343     dst.put(1234);
344     dst.put("hello");
345     assert(app.data == "true1234\"hello\"");
346 }
347 
348 @safe unittest {
349     auto app = appender!(char[]);
350     auto dst = jsonOutputRange(app);
351     foreach (n; parseJSONStream(`{"foo":42, "bar":true, "baz": [null, false]}`))
352         dst.put(n);
353     assert(app.data == "{\n\t\"foo\": 42,\n\t\"bar\": true,\n\t\"baz\": [\n\t\tnull,\n\t\tfalse\n\t]\n}");
354 }
355 
356 
357 /**
358  * Flags for configuring the JSON generator.
359  *
360  * These flags can be combined using a bitwise or operation.
361  */
362 enum GeneratorOptions {
363 	/// Default value - enable none of the supported options
364     init = 0,
365 
366     /// Avoid outputting whitespace to get a compact string representation
367     compact = 1<<0,
368 
369 	/// Output special float values as 'NaN' or 'Infinity' instead of 'null'
370     specialFloatLiterals = 1<<1,
371 
372 	/// Output all non-ASCII characters as unicode escape sequences
373     escapeUnicode = 1<<2,
374 }
375 
376 
377 @safe private void writeAsStringImpl(GeneratorOptions options, Output)(JSONValue value, ref Output output, size_t nesting_level = 0)
378     if (isOutputRange!(Output, char))
379 {
380     import funkwerk.stdx.data.json.taggedalgebraic : get;
381 
382     enum pretty_print = (options & GeneratorOptions.compact) == 0;
383 
384     void indent(size_t depth)
385     {
386         output.put('\n');
387         foreach (tab; 0 .. depth) output.put('\t');
388     }
389 
390     final switch (value.kind) {
391         case JSONValue.Kind.null_: output.put("null"); break;
392         case JSONValue.Kind.boolean: output.put(value == true ? "true" : "false"); break;
393         case JSONValue.Kind.double_: output.writeNumber!options(cast(double)value); break;
394         case JSONValue.Kind.integer: output.writeNumber(cast(long)value); break;
395         case JSONValue.Kind.bigInt: () @trusted {
396             auto val = cast(BigInt*)value;
397             if (val is null) throw new Exception("Null BigInt value");
398             output.writeNumber(*val);
399             }(); break;
400         case JSONValue.Kind..string: output.put('"'); output.escapeString!(options & GeneratorOptions.escapeUnicode)(get!string(value)); output.put('"'); break;
401         case JSONValue.Kind.object:
402             output.put('{');
403             bool first = true;
404             foreach (string k, ref e; get!(JSONValue[string])(value))
405             {
406                 if (!first) output.put(',');
407                 else first = false;
408                 static if (pretty_print) indent(nesting_level+1);
409                 output.put('\"');
410                 output.escapeString!(options & GeneratorOptions.escapeUnicode)(k);
411                 output.put(pretty_print ? `": ` : `":`);
412                 e.writeAsStringImpl!options(output, nesting_level+1);
413             }
414             static if (pretty_print)
415             {
416                 if (!first) indent(nesting_level);
417             }
418             output.put('}');
419             break;
420         case JSONValue.Kind.array:
421             output.put('[');
422             foreach (i, ref e; get!(JSONValue[])(value))
423             {
424                 if (i > 0) output.put(',');
425                 static if (pretty_print) indent(nesting_level+1);
426                 e.writeAsStringImpl!options(output, nesting_level+1);
427             }
428             static if (pretty_print)
429             {
430                 if (get!(JSONValue[])(value).length > 0) indent(nesting_level);
431             }
432             output.put(']');
433             break;
434     }
435 }
436 
437 private void writeNumber(GeneratorOptions options, R)(ref R dst, JSONNumber num) @trusted
438 {
439     import std.format;
440     import std.math;
441 
442     final switch (num.type)
443     {
444         case JSONNumber.Type.double_: dst.writeNumber!options(num.doubleValue); break;
445         case JSONNumber.Type.long_: dst.writeNumber(num.longValue); break;
446         case JSONNumber.Type.bigInt: dst.writeNumber(num.bigIntValue); break;
447     }
448 }
449 
450 private void writeNumber(GeneratorOptions options, R)(ref R dst, double num) @trusted
451 {
452     import std.format;
453     import std.math;
454 
455     static if (options & GeneratorOptions.specialFloatLiterals)
456     {
457         if (isNaN(num)) dst.put("NaN");
458         else if (num == +double.infinity) dst.put("Infinity");
459         else if (num == -double.infinity) dst.put("-Infinity");
460         else dst.formattedWrite("%.16g", num);
461     }
462     else
463     {
464         if (isNaN(num) || num == -double.infinity || num == double.infinity)
465             dst.put("null");
466         else dst.formattedWrite("%.16g", num);
467     }
468 }
469 
470 private void writeNumber(R)(ref R dst, long num) @trusted
471 {
472     import std.format;
473     dst.formattedWrite("%d", num);
474 }
475 
476 private void writeNumber(R)(ref R dst, BigInt num) @trusted
477 {
478     () @trusted { num.toString(str => dst.put(str), null); } ();
479 }
480 
481 @safe unittest
482 {
483     import std.math;
484     import std..string;
485 
486     auto num = toJSONValue("-67.199307");
487     auto exp = -67.199307;
488     assert(num.get!double.isClose(exp));
489 
490     auto snum = appender!string;
491     snum.writeNumber!(GeneratorOptions.init)(JSONNumber(num.get!double));
492     auto pnum = toJSONValue(snum.data);
493     assert(pnum.get!double.isClose(num.get!double));
494 }
495 
496 @safe unittest // special float values
497 {
498     static void test(GeneratorOptions options = GeneratorOptions.init)(double val, string expected)
499     {
500         auto dst = appender!string;
501         dst.writeNumber!options(val);
502         assert(dst.data == expected);
503     }
504 
505     test(double.nan, "null");
506     test(double.infinity, "null");
507     test(-double.infinity, "null");
508     test!(GeneratorOptions.specialFloatLiterals)(double.nan, "NaN");
509     test!(GeneratorOptions.specialFloatLiterals)(double.infinity, "Infinity");
510     test!(GeneratorOptions.specialFloatLiterals)(-double.infinity, "-Infinity");
511 }
512 
513 public void escapeString(bool use_surrogates = false, R)(ref R dst, string s)
514 {
515     import std.format;
516     import std.utf : decode;
517 
518     for (size_t pos = 0; pos < s.length; pos++)
519     {
520         immutable ch = s[pos];
521 
522         switch (ch)
523         {
524             case '\\': dst.put(`\\`); break;
525             case '\b': dst.put(`\b`); break;
526             case '\f': dst.put(`\f`); break;
527             case '\r': dst.put(`\r`); break;
528             case '\n': dst.put(`\n`); break;
529             case '\t': dst.put(`\t`); break;
530             case '\"': dst.put(`\"`); break;
531             default:
532                 static if (use_surrogates)
533                 {
534                     // output non-control char ASCII characters directly
535                     // note that 0x7F is the DEL control charactor
536                     if (ch >= 0x20 && ch < 0x7F)
537                     {
538                         dst.put(ch);
539                         break;
540                     }
541 
542                     dchar cp = decode(s, pos);
543                     pos--; // account for the next loop increment
544 
545                     // encode as one or two UTF-16 code points
546                     if (cp < 0x10000)
547                     { // in BMP -> 1 CP
548                         formattedWrite(dst, "\\u%04X", cp);
549                     }
550                     else
551                     { // not in BMP -> surrogate pair
552                         int first, last;
553                         cp -= 0x10000;
554                         first = 0xD800 | ((cp & 0xffc00) >> 10);
555                         last = 0xDC00 | (cp & 0x003ff);
556                         formattedWrite(dst, "\\u%04X\\u%04X", first, last);
557                     }
558                 }
559                 else
560                 {
561                     if (ch < 0x20 && ch != 0x7F) formattedWrite(dst, "\\u%04X", ch);
562                     else dst.put(ch);
563                 }
564                 break;
565         }
566     }
567 }
568 
569 @safe unittest
570 {
571     static void test(bool surrog)(string str, string expected)
572     {
573         auto res = appender!string;
574         res.escapeString!surrog(str);
575         assert(res.data == expected, res.data);
576     }
577 
578     test!false("hello", "hello");
579     test!false("hällo", "hällo");
580     test!false("a\U00010000b", "a\U00010000b");
581     test!false("a\u1234b", "a\u1234b");
582     test!false("\r\n\b\f\t\\\"", `\r\n\b\f\t\\\"`);
583     test!true("hello", "hello");
584     test!true("hällo", `h\u00E4llo`);
585     test!true("a\U00010000b", `a\uD800\uDC00b`);
586     test!true("a\u1234b", `a\u1234b`);
587     test!true("\r\n\b\f\t\\\"", `\r\n\b\f\t\\\"`);
588 }