| // Written in the D programming language. | |
| /** | |
| Implements functionality to read and write JavaScript Object Notation values. | |
| JavaScript Object Notation is a lightweight data interchange format commonly used in web services and configuration files. | |
| It's easy for humans to read and write, and it's easy for machines to parse and generate. | |
| $(RED Warning: While $(LREF JSONValue) is fine for small-scale use, at the range of hundreds of megabytes it is | |
| known to cause and exacerbate GC problems. If you encounter problems, try replacing it with a stream parser. See | |
| also $(LINK https://forum.dlang.org/post/dzfyaxypmkdrpakmycjv@forum.dlang.org).) | |
| Copyright: Copyright Jeremie Pelletier 2008 - 2009. | |
| License: $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0). | |
| Authors: Jeremie Pelletier, David Herberth | |
| References: $(LINK http://json.org/), $(LINK https://seriot.ch/projects/parsing_json.html) | |
| Source: $(PHOBOSSRC std/json.d) | |
| */ | |
| /* | |
| Copyright Jeremie Pelletier 2008 - 2009. | |
| Distributed under the Boost Software License, Version 1.0. | |
| (See accompanying file LICENSE_1_0.txt or copy at | |
| http://www.boost.org/LICENSE_1_0.txt) | |
| */ | |
| module std.json; | |
| import std.array; | |
| import std.conv; | |
| import std.range; | |
| import std.traits; | |
| /// | |
| @system unittest | |
| { | |
| import std.conv : to; | |
| // parse a file or string of json into a usable structure | |
| string s = `{ "language": "D", "rating": 3.5, "code": "42" }`; | |
| JSONValue j = parseJSON(s); | |
| // j and j["language"] return JSONValue, | |
| // j["language"].str returns a string | |
| assert(j["language"].str == "D"); | |
| assert(j["rating"].floating == 3.5); | |
| // check a type | |
| long x; | |
| if (const(JSONValue)* code = "code" in j) | |
| { | |
| if (code.type() == JSONType.integer) | |
| x = code.integer; | |
| else | |
| x = to!int(code.str); | |
| } | |
| // create a json struct | |
| JSONValue jj = [ "language": "D" ]; | |
| // rating doesnt exist yet, so use .object to assign | |
| jj.object["rating"] = JSONValue(3.5); | |
| // create an array to assign to list | |
| jj.object["list"] = JSONValue( ["a", "b", "c"] ); | |
| // list already exists, so .object optional | |
| jj["list"].array ~= JSONValue("D"); | |
| string jjStr = `{"language":"D","list":["a","b","c","D"],"rating":3.5}`; | |
| assert(jj.toString == jjStr); | |
| } | |
| /** | |
| String literals used to represent special float values within JSON strings. | |
| */ | |
| enum JSONFloatLiteral : string | |
| { | |
| nan = "NaN", /// String representation of floating-point NaN | |
| inf = "Infinite", /// String representation of floating-point Infinity | |
| negativeInf = "-Infinite", /// String representation of floating-point negative Infinity | |
| } | |
| /** | |
| Flags that control how JSON is encoded and parsed. | |
| */ | |
| enum JSONOptions | |
| { | |
| none, /// Standard parsing and encoding | |
| specialFloatLiterals = 0x1, /// Encode NaN and Inf float values as strings | |
| escapeNonAsciiChars = 0x2, /// Encode non-ASCII characters with a Unicode escape sequence | |
| doNotEscapeSlashes = 0x4, /// Do not escape slashes ('/') | |
| strictParsing = 0x8, /// Strictly follow RFC-8259 grammar when parsing | |
| preserveObjectOrder = 0x16, /// Preserve order of object keys when parsing | |
| } | |
| /** | |
| Enumeration of JSON types | |
| */ | |
| enum JSONType : byte | |
| { | |
| /// Indicates the type of a `JSONValue`. | |
| null_, | |
| string, /// ditto | |
| integer, /// ditto | |
| uinteger, /// ditto | |
| float_, /// ditto | |
| array, /// ditto | |
| object, /// ditto | |
| true_, /// ditto | |
| false_, /// ditto | |
| // FIXME: Find some way to deprecate the enum members below, which does NOT | |
| // create lots of spam-like deprecation warnings, which can't be fixed | |
| // by the user. See discussion on this issue at | |
| // https://forum.dlang.org/post/feudrhtxkaxxscwhhhff@forum.dlang.org | |
| /* deprecated("Use .null_") */ NULL = null_, | |
| /* deprecated("Use .string") */ STRING = string, | |
| /* deprecated("Use .integer") */ INTEGER = integer, | |
| /* deprecated("Use .uinteger") */ UINTEGER = uinteger, | |
| /* deprecated("Use .float_") */ FLOAT = float_, | |
| /* deprecated("Use .array") */ ARRAY = array, | |
| /* deprecated("Use .object") */ OBJECT = object, | |
| /* deprecated("Use .true_") */ TRUE = true_, | |
| /* deprecated("Use .false_") */ FALSE = false_, | |
| } | |
| deprecated("Use JSONType and the new enum member names") alias JSON_TYPE = JSONType; | |
| /** | |
| JSON value node | |
| */ | |
| struct JSONValue | |
| { | |
| import std.exception : enforce; | |
| import std.typecons : Tuple; | |
| alias OrderedObjectMember = Tuple!( | |
| string, "key", | |
| JSONValue, "value", | |
| ); | |
| union Store | |
| { | |
| struct Object | |
| { | |
| bool isOrdered; | |
| union | |
| { | |
| JSONValue[string] unordered; | |
| OrderedObjectMember[] ordered; | |
| } | |
| } | |
| string str; | |
| long integer; | |
| ulong uinteger; | |
| double floating; | |
| Object object; | |
| JSONValue[] array; | |
| } | |
| private Store store; | |
| private JSONType type_tag; | |
| /** | |
| Returns the JSONType of the value stored in this structure. | |
| */ | |
| @property JSONType type() const pure nothrow @safe @nogc | |
| { | |
| return type_tag; | |
| } | |
| /// | |
| @safe unittest | |
| { | |
| string s = "{ \"language\": \"D\" }"; | |
| JSONValue j = parseJSON(s); | |
| assert(j.type == JSONType.object); | |
| assert(j["language"].type == JSONType.string); | |
| } | |
| /*** | |
| * Value getter/setter for `JSONType.string`. | |
| * Throws: `JSONException` for read access if `type` is not | |
| * `JSONType.string`. | |
| */ | |
| @property string str() const pure @trusted return scope | |
| { | |
| enforce!JSONException(type == JSONType.string, | |
| "JSONValue is not a string"); | |
| return store.str; | |
| } | |
| /// ditto | |
| @property string str(return scope string v) pure nothrow @nogc @trusted return // TODO make @safe | |
| { | |
| assign(v); | |
| return v; | |
| } | |
| /// | |
| @safe unittest | |
| { | |
| JSONValue j = [ "language": "D" ]; | |
| // get value | |
| assert(j["language"].str == "D"); | |
| // change existing key to new string | |
| j["language"].str = "Perl"; | |
| assert(j["language"].str == "Perl"); | |
| } | |
| /*** | |
| * Value getter/setter for `JSONType.integer`. | |
| * Throws: `JSONException` for read access if `type` is not | |
| * `JSONType.integer`. | |
| */ | |
| @property long integer() const pure @safe | |
| { | |
| enforce!JSONException(type == JSONType.integer, | |
| "JSONValue is not an integer"); | |
| return store.integer; | |
| } | |
| /// ditto | |
| @property long integer(long v) pure nothrow @safe @nogc | |
| { | |
| assign(v); | |
| return store.integer; | |
| } | |
| /*** | |
| * Value getter/setter for `JSONType.uinteger`. | |
| * Throws: `JSONException` for read access if `type` is not | |
| * `JSONType.uinteger`. | |
| */ | |
| @property ulong uinteger() const pure @safe | |
| { | |
| enforce!JSONException(type == JSONType.uinteger, | |
| "JSONValue is not an unsigned integer"); | |
| return store.uinteger; | |
| } | |
| /// ditto | |
| @property ulong uinteger(ulong v) pure nothrow @safe @nogc | |
| { | |
| assign(v); | |
| return store.uinteger; | |
| } | |
| /*** | |
| * Value getter/setter for `JSONType.float_`. Note that despite | |
| * the name, this is a $(B 64)-bit `double`, not a 32-bit `float`. | |
| * Throws: `JSONException` for read access if `type` is not | |
| * `JSONType.float_`. | |
| */ | |
| @property double floating() const pure @safe | |
| { | |
| enforce!JSONException(type == JSONType.float_, | |
| "JSONValue is not a floating type"); | |
| return store.floating; | |
| } | |
| /// ditto | |
| @property double floating(double v) pure nothrow @safe @nogc | |
| { | |
| assign(v); | |
| return store.floating; | |
| } | |
| /*** | |
| * Value getter/setter for boolean stored in JSON. | |
| * Throws: `JSONException` for read access if `this.type` is not | |
| * `JSONType.true_` or `JSONType.false_`. | |
| */ | |
| @property bool boolean() const pure @safe | |
| { | |
| if (type == JSONType.true_) return true; | |
| if (type == JSONType.false_) return false; | |
| throw new JSONException("JSONValue is not a boolean type"); | |
| } | |
| /// ditto | |
| @property bool boolean(bool v) pure nothrow @safe @nogc | |
| { | |
| assign(v); | |
| return v; | |
| } | |
| /// | |
| @safe unittest | |
| { | |
| JSONValue j = true; | |
| assert(j.boolean == true); | |
| j.boolean = false; | |
| assert(j.boolean == false); | |
| j.integer = 12; | |
| import std.exception : assertThrown; | |
| assertThrown!JSONException(j.boolean); | |
| } | |
| /*** | |
| * Value getter/setter for unordered `JSONType.object`. | |
| * Throws: `JSONException` for read access if `type` is not | |
| * `JSONType.object` or the object is ordered. | |
| * Note: This is @system because of the following pattern: | |
| --- | |
| auto a = &(json.object()); | |
| json.uinteger = 0; // overwrite AA pointer | |
| (*a)["hello"] = "world"; // segmentation fault | |
| --- | |
| */ | |
| @property ref inout(JSONValue[string]) object() inout pure @system return | |
| { | |
| enforce!JSONException(type == JSONType.object, | |
| "JSONValue is not an object"); | |
| enforce!JSONException(!store.object.isOrdered, | |
| "JSONValue object is ordered, cannot return by ref"); | |
| return store.object.unordered; | |
| } | |
| /// ditto | |
| @property JSONValue[string] object(return scope JSONValue[string] v) pure nothrow @nogc @trusted // TODO make @safe | |
| { | |
| assign(v); | |
| return v; | |
| } | |
| /*** | |
| * Value getter for unordered `JSONType.object`. | |
| * Unlike `object`, this retrieves the object by value | |
| * and can be used in @safe code. | |
| * | |
| * One possible caveat is that, if the returned value is null, | |
| * modifications will not be visible: | |
| * --- | |
| * JSONValue json; | |
| * json.object = null; | |
| * json.objectNoRef["hello"] = JSONValue("world"); | |
| * assert("hello" !in json.object); | |
| * --- | |
| * | |
| * Throws: `JSONException` for read access if `type` is not | |
| * `JSONType.object`. | |
| */ | |
| @property inout(JSONValue[string]) objectNoRef() inout pure @trusted | |
| { | |
| enforce!JSONException(type == JSONType.object, | |
| "JSONValue is not an object"); | |
| if (store.object.isOrdered) | |
| { | |
| // Convert to unordered | |
| JSONValue[string] result; | |
| foreach (pair; store.object.ordered) | |
| result[pair.key] = pair.value; | |
| return cast(inout) result; | |
| } | |
| else | |
| return store.object.unordered; | |
| } | |
| /*** | |
| * Value getter/setter for ordered `JSONType.object`. | |
| * Throws: `JSONException` for read access if `type` is not | |
| * `JSONType.object` or the object is unordered. | |
| * Note: This is @system because of the following pattern: | |
| --- | |
| auto a = &(json.orderedObject()); | |
| json.uinteger = 0; // overwrite AA pointer | |
| (*a)["hello"] = "world"; // segmentation fault | |
| --- | |
| */ | |
| @property ref inout(OrderedObjectMember[]) orderedObject() inout pure @system return | |
| { | |
| enforce!JSONException(type == JSONType.object, | |
| "JSONValue is not an object"); | |
| enforce!JSONException(store.object.isOrdered, | |
| "JSONValue object is unordered, cannot return by ref"); | |
| return store.object.ordered; | |
| } | |
| /// ditto | |
| @property OrderedObjectMember[] orderedObject(return scope OrderedObjectMember[] v) pure nothrow @nogc @trusted // TODO make @safe | |
| { | |
| assign(v); | |
| return v; | |
| } | |
| /*** | |
| * Value getter for ordered `JSONType.object`. | |
| * Unlike `orderedObject`, this retrieves the object by value | |
| * and can be used in @safe code. | |
| */ | |
| @property inout(OrderedObjectMember[]) orderedObjectNoRef() inout pure @trusted | |
| { | |
| enforce!JSONException(type == JSONType.object, | |
| "JSONValue is not an object"); | |
| if (store.object.isOrdered) | |
| return store.object.ordered; | |
| else | |
| { | |
| // Convert to ordered | |
| OrderedObjectMember[] result; | |
| foreach (key, value; store.object.unordered) | |
| result ~= OrderedObjectMember(key, value); | |
| return cast(inout) result; | |
| } | |
| } | |
| /// Returns `true` if the order of keys of the represented object is being preserved. | |
| @property bool isOrdered() const pure @trusted | |
| { | |
| enforce!JSONException(type == JSONType.object, | |
| "JSONValue is not an object"); | |
| return store.object.isOrdered; | |
| } | |
| /*** | |
| * Value getter/setter for `JSONType.array`. | |
| * Throws: `JSONException` for read access if `type` is not | |
| * `JSONType.array`. | |
| * Note: This is @system because of the following pattern: | |
| --- | |
| auto a = &(json.array()); | |
| json.uinteger = 0; // overwrite array pointer | |
| (*a)[0] = "world"; // segmentation fault | |
| --- | |
| */ | |
| @property ref inout(JSONValue[]) array() scope return inout pure @system | |
| { | |
| enforce!JSONException(type == JSONType.array, | |
| "JSONValue is not an array"); | |
| return store.array; | |
| } | |
| /// ditto | |
| @property JSONValue[] array(return scope JSONValue[] v) pure nothrow @nogc @trusted scope // TODO make @safe | |
| { | |
| assign(v); | |
| return v; | |
| } | |
| /*** | |
| * Value getter for `JSONType.array`. | |
| * Unlike `array`, this retrieves the array by value and can be used in @safe code. | |
| * | |
| * One possible caveat is that, if you append to the returned array, | |
| * the new values aren't visible in the `JSONValue`: | |
| * --- | |
| * JSONValue json; | |
| * json.array = [JSONValue("hello")]; | |
| * json.arrayNoRef ~= JSONValue("world"); | |
| * assert(json.array.length == 1); | |
| * --- | |
| * | |
| * Throws: `JSONException` for read access if `type` is not | |
| * `JSONType.array`. | |
| */ | |
| @property inout(JSONValue[]) arrayNoRef() inout pure @trusted | |
| { | |
| enforce!JSONException(type == JSONType.array, | |
| "JSONValue is not an array"); | |
| return store.array; | |
| } | |
| /// Test whether the type is `JSONType.null_` | |
| @property bool isNull() const pure nothrow @safe @nogc | |
| { | |
| return type == JSONType.null_; | |
| } | |
| /*** | |
| * A convenience getter that returns this `JSONValue` as the specified D type. | |
| * Note: Only numeric types, `bool`, `string`, `JSONValue[string]`, and `JSONValue[]` types are accepted | |
| * Throws: `JSONException` if `T` cannot hold the contents of this `JSONValue` | |
| * `ConvException` in case of integer overflow when converting to `T` | |
| */ | |
| @property inout(T) get(T)() inout const pure @safe | |
| { | |
| static if (is(immutable T == immutable string)) | |
| { | |
| return str; | |
| } | |
| else static if (is(immutable T == immutable bool)) | |
| { | |
| return boolean; | |
| } | |
| else static if (isFloatingPoint!T) | |
| { | |
| switch (type) | |
| { | |
| case JSONType.float_: | |
| return cast(T) floating; | |
| case JSONType.uinteger: | |
| return cast(T) uinteger; | |
| case JSONType.integer: | |
| return cast(T) integer; | |
| default: | |
| throw new JSONException("JSONValue is not a number type"); | |
| } | |
| } | |
| else static if (isIntegral!T) | |
| { | |
| switch (type) | |
| { | |
| case JSONType.uinteger: | |
| return uinteger.to!T; | |
| case JSONType.integer: | |
| return integer.to!T; | |
| default: | |
| throw new JSONException("JSONValue is not a an integral type"); | |
| } | |
| } | |
| else | |
| { | |
| static assert(false, "Unsupported type"); | |
| } | |
| } | |
| // This specialization is needed because arrayNoRef requires inout | |
| @property inout(T) get(T : JSONValue[])() inout pure @trusted /// ditto | |
| { | |
| return arrayNoRef; | |
| } | |
| /// ditto | |
| @property inout(T) get(T : JSONValue[string])() inout pure @trusted | |
| { | |
| return object; | |
| } | |
| /// | |
| @safe unittest | |
| { | |
| import std.exception; | |
| import std.conv; | |
| string s = | |
| `{ | |
| "a": 123, | |
| "b": 3.1415, | |
| "c": "text", | |
| "d": true, | |
| "e": [1, 2, 3], | |
| "f": { "a": 1 }, | |
| "g": -45, | |
| "h": ` ~ ulong.max.to!string ~ `, | |
| }`; | |
| struct a { } | |
| immutable json = parseJSON(s); | |
| assert(json["a"].get!double == 123.0); | |
| assert(json["a"].get!int == 123); | |
| assert(json["a"].get!uint == 123); | |
| assert(json["b"].get!double == 3.1415); | |
| assertThrown!JSONException(json["b"].get!int); | |
| assert(json["c"].get!string == "text"); | |
| assert(json["d"].get!bool == true); | |
| assertNotThrown(json["e"].get!(JSONValue[])); | |
| assertNotThrown(json["f"].get!(JSONValue[string])); | |
| static assert(!__traits(compiles, json["a"].get!a)); | |
| assertThrown!JSONException(json["e"].get!float); | |
| assertThrown!JSONException(json["d"].get!(JSONValue[string])); | |
| assertThrown!JSONException(json["f"].get!(JSONValue[])); | |
| assert(json["g"].get!int == -45); | |
| assertThrown!ConvException(json["g"].get!uint); | |
| assert(json["h"].get!ulong == ulong.max); | |
| assertThrown!ConvException(json["h"].get!uint); | |
| assertNotThrown(json["h"].get!float); | |
| } | |
| private void assign(T)(T arg) | |
| { | |
| static if (is(T : typeof(null))) | |
| { | |
| type_tag = JSONType.null_; | |
| } | |
| else static if (is(T : string)) | |
| { | |
| type_tag = JSONType.string; | |
| store = Store(str: arg); | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=15884 | |
| else static if (isSomeString!T) | |
| { | |
| type_tag = JSONType.string; | |
| // FIXME: std.Array.Array(Range) is not deduced as 'pure' | |
| () @trusted { | |
| import std.utf : byUTF; | |
| store = Store(str: cast(immutable)(arg.byUTF!char.array)); | |
| }(); | |
| } | |
| else static if (is(T : bool)) | |
| { | |
| type_tag = arg ? JSONType.true_ : JSONType.false_; | |
| } | |
| else static if (is(T : ulong) && isUnsigned!T) | |
| { | |
| type_tag = JSONType.uinteger; | |
| store = Store(uinteger: arg); | |
| } | |
| else static if (is(T : long)) | |
| { | |
| type_tag = JSONType.integer; | |
| store = Store(integer: arg); | |
| } | |
| else static if (isFloatingPoint!T) | |
| { | |
| type_tag = JSONType.float_; | |
| store = Store(floating: arg); | |
| } | |
| else static if (is(T : Value[Key], Key, Value)) | |
| { | |
| static assert(is(Key : string), "AA key must be string"); | |
| type_tag = JSONType.object; | |
| static if (is(Value : JSONValue)) | |
| { | |
| store = Store(object: Store.Object(false, unordered: arg)); | |
| } | |
| else | |
| { | |
| JSONValue[string] aa; | |
| foreach (key, value; arg) | |
| aa[key] = JSONValue(value); | |
| store = Store(object: Store.Object(false, unordered: aa)); | |
| } | |
| } | |
| else static if (is(T : OrderedObjectMember[])) | |
| { | |
| type_tag = JSONType.object; | |
| store = Store(object: Store.Object(true, ordered: arg)); | |
| } | |
| else static if (isArray!T) | |
| { | |
| type_tag = JSONType.array; | |
| static if (is(ElementEncodingType!T : JSONValue)) | |
| { | |
| store = Store(array: arg); | |
| } | |
| else | |
| { | |
| JSONValue[] new_arg = new JSONValue[arg.length]; | |
| foreach (i, e; arg) | |
| new_arg[i] = JSONValue(e); | |
| store = Store(array: new_arg); | |
| } | |
| } | |
| else static if (is(T : JSONValue)) | |
| { | |
| type_tag = arg.type; | |
| store = arg.store; | |
| } | |
| else | |
| { | |
| static assert(false, text(`unable to convert type "`, T.stringof, `" to json`)); | |
| } | |
| } | |
| private void assignRef(T)(ref T arg) | |
| if (isStaticArray!T) | |
| { | |
| type_tag = JSONType.array; | |
| static if (is(ElementEncodingType!T : JSONValue)) | |
| { | |
| store = Store(array: arg); | |
| } | |
| else | |
| { | |
| JSONValue[] new_arg = new JSONValue[arg.length]; | |
| foreach (i, e; arg) | |
| new_arg[i] = JSONValue(e); | |
| store = Store(array: new_arg); | |
| } | |
| } | |
| /** | |
| * Constructor for `JSONValue`. If `arg` is a `JSONValue` | |
| * its value and type will be copied to the new `JSONValue`. | |
| * Note that this is a shallow copy: if type is `JSONType.object` | |
| * or `JSONType.array` then only the reference to the data will | |
| * be copied. | |
| * Otherwise, `arg` must be implicitly convertible to one of the | |
| * following types: `typeof(null)`, `string`, `ulong`, | |
| * `long`, `double`, an associative array `V[K]` for any `V` | |
| * and `K` i.e. a JSON object, any array or `bool`. The type will | |
| * be set accordingly. | |
| */ | |
| this(T)(T arg) | |
| if (!isStaticArray!T) | |
| { | |
| assign(arg); | |
| } | |
| /// Ditto | |
| this(T)(ref T arg) | |
| if (isStaticArray!T) | |
| { | |
| assignRef(arg); | |
| } | |
| /// Ditto | |
| this(T : JSONValue)(inout T arg) inout | |
| { | |
| store = arg.store; | |
| type_tag = arg.type; | |
| } | |
| /// | |
| @safe unittest | |
| { | |
| JSONValue j = JSONValue( "a string" ); | |
| j = JSONValue(42); | |
| j = JSONValue( [1, 2, 3] ); | |
| assert(j.type == JSONType.array); | |
| j = JSONValue( ["language": "D"] ); | |
| assert(j.type == JSONType.object); | |
| } | |
| /** | |
| * An enum value that can be used to obtain a `JSONValue` representing | |
| * an empty JSON object. | |
| */ | |
| enum emptyObject = JSONValue(string[string].init); | |
| /// | |
| @system unittest | |
| { | |
| JSONValue obj1 = JSONValue.emptyObject; | |
| assert(obj1.type == JSONType.object); | |
| obj1.object["a"] = JSONValue(1); | |
| assert(obj1.object["a"] == JSONValue(1)); | |
| JSONValue obj2 = JSONValue.emptyObject; | |
| assert("a" !in obj2.object); | |
| obj2.object["b"] = JSONValue(5); | |
| assert(obj1 != obj2); | |
| } | |
| /** | |
| * An enum value that can be used to obtain a `JSONValue` representing | |
| * an empty JSON object. | |
| * Unlike `emptyObject`, the order of inserted keys is preserved. | |
| */ | |
| enum emptyOrderedObject = { | |
| JSONValue v = void; | |
| v.orderedObject = null; | |
| return v; | |
| }(); | |
| /// | |
| @system unittest | |
| { | |
| JSONValue obj = JSONValue.emptyOrderedObject; | |
| assert(obj.type == JSONType.object); | |
| assert(obj.isOrdered); | |
| obj["b"] = JSONValue(2); | |
| obj["a"] = JSONValue(1); | |
| assert(obj["a"] == JSONValue(1)); | |
| assert(obj["b"] == JSONValue(2)); | |
| string[] keys; | |
| foreach (string k, JSONValue v; obj) | |
| keys ~= k; | |
| assert(keys == ["b", "a"]); | |
| } | |
| /** | |
| * An enum value that can be used to obtain a `JSONValue` representing | |
| * an empty JSON array. | |
| */ | |
| enum emptyArray = JSONValue(JSONValue[].init); | |
| /// | |
| @system unittest | |
| { | |
| JSONValue arr1 = JSONValue.emptyArray; | |
| assert(arr1.type == JSONType.array); | |
| assert(arr1.array.length == 0); | |
| arr1.array ~= JSONValue("Hello"); | |
| assert(arr1.array.length == 1); | |
| assert(arr1.array[0] == JSONValue("Hello")); | |
| JSONValue arr2 = JSONValue.emptyArray; | |
| assert(arr2.array.length == 0); | |
| assert(arr1 != arr2); | |
| } | |
| void opAssign(T)(T arg) | |
| if (!isStaticArray!T && !is(T : JSONValue)) | |
| { | |
| assign(arg); | |
| } | |
| void opAssign(T)(ref T arg) | |
| if (isStaticArray!T) | |
| { | |
| assignRef(arg); | |
| } | |
| /*** | |
| * Array syntax for JSON arrays. | |
| * Throws: `JSONException` if `type` is not `JSONType.array`. | |
| */ | |
| ref inout(JSONValue) opIndex(size_t i) inout pure @safe | |
| { | |
| auto a = this.arrayNoRef; | |
| enforce!JSONException(i < a.length, | |
| "JSONValue array index is out of range"); | |
| return a[i]; | |
| } | |
| /// | |
| @safe unittest | |
| { | |
| JSONValue j = JSONValue( [42, 43, 44] ); | |
| assert( j[0].integer == 42 ); | |
| assert( j[1].integer == 43 ); | |
| } | |
| /*** | |
| * Hash syntax for JSON objects. | |
| * Throws: `JSONException` if `type` is not `JSONType.object`. | |
| */ | |
| ref inout(JSONValue) opIndex(return scope string k) inout pure @safe | |
| { | |
| auto o = this.objectNoRef; | |
| return *enforce!JSONException(k in o, | |
| "Key not found: " ~ k); | |
| } | |
| /// | |
| @safe unittest | |
| { | |
| JSONValue j = JSONValue( ["language": "D"] ); | |
| assert( j["language"].str == "D" ); | |
| } | |
| /*** | |
| * Provides support for index assignments, which sets the | |
| * corresponding value of the JSON object's `key` field to `value`. | |
| * | |
| * If the `JSONValue` is `JSONType.null_`, then this function | |
| * initializes it with a JSON object and then performs | |
| * the index assignment. | |
| * | |
| * Throws: `JSONException` if `type` is not `JSONType.object` | |
| * or `JSONType.null_`. | |
| */ | |
| void opIndexAssign(T)(auto ref T value, string key) | |
| { | |
| enforce!JSONException( | |
| type == JSONType.object || | |
| type == JSONType.null_, | |
| "JSONValue must be object or null"); | |
| if (type == JSONType.object && isOrdered) | |
| { | |
| auto arr = this.orderedObjectNoRef; | |
| foreach (ref pair; arr) | |
| if (pair.key == key) | |
| { | |
| pair.value = value; | |
| return; | |
| } | |
| arr ~= OrderedObjectMember(key, JSONValue(value)); | |
| this.orderedObject = arr; | |
| } | |
| else | |
| { | |
| JSONValue[string] aa = null; | |
| if (type == JSONType.object) | |
| { | |
| aa = this.objectNoRef; | |
| } | |
| aa[key] = value; | |
| this.object = aa; | |
| } | |
| } | |
| /// | |
| @safe unittest | |
| { | |
| JSONValue j = JSONValue( ["language": "D"] ); | |
| j["language"].str = "Perl"; | |
| assert( j["language"].str == "Perl" ); | |
| } | |
| /// ditto | |
| void opIndexAssign(T)(T arg, size_t i) | |
| { | |
| auto a = this.arrayNoRef; | |
| enforce!JSONException(i < a.length, | |
| "JSONValue array index is out of range"); | |
| a[i] = arg; | |
| this.array = a; | |
| } | |
| /// | |
| @safe unittest | |
| { | |
| JSONValue j = JSONValue( ["Perl", "C"] ); | |
| j[1].str = "D"; | |
| assert( j[1].str == "D" ); | |
| } | |
| JSONValue opBinary(string op : "~", T)(T arg) | |
| { | |
| auto a = this.arrayNoRef; | |
| static if (isArray!T) | |
| { | |
| return JSONValue(a ~ JSONValue(arg).arrayNoRef); | |
| } | |
| else static if (is(T : JSONValue)) | |
| { | |
| return JSONValue(a ~ arg.arrayNoRef); | |
| } | |
| else | |
| { | |
| static assert(false, "argument is not an array or a JSONValue array"); | |
| } | |
| } | |
| void opOpAssign(string op : "~", T)(T arg) | |
| { | |
| auto a = this.arrayNoRef; | |
| static if (isArray!T) | |
| { | |
| a ~= JSONValue(arg).arrayNoRef; | |
| } | |
| else static if (is(T : JSONValue)) | |
| { | |
| a ~= arg.arrayNoRef; | |
| } | |
| else | |
| { | |
| static assert(false, "argument is not an array or a JSONValue array"); | |
| } | |
| this.array = a; | |
| } | |
| /** | |
| * Provides support for the `in` operator. | |
| * | |
| * Tests whether a key can be found in an object. | |
| * | |
| * Returns: | |
| * When found, the `inout(JSONValue)*` that matches to the key, | |
| * otherwise `null`. | |
| * | |
| * Throws: `JSONException` if the right hand side argument `JSONType` | |
| * is not `object`. | |
| */ | |
| inout(JSONValue)* opBinaryRight(string op : "in")(string k) inout @safe | |
| { | |
| return k in this.objectNoRef; | |
| } | |
| /// | |
| @safe unittest | |
| { | |
| JSONValue j = [ "language": "D", "author": "walter" ]; | |
| string a = ("author" in j).str; | |
| *("author" in j) = "Walter"; | |
| assert(j["author"].str == "Walter"); | |
| } | |
| /** | |
| * Compare two JSONValues for equality | |
| * | |
| * JSON arrays and objects are compared deeply. The order of object keys does not matter. | |
| * | |
| * Floating point numbers are compared for exact equality, not approximal equality. | |
| * | |
| * Different number types (unsigned, signed, and floating) will be compared by converting | |
| * them to a common type, in the same way that comparison of built-in D `int`, `uint` and | |
| * `float` works. | |
| * | |
| * Other than that, types must match exactly. | |
| * Empty arrays are not equal to empty objects, and booleans are never equal to integers. | |
| * | |
| * Returns: whether this `JSONValue` is equal to `rhs` | |
| */ | |
| bool opEquals(const JSONValue rhs) const @nogc nothrow pure @safe | |
| { | |
| return opEquals(rhs); | |
| } | |
| /// ditto | |
| bool opEquals(ref const JSONValue rhs) const @nogc nothrow pure @trusted | |
| { | |
| import std.algorithm.searching : canFind; | |
| // Default doesn't work well since store is a union. Compare only | |
| // what should be in store. | |
| // This is @trusted to remain nogc, nothrow, fast, and usable from @safe code. | |
| final switch (type_tag) | |
| { | |
| case JSONType.integer: | |
| switch (rhs.type_tag) | |
| { | |
| case JSONType.integer: | |
| return store.integer == rhs.store.integer; | |
| case JSONType.uinteger: | |
| return store.integer == rhs.store.uinteger; | |
| case JSONType.float_: | |
| return store.integer == rhs.store.floating; | |
| default: | |
| return false; | |
| } | |
| case JSONType.uinteger: | |
| switch (rhs.type_tag) | |
| { | |
| case JSONType.integer: | |
| return store.uinteger == rhs.store.integer; | |
| case JSONType.uinteger: | |
| return store.uinteger == rhs.store.uinteger; | |
| case JSONType.float_: | |
| return store.uinteger == rhs.store.floating; | |
| default: | |
| return false; | |
| } | |
| case JSONType.float_: | |
| switch (rhs.type_tag) | |
| { | |
| case JSONType.integer: | |
| return store.floating == rhs.store.integer; | |
| case JSONType.uinteger: | |
| return store.floating == rhs.store.uinteger; | |
| case JSONType.float_: | |
| return store.floating == rhs.store.floating; | |
| default: | |
| return false; | |
| } | |
| case JSONType.string: | |
| return type_tag == rhs.type_tag && store.str == rhs.store.str; | |
| case JSONType.object: | |
| if (rhs.type_tag != JSONType.object) | |
| return false; | |
| if (store.object.isOrdered) | |
| { | |
| if (rhs.store.object.isOrdered) | |
| { | |
| if (store.object.ordered.length != rhs.store.object.ordered.length) | |
| return false; | |
| foreach (ref pair; store.object.ordered) | |
| if (!rhs.store.object.ordered.canFind(pair)) | |
| return false; | |
| return true; | |
| } | |
| else | |
| { | |
| if (store.object.ordered.length != rhs.store.object.unordered.length) | |
| return false; | |
| foreach (ref pair; store.object.ordered) | |
| if (pair.key !in rhs.store.object.unordered || | |
| rhs.store.object.unordered[pair.key] != pair.value) | |
| return false; | |
| return true; | |
| } | |
| } | |
| else | |
| { | |
| if (rhs.store.object.isOrdered) | |
| { | |
| if (store.object.unordered.length != rhs.store.object.ordered.length) | |
| return false; | |
| foreach (ref pair; rhs.store.object.ordered) | |
| if (pair.key !in store.object.unordered || | |
| store.object.unordered[pair.key] != pair.value) | |
| return false; | |
| return true; | |
| } | |
| else | |
| return store.object.unordered == rhs.store.object.unordered; | |
| } | |
| case JSONType.array: | |
| return type_tag == rhs.type_tag && store.array == rhs.store.array; | |
| case JSONType.true_: | |
| case JSONType.false_: | |
| case JSONType.null_: | |
| return type_tag == rhs.type_tag; | |
| } | |
| } | |
| /// | |
| @safe unittest | |
| { | |
| assert(JSONValue(10).opEquals(JSONValue(10.0))); | |
| assert(JSONValue(10) != (JSONValue(10.5))); | |
| assert(JSONValue(1) != JSONValue(true)); | |
| assert(JSONValue.emptyArray != JSONValue.emptyObject); | |
| assert(parseJSON(`{"a": 1, "b": 2}`).opEquals(parseJSON(`{"b": 2, "a": 1}`))); | |
| } | |
| /// Implements the foreach `opApply` interface for json arrays. | |
| int opApply(scope int delegate(size_t index, ref JSONValue) dg) @system | |
| { | |
| int result; | |
| foreach (size_t index, ref value; array) | |
| { | |
| result = dg(index, value); | |
| if (result) | |
| break; | |
| } | |
| return result; | |
| } | |
| /// Implements the foreach `opApply` interface for json objects. | |
| int opApply(scope int delegate(string key, ref JSONValue) dg) @system | |
| { | |
| enforce!JSONException(type == JSONType.object, | |
| "JSONValue is not an object"); | |
| int result; | |
| if (isOrdered) | |
| { | |
| foreach (ref pair; orderedObject) | |
| { | |
| result = dg(pair.key, pair.value); | |
| if (result) | |
| break; | |
| } | |
| } | |
| else | |
| { | |
| foreach (string key, ref value; object) | |
| { | |
| result = dg(key, value); | |
| if (result) | |
| break; | |
| } | |
| } | |
| return result; | |
| } | |
| /*** | |
| * Implicitly calls `toJSON` on this JSONValue. | |
| * | |
| * $(I options) can be used to tweak the conversion behavior. | |
| */ | |
| string toString(in JSONOptions options = JSONOptions.none) const @safe | |
| { | |
| return toJSON(this, false, options); | |
| } | |
| /// | |
| void toString(Out)(Out sink, in JSONOptions options = JSONOptions.none) const | |
| { | |
| toJSON(sink, this, false, options); | |
| } | |
| /*** | |
| * Implicitly calls `toJSON` on this JSONValue, like `toString`, but | |
| * also passes $(I true) as $(I pretty) argument. | |
| * | |
| * $(I options) can be used to tweak the conversion behavior | |
| */ | |
| string toPrettyString(in JSONOptions options = JSONOptions.none) const @safe | |
| { | |
| return toJSON(this, true, options); | |
| } | |
| /// | |
| void toPrettyString(Out)(Out sink, in JSONOptions options = JSONOptions.none) const | |
| { | |
| toJSON(sink, this, true, options); | |
| } | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=20874 | |
| @system unittest | |
| { | |
| static struct MyCustomType | |
| { | |
| public string toString () const @system { return null; } | |
| alias toString this; | |
| } | |
| static struct B | |
| { | |
| public JSONValue asJSON() const @system { return JSONValue.init; } | |
| alias asJSON this; | |
| } | |
| if (false) // Just checking attributes | |
| { | |
| JSONValue json; | |
| MyCustomType ilovedlang; | |
| json = ilovedlang; | |
| json["foo"] = ilovedlang; | |
| auto s = ilovedlang in json; | |
| B b; | |
| json ~= b; | |
| json ~ b; | |
| } | |
| } | |
| /** | |
| Parses a serialized string and returns a tree of JSON values. | |
| Throws: $(LREF JSONException) if string does not follow the JSON grammar or the depth exceeds the max depth, | |
| $(LREF ConvException) if a number in the input cannot be represented by a native D type. | |
| Params: | |
| json = json-formatted string to parse | |
| maxDepth = maximum depth of nesting allowed, -1 disables depth checking | |
| options = enable decoding string representations of NaN/Inf as float values | |
| */ | |
| JSONValue parseJSON(T)(T json, int maxDepth = -1, JSONOptions options = JSONOptions.none) | |
| if (isSomeFiniteCharInputRange!T) | |
| { | |
| import std.ascii : isDigit, isHexDigit, toUpper, toLower; | |
| import std.typecons : Nullable, Yes; | |
| JSONValue root; | |
| root.type_tag = JSONType.null_; | |
| // Avoid UTF decoding when possible, as it is unnecessary when | |
| // processing JSON. | |
| static if (is(T : const(char)[])) | |
| alias Char = char; | |
| else | |
| alias Char = Unqual!(ElementType!T); | |
| int depth = -1; | |
| Nullable!Char next; | |
| int line = 1, pos = 0; | |
| immutable bool strict = (options & JSONOptions.strictParsing) != 0; | |
| immutable bool ordered = (options & JSONOptions.preserveObjectOrder) != 0; | |
| void error(string msg) | |
| { | |
| throw new JSONException(msg, line, pos); | |
| } | |
| if (json.empty) | |
| { | |
| if (strict) | |
| { | |
| error("Empty JSON body"); | |
| } | |
| return root; | |
| } | |
| bool isWhite(dchar c) | |
| { | |
| if (strict) | |
| { | |
| // RFC 7159 has a stricter definition of whitespace than general ASCII. | |
| return c == ' ' || c == '\t' || c == '\n' || c == '\r'; | |
| } | |
| import std.ascii : isWhite; | |
| // Accept ASCII NUL as whitespace in non-strict mode. | |
| return c == 0 || isWhite(c); | |
| } | |
| Char popChar() | |
| { | |
| if (json.empty) error("Unexpected end of data."); | |
| static if (is(T : const(char)[])) | |
| { | |
| Char c = json[0]; | |
| json = json[1..$]; | |
| } | |
| else | |
| { | |
| Char c = json.front; | |
| json.popFront(); | |
| } | |
| if (c == '\n') | |
| { | |
| line++; | |
| pos = 0; | |
| } | |
| else | |
| { | |
| pos++; | |
| } | |
| return c; | |
| } | |
| Char peekChar() | |
| { | |
| if (next.isNull) | |
| { | |
| if (json.empty) return '\0'; | |
| next = popChar(); | |
| } | |
| return next.get; | |
| } | |
| Nullable!Char peekCharNullable() | |
| { | |
| if (next.isNull && !json.empty) | |
| { | |
| next = popChar(); | |
| } | |
| return next; | |
| } | |
| void skipWhitespace() | |
| { | |
| while (true) | |
| { | |
| auto c = peekCharNullable(); | |
| if (c.isNull || | |
| !isWhite(c.get)) | |
| { | |
| return; | |
| } | |
| next.nullify(); | |
| } | |
| } | |
| Char getChar(bool SkipWhitespace = false)() | |
| { | |
| static if (SkipWhitespace) skipWhitespace(); | |
| Char c; | |
| if (!next.isNull) | |
| { | |
| c = next.get; | |
| next.nullify(); | |
| } | |
| else | |
| c = popChar(); | |
| return c; | |
| } | |
| void checkChar(bool SkipWhitespace = true)(char c, bool caseSensitive = true) | |
| { | |
| static if (SkipWhitespace) skipWhitespace(); | |
| auto c2 = getChar(); | |
| if (!caseSensitive) c2 = toLower(c2); | |
| if (c2 != c) error(text("Found '", c2, "' when expecting '", c, "'.")); | |
| } | |
| bool testChar(bool SkipWhitespace = true, bool CaseSensitive = true)(char c) | |
| { | |
| static if (SkipWhitespace) skipWhitespace(); | |
| auto c2 = peekChar(); | |
| static if (!CaseSensitive) c2 = toLower(c2); | |
| if (c2 != c) return false; | |
| getChar(); | |
| return true; | |
| } | |
| wchar parseWChar() | |
| { | |
| wchar val = 0; | |
| foreach_reverse (i; 0 .. 4) | |
| { | |
| auto hex = toUpper(getChar()); | |
| if (!isHexDigit(hex)) error("Expecting hex character"); | |
| val += (isDigit(hex) ? hex - '0' : hex - ('A' - 10)) << (4 * i); | |
| } | |
| return val; | |
| } | |
| string parseString() | |
| { | |
| import std.uni : isSurrogateHi, isSurrogateLo; | |
| import std.utf : encode, decode; | |
| auto str = appender!string(); | |
| Next: | |
| switch (peekChar()) | |
| { | |
| case '"': | |
| getChar(); | |
| break; | |
| case '\\': | |
| getChar(); | |
| auto c = getChar(); | |
| switch (c) | |
| { | |
| case '"': str.put('"'); break; | |
| case '\\': str.put('\\'); break; | |
| case '/': str.put('/'); break; | |
| case 'b': str.put('\b'); break; | |
| case 'f': str.put('\f'); break; | |
| case 'n': str.put('\n'); break; | |
| case 'r': str.put('\r'); break; | |
| case 't': str.put('\t'); break; | |
| case 'u': | |
| wchar wc = parseWChar(); | |
| dchar val; | |
| // Non-BMP characters are escaped as a pair of | |
| // UTF-16 surrogate characters (see RFC 4627). | |
| if (isSurrogateHi(wc)) | |
| { | |
| wchar[2] pair; | |
| pair[0] = wc; | |
| if (getChar() != '\\') error("Expected escaped low surrogate after escaped high surrogate"); | |
| if (getChar() != 'u') error("Expected escaped low surrogate after escaped high surrogate"); | |
| pair[1] = parseWChar(); | |
| size_t index = 0; | |
| val = decode(pair[], index); | |
| if (index != 2) error("Invalid escaped surrogate pair"); | |
| } | |
| else | |
| if (isSurrogateLo(wc)) | |
| error(text("Unexpected low surrogate")); | |
| else | |
| val = wc; | |
| char[4] buf; | |
| immutable len = encode!(Yes.useReplacementDchar)(buf, val); | |
| str.put(buf[0 .. len]); | |
| break; | |
| default: | |
| error(text("Invalid escape sequence '\\", c, "'.")); | |
| } | |
| goto Next; | |
| default: | |
| // RFC 7159 states that control characters U+0000 through | |
| // U+001F must not appear unescaped in a JSON string. | |
| // Note: std.ascii.isControl can't be used for this test | |
| // because it considers ASCII DEL (0x7f) to be a control | |
| // character but RFC 7159 does not. | |
| // Accept unescaped ASCII NULs in non-strict mode. | |
| auto c = getChar(); | |
| if (c < 0x20 && (strict || c != 0)) | |
| error("Illegal control character."); | |
| str.put(c); | |
| goto Next; | |
| } | |
| return str.data.length ? str.data : ""; | |
| } | |
| bool tryGetSpecialFloat(string str, out double val) { | |
| switch (str) | |
| { | |
| case JSONFloatLiteral.nan: | |
| val = double.nan; | |
| return true; | |
| case JSONFloatLiteral.inf: | |
| val = double.infinity; | |
| return true; | |
| case JSONFloatLiteral.negativeInf: | |
| val = -double.infinity; | |
| return true; | |
| default: | |
| return false; | |
| } | |
| } | |
| void parseValue(ref JSONValue value) | |
| { | |
| depth++; | |
| if (maxDepth != -1 && depth > maxDepth) error("Nesting too deep."); | |
| auto c = getChar!true(); | |
| switch (c) | |
| { | |
| case '{': | |
| if (ordered) | |
| { | |
| if (testChar('}')) | |
| { | |
| value.orderedObject = null; | |
| break; | |
| } | |
| JSONValue.OrderedObjectMember[] obj; | |
| do | |
| { | |
| skipWhitespace(); | |
| if (!strict && peekChar() == '}') | |
| { | |
| break; | |
| } | |
| checkChar('"'); | |
| string name = parseString(); | |
| checkChar(':'); | |
| JSONValue member; | |
| parseValue(member); | |
| obj ~= JSONValue.OrderedObjectMember(name, member); | |
| } | |
| while (testChar(',')); | |
| value.orderedObject = obj; | |
| checkChar('}'); | |
| } | |
| else | |
| { | |
| if (testChar('}')) | |
| { | |
| value.object = null; | |
| break; | |
| } | |
| JSONValue[string] obj; | |
| do | |
| { | |
| skipWhitespace(); | |
| if (!strict && peekChar() == '}') | |
| { | |
| break; | |
| } | |
| checkChar('"'); | |
| string name = parseString(); | |
| checkChar(':'); | |
| JSONValue member; | |
| parseValue(member); | |
| obj[name] = member; | |
| } | |
| while (testChar(',')); | |
| value.object = obj; | |
| checkChar('}'); | |
| } | |
| break; | |
| case '[': | |
| if (testChar(']')) | |
| { | |
| value.type_tag = JSONType.array; | |
| break; | |
| } | |
| JSONValue[] arr; | |
| do | |
| { | |
| skipWhitespace(); | |
| if (!strict && peekChar() == ']') | |
| { | |
| break; | |
| } | |
| JSONValue element; | |
| parseValue(element); | |
| arr ~= element; | |
| } | |
| while (testChar(',')); | |
| checkChar(']'); | |
| value.array = arr; | |
| break; | |
| case '"': | |
| auto str = parseString(); | |
| // if special float parsing is enabled, check if string represents NaN/Inf | |
| if ((options & JSONOptions.specialFloatLiterals) && | |
| tryGetSpecialFloat(str, value.store.floating)) | |
| { | |
| // found a special float, its value was placed in value.store.floating | |
| value.type_tag = JSONType.float_; | |
| break; | |
| } | |
| value.assign(str); | |
| break; | |
| case '0': .. case '9': | |
| case '-': | |
| auto number = appender!string(); | |
| bool isFloat, isNegative; | |
| void readInteger() | |
| { | |
| if (!isDigit(c)) error("Digit expected"); | |
| Next: number.put(c); | |
| if (isDigit(peekChar())) | |
| { | |
| c = getChar(); | |
| goto Next; | |
| } | |
| } | |
| if (c == '-') | |
| { | |
| number.put('-'); | |
| c = getChar(); | |
| isNegative = true; | |
| } | |
| if (strict && c == '0') | |
| { | |
| number.put('0'); | |
| if (isDigit(peekChar())) | |
| { | |
| error("Additional digits not allowed after initial zero digit"); | |
| } | |
| } | |
| else | |
| { | |
| readInteger(); | |
| } | |
| if (testChar('.')) | |
| { | |
| isFloat = true; | |
| number.put('.'); | |
| c = getChar(); | |
| readInteger(); | |
| } | |
| if (testChar!(false, false)('e')) | |
| { | |
| isFloat = true; | |
| number.put('e'); | |
| if (testChar('+')) number.put('+'); | |
| else if (testChar('-')) number.put('-'); | |
| c = getChar(); | |
| readInteger(); | |
| } | |
| string data = number.data; | |
| if (isFloat) | |
| { | |
| value.type_tag = JSONType.float_; | |
| value.store = JSONValue.Store(floating: parse!double(data)); | |
| } | |
| else | |
| { | |
| if (isNegative) | |
| { | |
| value.store = JSONValue.Store(integer: parse!long(data)); | |
| value.type_tag = JSONType.integer; | |
| } | |
| else | |
| { | |
| // only set the correct union member to not confuse CTFE | |
| ulong u = parse!ulong(data); | |
| if (u & (1UL << 63)) | |
| { | |
| value.store = JSONValue.Store(uinteger: u); | |
| value.type_tag = JSONType.uinteger; | |
| } | |
| else | |
| { | |
| value.store = JSONValue.Store(integer: u); | |
| value.type_tag = JSONType.integer; | |
| } | |
| } | |
| } | |
| break; | |
| case 'T': | |
| if (strict) goto default; | |
| goto case; | |
| case 't': | |
| value.type_tag = JSONType.true_; | |
| checkChar!false('r', strict); | |
| checkChar!false('u', strict); | |
| checkChar!false('e', strict); | |
| break; | |
| case 'F': | |
| if (strict) goto default; | |
| goto case; | |
| case 'f': | |
| value.type_tag = JSONType.false_; | |
| checkChar!false('a', strict); | |
| checkChar!false('l', strict); | |
| checkChar!false('s', strict); | |
| checkChar!false('e', strict); | |
| break; | |
| case 'N': | |
| if (strict) goto default; | |
| goto case; | |
| case 'n': | |
| value.type_tag = JSONType.null_; | |
| checkChar!false('u', strict); | |
| checkChar!false('l', strict); | |
| checkChar!false('l', strict); | |
| break; | |
| default: | |
| error(text("Unexpected character '", c, "'.")); | |
| } | |
| depth--; | |
| } | |
| parseValue(root); | |
| if (strict) | |
| { | |
| skipWhitespace(); | |
| if (!peekCharNullable().isNull) error("Trailing non-whitespace characters"); | |
| } | |
| return root; | |
| } | |
| @safe unittest | |
| { | |
| enum issue15742objectOfObject = `{ "key1": { "key2": 1 }}`; | |
| static assert(parseJSON(issue15742objectOfObject).type == JSONType.object); | |
| enum issue15742arrayOfArray = `[[1]]`; | |
| static assert(parseJSON(issue15742arrayOfArray).type == JSONType.array); | |
| } | |
| @safe unittest | |
| { | |
| // Ensure we can parse and use JSON from @safe code | |
| auto a = `{ "key1": { "key2": 1 }}`.parseJSON; | |
| assert(a["key1"]["key2"].integer == 1); | |
| assert(a.toString == `{"key1":{"key2":1}}`); | |
| } | |
| @system unittest | |
| { | |
| // Ensure we can parse JSON from a @system range. | |
| struct Range | |
| { | |
| string s; | |
| size_t index; | |
| @system | |
| { | |
| bool empty() { return index >= s.length; } | |
| void popFront() { index++; } | |
| char front() { return s[index]; } | |
| } | |
| } | |
| auto s = Range(`{ "key1": { "key2": 1 }}`); | |
| auto json = parseJSON(s); | |
| assert(json["key1"]["key2"].integer == 1); | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=20527 | |
| @safe unittest | |
| { | |
| static assert(parseJSON(`{"a" : 2}`)["a"].integer == 2); | |
| } | |
| /** | |
| Parses a serialized string and returns a tree of JSON values. | |
| Throws: $(LREF JSONException) if the depth exceeds the max depth. | |
| Params: | |
| json = json-formatted string to parse | |
| options = enable decoding string representations of NaN/Inf as float values | |
| */ | |
| JSONValue parseJSON(T)(T json, JSONOptions options) | |
| if (isSomeFiniteCharInputRange!T) | |
| { | |
| return parseJSON!T(json, -1, options); | |
| } | |
| /** | |
| Takes a tree of JSON values and returns the serialized string. | |
| Any Object types will be serialized in a key-sorted order. | |
| If `pretty` is false no whitespaces are generated. | |
| If `pretty` is true serialized string is formatted to be human-readable. | |
| Set the $(LREF JSONOptions.specialFloatLiterals) flag is set in `options` to encode NaN/Infinity as strings. | |
| */ | |
| string toJSON(const ref JSONValue root, in bool pretty = false, in JSONOptions options = JSONOptions.none) @safe | |
| { | |
| auto json = appender!string(); | |
| toJSON(json, root, pretty, options); | |
| return json.data; | |
| } | |
| /// | |
| void toJSON(Out)( | |
| auto ref Out json, | |
| const ref JSONValue root, | |
| in bool pretty = false, | |
| in JSONOptions options = JSONOptions.none) | |
| if (isOutputRange!(Out,char)) | |
| { | |
| void toStringImpl(Char)(string str) | |
| { | |
| json.put('"'); | |
| foreach (Char c; str) | |
| { | |
| switch (c) | |
| { | |
| case '"': json.put("\\\""); break; | |
| case '\\': json.put("\\\\"); break; | |
| case '/': | |
| if (!(options & JSONOptions.doNotEscapeSlashes)) | |
| json.put('\\'); | |
| json.put('/'); | |
| break; | |
| case '\b': json.put("\\b"); break; | |
| case '\f': json.put("\\f"); break; | |
| case '\n': json.put("\\n"); break; | |
| case '\r': json.put("\\r"); break; | |
| case '\t': json.put("\\t"); break; | |
| default: | |
| { | |
| import std.ascii : isControl; | |
| import std.utf : encode; | |
| // Make sure we do UTF decoding iff we want to | |
| // escape Unicode characters. | |
| assert(((options & JSONOptions.escapeNonAsciiChars) != 0) | |
| == is(Char == dchar), "JSONOptions.escapeNonAsciiChars needs dchar strings"); | |
| with (JSONOptions) if (isControl(c) || | |
| ((options & escapeNonAsciiChars) >= escapeNonAsciiChars && c >= 0x80)) | |
| { | |
| // Ensure non-BMP characters are encoded as a pair | |
| // of UTF-16 surrogate characters, as per RFC 4627. | |
| wchar[2] wchars; // 1 or 2 UTF-16 code units | |
| size_t wNum = encode(wchars, c); // number of UTF-16 code units | |
| foreach (wc; wchars[0 .. wNum]) | |
| { | |
| json.put("\\u"); | |
| foreach_reverse (i; 0 .. 4) | |
| { | |
| char ch = (wc >>> (4 * i)) & 0x0f; | |
| ch += ch < 10 ? '0' : 'A' - 10; | |
| json.put(ch); | |
| } | |
| } | |
| } | |
| else | |
| { | |
| json.put(c); | |
| } | |
| } | |
| } | |
| } | |
| json.put('"'); | |
| } | |
| void toString(string str) | |
| { | |
| // Avoid UTF decoding when possible, as it is unnecessary when | |
| // processing JSON. | |
| if (options & JSONOptions.escapeNonAsciiChars) | |
| toStringImpl!dchar(str); | |
| else | |
| toStringImpl!char(str); | |
| } | |
| /* make the function infer @system when json.put() is @system | |
| */ | |
| if (0) | |
| json.put(' '); | |
| /* Mark as @trusted because json.put() may be @system. This has difficulty | |
| * inferring @safe because it is recursive. | |
| */ | |
| void toValueImpl(ref const JSONValue value, ulong indentLevel) @trusted | |
| { | |
| void putTabs(ulong additionalIndent = 0) | |
| { | |
| if (pretty) | |
| foreach (i; 0 .. indentLevel + additionalIndent) | |
| json.put(" "); | |
| } | |
| void putEOL() | |
| { | |
| if (pretty) | |
| json.put('\n'); | |
| } | |
| void putCharAndEOL(char ch) | |
| { | |
| json.put(ch); | |
| putEOL(); | |
| } | |
| final switch (value.type) | |
| { | |
| case JSONType.object: | |
| if (value.isOrdered) | |
| { | |
| auto obj = value.orderedObjectNoRef; | |
| if (!obj.length) | |
| { | |
| json.put("{}"); | |
| } | |
| else | |
| { | |
| putCharAndEOL('{'); | |
| bool first = true; | |
| foreach (pair; obj) | |
| { | |
| if (!first) | |
| putCharAndEOL(','); | |
| first = false; | |
| putTabs(1); | |
| toString(pair.key); | |
| json.put(':'); | |
| if (pretty) | |
| json.put(' '); | |
| toValueImpl(pair.value, indentLevel + 1); | |
| } | |
| putEOL(); | |
| putTabs(); | |
| json.put('}'); | |
| } | |
| } | |
| else | |
| { | |
| auto obj = value.objectNoRef; | |
| if (!obj.length) | |
| { | |
| json.put("{}"); | |
| } | |
| else | |
| { | |
| putCharAndEOL('{'); | |
| bool first = true; | |
| void emit(R)(R names) | |
| { | |
| foreach (name; names) | |
| { | |
| auto member = obj[name]; | |
| if (!first) | |
| putCharAndEOL(','); | |
| first = false; | |
| putTabs(1); | |
| toString(name); | |
| json.put(':'); | |
| if (pretty) | |
| json.put(' '); | |
| toValueImpl(member, indentLevel + 1); | |
| } | |
| } | |
| import std.algorithm.sorting : sort; | |
| // https://issues.dlang.org/show_bug.cgi?id=14439 | |
| // auto names = obj.keys; // aa.keys can't be called in @safe code | |
| auto names = new string[obj.length]; | |
| size_t i = 0; | |
| foreach (k, v; obj) | |
| { | |
| names[i] = k; | |
| i++; | |
| } | |
| sort(names); | |
| emit(names); | |
| putEOL(); | |
| putTabs(); | |
| json.put('}'); | |
| } | |
| } | |
| break; | |
| case JSONType.array: | |
| auto arr = value.arrayNoRef; | |
| if (arr.empty) | |
| { | |
| json.put("[]"); | |
| } | |
| else | |
| { | |
| putCharAndEOL('['); | |
| foreach (i, el; arr) | |
| { | |
| if (i) | |
| putCharAndEOL(','); | |
| putTabs(1); | |
| toValueImpl(el, indentLevel + 1); | |
| } | |
| putEOL(); | |
| putTabs(); | |
| json.put(']'); | |
| } | |
| break; | |
| case JSONType.string: | |
| toString(value.str); | |
| break; | |
| case JSONType.integer: | |
| json.put(to!string(value.store.integer)); | |
| break; | |
| case JSONType.uinteger: | |
| json.put(to!string(value.store.uinteger)); | |
| break; | |
| case JSONType.float_: | |
| import std.math.traits : isNaN, isInfinity; | |
| auto val = value.store.floating; | |
| if (val.isNaN) | |
| { | |
| if (options & JSONOptions.specialFloatLiterals) | |
| { | |
| toString(JSONFloatLiteral.nan); | |
| } | |
| else | |
| { | |
| throw new JSONException( | |
| "Cannot encode NaN. Consider passing the specialFloatLiterals flag."); | |
| } | |
| } | |
| else if (val.isInfinity) | |
| { | |
| if (options & JSONOptions.specialFloatLiterals) | |
| { | |
| toString((val > 0) ? JSONFloatLiteral.inf : JSONFloatLiteral.negativeInf); | |
| } | |
| else | |
| { | |
| throw new JSONException( | |
| "Cannot encode Infinity. Consider passing the specialFloatLiterals flag."); | |
| } | |
| } | |
| else | |
| { | |
| import std.algorithm.searching : canFind; | |
| import std.format : sformat; | |
| // The correct formula for the number of decimal digits needed for lossless round | |
| // trips is actually: | |
| // ceil(log(pow(2.0, double.mant_dig - 1)) / log(10.0) + 1) == (double.dig + 2) | |
| // Anything less will round off (1 + double.epsilon) | |
| char[25] buf; | |
| auto result = buf[].sformat!"%.18g"(val); | |
| json.put(result); | |
| if (!result.canFind('e') && !result.canFind('.')) | |
| json.put(".0"); | |
| } | |
| break; | |
| case JSONType.true_: | |
| json.put("true"); | |
| break; | |
| case JSONType.false_: | |
| json.put("false"); | |
| break; | |
| case JSONType.null_: | |
| json.put("null"); | |
| break; | |
| } | |
| } | |
| toValueImpl(root, 0); | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=12897 | |
| @safe unittest | |
| { | |
| JSONValue jv0 = JSONValue("test测试"); | |
| assert(toJSON(jv0, false, JSONOptions.escapeNonAsciiChars) == `"test\u6D4B\u8BD5"`); | |
| JSONValue jv00 = JSONValue("test\u6D4B\u8BD5"); | |
| assert(toJSON(jv00, false, JSONOptions.none) == `"test测试"`); | |
| assert(toJSON(jv0, false, JSONOptions.none) == `"test测试"`); | |
| JSONValue jv1 = JSONValue("été"); | |
| assert(toJSON(jv1, false, JSONOptions.escapeNonAsciiChars) == `"\u00E9t\u00E9"`); | |
| JSONValue jv11 = JSONValue("\u00E9t\u00E9"); | |
| assert(toJSON(jv11, false, JSONOptions.none) == `"été"`); | |
| assert(toJSON(jv1, false, JSONOptions.none) == `"été"`); | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=20511 | |
| @system unittest | |
| { | |
| import std.format.write : formattedWrite; | |
| import std.range : nullSink, outputRangeObject; | |
| outputRangeObject!(const(char)[])(nullSink) | |
| .formattedWrite!"%s"(JSONValue.init); | |
| } | |
| // Issue 16432 - JSON incorrectly parses to string | |
| @safe unittest | |
| { | |
| // Floating points numbers are rounded to the nearest integer and thus get | |
| // incorrectly parsed | |
| import std.math.operations : isClose; | |
| string s = "{\"rating\": 3.0 }"; | |
| JSONValue j = parseJSON(s); | |
| assert(j["rating"].type == JSONType.float_); | |
| j = j.toString.parseJSON; | |
| assert(j["rating"].type == JSONType.float_); | |
| assert(isClose(j["rating"].floating, 3.0)); | |
| s = "{\"rating\": -3.0 }"; | |
| j = parseJSON(s); | |
| assert(j["rating"].type == JSONType.float_); | |
| j = j.toString.parseJSON; | |
| assert(j["rating"].type == JSONType.float_); | |
| assert(isClose(j["rating"].floating, -3.0)); | |
| // https://issues.dlang.org/show_bug.cgi?id=13660 | |
| auto jv1 = JSONValue(4.0); | |
| auto textual = jv1.toString(); | |
| auto jv2 = parseJSON(textual); | |
| assert(jv1.type == JSONType.float_); | |
| assert(textual == "4.0"); | |
| assert(jv2.type == JSONType.float_); | |
| } | |
| @safe unittest | |
| { | |
| // Adapted from https://github.com/dlang/phobos/pull/5005 | |
| // Result from toString is not checked here, because this | |
| // might differ (%e-like or %f-like output) depending | |
| // on OS and compiler optimization. | |
| import std.math.operations : isClose; | |
| // test positive extreme values | |
| JSONValue j; | |
| j["rating"] = 1e18 - 65; | |
| assert(isClose(j.toString.parseJSON["rating"].floating, 1e18 - 65)); | |
| j["rating"] = 1e18 - 64; | |
| assert(isClose(j.toString.parseJSON["rating"].floating, 1e18 - 64)); | |
| // negative extreme values | |
| j["rating"] = -1e18 + 65; | |
| assert(isClose(j.toString.parseJSON["rating"].floating, -1e18 + 65)); | |
| j["rating"] = -1e18 + 64; | |
| assert(isClose(j.toString.parseJSON["rating"].floating, -1e18 + 64)); | |
| } | |
| /** | |
| Exception thrown on JSON errors | |
| */ | |
| class JSONException : Exception | |
| { | |
| this(string msg, int line = 0, int pos = 0) pure nothrow @safe | |
| { | |
| if (line) | |
| super(text(msg, " (Line ", line, ":", pos, ")")); | |
| else | |
| super(msg); | |
| } | |
| this(string msg, string file, size_t line) pure nothrow @safe | |
| { | |
| super(msg, file, line); | |
| } | |
| } | |
| @system unittest | |
| { | |
| import std.exception; | |
| JSONValue jv = "123"; | |
| assert(jv.type == JSONType.string); | |
| assertNotThrown(jv.str); | |
| assertThrown!JSONException(jv.integer); | |
| assertThrown!JSONException(jv.uinteger); | |
| assertThrown!JSONException(jv.floating); | |
| assertThrown!JSONException(jv.object); | |
| assertThrown!JSONException(jv.array); | |
| assertThrown!JSONException(jv["aa"]); | |
| assertThrown!JSONException(jv[2]); | |
| jv = -3; | |
| assert(jv.type == JSONType.integer); | |
| assertNotThrown(jv.integer); | |
| jv = cast(uint) 3; | |
| assert(jv.type == JSONType.uinteger); | |
| assertNotThrown(jv.uinteger); | |
| jv = 3.0; | |
| assert(jv.type == JSONType.float_); | |
| assertNotThrown(jv.floating); | |
| jv = ["key" : "value"]; | |
| assert(jv.type == JSONType.object); | |
| assertNotThrown(jv.object); | |
| assertNotThrown(jv["key"]); | |
| assert("key" in jv); | |
| assert("notAnElement" !in jv); | |
| assertThrown!JSONException(jv["notAnElement"]); | |
| const cjv = jv; | |
| assert("key" in cjv); | |
| assertThrown!JSONException(cjv["notAnElement"]); | |
| foreach (string key, value; jv) | |
| { | |
| static assert(is(typeof(value) == JSONValue)); | |
| assert(key == "key"); | |
| assert(value.type == JSONType.string); | |
| assertNotThrown(value.str); | |
| assert(value.str == "value"); | |
| } | |
| jv = [3, 4, 5]; | |
| assert(jv.type == JSONType.array); | |
| assertNotThrown(jv.array); | |
| assertNotThrown(jv[2]); | |
| foreach (size_t index, value; jv) | |
| { | |
| static assert(is(typeof(value) == JSONValue)); | |
| assert(value.type == JSONType.integer); | |
| assertNotThrown(value.integer); | |
| assert(index == (value.integer-3)); | |
| } | |
| jv = null; | |
| assert(jv.type == JSONType.null_); | |
| assert(jv.isNull); | |
| jv = "foo"; | |
| assert(!jv.isNull); | |
| jv = JSONValue("value"); | |
| assert(jv.type == JSONType.string); | |
| assert(jv.str == "value"); | |
| JSONValue jv2 = JSONValue("value"); | |
| assert(jv2.type == JSONType.string); | |
| assert(jv2.str == "value"); | |
| JSONValue jv3 = JSONValue("\u001c"); | |
| assert(jv3.type == JSONType.string); | |
| assert(jv3.str == "\u001C"); | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=11504 | |
| @system unittest | |
| { | |
| JSONValue jv = 1; | |
| assert(jv.type == JSONType.integer); | |
| jv.str = "123"; | |
| assert(jv.type == JSONType.string); | |
| assert(jv.str == "123"); | |
| jv.integer = 1; | |
| assert(jv.type == JSONType.integer); | |
| assert(jv.integer == 1); | |
| jv.uinteger = 2u; | |
| assert(jv.type == JSONType.uinteger); | |
| assert(jv.uinteger == 2u); | |
| jv.floating = 1.5; | |
| assert(jv.type == JSONType.float_); | |
| assert(jv.floating == 1.5); | |
| jv.object = ["key" : JSONValue("value")]; | |
| assert(jv.type == JSONType.object); | |
| assert(jv.object == ["key" : JSONValue("value")]); | |
| jv.array = [JSONValue(1), JSONValue(2), JSONValue(3)]; | |
| assert(jv.type == JSONType.array); | |
| assert(jv.array == [JSONValue(1), JSONValue(2), JSONValue(3)]); | |
| jv = true; | |
| assert(jv.type == JSONType.true_); | |
| jv = false; | |
| assert(jv.type == JSONType.false_); | |
| enum E{True = true} | |
| jv = E.True; | |
| assert(jv.type == JSONType.true_); | |
| } | |
| @system pure unittest | |
| { | |
| // Adding new json element via array() / object() directly | |
| JSONValue jarr = JSONValue([10]); | |
| foreach (i; 0 .. 9) | |
| jarr.array ~= JSONValue(i); | |
| assert(jarr.array.length == 10); | |
| JSONValue jobj = JSONValue(["key" : JSONValue("value")]); | |
| foreach (i; 0 .. 9) | |
| jobj.object[text("key", i)] = JSONValue(text("value", i)); | |
| assert(jobj.object.length == 10); | |
| } | |
| @system pure unittest | |
| { | |
| // Adding new json element without array() / object() access | |
| JSONValue jarr = JSONValue([10]); | |
| foreach (i; 0 .. 9) | |
| jarr ~= [JSONValue(i)]; | |
| assert(jarr.array.length == 10); | |
| JSONValue jobj = JSONValue(["key" : JSONValue("value")]); | |
| foreach (i; 0 .. 9) | |
| jobj[text("key", i)] = JSONValue(text("value", i)); | |
| assert(jobj.object.length == 10); | |
| // No array alias | |
| auto jarr2 = jarr ~ [1,2,3]; | |
| jarr2[0] = 999; | |
| assert(jarr[0] == JSONValue(10)); | |
| } | |
| @system unittest | |
| { | |
| // @system because JSONValue.array is @system | |
| import std.exception; | |
| // An overly simple test suite, if it can parse a serializated string and | |
| // then use the resulting values tree to generate an identical | |
| // serialization, both the decoder and encoder works. | |
| auto jsons = [ | |
| `null`, | |
| `true`, | |
| `false`, | |
| `0`, | |
| `123`, | |
| `-4321`, | |
| `0.25`, | |
| `-0.25`, | |
| `""`, | |
| `"hello\nworld"`, | |
| `"\"\\\/\b\f\n\r\t"`, | |
| `[]`, | |
| `[12,"foo",true,false]`, | |
| `{}`, | |
| `{"a":1,"b":null}`, | |
| `{"goodbye":[true,"or",false,["test",42,{"nested":{"a":23.5,"b":0.140625}}]],` | |
| ~`"hello":{"array":[12,null,{}],"json":"is great"}}`, | |
| ]; | |
| enum dbl1_844 = `1.8446744073709568`; | |
| version (MinGW) | |
| jsons ~= dbl1_844 ~ `e+019`; | |
| else | |
| jsons ~= dbl1_844 ~ `e+19`; | |
| JSONValue val; | |
| string result; | |
| foreach (json; jsons) | |
| { | |
| try | |
| { | |
| val = parseJSON(json); | |
| enum pretty = false; | |
| result = toJSON(val, pretty); | |
| assert(result == json, text(result, " should be ", json)); | |
| } | |
| catch (JSONException e) | |
| { | |
| import std.stdio : writefln; | |
| writefln(text(json, "\n", e.toString())); | |
| } | |
| } | |
| // Should be able to correctly interpret unicode entities | |
| val = parseJSON(`"\u003C\u003E"`); | |
| assert(toJSON(val) == "\"\<\>\""); | |
| assert(val.to!string() == "\"\<\>\""); | |
| val = parseJSON(`"\u0391\u0392\u0393"`); | |
| assert(toJSON(val) == "\"\Α\Β\Γ\""); | |
| assert(val.to!string() == "\"\Α\Β\Γ\""); | |
| val = parseJSON(`"\u2660\u2666"`); | |
| assert(toJSON(val) == "\"\♠\♦\""); | |
| assert(val.to!string() == "\"\♠\♦\""); | |
| //0x7F is a control character (see Unicode spec) | |
| val = parseJSON(`"\u007F"`); | |
| assert(toJSON(val) == "\"\\u007F\""); | |
| assert(val.to!string() == "\"\\u007F\""); | |
| with(parseJSON(`""`)) | |
| assert(str == "" && str !is null); | |
| with(parseJSON(`[]`)) | |
| assert(!array.length); | |
| // Formatting | |
| val = parseJSON(`{"a":[null,{"x":1},{},[]]}`); | |
| assert(toJSON(val, true) == `{ | |
| "a": [ | |
| null, | |
| { | |
| "x": 1 | |
| }, | |
| {}, | |
| [] | |
| ] | |
| }`); | |
| } | |
| @safe unittest | |
| { | |
| auto json = `"hello\nworld"`; | |
| const jv = parseJSON(json); | |
| assert(jv.toString == json); | |
| assert(jv.toPrettyString == json); | |
| } | |
| @system pure unittest | |
| { | |
| // https://issues.dlang.org/show_bug.cgi?id=12969 | |
| JSONValue jv; | |
| jv["int"] = 123; | |
| assert(jv.type == JSONType.object); | |
| assert("int" in jv); | |
| assert(jv["int"].integer == 123); | |
| jv["array"] = [1, 2, 3, 4, 5]; | |
| assert(jv["array"].type == JSONType.array); | |
| assert(jv["array"][2].integer == 3); | |
| jv["str"] = "D language"; | |
| assert(jv["str"].type == JSONType.string); | |
| assert(jv["str"].str == "D language"); | |
| jv["bool"] = false; | |
| assert(jv["bool"].type == JSONType.false_); | |
| assert(jv.object.length == 4); | |
| jv = [5, 4, 3, 2, 1]; | |
| assert(jv.type == JSONType.array); | |
| assert(jv[3].integer == 2); | |
| } | |
| @safe unittest | |
| { | |
| auto s = q"EOF | |
| [ | |
| 1, | |
| 2, | |
| 3, | |
| potato | |
| ] | |
| EOF"; | |
| import std.exception; | |
| auto e = collectException!JSONException(parseJSON(s)); | |
| assert(e.msg == "Unexpected character 'p'. (Line 5:3)", e.msg); | |
| } | |
| // handling of special float values (NaN, Inf, -Inf) | |
| @safe unittest | |
| { | |
| import std.exception : assertThrown; | |
| import std.math.traits : isNaN, isInfinity; | |
| // expected representations of NaN and Inf | |
| enum { | |
| nanString = '"' ~ JSONFloatLiteral.nan ~ '"', | |
| infString = '"' ~ JSONFloatLiteral.inf ~ '"', | |
| negativeInfString = '"' ~ JSONFloatLiteral.negativeInf ~ '"', | |
| } | |
| // with the specialFloatLiterals option, encode NaN/Inf as strings | |
| assert(JSONValue(float.nan).toString(JSONOptions.specialFloatLiterals) == nanString); | |
| assert(JSONValue(double.infinity).toString(JSONOptions.specialFloatLiterals) == infString); | |
| assert(JSONValue(-real.infinity).toString(JSONOptions.specialFloatLiterals) == negativeInfString); | |
| // without the specialFloatLiterals option, throw on encoding NaN/Inf | |
| assertThrown!JSONException(JSONValue(float.nan).toString); | |
| assertThrown!JSONException(JSONValue(double.infinity).toString); | |
| assertThrown!JSONException(JSONValue(-real.infinity).toString); | |
| // when parsing json with specialFloatLiterals option, decode special strings as floats | |
| JSONValue jvNan = parseJSON(nanString, JSONOptions.specialFloatLiterals); | |
| JSONValue jvInf = parseJSON(infString, JSONOptions.specialFloatLiterals); | |
| JSONValue jvNegInf = parseJSON(negativeInfString, JSONOptions.specialFloatLiterals); | |
| assert(jvNan.floating.isNaN); | |
| assert(jvInf.floating.isInfinity && jvInf.floating > 0); | |
| assert(jvNegInf.floating.isInfinity && jvNegInf.floating < 0); | |
| // when parsing json without the specialFloatLiterals option, decode special strings as strings | |
| jvNan = parseJSON(nanString); | |
| jvInf = parseJSON(infString); | |
| jvNegInf = parseJSON(negativeInfString); | |
| assert(jvNan.str == JSONFloatLiteral.nan); | |
| assert(jvInf.str == JSONFloatLiteral.inf); | |
| assert(jvNegInf.str == JSONFloatLiteral.negativeInf); | |
| } | |
| pure nothrow @safe @nogc unittest | |
| { | |
| JSONValue testVal; | |
| testVal = "test"; | |
| testVal = 10; | |
| testVal = 10u; | |
| testVal = 1.0; | |
| testVal = (JSONValue[string]).init; | |
| testVal = JSONValue[].init; | |
| testVal = null; | |
| assert(testVal.isNull); | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=15884 | |
| pure nothrow @safe unittest | |
| { | |
| import std.typecons; | |
| void Test(C)() { | |
| C[] a = ['x']; | |
| JSONValue testVal = a; | |
| assert(testVal.type == JSONType.string); | |
| testVal = a.idup; | |
| assert(testVal.type == JSONType.string); | |
| } | |
| Test!char(); | |
| Test!wchar(); | |
| Test!dchar(); | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=15885 | |
| @safe unittest | |
| { | |
| enum bool realInDoublePrecision = real.mant_dig == double.mant_dig; | |
| static bool test(const double num0) | |
| { | |
| import std.math.operations : feqrel; | |
| const json0 = JSONValue(num0); | |
| const num1 = to!double(toJSON(json0)); | |
| static if (realInDoublePrecision) | |
| return feqrel(num1, num0) >= (double.mant_dig - 1); | |
| else | |
| return num1 == num0; | |
| } | |
| assert(test( 0.23)); | |
| assert(test(-0.23)); | |
| assert(test(1.223e+24)); | |
| assert(test(23.4)); | |
| assert(test(0.0012)); | |
| assert(test(30738.22)); | |
| assert(test(1 + double.epsilon)); | |
| assert(test(double.min_normal)); | |
| static if (realInDoublePrecision) | |
| assert(test(-double.max / 2)); | |
| else | |
| assert(test(-double.max)); | |
| const minSub = double.min_normal * double.epsilon; | |
| assert(test(minSub)); | |
| assert(test(3*minSub)); | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=17555 | |
| @safe unittest | |
| { | |
| import std.exception : assertThrown; | |
| assertThrown!JSONException(parseJSON("\"a\nb\"")); | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=17556 | |
| @safe unittest | |
| { | |
| auto v = JSONValue("\U0001D11E"); | |
| auto j = toJSON(v, false, JSONOptions.escapeNonAsciiChars); | |
| assert(j == `"\uD834\uDD1E"`); | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=5904 | |
| @safe unittest | |
| { | |
| string s = `"\uD834\uDD1E"`; | |
| auto j = parseJSON(s); | |
| assert(j.str == "\U0001D11E"); | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=17557 | |
| @safe unittest | |
| { | |
| assert(parseJSON("\"\xFF\"").str == "\xFF"); | |
| assert(parseJSON("\"\U0001D11E\"").str == "\U0001D11E"); | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=17553 | |
| @safe unittest | |
| { | |
| auto v = JSONValue("\xFF"); | |
| assert(toJSON(v) == "\"\xFF\""); | |
| } | |
| @safe unittest | |
| { | |
| import std.utf; | |
| assert(parseJSON("\"\xFF\"".byChar).str == "\xFF"); | |
| assert(parseJSON("\"\U0001D11E\"".byChar).str == "\U0001D11E"); | |
| } | |
| // JSONOptions.doNotEscapeSlashes (https://issues.dlang.org/show_bug.cgi?id=17587) | |
| @safe unittest | |
| { | |
| assert(parseJSON(`"/"`).toString == `"\/"`); | |
| assert(parseJSON(`"\/"`).toString == `"\/"`); | |
| assert(parseJSON(`"/"`).toString(JSONOptions.doNotEscapeSlashes) == `"/"`); | |
| assert(parseJSON(`"\/"`).toString(JSONOptions.doNotEscapeSlashes) == `"/"`); | |
| } | |
| // JSONOptions.strictParsing (https://issues.dlang.org/show_bug.cgi?id=16639) | |
| @safe unittest | |
| { | |
| import std.exception : assertThrown; | |
| // Unescaped ASCII NULs | |
| assert(parseJSON("[\0]").type == JSONType.array); | |
| assertThrown!JSONException(parseJSON("[\0]", JSONOptions.strictParsing)); | |
| assert(parseJSON("\"\0\"").str == "\0"); | |
| assertThrown!JSONException(parseJSON("\"\0\"", JSONOptions.strictParsing)); | |
| // Unescaped ASCII DEL (0x7f) in strings | |
| assert(parseJSON("\"\x7f\"").str == "\x7f"); | |
| assert(parseJSON("\"\x7f\"", JSONOptions.strictParsing).str == "\x7f"); | |
| // "true", "false", "null" case sensitivity | |
| assert(parseJSON("true").type == JSONType.true_); | |
| assert(parseJSON("true", JSONOptions.strictParsing).type == JSONType.true_); | |
| assert(parseJSON("True").type == JSONType.true_); | |
| assertThrown!JSONException(parseJSON("True", JSONOptions.strictParsing)); | |
| assert(parseJSON("tRUE").type == JSONType.true_); | |
| assertThrown!JSONException(parseJSON("tRUE", JSONOptions.strictParsing)); | |
| assert(parseJSON("false").type == JSONType.false_); | |
| assert(parseJSON("false", JSONOptions.strictParsing).type == JSONType.false_); | |
| assert(parseJSON("False").type == JSONType.false_); | |
| assertThrown!JSONException(parseJSON("False", JSONOptions.strictParsing)); | |
| assert(parseJSON("fALSE").type == JSONType.false_); | |
| assertThrown!JSONException(parseJSON("fALSE", JSONOptions.strictParsing)); | |
| assert(parseJSON("null").type == JSONType.null_); | |
| assert(parseJSON("null", JSONOptions.strictParsing).type == JSONType.null_); | |
| assert(parseJSON("Null").type == JSONType.null_); | |
| assertThrown!JSONException(parseJSON("Null", JSONOptions.strictParsing)); | |
| assert(parseJSON("nULL").type == JSONType.null_); | |
| assertThrown!JSONException(parseJSON("nULL", JSONOptions.strictParsing)); | |
| // Whitespace characters | |
| assert(parseJSON("[\f\v]").type == JSONType.array); | |
| assertThrown!JSONException(parseJSON("[\f\v]", JSONOptions.strictParsing)); | |
| assert(parseJSON("[ \t\r\n]").type == JSONType.array); | |
| assert(parseJSON("[ \t\r\n]", JSONOptions.strictParsing).type == JSONType.array); | |
| // Empty input | |
| assert(parseJSON("").type == JSONType.null_); | |
| assertThrown!JSONException(parseJSON("", JSONOptions.strictParsing)); | |
| // Numbers with leading '0's | |
| assert(parseJSON("01").integer == 1); | |
| assertThrown!JSONException(parseJSON("01", JSONOptions.strictParsing)); | |
| assert(parseJSON("-01").integer == -1); | |
| assertThrown!JSONException(parseJSON("-01", JSONOptions.strictParsing)); | |
| assert(parseJSON("0.01").floating == 0.01); | |
| assert(parseJSON("0.01", JSONOptions.strictParsing).floating == 0.01); | |
| assert(parseJSON("0e1").floating == 0); | |
| assert(parseJSON("0e1", JSONOptions.strictParsing).floating == 0); | |
| // Trailing characters after JSON value | |
| assert(parseJSON(`""asdf`).str == ""); | |
| assertThrown!JSONException(parseJSON(`""asdf`, JSONOptions.strictParsing)); | |
| assert(parseJSON("987\0").integer == 987); | |
| assertThrown!JSONException(parseJSON("987\0", JSONOptions.strictParsing)); | |
| assert(parseJSON("987\0\0").integer == 987); | |
| assertThrown!JSONException(parseJSON("987\0\0", JSONOptions.strictParsing)); | |
| assert(parseJSON("[]]").type == JSONType.array); | |
| assertThrown!JSONException(parseJSON("[]]", JSONOptions.strictParsing)); | |
| assert(parseJSON("123 \t\r\n").integer == 123); // Trailing whitespace is OK | |
| assert(parseJSON("123 \t\r\n", JSONOptions.strictParsing).integer == 123); | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.iteration : map; | |
| import std.array : array; | |
| import std.exception : assertThrown; | |
| string s = `{ "a" : [1,2,3,], }`; | |
| JSONValue j = parseJSON(s); | |
| assert(j["a"].array().map!(i => i.integer()).array == [1,2,3]); | |
| assertThrown(parseJSON(s, -1, JSONOptions.strictParsing)); | |
| } | |
| @system unittest | |
| { | |
| import std.algorithm.iteration : map; | |
| import std.array : array; | |
| import std.exception : assertThrown; | |
| string s = `{ "a" : { } , }`; | |
| JSONValue j = parseJSON(s); | |
| assert("a" in j); | |
| auto t = j["a"].object(); | |
| assert(t.empty); | |
| assertThrown(parseJSON(s, -1, JSONOptions.strictParsing)); | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=20330 | |
| @safe unittest | |
| { | |
| import std.array : appender; | |
| string s = `{"a":[1,2,3]}`; | |
| JSONValue j = parseJSON(s); | |
| auto app = appender!string(); | |
| j.toString(app); | |
| assert(app.data == s, app.data); | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=20330 | |
| @safe unittest | |
| { | |
| import std.array : appender; | |
| import std.format.write : formattedWrite; | |
| string s = | |
| `{ | |
| "a": [ | |
| 1, | |
| 2, | |
| 3 | |
| ] | |
| }`; | |
| JSONValue j = parseJSON(s); | |
| auto app = appender!string(); | |
| j.toPrettyString(app); | |
| assert(app.data == s, app.data); | |
| } | |
| // https://issues.dlang.org/show_bug.cgi?id=24823 - JSONOptions.preserveObjectOrder | |
| @safe unittest | |
| { | |
| import std.array : appender; | |
| string s = `{"b":2,"a":1}`; | |
| JSONValue j = parseJSON(s, -1, JSONOptions.preserveObjectOrder); | |
| auto app = appender!string(); | |
| j.toString(app); | |
| assert(app.data == s, app.data); | |
| } |