]> git.ipfire.org Git - thirdparty/gcc.git/blame - libphobos/src/std/json.d
d: Import dmd b8384668f, druntime e6caaab9, phobos 5ab9ad256 (v2.098.0-beta.1)
[thirdparty/gcc.git] / libphobos / src / std / json.d
CommitLineData
b4c522fa
IB
1// Written in the D programming language.
2
3/**
4JavaScript Object Notation
5
6Copyright: Copyright Jeremie Pelletier 2008 - 2009.
7License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
8Authors: Jeremie Pelletier, David Herberth
5fee5ec3
IB
9References: $(LINK http://json.org/), $(LINK http://seriot.ch/parsing_json.html)
10Source: $(PHOBOSSRC std/json.d)
b4c522fa
IB
11*/
12/*
13 Copyright Jeremie Pelletier 2008 - 2009.
14Distributed under the Boost Software License, Version 1.0.
15 (See accompanying file LICENSE_1_0.txt or copy at
16 http://www.boost.org/LICENSE_1_0.txt)
17*/
18module std.json;
19
20import std.array;
21import std.conv;
22import std.range.primitives;
23import std.traits;
24
25///
26@system unittest
27{
28 import std.conv : to;
29
30 // parse a file or string of json into a usable structure
31 string s = `{ "language": "D", "rating": 3.5, "code": "42" }`;
32 JSONValue j = parseJSON(s);
33 // j and j["language"] return JSONValue,
34 // j["language"].str returns a string
35 assert(j["language"].str == "D");
36 assert(j["rating"].floating == 3.5);
37
38 // check a type
39 long x;
40 if (const(JSONValue)* code = "code" in j)
41 {
5fee5ec3 42 if (code.type() == JSONType.integer)
b4c522fa
IB
43 x = code.integer;
44 else
45 x = to!int(code.str);
46 }
47
48 // create a json struct
49 JSONValue jj = [ "language": "D" ];
50 // rating doesnt exist yet, so use .object to assign
51 jj.object["rating"] = JSONValue(3.5);
52 // create an array to assign to list
53 jj.object["list"] = JSONValue( ["a", "b", "c"] );
54 // list already exists, so .object optional
55 jj["list"].array ~= JSONValue("D");
56
57 string jjStr = `{"language":"D","list":["a","b","c","D"],"rating":3.5}`;
58 assert(jj.toString == jjStr);
59}
60
61/**
62String literals used to represent special float values within JSON strings.
63*/
64enum JSONFloatLiteral : string
65{
66 nan = "NaN", /// string representation of floating-point NaN
67 inf = "Infinite", /// string representation of floating-point Infinity
68 negativeInf = "-Infinite", /// string representation of floating-point negative Infinity
69}
70
71/**
72Flags that control how json is encoded and parsed.
73*/
74enum JSONOptions
75{
76 none, /// standard parsing
77 specialFloatLiterals = 0x1, /// encode NaN and Inf float values as strings
78 escapeNonAsciiChars = 0x2, /// encode non ascii characters with an unicode escape sequence
79 doNotEscapeSlashes = 0x4, /// do not escape slashes ('/')
5fee5ec3 80 strictParsing = 0x8, /// Strictly follow RFC-8259 grammar when parsing
b4c522fa
IB
81}
82
83/**
84JSON type enumeration
85*/
5fee5ec3 86enum JSONType : byte
b4c522fa 87{
5fee5ec3
IB
88 /// Indicates the type of a `JSONValue`.
89 null_,
90 string, /// ditto
91 integer, /// ditto
92 uinteger, /// ditto
93 float_, /// ditto
94 array, /// ditto
95 object, /// ditto
96 true_, /// ditto
97 false_, /// ditto
98 // FIXME: Find some way to deprecate the enum members below, which does NOT
99 // create lots of spam-like deprecation warnings, which can't be fixed
100 // by the user. See discussion on this issue at
101 // https://forum.dlang.org/post/feudrhtxkaxxscwhhhff@forum.dlang.org
102 /* deprecated("Use .null_") */ NULL = null_,
103 /* deprecated("Use .string") */ STRING = string,
104 /* deprecated("Use .integer") */ INTEGER = integer,
105 /* deprecated("Use .uinteger") */ UINTEGER = uinteger,
106 /* deprecated("Use .float_") */ FLOAT = float_,
107 /* deprecated("Use .array") */ ARRAY = array,
108 /* deprecated("Use .object") */ OBJECT = object,
109 /* deprecated("Use .true_") */ TRUE = true_,
110 /* deprecated("Use .false_") */ FALSE = false_,
b4c522fa
IB
111}
112
5fee5ec3
IB
113deprecated("Use JSONType and the new enum member names") alias JSON_TYPE = JSONType;
114
b4c522fa
IB
115/**
116JSON value node
117*/
118struct JSONValue
119{
5fee5ec3 120 import std.exception : enforce;
b4c522fa
IB
121
122 union Store
123 {
124 string str;
125 long integer;
126 ulong uinteger;
127 double floating;
128 JSONValue[string] object;
129 JSONValue[] array;
130 }
131 private Store store;
5fee5ec3 132 private JSONType type_tag;
b4c522fa
IB
133
134 /**
5fee5ec3 135 Returns the JSONType of the value stored in this structure.
b4c522fa 136 */
5fee5ec3 137 @property JSONType type() const pure nothrow @safe @nogc
b4c522fa
IB
138 {
139 return type_tag;
140 }
141 ///
142 @safe unittest
143 {
144 string s = "{ \"language\": \"D\" }";
145 JSONValue j = parseJSON(s);
5fee5ec3
IB
146 assert(j.type == JSONType.object);
147 assert(j["language"].type == JSONType.string);
b4c522fa
IB
148 }
149
150 /***
5fee5ec3
IB
151 * Value getter/setter for `JSONType.string`.
152 * Throws: `JSONException` for read access if `type` is not
153 * `JSONType.string`.
b4c522fa 154 */
5fee5ec3 155 @property string str() const pure @trusted return scope
b4c522fa 156 {
5fee5ec3 157 enforce!JSONException(type == JSONType.string,
b4c522fa
IB
158 "JSONValue is not a string");
159 return store.str;
160 }
161 /// ditto
5fee5ec3 162 @property string str(return string v) pure nothrow @nogc @trusted return // TODO make @safe
b4c522fa
IB
163 {
164 assign(v);
165 return v;
166 }
167 ///
168 @safe unittest
169 {
170 JSONValue j = [ "language": "D" ];
171
172 // get value
173 assert(j["language"].str == "D");
174
175 // change existing key to new string
176 j["language"].str = "Perl";
177 assert(j["language"].str == "Perl");
178 }
179
180 /***
5fee5ec3
IB
181 * Value getter/setter for `JSONType.integer`.
182 * Throws: `JSONException` for read access if `type` is not
183 * `JSONType.integer`.
b4c522fa 184 */
5fee5ec3 185 @property long integer() const pure @safe
b4c522fa 186 {
5fee5ec3 187 enforce!JSONException(type == JSONType.integer,
b4c522fa
IB
188 "JSONValue is not an integer");
189 return store.integer;
190 }
191 /// ditto
192 @property long integer(long v) pure nothrow @safe @nogc
193 {
194 assign(v);
195 return store.integer;
196 }
197
198 /***
5fee5ec3
IB
199 * Value getter/setter for `JSONType.uinteger`.
200 * Throws: `JSONException` for read access if `type` is not
201 * `JSONType.uinteger`.
b4c522fa 202 */
5fee5ec3 203 @property ulong uinteger() const pure @safe
b4c522fa 204 {
5fee5ec3 205 enforce!JSONException(type == JSONType.uinteger,
b4c522fa
IB
206 "JSONValue is not an unsigned integer");
207 return store.uinteger;
208 }
209 /// ditto
210 @property ulong uinteger(ulong v) pure nothrow @safe @nogc
211 {
212 assign(v);
213 return store.uinteger;
214 }
215
216 /***
5fee5ec3 217 * Value getter/setter for `JSONType.float_`. Note that despite
b4c522fa 218 * the name, this is a $(B 64)-bit `double`, not a 32-bit `float`.
5fee5ec3
IB
219 * Throws: `JSONException` for read access if `type` is not
220 * `JSONType.float_`.
b4c522fa 221 */
5fee5ec3 222 @property double floating() const pure @safe
b4c522fa 223 {
5fee5ec3 224 enforce!JSONException(type == JSONType.float_,
b4c522fa
IB
225 "JSONValue is not a floating type");
226 return store.floating;
227 }
228 /// ditto
229 @property double floating(double v) pure nothrow @safe @nogc
230 {
231 assign(v);
232 return store.floating;
233 }
234
235 /***
5fee5ec3
IB
236 * Value getter/setter for boolean stored in JSON.
237 * Throws: `JSONException` for read access if `this.type` is not
238 * `JSONType.true_` or `JSONType.false_`.
239 */
240 @property bool boolean() const pure @safe
241 {
242 if (type == JSONType.true_) return true;
243 if (type == JSONType.false_) return false;
244
245 throw new JSONException("JSONValue is not a boolean type");
246 }
247 /// ditto
248 @property bool boolean(bool v) pure nothrow @safe @nogc
249 {
250 assign(v);
251 return v;
252 }
253 ///
254 @safe unittest
255 {
256 JSONValue j = true;
257 assert(j.boolean == true);
258
259 j.boolean = false;
260 assert(j.boolean == false);
261
262 j.integer = 12;
263 import std.exception : assertThrown;
264 assertThrown!JSONException(j.boolean);
265 }
266
267 /***
268 * Value getter/setter for `JSONType.object`.
269 * Throws: `JSONException` for read access if `type` is not
270 * `JSONType.object`.
b4c522fa
IB
271 * Note: this is @system because of the following pattern:
272 ---
273 auto a = &(json.object());
274 json.uinteger = 0; // overwrite AA pointer
275 (*a)["hello"] = "world"; // segmentation fault
276 ---
277 */
5fee5ec3 278 @property ref inout(JSONValue[string]) object() inout pure @system return
b4c522fa 279 {
5fee5ec3 280 enforce!JSONException(type == JSONType.object,
b4c522fa
IB
281 "JSONValue is not an object");
282 return store.object;
283 }
284 /// ditto
5fee5ec3 285 @property JSONValue[string] object(return JSONValue[string] v) pure nothrow @nogc @trusted // TODO make @safe
b4c522fa
IB
286 {
287 assign(v);
288 return v;
289 }
290
291 /***
5fee5ec3
IB
292 * Value getter for `JSONType.object`.
293 * Unlike `object`, this retrieves the object by value and can be used in @safe code.
b4c522fa
IB
294 *
295 * A caveat is that, if the returned value is null, modifications will not be visible:
296 * ---
297 * JSONValue json;
298 * json.object = null;
299 * json.objectNoRef["hello"] = JSONValue("world");
300 * assert("hello" !in json.object);
301 * ---
302 *
5fee5ec3
IB
303 * Throws: `JSONException` for read access if `type` is not
304 * `JSONType.object`.
b4c522fa
IB
305 */
306 @property inout(JSONValue[string]) objectNoRef() inout pure @trusted
307 {
5fee5ec3 308 enforce!JSONException(type == JSONType.object,
b4c522fa
IB
309 "JSONValue is not an object");
310 return store.object;
311 }
312
313 /***
5fee5ec3
IB
314 * Value getter/setter for `JSONType.array`.
315 * Throws: `JSONException` for read access if `type` is not
316 * `JSONType.array`.
b4c522fa
IB
317 * Note: this is @system because of the following pattern:
318 ---
319 auto a = &(json.array());
320 json.uinteger = 0; // overwrite array pointer
321 (*a)[0] = "world"; // segmentation fault
322 ---
323 */
324 @property ref inout(JSONValue[]) array() inout pure @system
325 {
5fee5ec3 326 enforce!JSONException(type == JSONType.array,
b4c522fa
IB
327 "JSONValue is not an array");
328 return store.array;
329 }
330 /// ditto
5fee5ec3 331 @property JSONValue[] array(return JSONValue[] v) pure nothrow @nogc @trusted scope // TODO make @safe
b4c522fa
IB
332 {
333 assign(v);
334 return v;
335 }
336
337 /***
5fee5ec3
IB
338 * Value getter for `JSONType.array`.
339 * Unlike `array`, this retrieves the array by value and can be used in @safe code.
b4c522fa
IB
340 *
341 * A caveat is that, if you append to the returned array, the new values aren't visible in the
342 * JSONValue:
343 * ---
344 * JSONValue json;
345 * json.array = [JSONValue("hello")];
346 * json.arrayNoRef ~= JSONValue("world");
347 * assert(json.array.length == 1);
348 * ---
349 *
5fee5ec3
IB
350 * Throws: `JSONException` for read access if `type` is not
351 * `JSONType.array`.
b4c522fa
IB
352 */
353 @property inout(JSONValue[]) arrayNoRef() inout pure @trusted
354 {
5fee5ec3 355 enforce!JSONException(type == JSONType.array,
b4c522fa
IB
356 "JSONValue is not an array");
357 return store.array;
358 }
359
5fee5ec3 360 /// Test whether the type is `JSONType.null_`
b4c522fa
IB
361 @property bool isNull() const pure nothrow @safe @nogc
362 {
5fee5ec3 363 return type == JSONType.null_;
b4c522fa
IB
364 }
365
5fee5ec3
IB
366 /***
367 * Generic type value getter
368 * A convenience getter that returns this `JSONValue` as the specified D type.
369 * Note: only numeric, `bool`, `string`, `JSONValue[string]` and `JSONValue[]` types are accepted
370 * Throws: `JSONException` if `T` cannot hold the contents of this `JSONValue`
371 * `ConvException` in case of integer overflow when converting to `T`
372 */
373 @property inout(T) get(T)() inout const pure @safe
374 {
375 static if (is(immutable T == immutable string))
376 {
377 return str;
378 }
379 else static if (is(immutable T == immutable bool))
380 {
381 return boolean;
382 }
383 else static if (isFloatingPoint!T)
384 {
385 switch (type)
386 {
387 case JSONType.float_:
388 return cast(T) floating;
389 case JSONType.uinteger:
390 return cast(T) uinteger;
391 case JSONType.integer:
392 return cast(T) integer;
393 default:
394 throw new JSONException("JSONValue is not a number type");
395 }
396 }
397 else static if (isIntegral!T)
398 {
399 switch (type)
400 {
401 case JSONType.uinteger:
402 return uinteger.to!T;
403 case JSONType.integer:
404 return integer.to!T;
405 default:
406 throw new JSONException("JSONValue is not a an integral type");
407 }
408 }
409 else
410 {
411 static assert(false, "Unsupported type");
412 }
413 }
414 // This specialization is needed because arrayNoRef requires inout
415 @property inout(T) get(T : JSONValue[])() inout pure @trusted /// ditto
416 {
417 return arrayNoRef;
418 }
419 /// ditto
420 @property inout(T) get(T : JSONValue[string])() inout pure @trusted
421 {
422 return object;
423 }
424 ///
425 @safe unittest
426 {
427 import std.exception;
428 import std.conv;
429 string s =
430 `{
431 "a": 123,
432 "b": 3.1415,
433 "c": "text",
434 "d": true,
435 "e": [1, 2, 3],
436 "f": { "a": 1 },
437 "g": -45,
438 "h": ` ~ ulong.max.to!string ~ `,
439 }`;
440
441 struct a { }
442
443 immutable json = parseJSON(s);
444 assert(json["a"].get!double == 123.0);
445 assert(json["a"].get!int == 123);
446 assert(json["a"].get!uint == 123);
447 assert(json["b"].get!double == 3.1415);
448 assertThrown!JSONException(json["b"].get!int);
449 assert(json["c"].get!string == "text");
450 assert(json["d"].get!bool == true);
451 assertNotThrown(json["e"].get!(JSONValue[]));
452 assertNotThrown(json["f"].get!(JSONValue[string]));
453 static assert(!__traits(compiles, json["a"].get!a));
454 assertThrown!JSONException(json["e"].get!float);
455 assertThrown!JSONException(json["d"].get!(JSONValue[string]));
456 assertThrown!JSONException(json["f"].get!(JSONValue[]));
457 assert(json["g"].get!int == -45);
458 assertThrown!ConvException(json["g"].get!uint);
459 assert(json["h"].get!ulong == ulong.max);
460 assertThrown!ConvException(json["h"].get!uint);
461 assertNotThrown(json["h"].get!float);
462 }
463
464 private void assign(T)(T arg)
b4c522fa
IB
465 {
466 static if (is(T : typeof(null)))
467 {
5fee5ec3 468 type_tag = JSONType.null_;
b4c522fa
IB
469 }
470 else static if (is(T : string))
471 {
5fee5ec3 472 type_tag = JSONType.string;
b4c522fa
IB
473 string t = arg;
474 () @trusted { store.str = t; }();
475 }
5fee5ec3
IB
476 // https://issues.dlang.org/show_bug.cgi?id=15884
477 else static if (isSomeString!T)
b4c522fa 478 {
5fee5ec3
IB
479 type_tag = JSONType.string;
480 // FIXME: std.Array.Array(Range) is not deduced as 'pure'
b4c522fa
IB
481 () @trusted {
482 import std.utf : byUTF;
483 store.str = cast(immutable)(arg.byUTF!char.array);
484 }();
485 }
486 else static if (is(T : bool))
487 {
5fee5ec3 488 type_tag = arg ? JSONType.true_ : JSONType.false_;
b4c522fa
IB
489 }
490 else static if (is(T : ulong) && isUnsigned!T)
491 {
5fee5ec3 492 type_tag = JSONType.uinteger;
b4c522fa
IB
493 store.uinteger = arg;
494 }
495 else static if (is(T : long))
496 {
5fee5ec3 497 type_tag = JSONType.integer;
b4c522fa
IB
498 store.integer = arg;
499 }
500 else static if (isFloatingPoint!T)
501 {
5fee5ec3 502 type_tag = JSONType.float_;
b4c522fa
IB
503 store.floating = arg;
504 }
505 else static if (is(T : Value[Key], Key, Value))
506 {
507 static assert(is(Key : string), "AA key must be string");
5fee5ec3 508 type_tag = JSONType.object;
b4c522fa
IB
509 static if (is(Value : JSONValue))
510 {
511 JSONValue[string] t = arg;
512 () @trusted { store.object = t; }();
513 }
514 else
515 {
516 JSONValue[string] aa;
517 foreach (key, value; arg)
518 aa[key] = JSONValue(value);
519 () @trusted { store.object = aa; }();
520 }
521 }
522 else static if (isArray!T)
523 {
5fee5ec3 524 type_tag = JSONType.array;
b4c522fa
IB
525 static if (is(ElementEncodingType!T : JSONValue))
526 {
527 JSONValue[] t = arg;
528 () @trusted { store.array = t; }();
529 }
530 else
531 {
532 JSONValue[] new_arg = new JSONValue[arg.length];
533 foreach (i, e; arg)
534 new_arg[i] = JSONValue(e);
535 () @trusted { store.array = new_arg; }();
536 }
537 }
538 else static if (is(T : JSONValue))
539 {
540 type_tag = arg.type;
541 store = arg.store;
542 }
543 else
544 {
545 static assert(false, text(`unable to convert type "`, T.stringof, `" to json`));
546 }
547 }
548
549 private void assignRef(T)(ref T arg) if (isStaticArray!T)
550 {
5fee5ec3 551 type_tag = JSONType.array;
b4c522fa
IB
552 static if (is(ElementEncodingType!T : JSONValue))
553 {
554 store.array = arg;
555 }
556 else
557 {
558 JSONValue[] new_arg = new JSONValue[arg.length];
559 foreach (i, e; arg)
560 new_arg[i] = JSONValue(e);
561 store.array = new_arg;
562 }
563 }
564
565 /**
5fee5ec3
IB
566 * Constructor for `JSONValue`. If `arg` is a `JSONValue`
567 * its value and type will be copied to the new `JSONValue`.
568 * Note that this is a shallow copy: if type is `JSONType.object`
569 * or `JSONType.array` then only the reference to the data will
b4c522fa 570 * be copied.
5fee5ec3
IB
571 * Otherwise, `arg` must be implicitly convertible to one of the
572 * following types: `typeof(null)`, `string`, `ulong`,
573 * `long`, `double`, an associative array `V[K]` for any `V`
574 * and `K` i.e. a JSON object, any array or `bool`. The type will
b4c522fa
IB
575 * be set accordingly.
576 */
577 this(T)(T arg) if (!isStaticArray!T)
578 {
579 assign(arg);
580 }
581 /// Ditto
582 this(T)(ref T arg) if (isStaticArray!T)
583 {
584 assignRef(arg);
585 }
586 /// Ditto
587 this(T : JSONValue)(inout T arg) inout
588 {
589 store = arg.store;
590 type_tag = arg.type;
591 }
592 ///
593 @safe unittest
594 {
595 JSONValue j = JSONValue( "a string" );
596 j = JSONValue(42);
597
598 j = JSONValue( [1, 2, 3] );
5fee5ec3 599 assert(j.type == JSONType.array);
b4c522fa
IB
600
601 j = JSONValue( ["language": "D"] );
5fee5ec3 602 assert(j.type == JSONType.object);
b4c522fa
IB
603 }
604
605 void opAssign(T)(T arg) if (!isStaticArray!T && !is(T : JSONValue))
606 {
607 assign(arg);
608 }
609
610 void opAssign(T)(ref T arg) if (isStaticArray!T)
611 {
612 assignRef(arg);
613 }
614
615 /***
616 * Array syntax for json arrays.
5fee5ec3 617 * Throws: `JSONException` if `type` is not `JSONType.array`.
b4c522fa
IB
618 */
619 ref inout(JSONValue) opIndex(size_t i) inout pure @safe
620 {
621 auto a = this.arrayNoRef;
5fee5ec3 622 enforce!JSONException(i < a.length,
b4c522fa
IB
623 "JSONValue array index is out of range");
624 return a[i];
625 }
626 ///
627 @safe unittest
628 {
629 JSONValue j = JSONValue( [42, 43, 44] );
630 assert( j[0].integer == 42 );
631 assert( j[1].integer == 43 );
632 }
633
634 /***
635 * Hash syntax for json objects.
5fee5ec3 636 * Throws: `JSONException` if `type` is not `JSONType.object`.
b4c522fa 637 */
5fee5ec3 638 ref inout(JSONValue) opIndex(return string k) inout pure @safe
b4c522fa
IB
639 {
640 auto o = this.objectNoRef;
641 return *enforce!JSONException(k in o,
642 "Key not found: " ~ k);
643 }
644 ///
645 @safe unittest
646 {
647 JSONValue j = JSONValue( ["language": "D"] );
648 assert( j["language"].str == "D" );
649 }
650
651 /***
5fee5ec3 652 * Operator sets `value` for element of JSON object by `key`.
b4c522fa
IB
653 *
654 * If JSON value is null, then operator initializes it with object and then
5fee5ec3 655 * sets `value` for it.
b4c522fa 656 *
5fee5ec3
IB
657 * Throws: `JSONException` if `type` is not `JSONType.object`
658 * or `JSONType.null_`.
b4c522fa 659 */
5fee5ec3 660 void opIndexAssign(T)(auto ref T value, string key)
b4c522fa 661 {
5fee5ec3 662 enforce!JSONException(type == JSONType.object || type == JSONType.null_,
b4c522fa
IB
663 "JSONValue must be object or null");
664 JSONValue[string] aa = null;
5fee5ec3 665 if (type == JSONType.object)
b4c522fa
IB
666 {
667 aa = this.objectNoRef;
668 }
669
670 aa[key] = value;
671 this.object = aa;
672 }
673 ///
674 @safe unittest
675 {
676 JSONValue j = JSONValue( ["language": "D"] );
677 j["language"].str = "Perl";
678 assert( j["language"].str == "Perl" );
679 }
680
5fee5ec3 681 void opIndexAssign(T)(T arg, size_t i)
b4c522fa
IB
682 {
683 auto a = this.arrayNoRef;
5fee5ec3 684 enforce!JSONException(i < a.length,
b4c522fa
IB
685 "JSONValue array index is out of range");
686 a[i] = arg;
687 this.array = a;
688 }
689 ///
690 @safe unittest
691 {
692 JSONValue j = JSONValue( ["Perl", "C"] );
693 j[1].str = "D";
694 assert( j[1].str == "D" );
695 }
696
5fee5ec3 697 JSONValue opBinary(string op : "~", T)(T arg)
b4c522fa
IB
698 {
699 auto a = this.arrayNoRef;
700 static if (isArray!T)
701 {
702 return JSONValue(a ~ JSONValue(arg).arrayNoRef);
703 }
704 else static if (is(T : JSONValue))
705 {
706 return JSONValue(a ~ arg.arrayNoRef);
707 }
708 else
709 {
710 static assert(false, "argument is not an array or a JSONValue array");
711 }
712 }
713
5fee5ec3 714 void opOpAssign(string op : "~", T)(T arg)
b4c522fa
IB
715 {
716 auto a = this.arrayNoRef;
717 static if (isArray!T)
718 {
719 a ~= JSONValue(arg).arrayNoRef;
720 }
721 else static if (is(T : JSONValue))
722 {
723 a ~= arg.arrayNoRef;
724 }
725 else
726 {
727 static assert(false, "argument is not an array or a JSONValue array");
728 }
729 this.array = a;
730 }
731
732 /**
5fee5ec3 733 * Support for the `in` operator.
b4c522fa
IB
734 *
735 * Tests wether a key can be found in an object.
736 *
737 * Returns:
5fee5ec3
IB
738 * when found, the `const(JSONValue)*` that matches to the key,
739 * otherwise `null`.
b4c522fa 740 *
5fee5ec3
IB
741 * Throws: `JSONException` if the right hand side argument `JSONType`
742 * is not `object`.
b4c522fa
IB
743 */
744 auto opBinaryRight(string op : "in")(string k) const @safe
745 {
746 return k in this.objectNoRef;
747 }
748 ///
749 @safe unittest
750 {
751 JSONValue j = [ "language": "D", "author": "walter" ];
752 string a = ("author" in j).str;
753 }
754
755 bool opEquals(const JSONValue rhs) const @nogc nothrow pure @safe
756 {
757 return opEquals(rhs);
758 }
759
760 bool opEquals(ref const JSONValue rhs) const @nogc nothrow pure @trusted
761 {
762 // Default doesn't work well since store is a union. Compare only
763 // what should be in store.
764 // This is @trusted to remain nogc, nothrow, fast, and usable from @safe code.
b4c522fa
IB
765
766 final switch (type_tag)
767 {
5fee5ec3
IB
768 case JSONType.integer:
769 switch (rhs.type_tag)
770 {
771 case JSONType.integer:
772 return store.integer == rhs.store.integer;
773 case JSONType.uinteger:
774 return store.integer == rhs.store.uinteger;
775 case JSONType.float_:
776 return store.integer == rhs.store.floating;
777 default:
778 return false;
779 }
780 case JSONType.uinteger:
781 switch (rhs.type_tag)
782 {
783 case JSONType.integer:
784 return store.uinteger == rhs.store.integer;
785 case JSONType.uinteger:
786 return store.uinteger == rhs.store.uinteger;
787 case JSONType.float_:
788 return store.uinteger == rhs.store.floating;
789 default:
790 return false;
791 }
792 case JSONType.float_:
793 switch (rhs.type_tag)
794 {
795 case JSONType.integer:
796 return store.floating == rhs.store.integer;
797 case JSONType.uinteger:
798 return store.floating == rhs.store.uinteger;
799 case JSONType.float_:
800 return store.floating == rhs.store.floating;
801 default:
802 return false;
803 }
804 case JSONType.string:
805 return type_tag == rhs.type_tag && store.str == rhs.store.str;
806 case JSONType.object:
807 return type_tag == rhs.type_tag && store.object == rhs.store.object;
808 case JSONType.array:
809 return type_tag == rhs.type_tag && store.array == rhs.store.array;
810 case JSONType.true_:
811 case JSONType.false_:
812 case JSONType.null_:
813 return type_tag == rhs.type_tag;
b4c522fa
IB
814 }
815 }
816
5fee5ec3
IB
817 ///
818 @safe unittest
819 {
820 assert(JSONValue(0u) == JSONValue(0));
821 assert(JSONValue(0u) == JSONValue(0.0));
822 assert(JSONValue(0) == JSONValue(0.0));
823 }
824
825 /// Implements the foreach `opApply` interface for json arrays.
b4c522fa
IB
826 int opApply(scope int delegate(size_t index, ref JSONValue) dg) @system
827 {
828 int result;
829
830 foreach (size_t index, ref value; array)
831 {
832 result = dg(index, value);
833 if (result)
834 break;
835 }
836
837 return result;
838 }
839
5fee5ec3 840 /// Implements the foreach `opApply` interface for json objects.
b4c522fa
IB
841 int opApply(scope int delegate(string key, ref JSONValue) dg) @system
842 {
5fee5ec3 843 enforce!JSONException(type == JSONType.object,
b4c522fa
IB
844 "JSONValue is not an object");
845 int result;
846
847 foreach (string key, ref value; object)
848 {
849 result = dg(key, value);
850 if (result)
851 break;
852 }
853
854 return result;
855 }
856
857 /***
5fee5ec3 858 * Implicitly calls `toJSON` on this JSONValue.
b4c522fa
IB
859 *
860 * $(I options) can be used to tweak the conversion behavior.
861 */
862 string toString(in JSONOptions options = JSONOptions.none) const @safe
863 {
864 return toJSON(this, false, options);
865 }
866
5fee5ec3
IB
867 ///
868 void toString(Out)(Out sink, in JSONOptions options = JSONOptions.none) const
869 {
870 toJSON(sink, this, false, options);
871 }
872
b4c522fa 873 /***
5fee5ec3 874 * Implicitly calls `toJSON` on this JSONValue, like `toString`, but
b4c522fa
IB
875 * also passes $(I true) as $(I pretty) argument.
876 *
877 * $(I options) can be used to tweak the conversion behavior
878 */
879 string toPrettyString(in JSONOptions options = JSONOptions.none) const @safe
880 {
881 return toJSON(this, true, options);
882 }
5fee5ec3
IB
883
884 ///
885 void toPrettyString(Out)(Out sink, in JSONOptions options = JSONOptions.none) const
886 {
887 toJSON(sink, this, true, options);
888 }
889}
890
891// https://issues.dlang.org/show_bug.cgi?id=20874
892@system unittest
893{
894 static struct MyCustomType
895 {
896 public string toString () const @system { return null; }
897 alias toString this;
898 }
899
900 static struct B
901 {
902 public JSONValue asJSON() const @system { return JSONValue.init; }
903 alias asJSON this;
904 }
905
906 if (false) // Just checking attributes
907 {
908 JSONValue json;
909 MyCustomType ilovedlang;
910 json = ilovedlang;
911 json["foo"] = ilovedlang;
912 auto s = ilovedlang in json;
913
914 B b;
915 json ~= b;
916 json ~ b;
917 }
b4c522fa
IB
918}
919
920/**
921Parses a serialized string and returns a tree of JSON values.
5fee5ec3
IB
922Throws: $(LREF JSONException) if string does not follow the JSON grammar or the depth exceeds the max depth,
923 $(LREF ConvException) if a number in the input cannot be represented by a native D type.
b4c522fa
IB
924Params:
925 json = json-formatted string to parse
926 maxDepth = maximum depth of nesting allowed, -1 disables depth checking
927 options = enable decoding string representations of NaN/Inf as float values
928*/
929JSONValue parseJSON(T)(T json, int maxDepth = -1, JSONOptions options = JSONOptions.none)
930if (isInputRange!T && !isInfinite!T && isSomeChar!(ElementEncodingType!T))
931{
5fee5ec3
IB
932 import std.ascii : isDigit, isHexDigit, toUpper, toLower;
933 import std.typecons : Nullable, Yes;
b4c522fa 934 JSONValue root;
5fee5ec3 935 root.type_tag = JSONType.null_;
b4c522fa
IB
936
937 // Avoid UTF decoding when possible, as it is unnecessary when
938 // processing JSON.
939 static if (is(T : const(char)[]))
940 alias Char = char;
941 else
942 alias Char = Unqual!(ElementType!T);
943
b4c522fa 944 int depth = -1;
5fee5ec3 945 Nullable!Char next;
b4c522fa 946 int line = 1, pos = 0;
5fee5ec3 947 immutable bool strict = (options & JSONOptions.strictParsing) != 0;
b4c522fa
IB
948
949 void error(string msg)
950 {
951 throw new JSONException(msg, line, pos);
952 }
953
5fee5ec3
IB
954 if (json.empty)
955 {
956 if (strict)
957 {
958 error("Empty JSON body");
959 }
960 return root;
961 }
962
963 bool isWhite(dchar c)
964 {
965 if (strict)
966 {
967 // RFC 7159 has a stricter definition of whitespace than general ASCII.
968 return c == ' ' || c == '\t' || c == '\n' || c == '\r';
969 }
970 import std.ascii : isWhite;
971 // Accept ASCII NUL as whitespace in non-strict mode.
972 return c == 0 || isWhite(c);
973 }
974
b4c522fa
IB
975 Char popChar()
976 {
977 if (json.empty) error("Unexpected end of data.");
978 static if (is(T : const(char)[]))
979 {
980 Char c = json[0];
981 json = json[1..$];
982 }
983 else
984 {
985 Char c = json.front;
986 json.popFront();
987 }
988
989 if (c == '\n')
990 {
991 line++;
992 pos = 0;
993 }
994 else
995 {
996 pos++;
997 }
998
999 return c;
1000 }
1001
1002 Char peekChar()
1003 {
5fee5ec3 1004 if (next.isNull)
b4c522fa
IB
1005 {
1006 if (json.empty) return '\0';
1007 next = popChar();
1008 }
5fee5ec3
IB
1009 return next.get;
1010 }
1011
1012 Nullable!Char peekCharNullable()
1013 {
1014 if (next.isNull && !json.empty)
1015 {
1016 next = popChar();
1017 }
b4c522fa
IB
1018 return next;
1019 }
1020
1021 void skipWhitespace()
1022 {
5fee5ec3
IB
1023 while (true)
1024 {
1025 auto c = peekCharNullable();
1026 if (c.isNull ||
1027 !isWhite(c.get))
1028 {
1029 return;
1030 }
1031 next.nullify();
1032 }
b4c522fa
IB
1033 }
1034
1035 Char getChar(bool SkipWhitespace = false)()
1036 {
1037 static if (SkipWhitespace) skipWhitespace();
1038
1039 Char c;
5fee5ec3 1040 if (!next.isNull)
b4c522fa 1041 {
5fee5ec3
IB
1042 c = next.get;
1043 next.nullify();
b4c522fa
IB
1044 }
1045 else
1046 c = popChar();
1047
1048 return c;
1049 }
1050
5fee5ec3 1051 void checkChar(bool SkipWhitespace = true)(char c, bool caseSensitive = true)
b4c522fa
IB
1052 {
1053 static if (SkipWhitespace) skipWhitespace();
1054 auto c2 = getChar();
5fee5ec3 1055 if (!caseSensitive) c2 = toLower(c2);
b4c522fa
IB
1056
1057 if (c2 != c) error(text("Found '", c2, "' when expecting '", c, "'."));
1058 }
1059
1060 bool testChar(bool SkipWhitespace = true, bool CaseSensitive = true)(char c)
1061 {
1062 static if (SkipWhitespace) skipWhitespace();
1063 auto c2 = peekChar();
1064 static if (!CaseSensitive) c2 = toLower(c2);
1065
1066 if (c2 != c) return false;
1067
1068 getChar();
1069 return true;
1070 }
1071
1072 wchar parseWChar()
1073 {
1074 wchar val = 0;
1075 foreach_reverse (i; 0 .. 4)
1076 {
1077 auto hex = toUpper(getChar());
1078 if (!isHexDigit(hex)) error("Expecting hex character");
1079 val += (isDigit(hex) ? hex - '0' : hex - ('A' - 10)) << (4 * i);
1080 }
1081 return val;
1082 }
1083
1084 string parseString()
1085 {
b4c522fa
IB
1086 import std.uni : isSurrogateHi, isSurrogateLo;
1087 import std.utf : encode, decode;
1088
1089 auto str = appender!string();
1090
1091 Next:
1092 switch (peekChar())
1093 {
1094 case '"':
1095 getChar();
1096 break;
1097
1098 case '\\':
1099 getChar();
1100 auto c = getChar();
1101 switch (c)
1102 {
1103 case '"': str.put('"'); break;
1104 case '\\': str.put('\\'); break;
1105 case '/': str.put('/'); break;
1106 case 'b': str.put('\b'); break;
1107 case 'f': str.put('\f'); break;
1108 case 'n': str.put('\n'); break;
1109 case 'r': str.put('\r'); break;
1110 case 't': str.put('\t'); break;
1111 case 'u':
1112 wchar wc = parseWChar();
1113 dchar val;
1114 // Non-BMP characters are escaped as a pair of
1115 // UTF-16 surrogate characters (see RFC 4627).
1116 if (isSurrogateHi(wc))
1117 {
1118 wchar[2] pair;
1119 pair[0] = wc;
1120 if (getChar() != '\\') error("Expected escaped low surrogate after escaped high surrogate");
1121 if (getChar() != 'u') error("Expected escaped low surrogate after escaped high surrogate");
1122 pair[1] = parseWChar();
1123 size_t index = 0;
1124 val = decode(pair[], index);
1125 if (index != 2) error("Invalid escaped surrogate pair");
1126 }
1127 else
1128 if (isSurrogateLo(wc))
1129 error(text("Unexpected low surrogate"));
1130 else
1131 val = wc;
1132
1133 char[4] buf;
1134 immutable len = encode!(Yes.useReplacementDchar)(buf, val);
1135 str.put(buf[0 .. len]);
1136 break;
1137
1138 default:
1139 error(text("Invalid escape sequence '\\", c, "'."));
1140 }
1141 goto Next;
1142
1143 default:
1144 // RFC 7159 states that control characters U+0000 through
1145 // U+001F must not appear unescaped in a JSON string.
5fee5ec3
IB
1146 // Note: std.ascii.isControl can't be used for this test
1147 // because it considers ASCII DEL (0x7f) to be a control
1148 // character but RFC 7159 does not.
1149 // Accept unescaped ASCII NULs in non-strict mode.
b4c522fa 1150 auto c = getChar();
5fee5ec3 1151 if (c < 0x20 && (strict || c != 0))
b4c522fa
IB
1152 error("Illegal control character.");
1153 str.put(c);
1154 goto Next;
1155 }
1156
1157 return str.data.length ? str.data : "";
1158 }
1159
1160 bool tryGetSpecialFloat(string str, out double val) {
1161 switch (str)
1162 {
1163 case JSONFloatLiteral.nan:
1164 val = double.nan;
1165 return true;
1166 case JSONFloatLiteral.inf:
1167 val = double.infinity;
1168 return true;
1169 case JSONFloatLiteral.negativeInf:
1170 val = -double.infinity;
1171 return true;
1172 default:
1173 return false;
1174 }
1175 }
1176
1177 void parseValue(ref JSONValue value)
1178 {
1179 depth++;
1180
1181 if (maxDepth != -1 && depth > maxDepth) error("Nesting too deep.");
1182
1183 auto c = getChar!true();
1184
1185 switch (c)
1186 {
1187 case '{':
1188 if (testChar('}'))
1189 {
1190 value.object = null;
1191 break;
1192 }
1193
1194 JSONValue[string] obj;
1195 do
1196 {
5fee5ec3
IB
1197 skipWhitespace();
1198 if (!strict && peekChar() == '}')
1199 {
1200 break;
1201 }
b4c522fa
IB
1202 checkChar('"');
1203 string name = parseString();
1204 checkChar(':');
1205 JSONValue member;
1206 parseValue(member);
1207 obj[name] = member;
1208 }
1209 while (testChar(','));
1210 value.object = obj;
1211
1212 checkChar('}');
1213 break;
1214
1215 case '[':
1216 if (testChar(']'))
1217 {
5fee5ec3 1218 value.type_tag = JSONType.array;
b4c522fa
IB
1219 break;
1220 }
1221
1222 JSONValue[] arr;
1223 do
1224 {
5fee5ec3
IB
1225 skipWhitespace();
1226 if (!strict && peekChar() == ']')
1227 {
1228 break;
1229 }
b4c522fa
IB
1230 JSONValue element;
1231 parseValue(element);
1232 arr ~= element;
1233 }
1234 while (testChar(','));
1235
1236 checkChar(']');
1237 value.array = arr;
1238 break;
1239
1240 case '"':
1241 auto str = parseString();
1242
1243 // if special float parsing is enabled, check if string represents NaN/Inf
1244 if ((options & JSONOptions.specialFloatLiterals) &&
1245 tryGetSpecialFloat(str, value.store.floating))
1246 {
1247 // found a special float, its value was placed in value.store.floating
5fee5ec3 1248 value.type_tag = JSONType.float_;
b4c522fa
IB
1249 break;
1250 }
1251
5fee5ec3 1252 value.assign(str);
b4c522fa
IB
1253 break;
1254
1255 case '0': .. case '9':
1256 case '-':
1257 auto number = appender!string();
1258 bool isFloat, isNegative;
1259
1260 void readInteger()
1261 {
1262 if (!isDigit(c)) error("Digit expected");
1263
1264 Next: number.put(c);
1265
1266 if (isDigit(peekChar()))
1267 {
1268 c = getChar();
1269 goto Next;
1270 }
1271 }
1272
1273 if (c == '-')
1274 {
1275 number.put('-');
1276 c = getChar();
1277 isNegative = true;
1278 }
1279
5fee5ec3
IB
1280 if (strict && c == '0')
1281 {
1282 number.put('0');
1283 if (isDigit(peekChar()))
1284 {
1285 error("Additional digits not allowed after initial zero digit");
1286 }
1287 }
1288 else
1289 {
1290 readInteger();
1291 }
b4c522fa
IB
1292
1293 if (testChar('.'))
1294 {
1295 isFloat = true;
1296 number.put('.');
1297 c = getChar();
1298 readInteger();
1299 }
1300 if (testChar!(false, false)('e'))
1301 {
1302 isFloat = true;
1303 number.put('e');
1304 if (testChar('+')) number.put('+');
1305 else if (testChar('-')) number.put('-');
1306 c = getChar();
1307 readInteger();
1308 }
1309
1310 string data = number.data;
1311 if (isFloat)
1312 {
5fee5ec3 1313 value.type_tag = JSONType.float_;
b4c522fa
IB
1314 value.store.floating = parse!double(data);
1315 }
1316 else
1317 {
1318 if (isNegative)
5fee5ec3 1319 {
b4c522fa 1320 value.store.integer = parse!long(data);
5fee5ec3
IB
1321 value.type_tag = JSONType.integer;
1322 }
b4c522fa 1323 else
5fee5ec3
IB
1324 {
1325 // only set the correct union member to not confuse CTFE
1326 ulong u = parse!ulong(data);
1327 if (u & (1UL << 63))
1328 {
1329 value.store.uinteger = u;
1330 value.type_tag = JSONType.uinteger;
1331 }
1332 else
1333 {
1334 value.store.integer = u;
1335 value.type_tag = JSONType.integer;
1336 }
1337 }
b4c522fa
IB
1338 }
1339 break;
1340
b4c522fa 1341 case 'T':
5fee5ec3
IB
1342 if (strict) goto default;
1343 goto case;
1344 case 't':
1345 value.type_tag = JSONType.true_;
1346 checkChar!false('r', strict);
1347 checkChar!false('u', strict);
1348 checkChar!false('e', strict);
b4c522fa
IB
1349 break;
1350
b4c522fa 1351 case 'F':
5fee5ec3
IB
1352 if (strict) goto default;
1353 goto case;
1354 case 'f':
1355 value.type_tag = JSONType.false_;
1356 checkChar!false('a', strict);
1357 checkChar!false('l', strict);
1358 checkChar!false('s', strict);
1359 checkChar!false('e', strict);
b4c522fa
IB
1360 break;
1361
b4c522fa 1362 case 'N':
5fee5ec3
IB
1363 if (strict) goto default;
1364 goto case;
1365 case 'n':
1366 value.type_tag = JSONType.null_;
1367 checkChar!false('u', strict);
1368 checkChar!false('l', strict);
1369 checkChar!false('l', strict);
b4c522fa
IB
1370 break;
1371
1372 default:
1373 error(text("Unexpected character '", c, "'."));
1374 }
1375
1376 depth--;
1377 }
1378
1379 parseValue(root);
5fee5ec3
IB
1380 if (strict)
1381 {
1382 skipWhitespace();
1383 if (!peekCharNullable().isNull) error("Trailing non-whitespace characters");
1384 }
b4c522fa
IB
1385 return root;
1386}
1387
1388@safe unittest
1389{
1390 enum issue15742objectOfObject = `{ "key1": { "key2": 1 }}`;
5fee5ec3 1391 static assert(parseJSON(issue15742objectOfObject).type == JSONType.object);
b4c522fa
IB
1392
1393 enum issue15742arrayOfArray = `[[1]]`;
5fee5ec3 1394 static assert(parseJSON(issue15742arrayOfArray).type == JSONType.array);
b4c522fa
IB
1395}
1396
1397@safe unittest
1398{
1399 // Ensure we can parse and use JSON from @safe code
1400 auto a = `{ "key1": { "key2": 1 }}`.parseJSON;
1401 assert(a["key1"]["key2"].integer == 1);
1402 assert(a.toString == `{"key1":{"key2":1}}`);
1403}
1404
1405@system unittest
1406{
1407 // Ensure we can parse JSON from a @system range.
1408 struct Range
1409 {
1410 string s;
1411 size_t index;
1412 @system
1413 {
1414 bool empty() { return index >= s.length; }
1415 void popFront() { index++; }
1416 char front() { return s[index]; }
1417 }
1418 }
1419 auto s = Range(`{ "key1": { "key2": 1 }}`);
1420 auto json = parseJSON(s);
1421 assert(json["key1"]["key2"].integer == 1);
1422}
1423
5fee5ec3
IB
1424// https://issues.dlang.org/show_bug.cgi?id=20527
1425@safe unittest
1426{
1427 static assert(parseJSON(`{"a" : 2}`)["a"].integer == 2);
1428}
1429
b4c522fa
IB
1430/**
1431Parses a serialized string and returns a tree of JSON values.
5fee5ec3 1432Throws: $(LREF JSONException) if the depth exceeds the max depth.
b4c522fa
IB
1433Params:
1434 json = json-formatted string to parse
1435 options = enable decoding string representations of NaN/Inf as float values
1436*/
1437JSONValue parseJSON(T)(T json, JSONOptions options)
1438if (isInputRange!T && !isInfinite!T && isSomeChar!(ElementEncodingType!T))
1439{
1440 return parseJSON!T(json, -1, options);
1441}
1442
b4c522fa
IB
1443/**
1444Takes a tree of JSON values and returns the serialized string.
1445
1446Any Object types will be serialized in a key-sorted order.
1447
5fee5ec3
IB
1448If `pretty` is false no whitespaces are generated.
1449If `pretty` is true serialized string is formatted to be human-readable.
1450Set the $(LREF JSONOptions.specialFloatLiterals) flag is set in `options` to encode NaN/Infinity as strings.
b4c522fa
IB
1451*/
1452string toJSON(const ref JSONValue root, in bool pretty = false, in JSONOptions options = JSONOptions.none) @safe
1453{
1454 auto json = appender!string();
5fee5ec3
IB
1455 toJSON(json, root, pretty, options);
1456 return json.data;
1457}
b4c522fa 1458
5fee5ec3
IB
1459///
1460void toJSON(Out)(
1461 auto ref Out json,
1462 const ref JSONValue root,
1463 in bool pretty = false,
1464 in JSONOptions options = JSONOptions.none)
1465if (isOutputRange!(Out,char))
1466{
1467 void toStringImpl(Char)(string str)
b4c522fa
IB
1468 {
1469 json.put('"');
1470
1471 foreach (Char c; str)
1472 {
1473 switch (c)
1474 {
1475 case '"': json.put("\\\""); break;
1476 case '\\': json.put("\\\\"); break;
1477
1478 case '/':
1479 if (!(options & JSONOptions.doNotEscapeSlashes))
1480 json.put('\\');
1481 json.put('/');
1482 break;
1483
1484 case '\b': json.put("\\b"); break;
1485 case '\f': json.put("\\f"); break;
1486 case '\n': json.put("\\n"); break;
1487 case '\r': json.put("\\r"); break;
1488 case '\t': json.put("\\t"); break;
1489 default:
1490 {
1491 import std.ascii : isControl;
1492 import std.utf : encode;
1493
1494 // Make sure we do UTF decoding iff we want to
1495 // escape Unicode characters.
1496 assert(((options & JSONOptions.escapeNonAsciiChars) != 0)
5fee5ec3 1497 == is(Char == dchar), "JSONOptions.escapeNonAsciiChars needs dchar strings");
b4c522fa
IB
1498
1499 with (JSONOptions) if (isControl(c) ||
1500 ((options & escapeNonAsciiChars) >= escapeNonAsciiChars && c >= 0x80))
1501 {
1502 // Ensure non-BMP characters are encoded as a pair
1503 // of UTF-16 surrogate characters, as per RFC 4627.
1504 wchar[2] wchars; // 1 or 2 UTF-16 code units
1505 size_t wNum = encode(wchars, c); // number of UTF-16 code units
1506 foreach (wc; wchars[0 .. wNum])
1507 {
1508 json.put("\\u");
1509 foreach_reverse (i; 0 .. 4)
1510 {
1511 char ch = (wc >>> (4 * i)) & 0x0f;
1512 ch += ch < 10 ? '0' : 'A' - 10;
1513 json.put(ch);
1514 }
1515 }
1516 }
1517 else
1518 {
1519 json.put(c);
1520 }
1521 }
1522 }
1523 }
1524
1525 json.put('"');
1526 }
1527
5fee5ec3 1528 void toString(string str)
b4c522fa
IB
1529 {
1530 // Avoid UTF decoding when possible, as it is unnecessary when
1531 // processing JSON.
1532 if (options & JSONOptions.escapeNonAsciiChars)
1533 toStringImpl!dchar(str);
1534 else
1535 toStringImpl!char(str);
1536 }
1537
5fee5ec3
IB
1538 // recursive @safe inference is broken here
1539 // workaround: if json.put is @safe, we should be too,
1540 // so annotate the recursion as @safe manually
1541 static if (isSafe!({ json.put(""); }))
1542 {
1543 void delegate(ref const JSONValue, ulong) @safe toValue;
1544 }
1545 else
1546 {
1547 void delegate(ref const JSONValue, ulong) @system toValue;
1548 }
1549
1550 void toValueImpl(ref const JSONValue value, ulong indentLevel)
b4c522fa
IB
1551 {
1552 void putTabs(ulong additionalIndent = 0)
1553 {
1554 if (pretty)
1555 foreach (i; 0 .. indentLevel + additionalIndent)
1556 json.put(" ");
1557 }
1558 void putEOL()
1559 {
1560 if (pretty)
1561 json.put('\n');
1562 }
1563 void putCharAndEOL(char ch)
1564 {
1565 json.put(ch);
1566 putEOL();
1567 }
1568
1569 final switch (value.type)
1570 {
5fee5ec3 1571 case JSONType.object:
b4c522fa
IB
1572 auto obj = value.objectNoRef;
1573 if (!obj.length)
1574 {
1575 json.put("{}");
1576 }
1577 else
1578 {
1579 putCharAndEOL('{');
1580 bool first = true;
1581
1582 void emit(R)(R names)
1583 {
1584 foreach (name; names)
1585 {
1586 auto member = obj[name];
1587 if (!first)
1588 putCharAndEOL(',');
1589 first = false;
1590 putTabs(1);
1591 toString(name);
1592 json.put(':');
1593 if (pretty)
1594 json.put(' ');
1595 toValue(member, indentLevel + 1);
1596 }
1597 }
1598
1599 import std.algorithm.sorting : sort;
5fee5ec3 1600 // https://issues.dlang.org/show_bug.cgi?id=14439
b4c522fa
IB
1601 // auto names = obj.keys; // aa.keys can't be called in @safe code
1602 auto names = new string[obj.length];
1603 size_t i = 0;
1604 foreach (k, v; obj)
1605 {
1606 names[i] = k;
1607 i++;
1608 }
1609 sort(names);
1610 emit(names);
1611
1612 putEOL();
1613 putTabs();
1614 json.put('}');
1615 }
1616 break;
1617
5fee5ec3 1618 case JSONType.array:
b4c522fa
IB
1619 auto arr = value.arrayNoRef;
1620 if (arr.empty)
1621 {
1622 json.put("[]");
1623 }
1624 else
1625 {
1626 putCharAndEOL('[');
1627 foreach (i, el; arr)
1628 {
1629 if (i)
1630 putCharAndEOL(',');
1631 putTabs(1);
1632 toValue(el, indentLevel + 1);
1633 }
1634 putEOL();
1635 putTabs();
1636 json.put(']');
1637 }
1638 break;
1639
5fee5ec3 1640 case JSONType.string:
b4c522fa
IB
1641 toString(value.str);
1642 break;
1643
5fee5ec3 1644 case JSONType.integer:
b4c522fa
IB
1645 json.put(to!string(value.store.integer));
1646 break;
1647
5fee5ec3 1648 case JSONType.uinteger:
b4c522fa
IB
1649 json.put(to!string(value.store.uinteger));
1650 break;
1651
5fee5ec3
IB
1652 case JSONType.float_:
1653 import std.math.traits : isNaN, isInfinity;
b4c522fa
IB
1654
1655 auto val = value.store.floating;
1656
1657 if (val.isNaN)
1658 {
1659 if (options & JSONOptions.specialFloatLiterals)
1660 {
1661 toString(JSONFloatLiteral.nan);
1662 }
1663 else
1664 {
1665 throw new JSONException(
1666 "Cannot encode NaN. Consider passing the specialFloatLiterals flag.");
1667 }
1668 }
1669 else if (val.isInfinity)
1670 {
1671 if (options & JSONOptions.specialFloatLiterals)
1672 {
1673 toString((val > 0) ? JSONFloatLiteral.inf : JSONFloatLiteral.negativeInf);
1674 }
1675 else
1676 {
1677 throw new JSONException(
1678 "Cannot encode Infinity. Consider passing the specialFloatLiterals flag.");
1679 }
1680 }
1681 else
1682 {
5fee5ec3
IB
1683 import std.algorithm.searching : canFind;
1684 import std.format : sformat;
b4c522fa
IB
1685 // The correct formula for the number of decimal digits needed for lossless round
1686 // trips is actually:
1687 // ceil(log(pow(2.0, double.mant_dig - 1)) / log(10.0) + 1) == (double.dig + 2)
1688 // Anything less will round off (1 + double.epsilon)
5fee5ec3
IB
1689 char[25] buf;
1690 auto result = buf[].sformat!"%.18g"(val);
1691 json.put(result);
1692 if (!result.canFind('e') && !result.canFind('.'))
1693 json.put(".0");
b4c522fa
IB
1694 }
1695 break;
1696
5fee5ec3 1697 case JSONType.true_:
b4c522fa
IB
1698 json.put("true");
1699 break;
1700
5fee5ec3 1701 case JSONType.false_:
b4c522fa
IB
1702 json.put("false");
1703 break;
1704
5fee5ec3 1705 case JSONType.null_:
b4c522fa
IB
1706 json.put("null");
1707 break;
1708 }
1709 }
1710
5fee5ec3
IB
1711 toValue = &toValueImpl;
1712
b4c522fa 1713 toValue(root, 0);
b4c522fa
IB
1714}
1715
5fee5ec3
IB
1716 // https://issues.dlang.org/show_bug.cgi?id=12897
1717@safe unittest
b4c522fa
IB
1718{
1719 JSONValue jv0 = JSONValue("test测试");
1720 assert(toJSON(jv0, false, JSONOptions.escapeNonAsciiChars) == `"test\u6D4B\u8BD5"`);
1721 JSONValue jv00 = JSONValue("test\u6D4B\u8BD5");
1722 assert(toJSON(jv00, false, JSONOptions.none) == `"test测试"`);
1723 assert(toJSON(jv0, false, JSONOptions.none) == `"test测试"`);
1724 JSONValue jv1 = JSONValue("été");
1725 assert(toJSON(jv1, false, JSONOptions.escapeNonAsciiChars) == `"\u00E9t\u00E9"`);
1726 JSONValue jv11 = JSONValue("\u00E9t\u00E9");
1727 assert(toJSON(jv11, false, JSONOptions.none) == `"été"`);
1728 assert(toJSON(jv1, false, JSONOptions.none) == `"été"`);
1729}
1730
5fee5ec3
IB
1731// https://issues.dlang.org/show_bug.cgi?id=20511
1732@system unittest
1733{
1734 import std.format.write : formattedWrite;
1735 import std.range : nullSink, outputRangeObject;
1736
1737 outputRangeObject!(const(char)[])(nullSink)
1738 .formattedWrite!"%s"(JSONValue.init);
1739}
1740
1741// Issue 16432 - JSON incorrectly parses to string
1742@safe unittest
1743{
1744 // Floating points numbers are rounded to the nearest integer and thus get
1745 // incorrectly parsed
1746
1747 import std.math.operations : isClose;
1748
1749 string s = "{\"rating\": 3.0 }";
1750 JSONValue j = parseJSON(s);
1751 assert(j["rating"].type == JSONType.float_);
1752 j = j.toString.parseJSON;
1753 assert(j["rating"].type == JSONType.float_);
1754 assert(isClose(j["rating"].floating, 3.0));
1755
1756 s = "{\"rating\": -3.0 }";
1757 j = parseJSON(s);
1758 assert(j["rating"].type == JSONType.float_);
1759 j = j.toString.parseJSON;
1760 assert(j["rating"].type == JSONType.float_);
1761 assert(isClose(j["rating"].floating, -3.0));
1762
1763 // https://issues.dlang.org/show_bug.cgi?id=13660
1764 auto jv1 = JSONValue(4.0);
1765 auto textual = jv1.toString();
1766 auto jv2 = parseJSON(textual);
1767 assert(jv1.type == JSONType.float_);
1768 assert(textual == "4.0");
1769 assert(jv2.type == JSONType.float_);
1770}
1771
1772@safe unittest
1773{
1774 // Adapted from https://github.com/dlang/phobos/pull/5005
1775 // Result from toString is not checked here, because this
1776 // might differ (%e-like or %f-like output) depending
1777 // on OS and compiler optimization.
1778 import std.math.operations : isClose;
1779
1780 // test positive extreme values
1781 JSONValue j;
1782 j["rating"] = 1e18 - 65;
1783 assert(isClose(j.toString.parseJSON["rating"].floating, 1e18 - 65));
1784
1785 j["rating"] = 1e18 - 64;
1786 assert(isClose(j.toString.parseJSON["rating"].floating, 1e18 - 64));
1787
1788 // negative extreme values
1789 j["rating"] = -1e18 + 65;
1790 assert(isClose(j.toString.parseJSON["rating"].floating, -1e18 + 65));
1791
1792 j["rating"] = -1e18 + 64;
1793 assert(isClose(j.toString.parseJSON["rating"].floating, -1e18 + 64));
1794}
1795
b4c522fa
IB
1796/**
1797Exception thrown on JSON errors
1798*/
1799class JSONException : Exception
1800{
1801 this(string msg, int line = 0, int pos = 0) pure nothrow @safe
1802 {
1803 if (line)
1804 super(text(msg, " (Line ", line, ":", pos, ")"));
1805 else
1806 super(msg);
1807 }
1808
1809 this(string msg, string file, size_t line) pure nothrow @safe
1810 {
1811 super(msg, file, line);
1812 }
1813}
1814
1815
1816@system unittest
1817{
1818 import std.exception;
1819 JSONValue jv = "123";
5fee5ec3 1820 assert(jv.type == JSONType.string);
b4c522fa
IB
1821 assertNotThrown(jv.str);
1822 assertThrown!JSONException(jv.integer);
1823 assertThrown!JSONException(jv.uinteger);
1824 assertThrown!JSONException(jv.floating);
1825 assertThrown!JSONException(jv.object);
1826 assertThrown!JSONException(jv.array);
1827 assertThrown!JSONException(jv["aa"]);
1828 assertThrown!JSONException(jv[2]);
1829
1830 jv = -3;
5fee5ec3 1831 assert(jv.type == JSONType.integer);
b4c522fa
IB
1832 assertNotThrown(jv.integer);
1833
1834 jv = cast(uint) 3;
5fee5ec3 1835 assert(jv.type == JSONType.uinteger);
b4c522fa
IB
1836 assertNotThrown(jv.uinteger);
1837
1838 jv = 3.0;
5fee5ec3 1839 assert(jv.type == JSONType.float_);
b4c522fa
IB
1840 assertNotThrown(jv.floating);
1841
1842 jv = ["key" : "value"];
5fee5ec3 1843 assert(jv.type == JSONType.object);
b4c522fa
IB
1844 assertNotThrown(jv.object);
1845 assertNotThrown(jv["key"]);
1846 assert("key" in jv);
1847 assert("notAnElement" !in jv);
1848 assertThrown!JSONException(jv["notAnElement"]);
1849 const cjv = jv;
1850 assert("key" in cjv);
1851 assertThrown!JSONException(cjv["notAnElement"]);
1852
1853 foreach (string key, value; jv)
1854 {
1855 static assert(is(typeof(value) == JSONValue));
1856 assert(key == "key");
5fee5ec3 1857 assert(value.type == JSONType.string);
b4c522fa
IB
1858 assertNotThrown(value.str);
1859 assert(value.str == "value");
1860 }
1861
1862 jv = [3, 4, 5];
5fee5ec3 1863 assert(jv.type == JSONType.array);
b4c522fa
IB
1864 assertNotThrown(jv.array);
1865 assertNotThrown(jv[2]);
1866 foreach (size_t index, value; jv)
1867 {
1868 static assert(is(typeof(value) == JSONValue));
5fee5ec3 1869 assert(value.type == JSONType.integer);
b4c522fa
IB
1870 assertNotThrown(value.integer);
1871 assert(index == (value.integer-3));
1872 }
1873
1874 jv = null;
5fee5ec3 1875 assert(jv.type == JSONType.null_);
b4c522fa
IB
1876 assert(jv.isNull);
1877 jv = "foo";
1878 assert(!jv.isNull);
1879
1880 jv = JSONValue("value");
5fee5ec3 1881 assert(jv.type == JSONType.string);
b4c522fa
IB
1882 assert(jv.str == "value");
1883
1884 JSONValue jv2 = JSONValue("value");
5fee5ec3 1885 assert(jv2.type == JSONType.string);
b4c522fa
IB
1886 assert(jv2.str == "value");
1887
1888 JSONValue jv3 = JSONValue("\u001c");
5fee5ec3 1889 assert(jv3.type == JSONType.string);
b4c522fa
IB
1890 assert(jv3.str == "\u001C");
1891}
1892
5fee5ec3 1893// https://issues.dlang.org/show_bug.cgi?id=11504
b4c522fa
IB
1894@system unittest
1895{
b4c522fa 1896 JSONValue jv = 1;
5fee5ec3 1897 assert(jv.type == JSONType.integer);
b4c522fa
IB
1898
1899 jv.str = "123";
5fee5ec3 1900 assert(jv.type == JSONType.string);
b4c522fa
IB
1901 assert(jv.str == "123");
1902
1903 jv.integer = 1;
5fee5ec3 1904 assert(jv.type == JSONType.integer);
b4c522fa
IB
1905 assert(jv.integer == 1);
1906
1907 jv.uinteger = 2u;
5fee5ec3 1908 assert(jv.type == JSONType.uinteger);
b4c522fa
IB
1909 assert(jv.uinteger == 2u);
1910
1911 jv.floating = 1.5;
5fee5ec3 1912 assert(jv.type == JSONType.float_);
b4c522fa
IB
1913 assert(jv.floating == 1.5);
1914
1915 jv.object = ["key" : JSONValue("value")];
5fee5ec3 1916 assert(jv.type == JSONType.object);
b4c522fa
IB
1917 assert(jv.object == ["key" : JSONValue("value")]);
1918
1919 jv.array = [JSONValue(1), JSONValue(2), JSONValue(3)];
5fee5ec3 1920 assert(jv.type == JSONType.array);
b4c522fa
IB
1921 assert(jv.array == [JSONValue(1), JSONValue(2), JSONValue(3)]);
1922
1923 jv = true;
5fee5ec3 1924 assert(jv.type == JSONType.true_);
b4c522fa
IB
1925
1926 jv = false;
5fee5ec3 1927 assert(jv.type == JSONType.false_);
b4c522fa
IB
1928
1929 enum E{True = true}
1930 jv = E.True;
5fee5ec3 1931 assert(jv.type == JSONType.true_);
b4c522fa
IB
1932}
1933
1934@system pure unittest
1935{
1936 // Adding new json element via array() / object() directly
1937
1938 JSONValue jarr = JSONValue([10]);
1939 foreach (i; 0 .. 9)
1940 jarr.array ~= JSONValue(i);
1941 assert(jarr.array.length == 10);
1942
1943 JSONValue jobj = JSONValue(["key" : JSONValue("value")]);
1944 foreach (i; 0 .. 9)
1945 jobj.object[text("key", i)] = JSONValue(text("value", i));
1946 assert(jobj.object.length == 10);
1947}
1948
1949@system pure unittest
1950{
1951 // Adding new json element without array() / object() access
1952
1953 JSONValue jarr = JSONValue([10]);
1954 foreach (i; 0 .. 9)
1955 jarr ~= [JSONValue(i)];
1956 assert(jarr.array.length == 10);
1957
1958 JSONValue jobj = JSONValue(["key" : JSONValue("value")]);
1959 foreach (i; 0 .. 9)
1960 jobj[text("key", i)] = JSONValue(text("value", i));
1961 assert(jobj.object.length == 10);
1962
1963 // No array alias
1964 auto jarr2 = jarr ~ [1,2,3];
1965 jarr2[0] = 999;
1966 assert(jarr[0] == JSONValue(10));
1967}
1968
1969@system unittest
1970{
1971 // @system because JSONValue.array is @system
1972 import std.exception;
1973
1974 // An overly simple test suite, if it can parse a serializated string and
1975 // then use the resulting values tree to generate an identical
1976 // serialization, both the decoder and encoder works.
1977
1978 auto jsons = [
1979 `null`,
1980 `true`,
1981 `false`,
1982 `0`,
1983 `123`,
1984 `-4321`,
1985 `0.25`,
1986 `-0.25`,
1987 `""`,
1988 `"hello\nworld"`,
1989 `"\"\\\/\b\f\n\r\t"`,
1990 `[]`,
1991 `[12,"foo",true,false]`,
1992 `{}`,
1993 `{"a":1,"b":null}`,
1994 `{"goodbye":[true,"or",false,["test",42,{"nested":{"a":23.5,"b":0.140625}}]],`
1995 ~`"hello":{"array":[12,null,{}],"json":"is great"}}`,
1996 ];
1997
1998 enum dbl1_844 = `1.8446744073709568`;
1999 version (MinGW)
2000 jsons ~= dbl1_844 ~ `e+019`;
2001 else
2002 jsons ~= dbl1_844 ~ `e+19`;
2003
2004 JSONValue val;
2005 string result;
2006 foreach (json; jsons)
2007 {
2008 try
2009 {
2010 val = parseJSON(json);
2011 enum pretty = false;
2012 result = toJSON(val, pretty);
2013 assert(result == json, text(result, " should be ", json));
2014 }
2015 catch (JSONException e)
2016 {
2017 import std.stdio : writefln;
2018 writefln(text(json, "\n", e.toString()));
2019 }
2020 }
2021
2022 // Should be able to correctly interpret unicode entities
2023 val = parseJSON(`"\u003C\u003E"`);
2024 assert(toJSON(val) == "\"\&lt;\&gt;\"");
2025 assert(val.to!string() == "\"\&lt;\&gt;\"");
2026 val = parseJSON(`"\u0391\u0392\u0393"`);
2027 assert(toJSON(val) == "\"\&Alpha;\&Beta;\&Gamma;\"");
2028 assert(val.to!string() == "\"\&Alpha;\&Beta;\&Gamma;\"");
2029 val = parseJSON(`"\u2660\u2666"`);
2030 assert(toJSON(val) == "\"\&spades;\&diams;\"");
2031 assert(val.to!string() == "\"\&spades;\&diams;\"");
2032
2033 //0x7F is a control character (see Unicode spec)
2034 val = parseJSON(`"\u007F"`);
2035 assert(toJSON(val) == "\"\\u007F\"");
2036 assert(val.to!string() == "\"\\u007F\"");
2037
2038 with(parseJSON(`""`))
2039 assert(str == "" && str !is null);
2040 with(parseJSON(`[]`))
2041 assert(!array.length);
2042
2043 // Formatting
2044 val = parseJSON(`{"a":[null,{"x":1},{},[]]}`);
2045 assert(toJSON(val, true) == `{
2046 "a": [
2047 null,
2048 {
2049 "x": 1
2050 },
2051 {},
2052 []
2053 ]
2054}`);
2055}
2056
2057@safe unittest
2058{
2059 auto json = `"hello\nworld"`;
2060 const jv = parseJSON(json);
2061 assert(jv.toString == json);
2062 assert(jv.toPrettyString == json);
2063}
2064
2065@system pure unittest
2066{
5fee5ec3 2067 // https://issues.dlang.org/show_bug.cgi?id=12969
b4c522fa
IB
2068
2069 JSONValue jv;
2070 jv["int"] = 123;
2071
5fee5ec3 2072 assert(jv.type == JSONType.object);
b4c522fa
IB
2073 assert("int" in jv);
2074 assert(jv["int"].integer == 123);
2075
2076 jv["array"] = [1, 2, 3, 4, 5];
2077
5fee5ec3 2078 assert(jv["array"].type == JSONType.array);
b4c522fa
IB
2079 assert(jv["array"][2].integer == 3);
2080
2081 jv["str"] = "D language";
5fee5ec3 2082 assert(jv["str"].type == JSONType.string);
b4c522fa
IB
2083 assert(jv["str"].str == "D language");
2084
2085 jv["bool"] = false;
5fee5ec3 2086 assert(jv["bool"].type == JSONType.false_);
b4c522fa
IB
2087
2088 assert(jv.object.length == 4);
2089
2090 jv = [5, 4, 3, 2, 1];
5fee5ec3
IB
2091 assert(jv.type == JSONType.array);
2092 assert(jv[3].integer == 2);
b4c522fa
IB
2093}
2094
2095@safe unittest
2096{
2097 auto s = q"EOF
2098[
2099 1,
2100 2,
2101 3,
2102 potato
2103]
2104EOF";
2105
2106 import std.exception;
2107
2108 auto e = collectException!JSONException(parseJSON(s));
2109 assert(e.msg == "Unexpected character 'p'. (Line 5:3)", e.msg);
2110}
2111
2112// handling of special float values (NaN, Inf, -Inf)
2113@safe unittest
2114{
2115 import std.exception : assertThrown;
5fee5ec3 2116 import std.math.traits : isNaN, isInfinity;
b4c522fa
IB
2117
2118 // expected representations of NaN and Inf
2119 enum {
2120 nanString = '"' ~ JSONFloatLiteral.nan ~ '"',
2121 infString = '"' ~ JSONFloatLiteral.inf ~ '"',
2122 negativeInfString = '"' ~ JSONFloatLiteral.negativeInf ~ '"',
2123 }
2124
2125 // with the specialFloatLiterals option, encode NaN/Inf as strings
2126 assert(JSONValue(float.nan).toString(JSONOptions.specialFloatLiterals) == nanString);
2127 assert(JSONValue(double.infinity).toString(JSONOptions.specialFloatLiterals) == infString);
2128 assert(JSONValue(-real.infinity).toString(JSONOptions.specialFloatLiterals) == negativeInfString);
2129
2130 // without the specialFloatLiterals option, throw on encoding NaN/Inf
2131 assertThrown!JSONException(JSONValue(float.nan).toString);
2132 assertThrown!JSONException(JSONValue(double.infinity).toString);
2133 assertThrown!JSONException(JSONValue(-real.infinity).toString);
2134
2135 // when parsing json with specialFloatLiterals option, decode special strings as floats
2136 JSONValue jvNan = parseJSON(nanString, JSONOptions.specialFloatLiterals);
2137 JSONValue jvInf = parseJSON(infString, JSONOptions.specialFloatLiterals);
2138 JSONValue jvNegInf = parseJSON(negativeInfString, JSONOptions.specialFloatLiterals);
2139
2140 assert(jvNan.floating.isNaN);
2141 assert(jvInf.floating.isInfinity && jvInf.floating > 0);
2142 assert(jvNegInf.floating.isInfinity && jvNegInf.floating < 0);
2143
2144 // when parsing json without the specialFloatLiterals option, decode special strings as strings
2145 jvNan = parseJSON(nanString);
2146 jvInf = parseJSON(infString);
2147 jvNegInf = parseJSON(negativeInfString);
2148
2149 assert(jvNan.str == JSONFloatLiteral.nan);
2150 assert(jvInf.str == JSONFloatLiteral.inf);
2151 assert(jvNegInf.str == JSONFloatLiteral.negativeInf);
2152}
2153
2154pure nothrow @safe @nogc unittest
2155{
2156 JSONValue testVal;
2157 testVal = "test";
2158 testVal = 10;
2159 testVal = 10u;
2160 testVal = 1.0;
2161 testVal = (JSONValue[string]).init;
2162 testVal = JSONValue[].init;
2163 testVal = null;
2164 assert(testVal.isNull);
2165}
2166
5fee5ec3
IB
2167// https://issues.dlang.org/show_bug.cgi?id=15884
2168pure nothrow @safe unittest
b4c522fa
IB
2169{
2170 import std.typecons;
2171 void Test(C)() {
2172 C[] a = ['x'];
2173 JSONValue testVal = a;
5fee5ec3 2174 assert(testVal.type == JSONType.string);
b4c522fa 2175 testVal = a.idup;
5fee5ec3 2176 assert(testVal.type == JSONType.string);
b4c522fa
IB
2177 }
2178 Test!char();
2179 Test!wchar();
2180 Test!dchar();
2181}
2182
5fee5ec3
IB
2183// https://issues.dlang.org/show_bug.cgi?id=15885
2184@safe unittest
b4c522fa
IB
2185{
2186 enum bool realInDoublePrecision = real.mant_dig == double.mant_dig;
2187
2188 static bool test(const double num0)
2189 {
5fee5ec3 2190 import std.math.operations : feqrel;
b4c522fa
IB
2191 const json0 = JSONValue(num0);
2192 const num1 = to!double(toJSON(json0));
2193 static if (realInDoublePrecision)
2194 return feqrel(num1, num0) >= (double.mant_dig - 1);
2195 else
2196 return num1 == num0;
2197 }
2198
2199 assert(test( 0.23));
2200 assert(test(-0.23));
2201 assert(test(1.223e+24));
2202 assert(test(23.4));
2203 assert(test(0.0012));
2204 assert(test(30738.22));
2205
2206 assert(test(1 + double.epsilon));
2207 assert(test(double.min_normal));
2208 static if (realInDoublePrecision)
2209 assert(test(-double.max / 2));
2210 else
2211 assert(test(-double.max));
2212
2213 const minSub = double.min_normal * double.epsilon;
2214 assert(test(minSub));
2215 assert(test(3*minSub));
2216}
2217
5fee5ec3
IB
2218// https://issues.dlang.org/show_bug.cgi?id=17555
2219@safe unittest
b4c522fa
IB
2220{
2221 import std.exception : assertThrown;
2222
2223 assertThrown!JSONException(parseJSON("\"a\nb\""));
2224}
2225
5fee5ec3
IB
2226// https://issues.dlang.org/show_bug.cgi?id=17556
2227@safe unittest
b4c522fa
IB
2228{
2229 auto v = JSONValue("\U0001D11E");
2230 auto j = toJSON(v, false, JSONOptions.escapeNonAsciiChars);
2231 assert(j == `"\uD834\uDD1E"`);
2232}
2233
5fee5ec3
IB
2234// https://issues.dlang.org/show_bug.cgi?id=5904
2235@safe unittest
b4c522fa
IB
2236{
2237 string s = `"\uD834\uDD1E"`;
2238 auto j = parseJSON(s);
2239 assert(j.str == "\U0001D11E");
2240}
2241
5fee5ec3
IB
2242// https://issues.dlang.org/show_bug.cgi?id=17557
2243@safe unittest
b4c522fa
IB
2244{
2245 assert(parseJSON("\"\xFF\"").str == "\xFF");
2246 assert(parseJSON("\"\U0001D11E\"").str == "\U0001D11E");
2247}
2248
5fee5ec3
IB
2249// https://issues.dlang.org/show_bug.cgi?id=17553
2250@safe unittest
b4c522fa
IB
2251{
2252 auto v = JSONValue("\xFF");
2253 assert(toJSON(v) == "\"\xFF\"");
2254}
2255
2256@safe unittest
2257{
2258 import std.utf;
2259 assert(parseJSON("\"\xFF\"".byChar).str == "\xFF");
2260 assert(parseJSON("\"\U0001D11E\"".byChar).str == "\U0001D11E");
2261}
2262
5fee5ec3
IB
2263// JSONOptions.doNotEscapeSlashes (https://issues.dlang.org/show_bug.cgi?id=17587)
2264@safe unittest
b4c522fa
IB
2265{
2266 assert(parseJSON(`"/"`).toString == `"\/"`);
2267 assert(parseJSON(`"\/"`).toString == `"\/"`);
2268 assert(parseJSON(`"/"`).toString(JSONOptions.doNotEscapeSlashes) == `"/"`);
2269 assert(parseJSON(`"\/"`).toString(JSONOptions.doNotEscapeSlashes) == `"/"`);
2270}
5fee5ec3
IB
2271
2272// JSONOptions.strictParsing (https://issues.dlang.org/show_bug.cgi?id=16639)
2273@safe unittest
2274{
2275 import std.exception : assertThrown;
2276
2277 // Unescaped ASCII NULs
2278 assert(parseJSON("[\0]").type == JSONType.array);
2279 assertThrown!JSONException(parseJSON("[\0]", JSONOptions.strictParsing));
2280 assert(parseJSON("\"\0\"").str == "\0");
2281 assertThrown!JSONException(parseJSON("\"\0\"", JSONOptions.strictParsing));
2282
2283 // Unescaped ASCII DEL (0x7f) in strings
2284 assert(parseJSON("\"\x7f\"").str == "\x7f");
2285 assert(parseJSON("\"\x7f\"", JSONOptions.strictParsing).str == "\x7f");
2286
2287 // "true", "false", "null" case sensitivity
2288 assert(parseJSON("true").type == JSONType.true_);
2289 assert(parseJSON("true", JSONOptions.strictParsing).type == JSONType.true_);
2290 assert(parseJSON("True").type == JSONType.true_);
2291 assertThrown!JSONException(parseJSON("True", JSONOptions.strictParsing));
2292 assert(parseJSON("tRUE").type == JSONType.true_);
2293 assertThrown!JSONException(parseJSON("tRUE", JSONOptions.strictParsing));
2294
2295 assert(parseJSON("false").type == JSONType.false_);
2296 assert(parseJSON("false", JSONOptions.strictParsing).type == JSONType.false_);
2297 assert(parseJSON("False").type == JSONType.false_);
2298 assertThrown!JSONException(parseJSON("False", JSONOptions.strictParsing));
2299 assert(parseJSON("fALSE").type == JSONType.false_);
2300 assertThrown!JSONException(parseJSON("fALSE", JSONOptions.strictParsing));
2301
2302 assert(parseJSON("null").type == JSONType.null_);
2303 assert(parseJSON("null", JSONOptions.strictParsing).type == JSONType.null_);
2304 assert(parseJSON("Null").type == JSONType.null_);
2305 assertThrown!JSONException(parseJSON("Null", JSONOptions.strictParsing));
2306 assert(parseJSON("nULL").type == JSONType.null_);
2307 assertThrown!JSONException(parseJSON("nULL", JSONOptions.strictParsing));
2308
2309 // Whitespace characters
2310 assert(parseJSON("[\f\v]").type == JSONType.array);
2311 assertThrown!JSONException(parseJSON("[\f\v]", JSONOptions.strictParsing));
2312 assert(parseJSON("[ \t\r\n]").type == JSONType.array);
2313 assert(parseJSON("[ \t\r\n]", JSONOptions.strictParsing).type == JSONType.array);
2314
2315 // Empty input
2316 assert(parseJSON("").type == JSONType.null_);
2317 assertThrown!JSONException(parseJSON("", JSONOptions.strictParsing));
2318
2319 // Numbers with leading '0's
2320 assert(parseJSON("01").integer == 1);
2321 assertThrown!JSONException(parseJSON("01", JSONOptions.strictParsing));
2322 assert(parseJSON("-01").integer == -1);
2323 assertThrown!JSONException(parseJSON("-01", JSONOptions.strictParsing));
2324 assert(parseJSON("0.01").floating == 0.01);
2325 assert(parseJSON("0.01", JSONOptions.strictParsing).floating == 0.01);
2326 assert(parseJSON("0e1").floating == 0);
2327 assert(parseJSON("0e1", JSONOptions.strictParsing).floating == 0);
2328
2329 // Trailing characters after JSON value
2330 assert(parseJSON(`""asdf`).str == "");
2331 assertThrown!JSONException(parseJSON(`""asdf`, JSONOptions.strictParsing));
2332 assert(parseJSON("987\0").integer == 987);
2333 assertThrown!JSONException(parseJSON("987\0", JSONOptions.strictParsing));
2334 assert(parseJSON("987\0\0").integer == 987);
2335 assertThrown!JSONException(parseJSON("987\0\0", JSONOptions.strictParsing));
2336 assert(parseJSON("[]]").type == JSONType.array);
2337 assertThrown!JSONException(parseJSON("[]]", JSONOptions.strictParsing));
2338 assert(parseJSON("123 \t\r\n").integer == 123); // Trailing whitespace is OK
2339 assert(parseJSON("123 \t\r\n", JSONOptions.strictParsing).integer == 123);
2340}
2341
2342@system unittest
2343{
2344 import std.algorithm.iteration : map;
2345 import std.array : array;
2346 import std.exception : assertThrown;
2347
2348 string s = `{ "a" : [1,2,3,], }`;
2349 JSONValue j = parseJSON(s);
2350 assert(j["a"].array().map!(i => i.integer()).array == [1,2,3]);
2351
2352 assertThrown(parseJSON(s, -1, JSONOptions.strictParsing));
2353}
2354
2355@system unittest
2356{
2357 import std.algorithm.iteration : map;
2358 import std.array : array;
2359 import std.exception : assertThrown;
2360
2361 string s = `{ "a" : { } , }`;
2362 JSONValue j = parseJSON(s);
2363 assert("a" in j);
2364 auto t = j["a"].object();
2365 assert(t.empty);
2366
2367 assertThrown(parseJSON(s, -1, JSONOptions.strictParsing));
2368}
2369
2370// https://issues.dlang.org/show_bug.cgi?id=20330
2371@safe unittest
2372{
2373 import std.array : appender;
2374
2375 string s = `{"a":[1,2,3]}`;
2376 JSONValue j = parseJSON(s);
2377
2378 auto app = appender!string();
2379 j.toString(app);
2380
2381 assert(app.data == s, app.data);
2382}
2383
2384// https://issues.dlang.org/show_bug.cgi?id=20330
2385@safe unittest
2386{
2387 import std.array : appender;
2388 import std.format.write : formattedWrite;
2389
2390 string s =
2391`{
2392 "a": [
2393 1,
2394 2,
2395 3
2396 ]
2397}`;
2398 JSONValue j = parseJSON(s);
2399
2400 auto app = appender!string();
2401 j.toPrettyString(app);
2402
2403 assert(app.data == s, app.data);
2404}