// Written in the D programming language. /** JavaScript Object Notation Copyright: Copyright Jeremie Pelletier 2008 - 2009. License: Boost License 1.0. 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) == "\"\<\>\""); val = parseJSON(`"\u0391\u0392\u0393"`); assert(toJSON(&val) == "\"\Α\Β\Γ\""); val = parseJSON(`"\u2660\u2666"`); assert(toJSON(&val) == "\"\♠\♦\""); }