phobos/std/json.d

507 lines
17 KiB
D

// Written in the D programming language.
/**
JavaScript Object Notation
Copyright: Copyright Jeremie Pelletier 2008 - 2009.
License: <a href="http://www.boost.org/LICENSE_1_0.txt">Boost License 1.0</a>.
Authors: Jeremie Pelletier
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;
private {
// Prevent conflicts from these generic names
alias std.utf.stride UTFStride;
alias std.utf.decode toUnicode;
}
/**
JSON type enumeration
*/
enum JSON_TYPE : byte {
/// Indicates the type of a $(D JSONValue).
STRING,
INTEGER, /// ditto
UINTEGER,/// integers > 2^63-1
FLOAT, /// ditto
OBJECT, /// ditto
ARRAY, /// ditto
TRUE, /// ditto
FALSE, /// ditto
NULL /// ditto
}
/**
JSON value node
*/
struct JSONValue {
union {
/// Value when $(D type) is $(D JSON_TYPE.STRING)
string str;
/// Value when $(D type) is $(D JSON_TYPE.INTEGER)
long integer;
/// Value when $(D type) is $(D JSON_TYPE.UINTEGER)
ulong uinteger;
/// Value when $(D type) is $(D JSON_TYPE.FLOAT)
real floating;
/// Value when $(D type) is $(D JSON_TYPE.OBJECT)
JSONValue[string] object;
/// Value when $(D type) is $(D JSON_TYPE.ARRAY)
JSONValue[] array;
}
/// Specifies the _type of the value stored in this structure.
JSON_TYPE type;
/// array syntax for json arrays
ref JSONValue opIndex(size_t i)
in { assert(type == JSON_TYPE.ARRAY, "json type is not array"); }
body
{
return array[i];
}
/// hash syntax for json objects
ref JSONValue opIndex(string k)
in { assert(type == JSON_TYPE.OBJECT, "json type is not object"); }
body
{
return object[k];
}
}
/**
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 = 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;
}
void parseValue(JSONValue* value) {
depth++;
if(maxDepth != -1 && depth > maxDepth) error("Nesting too deep.");
auto c = getChar!true();
switch(c) {
case '{':
value.type = JSON_TYPE.OBJECT;
value.object = null;
if(testChar('}')) break;
do {
checkChar('"');
string name = parseString();
checkChar(':');
JSONValue member = void;
parseValue(&member);
value.object[name] = member;
} while(testChar(','));
checkChar('}');
break;
case '[':
value.type = JSON_TYPE.ARRAY;
value.array = null;
if(testChar(']')) break;
do {
JSONValue element = void;
parseValue(&element);
value.array ~= element;
} while(testChar(','));
checkChar(']');
break;
case '"':
value.type = JSON_TYPE.STRING;
value.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 = JSON_TYPE.FLOAT;
value.floating = parse!real(data);
}
else {
if (isNegative)
value.integer = parse!long(data);
else
value.uinteger = parse!ulong(data);
value.type = !isNegative && value.uinteger & (1UL << 63) ? JSON_TYPE.UINTEGER : JSON_TYPE.INTEGER;
}
break;
case 't':
case 'T':
value.type = JSON_TYPE.TRUE;
checkChar!(false, false)('r');
checkChar!(false, false)('u');
checkChar!(false, false)('e');
break;
case 'f':
case 'F':
value.type = 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 = 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.
*/
string toJSON(in JSONValue* root) {
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,
(string msg){throw new JSONException(msg);});
}
}
json.put('"');
}
void toValue(in JSONValue* value) {
final switch(value.type) {
case JSON_TYPE.OBJECT:
json.put('{');
bool first = true;
foreach(name, member; value.object) {
if(first) first = false;
else json.put(',');
toString(name);
json.put(':');
toValue(&member);
}
json.put('}');
break;
case JSON_TYPE.ARRAY:
json.put('[');
auto length = value.array.length;
foreach (i; 0 .. length) {
if(i) json.put(',');
toValue(&value.array[i]);
}
json.put(']');
break;
case JSON_TYPE.STRING:
toString(value.str);
break;
case JSON_TYPE.INTEGER:
json.put(to!string(value.integer));
break;
case JSON_TYPE.UINTEGER:
json.put(to!string(value.uinteger));
break;
case JSON_TYPE.FLOAT:
json.put(to!string(value.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);
return json.data;
}
private void appendJSONChar(Appender!string* dst, dchar c,
scope void delegate(string) error)
{
if(isControl(c)) error("Illegal control character.");
dst.put(c);
// int stride = UTFStride((&c)[0 .. 1], 0);
// if(stride == 1) {
// if(isControl(c)) error("Illegal control character.");
// dst.put(c);
// }
// else {
// char[6] utf = void;
// utf[0] = c;
// foreach(i; 1 .. stride) utf[i] = next;
// size_t index = 0;
// if(isControl(toUnicode(utf[0 .. stride], index)))
// error("Illegal control character");
// dst.put(utf[0 .. stride]);
// }
}
/**
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);
}
}
version(unittest) import std.stdio;
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`,
`""`,
`1.223e+24`,
`"hello\nworld"`,
`"\"\\\/\b\f\n\r\t"`,
`[]`,
`[12,"foo",true,false]`,
`{}`,
`{"a":1,"b":null}`,
// Currently broken
// `{"hello":{"json":"is great","array":[12,null,{}]},"goodbye":[true,"or",false,["test",42,{"nested":{"a":23.54,"b":0.0012}}]]}`
];
JSONValue val;
string result;
foreach(json; jsons) {
try {
val = parseJSON(json);
result = toJSON(&val);
assert(result == json, text(result, " should be ", json));
}
catch(JSONException e) {
writefln(text(json, "\n", e.toString()));
}
}
// Should be able to correctly interpret unicode entities
val = parseJSON(`"\u003C\u003E"`);
assert(toJSON(&val) == "\"\&lt;\&gt;\"");
val = parseJSON(`"\u0391\u0392\u0393"`);
assert(toJSON(&val) == "\"\&Alpha;\&Beta;\&Gamma;\"");
val = parseJSON(`"\u2660\u2666"`);
assert(toJSON(&val) == "\"\&spades;\&diams;\"");
}