]>
Commit | Line | Data |
---|---|---|
b4c522fa IB |
1 | // Written in the D programming language. |
2 | ||
3 | /** | |
4 | JavaScript Object Notation | |
5 | ||
6 | Copyright: Copyright Jeremie Pelletier 2008 - 2009. | |
7 | License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). | |
8 | Authors: Jeremie Pelletier, David Herberth | |
5fee5ec3 IB |
9 | References: $(LINK http://json.org/), $(LINK http://seriot.ch/parsing_json.html) |
10 | Source: $(PHOBOSSRC std/json.d) | |
b4c522fa IB |
11 | */ |
12 | /* | |
13 | Copyright Jeremie Pelletier 2008 - 2009. | |
14 | Distributed 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 | */ | |
18 | module std.json; | |
19 | ||
20 | import std.array; | |
21 | import std.conv; | |
22 | import std.range.primitives; | |
23 | import 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 | /** | |
62 | String literals used to represent special float values within JSON strings. | |
63 | */ | |
64 | enum 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 | /** | |
72 | Flags that control how json is encoded and parsed. | |
73 | */ | |
74 | enum 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 | /** | |
84 | JSON type enumeration | |
85 | */ | |
5fee5ec3 | 86 | enum 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 |
113 | deprecated("Use JSONType and the new enum member names") alias JSON_TYPE = JSONType; |
114 | ||
b4c522fa IB |
115 | /** |
116 | JSON value node | |
117 | */ | |
118 | struct 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 | /** | |
921 | Parses a serialized string and returns a tree of JSON values. | |
5fee5ec3 IB |
922 | Throws: $(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 |
924 | Params: |
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 | */ | |
929 | JSONValue parseJSON(T)(T json, int maxDepth = -1, JSONOptions options = JSONOptions.none) | |
930 | if (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 | /** |
1431 | Parses a serialized string and returns a tree of JSON values. | |
5fee5ec3 | 1432 | Throws: $(LREF JSONException) if the depth exceeds the max depth. |
b4c522fa IB |
1433 | Params: |
1434 | json = json-formatted string to parse | |
1435 | options = enable decoding string representations of NaN/Inf as float values | |
1436 | */ | |
1437 | JSONValue parseJSON(T)(T json, JSONOptions options) | |
1438 | if (isInputRange!T && !isInfinite!T && isSomeChar!(ElementEncodingType!T)) | |
1439 | { | |
1440 | return parseJSON!T(json, -1, options); | |
1441 | } | |
1442 | ||
b4c522fa IB |
1443 | /** |
1444 | Takes a tree of JSON values and returns the serialized string. | |
1445 | ||
1446 | Any Object types will be serialized in a key-sorted order. | |
1447 | ||
5fee5ec3 IB |
1448 | If `pretty` is false no whitespaces are generated. |
1449 | If `pretty` is true serialized string is formatted to be human-readable. | |
1450 | Set the $(LREF JSONOptions.specialFloatLiterals) flag is set in `options` to encode NaN/Infinity as strings. | |
b4c522fa IB |
1451 | */ |
1452 | string 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 | /// |
1460 | void toJSON(Out)( | |
1461 | auto ref Out json, | |
1462 | const ref JSONValue root, | |
1463 | in bool pretty = false, | |
1464 | in JSONOptions options = JSONOptions.none) | |
1465 | if (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 | /** |
1797 | Exception thrown on JSON errors | |
1798 | */ | |
1799 | class 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) == "\"\<\>\""); | |
2025 | assert(val.to!string() == "\"\<\>\""); | |
2026 | val = parseJSON(`"\u0391\u0392\u0393"`); | |
2027 | assert(toJSON(val) == "\"\Α\Β\Γ\""); | |
2028 | assert(val.to!string() == "\"\Α\Β\Γ\""); | |
2029 | val = parseJSON(`"\u2660\u2666"`); | |
2030 | assert(toJSON(val) == "\"\♠\♦\""); | |
2031 | assert(val.to!string() == "\"\♠\♦\""); | |
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 | ] | |
2104 | EOF"; | |
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 | ||
2154 | pure 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 |
2168 | pure 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 | } |