mirror of
https://github.com/dlang/phobos.git
synced 2025-04-26 21:22:20 +03:00
1192 lines
33 KiB
D
1192 lines
33 KiB
D
// Written in the D programming language.
|
|
|
|
/**
|
|
JavaScript Object Notation
|
|
|
|
Copyright: Copyright Jeremie Pelletier 2008 - 2009.
|
|
License: $(WEB www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
|
|
Authors: Jeremie Pelletier, David Herberth
|
|
References: $(LINK http://json.org/)
|
|
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.ascii;
|
|
import std.conv;
|
|
import std.range;
|
|
import std.utf;
|
|
import std.traits;
|
|
import std.exception;
|
|
|
|
private
|
|
{
|
|
// Prevent conflicts from these generic names
|
|
alias UTFStride = std.utf.stride;
|
|
alias toUnicode = std.utf.decode;
|
|
}
|
|
|
|
/**
|
|
JSON type enumeration
|
|
*/
|
|
enum JSON_TYPE : byte
|
|
{
|
|
/// Indicates the type of a $(D JSONValue).
|
|
STRING,
|
|
INTEGER, /// ditto
|
|
UINTEGER,/// ditto
|
|
FLOAT, /// ditto
|
|
OBJECT, /// ditto
|
|
ARRAY, /// ditto
|
|
TRUE, /// ditto
|
|
FALSE, /// ditto
|
|
NULL /// ditto
|
|
}
|
|
|
|
/**
|
|
JSON value node
|
|
*/
|
|
struct JSONValue
|
|
{
|
|
union Store
|
|
{
|
|
string str;
|
|
long integer;
|
|
ulong uinteger;
|
|
double floating;
|
|
JSONValue[string] object;
|
|
JSONValue[] array;
|
|
}
|
|
private Store store;
|
|
private JSON_TYPE type_tag;
|
|
|
|
/// Specifies the _type of the value stored in this structure.
|
|
@property JSON_TYPE type() const
|
|
{
|
|
return type_tag;
|
|
}
|
|
|
|
/**
|
|
$(RED Deprecated. Instead, please assign the value with the adequate
|
|
type to $(D JSONValue) directly. This will be removed in
|
|
June 2015.)
|
|
|
|
Sets the _type of this $(D JSONValue). Previous content is cleared.
|
|
*/
|
|
deprecated("Please assign the value with the adequate type to JSONValue directly.")
|
|
@property JSON_TYPE type(JSON_TYPE newType)
|
|
{
|
|
if (type_tag != newType
|
|
&& ((type_tag != JSON_TYPE.INTEGER && type_tag != JSON_TYPE.UINTEGER)
|
|
|| (newType != JSON_TYPE.INTEGER && newType != JSON_TYPE.UINTEGER)))
|
|
{
|
|
final switch (newType)
|
|
{
|
|
case JSON_TYPE.STRING:
|
|
store.str = store.str.init;
|
|
break;
|
|
case JSON_TYPE.INTEGER:
|
|
store.integer = store.integer.init;
|
|
break;
|
|
case JSON_TYPE.UINTEGER:
|
|
store.uinteger = store.uinteger.init;
|
|
break;
|
|
case JSON_TYPE.FLOAT:
|
|
store.floating = store.floating.init;
|
|
break;
|
|
case JSON_TYPE.OBJECT:
|
|
store.object = store.object.init;
|
|
break;
|
|
case JSON_TYPE.ARRAY:
|
|
store.array = store.array.init;
|
|
break;
|
|
case JSON_TYPE.TRUE:
|
|
case JSON_TYPE.FALSE:
|
|
case JSON_TYPE.NULL:
|
|
break;
|
|
}
|
|
}
|
|
return type_tag = newType;
|
|
}
|
|
|
|
/// Value getter/setter for $(D JSON_TYPE.STRING).
|
|
/// Throws $(D JSONException) for read access if $(D type) is not $(D JSON_TYPE.STRING).
|
|
@property inout(string) str() inout
|
|
{
|
|
enforce!JSONException(type == JSON_TYPE.STRING,
|
|
"JSONValue is not a string");
|
|
return store.str;
|
|
}
|
|
/// ditto
|
|
@property string str(string v)
|
|
{
|
|
assign(v);
|
|
return store.str;
|
|
}
|
|
|
|
/// Value getter/setter for $(D JSON_TYPE.INTEGER).
|
|
/// Throws $(D JSONException) for read access if $(D type) is not $(D JSON_TYPE.INTEGER).
|
|
@property inout(long) integer() inout
|
|
{
|
|
enforce!JSONException(type == JSON_TYPE.INTEGER,
|
|
"JSONValue is not an integer");
|
|
return store.integer;
|
|
}
|
|
/// ditto
|
|
@property long integer(long v)
|
|
{
|
|
assign(v);
|
|
return store.integer;
|
|
}
|
|
|
|
/// Value getter/setter for $(D JSON_TYPE.UINTEGER).
|
|
/// Throws $(D JSONException) for read access if $(D type) is not $(D JSON_TYPE.UINTEGER).
|
|
@property inout(ulong) uinteger() inout
|
|
{
|
|
enforce!JSONException(type == JSON_TYPE.UINTEGER,
|
|
"JSONValue is not an unsigned integer");
|
|
return store.uinteger;
|
|
}
|
|
/// ditto
|
|
@property ulong uinteger(ulong v)
|
|
{
|
|
assign(v);
|
|
return store.uinteger;
|
|
}
|
|
|
|
/// Value getter/setter for $(D JSON_TYPE.FLOAT).
|
|
/// Throws $(D JSONException) for read access if $(D type) is not $(D JSON_TYPE.FLOAT).
|
|
@property inout(double) floating() inout
|
|
{
|
|
enforce!JSONException(type == JSON_TYPE.FLOAT,
|
|
"JSONValue is not a floating type");
|
|
return store.floating;
|
|
}
|
|
/// ditto
|
|
@property double floating(double v)
|
|
{
|
|
assign(v);
|
|
return store.floating;
|
|
}
|
|
|
|
/// Value getter/setter for $(D JSON_TYPE.OBJECT).
|
|
/// Throws $(D JSONException) for read access if $(D type) is not $(D JSON_TYPE.OBJECT).
|
|
@property ref inout(JSONValue[string]) object() inout
|
|
{
|
|
enforce!JSONException(type == JSON_TYPE.OBJECT,
|
|
"JSONValue is not an object");
|
|
return store.object;
|
|
}
|
|
/// ditto
|
|
@property JSONValue[string] object(JSONValue[string] v)
|
|
{
|
|
assign(v);
|
|
return store.object;
|
|
}
|
|
|
|
/// Value getter/setter for $(D JSON_TYPE.ARRAY).
|
|
/// Throws $(D JSONException) for read access if $(D type) is not $(D JSON_TYPE.ARRAY).
|
|
@property ref inout(JSONValue[]) array() inout
|
|
{
|
|
enforce!JSONException(type == JSON_TYPE.ARRAY,
|
|
"JSONValue is not an array");
|
|
return store.array;
|
|
}
|
|
/// ditto
|
|
@property JSONValue[] array(JSONValue[] v)
|
|
{
|
|
assign(v);
|
|
return store.array;
|
|
}
|
|
|
|
private void assign(T)(T arg)
|
|
{
|
|
static if(is(T : typeof(null)))
|
|
{
|
|
type_tag = JSON_TYPE.NULL;
|
|
}
|
|
else static if(is(T : string))
|
|
{
|
|
type_tag = JSON_TYPE.STRING;
|
|
store.str = arg;
|
|
}
|
|
else static if(is(T : bool))
|
|
{
|
|
type_tag = arg ? JSON_TYPE.TRUE : JSON_TYPE.FALSE;
|
|
}
|
|
else static if(is(T : ulong) && isUnsigned!T)
|
|
{
|
|
type_tag = JSON_TYPE.UINTEGER;
|
|
store.uinteger = arg;
|
|
}
|
|
else static if(is(T : long))
|
|
{
|
|
type_tag = JSON_TYPE.INTEGER;
|
|
store.integer = arg;
|
|
}
|
|
else static if(isFloatingPoint!T)
|
|
{
|
|
type_tag = JSON_TYPE.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 = JSON_TYPE.OBJECT;
|
|
static if(is(Value : JSONValue)) {
|
|
store.object = arg;
|
|
}
|
|
else
|
|
{
|
|
JSONValue[string] aa;
|
|
foreach(key, value; arg)
|
|
aa[key] = JSONValue(value);
|
|
store.object = aa;
|
|
}
|
|
}
|
|
else static if(isArray!T)
|
|
{
|
|
type_tag = JSON_TYPE.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;
|
|
}
|
|
}
|
|
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 = JSON_TYPE.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 $(D JSONValue). If $(D arg) is a $(D JSONValue)
|
|
* its value and type will be copied to the new $(D JSONValue).
|
|
* Note that this is a shallow copy: if type is $(D JSON_TYPE.OBJECT)
|
|
* or $(D JSON_TYPE.ARRAY) then only the reference to the data will
|
|
* be copied.
|
|
* Otherwise, $(D arg) must be implicitly convertible to one of the
|
|
* following types: $(D typeof(null)), $(D string), $(D ulong),
|
|
* $(D long), $(D double), an associative array $(D V[K]) for any $(D V)
|
|
* and $(D K) i.e. a JSON object, any array or $(D 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;
|
|
}
|
|
|
|
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 $(D JSONException) if $(D type) is not $(D JSON_TYPE.ARRAY).
|
|
ref inout(JSONValue) opIndex(size_t i) inout
|
|
{
|
|
enforce!JSONException(type == JSON_TYPE.ARRAY,
|
|
"JSONValue is not an array");
|
|
enforceEx!JSONException(i < store.array.length,
|
|
"JSONValue array index is out of range");
|
|
return store.array[i];
|
|
}
|
|
|
|
/// Hash syntax for json objects.
|
|
/// Throws $(D JSONException) if $(D type) is not $(D JSON_TYPE.OBJECT).
|
|
ref inout(JSONValue) opIndex(string k) inout
|
|
{
|
|
enforce!JSONException(type == JSON_TYPE.OBJECT,
|
|
"JSONValue is not an object");
|
|
return *enforce!JSONException(k in store.object,
|
|
"Key not found: " ~ k);
|
|
}
|
|
|
|
void opIndexAssign(T)(T arg, size_t i)
|
|
{
|
|
enforceEx!JSONException(type == JSON_TYPE.ARRAY,
|
|
"JSONValue is not an array");
|
|
enforceEx!JSONException(i < store.array.length,
|
|
"JSONValue array index is out of range");
|
|
store.array[i] = arg;
|
|
}
|
|
|
|
void opIndexAssign(T)(T arg, string k)
|
|
{
|
|
enforceEx!JSONException(type == JSON_TYPE.OBJECT,
|
|
"JSONValue is not an object");
|
|
store.object[k] = arg;
|
|
}
|
|
|
|
JSONValue opBinary(string op : "~", T)(T arg)
|
|
{
|
|
enforceEx!JSONException(type == JSON_TYPE.ARRAY,
|
|
"JSONValue is not an array");
|
|
static if(isArray!T)
|
|
{
|
|
JSONValue newArray = JSONValue(this.store.array.dup);
|
|
newArray.store.array ~= JSONValue(arg).store.array;
|
|
return newArray;
|
|
}
|
|
else static if(is(T : JSONValue))
|
|
{
|
|
enforceEx!JSONException(arg.type == JSON_TYPE.ARRAY,
|
|
"JSONValue is not an array");
|
|
JSONValue newArray = JSONValue(this.store.array.dup);
|
|
newArray.store.array ~= arg.store.array;
|
|
return newArray;
|
|
}
|
|
else
|
|
{
|
|
static assert(false, "argument is not an array or a JSONValue array");
|
|
}
|
|
}
|
|
|
|
void opOpAssign(string op : "~", T)(T arg)
|
|
{
|
|
enforceEx!JSONException(type == JSON_TYPE.ARRAY,
|
|
"JSONValue is not an array");
|
|
static if(isArray!T)
|
|
{
|
|
store.array ~= JSONValue(arg).store.array;
|
|
}
|
|
else static if(is(T : JSONValue))
|
|
{
|
|
enforceEx!JSONException(arg.type == JSON_TYPE.ARRAY,
|
|
"JSONValue is not an array");
|
|
store.array ~= arg.store.array;
|
|
}
|
|
else
|
|
{
|
|
static assert(false, "argument is not an array or a JSONValue array");
|
|
}
|
|
}
|
|
|
|
auto opBinaryRight(string op : "in")(string k) const
|
|
{
|
|
enforce!JSONException(type == JSON_TYPE.OBJECT,
|
|
"JSONValue is not an object");
|
|
return k in store.object;
|
|
}
|
|
|
|
/// Implements the foreach $(D opApply) interface for json arrays.
|
|
int opApply(int delegate(size_t index, ref JSONValue) dg)
|
|
{
|
|
enforce!JSONException(type == JSON_TYPE.ARRAY,
|
|
"JSONValue is not an array");
|
|
int result;
|
|
|
|
foreach(size_t index, ref value; store.array)
|
|
{
|
|
result = dg(index, value);
|
|
if(result)
|
|
break;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// Implements the foreach $(D opApply) interface for json objects.
|
|
int opApply(int delegate(string key, ref JSONValue) dg)
|
|
{
|
|
enforce!JSONException(type == JSON_TYPE.OBJECT,
|
|
"JSONValue is not an object");
|
|
int result;
|
|
|
|
foreach(string key, ref value; store.object)
|
|
{
|
|
result = dg(key, value);
|
|
if(result)
|
|
break;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// Implicitly calls $(D toJSON) on this JSONValue.
|
|
string toString() const
|
|
{
|
|
return toJSON(&this);
|
|
}
|
|
|
|
/// Implicitly calls $(D toJSON) on this JSONValue, like $(D toString), but
|
|
/// also passes $(I true) as $(I pretty) argument.
|
|
string toPrettyString() const
|
|
{
|
|
return toJSON(&this, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
Parses a serialized string and returns a tree of JSON values.
|
|
*/
|
|
JSONValue parseJSON(T)(T json, int maxDepth = -1) if(isInputRange!T)
|
|
{
|
|
JSONValue root = void;
|
|
root.type_tag = JSON_TYPE.NULL;
|
|
|
|
if(json.empty) return root;
|
|
|
|
int depth = -1;
|
|
dchar next = 0;
|
|
int line = 1, pos = 1;
|
|
|
|
void error(string msg)
|
|
{
|
|
throw new JSONException(msg, line, pos);
|
|
}
|
|
|
|
dchar peekChar()
|
|
{
|
|
if(!next)
|
|
{
|
|
if(json.empty) return '\0';
|
|
next = json.front;
|
|
json.popFront();
|
|
}
|
|
return next;
|
|
}
|
|
|
|
void skipWhitespace()
|
|
{
|
|
while(isWhite(peekChar())) next = 0;
|
|
}
|
|
|
|
dchar getChar(bool SkipWhitespace = false)()
|
|
{
|
|
static if(SkipWhitespace) skipWhitespace();
|
|
|
|
dchar c = void;
|
|
if(next)
|
|
{
|
|
c = next;
|
|
next = 0;
|
|
}
|
|
else
|
|
{
|
|
if(json.empty) error("Unexpected end of data.");
|
|
c = json.front;
|
|
json.popFront();
|
|
}
|
|
|
|
if(c == '\n' || (c == '\r' && peekChar() != '\n'))
|
|
{
|
|
line++;
|
|
pos = 1;
|
|
}
|
|
else
|
|
{
|
|
pos++;
|
|
}
|
|
|
|
return c;
|
|
}
|
|
|
|
void checkChar(bool SkipWhitespace = true, bool CaseSensitive = true)(char c)
|
|
{
|
|
static if(SkipWhitespace) skipWhitespace();
|
|
auto c2 = getChar();
|
|
static 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;
|
|
}
|
|
|
|
string parseString()
|
|
{
|
|
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':
|
|
dchar 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);
|
|
}
|
|
char[4] buf = void;
|
|
str.put(toUTF8(buf, val));
|
|
break;
|
|
|
|
default:
|
|
error(text("Invalid escape sequence '\\", c, "'."));
|
|
}
|
|
goto Next;
|
|
|
|
default:
|
|
auto c = getChar();
|
|
appendJSONChar(&str, c, &error);
|
|
goto Next;
|
|
}
|
|
|
|
return str.data ? str.data : "";
|
|
}
|
|
|
|
void parseValue(JSONValue* value)
|
|
{
|
|
depth++;
|
|
|
|
if(maxDepth != -1 && depth > maxDepth) error("Nesting too deep.");
|
|
|
|
auto c = getChar!true();
|
|
|
|
switch(c)
|
|
{
|
|
case '{':
|
|
value.type_tag = JSON_TYPE.OBJECT;
|
|
value.store.object = null;
|
|
|
|
if(testChar('}')) break;
|
|
|
|
do
|
|
{
|
|
checkChar('"');
|
|
string name = parseString();
|
|
checkChar(':');
|
|
JSONValue member = void;
|
|
parseValue(&member);
|
|
value.store.object[name] = member;
|
|
}
|
|
while(testChar(','));
|
|
|
|
checkChar('}');
|
|
break;
|
|
|
|
case '[':
|
|
value.type_tag = JSON_TYPE.ARRAY;
|
|
|
|
if(testChar(']'))
|
|
{
|
|
value.store.array = cast(JSONValue[]) "";
|
|
break;
|
|
}
|
|
|
|
value.store.array = null;
|
|
|
|
do
|
|
{
|
|
JSONValue element = void;
|
|
parseValue(&element);
|
|
value.store.array ~= element;
|
|
}
|
|
while(testChar(','));
|
|
|
|
checkChar(']');
|
|
break;
|
|
|
|
case '"':
|
|
value.type_tag = JSON_TYPE.STRING;
|
|
value.store.str = parseString();
|
|
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;
|
|
}
|
|
|
|
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 = JSON_TYPE.FLOAT;
|
|
value.store.floating = parse!double(data);
|
|
}
|
|
else
|
|
{
|
|
if (isNegative)
|
|
value.store.integer = parse!long(data);
|
|
else
|
|
value.store.uinteger = parse!ulong(data);
|
|
|
|
value.type_tag = !isNegative && value.store.uinteger & (1UL << 63) ? JSON_TYPE.UINTEGER : JSON_TYPE.INTEGER;
|
|
}
|
|
break;
|
|
|
|
case 't':
|
|
case 'T':
|
|
value.type_tag = JSON_TYPE.TRUE;
|
|
checkChar!(false, false)('r');
|
|
checkChar!(false, false)('u');
|
|
checkChar!(false, false)('e');
|
|
break;
|
|
|
|
case 'f':
|
|
case 'F':
|
|
value.type_tag = JSON_TYPE.FALSE;
|
|
checkChar!(false, false)('a');
|
|
checkChar!(false, false)('l');
|
|
checkChar!(false, false)('s');
|
|
checkChar!(false, false)('e');
|
|
break;
|
|
|
|
case 'n':
|
|
case 'N':
|
|
value.type_tag = JSON_TYPE.NULL;
|
|
checkChar!(false, false)('u');
|
|
checkChar!(false, false)('l');
|
|
checkChar!(false, false)('l');
|
|
break;
|
|
|
|
default:
|
|
error(text("Unexpected character '", c, "'."));
|
|
}
|
|
|
|
depth--;
|
|
}
|
|
|
|
parseValue(&root);
|
|
return root;
|
|
}
|
|
|
|
/**
|
|
Takes a tree of JSON values and returns the serialized string.
|
|
|
|
Any Object types will be serialized in a key-sorted order.
|
|
|
|
If $(D pretty) is false no whitespaces are generated.
|
|
If $(D pretty) is true serialized string is formatted to be human-readable.
|
|
*/
|
|
string toJSON(in JSONValue* root, in bool pretty = false)
|
|
{
|
|
auto json = appender!string();
|
|
|
|
void toString(string str)
|
|
{
|
|
json.put('"');
|
|
|
|
foreach (dchar c; str)
|
|
{
|
|
switch(c)
|
|
{
|
|
case '"': json.put("\\\""); break;
|
|
case '\\': json.put("\\\\"); break;
|
|
case '/': 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:
|
|
appendJSONChar(&json, c,
|
|
(msg) { throw new JSONException(msg); });
|
|
}
|
|
}
|
|
|
|
json.put('"');
|
|
}
|
|
|
|
void toValue(in JSONValue* value, ulong indentLevel)
|
|
{
|
|
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 JSON_TYPE.OBJECT:
|
|
if(!value.store.object.length)
|
|
{
|
|
json.put("{}");
|
|
}
|
|
else
|
|
{
|
|
putCharAndEOL('{');
|
|
bool first = true;
|
|
|
|
void emit(R)(R names)
|
|
{
|
|
foreach (name; names)
|
|
{
|
|
auto member = value.store.object[name];
|
|
if(!first)
|
|
putCharAndEOL(',');
|
|
first = false;
|
|
putTabs(1);
|
|
toString(name);
|
|
json.put(':');
|
|
if(pretty)
|
|
json.put(' ');
|
|
toValue(&member, indentLevel + 1);
|
|
}
|
|
}
|
|
|
|
import std.algorithm : sort;
|
|
auto names = value.store.object.keys;
|
|
sort(names);
|
|
emit(names);
|
|
|
|
putEOL();
|
|
putTabs();
|
|
json.put('}');
|
|
}
|
|
break;
|
|
|
|
case JSON_TYPE.ARRAY:
|
|
if(value.store.array.empty)
|
|
{
|
|
json.put("[]");
|
|
}
|
|
else
|
|
{
|
|
putCharAndEOL('[');
|
|
foreach (i, ref el; value.store.array)
|
|
{
|
|
if(i)
|
|
putCharAndEOL(',');
|
|
putTabs(1);
|
|
toValue(&el, indentLevel + 1);
|
|
}
|
|
putEOL();
|
|
putTabs();
|
|
json.put(']');
|
|
}
|
|
break;
|
|
|
|
case JSON_TYPE.STRING:
|
|
toString(value.store.str);
|
|
break;
|
|
|
|
case JSON_TYPE.INTEGER:
|
|
json.put(to!string(value.store.integer));
|
|
break;
|
|
|
|
case JSON_TYPE.UINTEGER:
|
|
json.put(to!string(value.store.uinteger));
|
|
break;
|
|
|
|
case JSON_TYPE.FLOAT:
|
|
json.put(to!string(value.store.floating));
|
|
break;
|
|
|
|
case JSON_TYPE.TRUE:
|
|
json.put("true");
|
|
break;
|
|
|
|
case JSON_TYPE.FALSE:
|
|
json.put("false");
|
|
break;
|
|
|
|
case JSON_TYPE.NULL:
|
|
json.put("null");
|
|
break;
|
|
}
|
|
}
|
|
|
|
toValue(root, 0);
|
|
return json.data;
|
|
}
|
|
|
|
private void appendJSONChar(Appender!string* dst, dchar c,
|
|
scope void delegate(string) error)
|
|
{
|
|
import std.uni : isControl;
|
|
|
|
if(isControl(c))
|
|
error("Illegal control character.");
|
|
dst.put(c);
|
|
}
|
|
|
|
/**
|
|
Exception thrown on JSON errors
|
|
*/
|
|
class JSONException : Exception
|
|
{
|
|
this(string msg, int line = 0, int pos = 0)
|
|
{
|
|
if(line)
|
|
super(text(msg, " (Line ", line, ":", pos, ")"));
|
|
else
|
|
super(msg);
|
|
}
|
|
|
|
this(string msg, string file, size_t line)
|
|
{
|
|
super(msg, file, line);
|
|
}
|
|
}
|
|
|
|
|
|
unittest
|
|
{
|
|
JSONValue jv = "123";
|
|
assert(jv.type == JSON_TYPE.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 == JSON_TYPE.INTEGER);
|
|
assertNotThrown(jv.integer);
|
|
|
|
jv = cast(uint)3;
|
|
assert(jv.type == JSON_TYPE.UINTEGER);
|
|
assertNotThrown(jv.uinteger);
|
|
|
|
jv = 3.0f;
|
|
assert(jv.type == JSON_TYPE.FLOAT);
|
|
assertNotThrown(jv.floating);
|
|
|
|
jv = ["key" : "value"];
|
|
assert(jv.type == JSON_TYPE.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 == JSON_TYPE.STRING);
|
|
assertNotThrown(value.str);
|
|
assert(value.str == "value");
|
|
}
|
|
|
|
jv = [3, 4, 5];
|
|
assert(jv.type == JSON_TYPE.ARRAY);
|
|
assertNotThrown(jv.array);
|
|
assertNotThrown(jv[2]);
|
|
foreach(size_t index, value; jv)
|
|
{
|
|
static assert(is(typeof(value) == JSONValue));
|
|
assert(value.type == JSON_TYPE.INTEGER);
|
|
assertNotThrown(value.integer);
|
|
assert(index == (value.integer-3));
|
|
}
|
|
|
|
jv = JSONValue("value");
|
|
assert(jv.type == JSON_TYPE.STRING);
|
|
assert(jv.str == "value");
|
|
|
|
JSONValue jv2 = JSONValue("value");
|
|
assert(jv2.type == JSON_TYPE.STRING);
|
|
assert(jv2.str == "value");
|
|
}
|
|
|
|
unittest
|
|
{
|
|
// Bugzilla 11504
|
|
|
|
JSONValue jv = 1;
|
|
assert(jv.type == JSON_TYPE.INTEGER);
|
|
|
|
jv.str = "123";
|
|
assert(jv.type == JSON_TYPE.STRING);
|
|
assert(jv.str == "123");
|
|
|
|
jv.integer = 1;
|
|
assert(jv.type == JSON_TYPE.INTEGER);
|
|
assert(jv.integer == 1);
|
|
|
|
jv.uinteger = 2u;
|
|
assert(jv.type == JSON_TYPE.UINTEGER);
|
|
assert(jv.uinteger == 2u);
|
|
|
|
jv.floating = 1.5f;
|
|
assert(jv.type == JSON_TYPE.FLOAT);
|
|
assert(jv.floating == 1.5f);
|
|
|
|
jv.object = ["key" : JSONValue("value")];
|
|
assert(jv.type == JSON_TYPE.OBJECT);
|
|
assert(jv.object == ["key" : JSONValue("value")]);
|
|
|
|
jv.array = [JSONValue(1), JSONValue(2), JSONValue(3)];
|
|
assert(jv.type == JSON_TYPE.ARRAY);
|
|
assert(jv.array == [JSONValue(1), JSONValue(2), JSONValue(3)]);
|
|
|
|
jv = true;
|
|
assert(jv.type == JSON_TYPE.TRUE);
|
|
|
|
jv = false;
|
|
assert(jv.type == JSON_TYPE.FALSE);
|
|
|
|
enum E{True = true}
|
|
jv = E.True;
|
|
assert(jv.type == JSON_TYPE.TRUE);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
unittest
|
|
{
|
|
// 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.23`,
|
|
`-0.23`,
|
|
`""`,
|
|
`"hello\nworld"`,
|
|
`"\"\\\/\b\f\n\r\t"`,
|
|
`[]`,
|
|
`[12,"foo",true,false]`,
|
|
`{}`,
|
|
`{"a":1,"b":null}`,
|
|
`{"goodbye":[true,"or",false,["test",42,{"nested":{"a":23.54,"b":0.0012}}]],"hello":{"array":[12,null,{}],"json":"is great"}}`,
|
|
];
|
|
|
|
version (MinGW)
|
|
jsons ~= `1.223e+024`;
|
|
else
|
|
jsons ~= `1.223e+24`;
|
|
|
|
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)
|
|
assertThrown(parseJSON(`{ "foo": "` ~ "\u007F" ~ `"}`));
|
|
|
|
with(parseJSON(`""`))
|
|
assert(str == "" && str !is null);
|
|
with(parseJSON(`[]`))
|
|
assert(!array.length && array !is null);
|
|
|
|
// Formatting
|
|
val = parseJSON(`{"a":[null,{"x":1},{},[]]}`);
|
|
assert(toJSON(&val, true) == `{
|
|
"a": [
|
|
null,
|
|
{
|
|
"x": 1
|
|
},
|
|
{},
|
|
[]
|
|
]
|
|
}`);
|
|
}
|
|
|
|
unittest {
|
|
auto json = `"hello\nworld"`;
|
|
const jv = parseJSON(json);
|
|
assert(jv.toString == json);
|
|
assert(jv.toPrettyString == json);
|
|
}
|
|
|
|
deprecated unittest
|
|
{
|
|
// Bugzilla 12332
|
|
|
|
JSONValue jv;
|
|
jv.type = JSON_TYPE.INTEGER;
|
|
jv = 1;
|
|
assert(jv.type == JSON_TYPE.INTEGER);
|
|
assert(jv.integer == 1);
|
|
jv.type = JSON_TYPE.UINTEGER;
|
|
assert(jv.uinteger == 1);
|
|
|
|
jv.type = JSON_TYPE.STRING;
|
|
assertThrown!JSONException(jv.integer == 1);
|
|
assert(jv.str is null);
|
|
jv.str = "123";
|
|
assert(jv.str == "123");
|
|
jv.type = JSON_TYPE.STRING;
|
|
assert(jv.str == "123");
|
|
|
|
jv.type = JSON_TYPE.TRUE;
|
|
assert(jv.type == JSON_TYPE.TRUE);
|
|
}
|