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 }