mirror of
https://github.com/dlang/phobos.git
synced 2025-04-26 13:10:35 +03:00
2797 lines
84 KiB
D
2797 lines
84 KiB
D
// 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;
|
|
string t = arg;
|
|
() @trusted { store.str = t; }();
|
|
}
|
|
// 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.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.uinteger = arg;
|
|
}
|
|
else static if (is(T : long))
|
|
{
|
|
type_tag = JSONType.integer;
|
|
store.integer = arg;
|
|
}
|
|
else static if (isFloatingPoint!T)
|
|
{
|
|
type_tag = JSONType.float_;
|
|
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))
|
|
{
|
|
JSONValue[string] t = arg;
|
|
() @trusted {
|
|
store.object.isOrdered = false;
|
|
store.object.unordered = t;
|
|
}();
|
|
}
|
|
else
|
|
{
|
|
JSONValue[string] aa;
|
|
foreach (key, value; arg)
|
|
aa[key] = JSONValue(value);
|
|
() @trusted {
|
|
store.object.isOrdered = false;
|
|
store.object.unordered = aa;
|
|
}();
|
|
}
|
|
}
|
|
else static if (is(T : OrderedObjectMember[]))
|
|
{
|
|
type_tag = JSONType.object;
|
|
() @trusted {
|
|
store.object.isOrdered = true;
|
|
store.object.ordered = arg;
|
|
}();
|
|
}
|
|
else static if (isArray!T)
|
|
{
|
|
type_tag = JSONType.array;
|
|
static if (is(ElementEncodingType!T : JSONValue))
|
|
{
|
|
JSONValue[] t = arg;
|
|
() @trusted { store.array = t; }();
|
|
}
|
|
else
|
|
{
|
|
JSONValue[] new_arg = new JSONValue[arg.length];
|
|
foreach (i, e; arg)
|
|
new_arg[i] = JSONValue(e);
|
|
() @trusted { 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.array = arg;
|
|
}
|
|
else
|
|
{
|
|
JSONValue[] new_arg = new JSONValue[arg.length];
|
|
foreach (i, e; arg)
|
|
new_arg[i] = JSONValue(e);
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// Calculate a numerical hash value for this value,
|
|
/// allowing `JSONValue` to be used in associative arrays.
|
|
hash_t toHash() const @nogc nothrow pure @trusted
|
|
{
|
|
final switch (type_tag)
|
|
{
|
|
case JSONType.integer:
|
|
return hashOf(store.integer);
|
|
case JSONType.uinteger:
|
|
return hashOf(store.uinteger);
|
|
case JSONType.float_:
|
|
return hashOf(store.floating);
|
|
case JSONType.string:
|
|
return hashOf(store.str);
|
|
case JSONType.object:
|
|
hash_t result = hashOf(type_tag);
|
|
if (!store.object.isOrdered)
|
|
foreach (ref pair; store.object.unordered.byKeyValue)
|
|
result ^= hashOf(pair.key, pair.value.toHash());
|
|
else
|
|
foreach (ref pair; store.object.ordered)
|
|
result ^= hashOf(pair.key, pair.value.toHash());
|
|
return result;
|
|
case JSONType.array:
|
|
hash_t result = hashOf(type_tag);
|
|
foreach (ref v; store.array)
|
|
result = hashOf(v, result);
|
|
return result;
|
|
case JSONType.true_:
|
|
case JSONType.false_:
|
|
case JSONType.null_:
|
|
return hashOf(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.floating = parse!double(data);
|
|
}
|
|
else
|
|
{
|
|
if (isNegative)
|
|
{
|
|
value.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.uinteger = u;
|
|
value.type_tag = JSONType.uinteger;
|
|
}
|
|
else
|
|
{
|
|
value.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);
|
|
}
|
|
|
|
@safe unittest
|
|
{
|
|
bool[JSONValue] aa;
|
|
aa[JSONValue("test")] = true;
|
|
assert(parseJSON(`"test"`) in aa);
|
|
assert(parseJSON(`"boo"`) !in aa);
|
|
|
|
aa[JSONValue(int(5))] = true;
|
|
assert(JSONValue(int(5)) in aa);
|
|
assert(JSONValue(uint(5)) in aa);
|
|
}
|