/**
* JavaScript Object Notation
*
* Authors:
* Jeremie Pelletier
*
* References:
* $(LINK http://json.org/)
*
* License:
* Public Domain
*/
module std.text.JSON;
import std.generic.Conv;
import std.generic.Range;
import std.text.CType;
import std.text.Unicode;
import std.text.UTF;
/**
JSON value types
*/
enum JSON_TYPE : byte {
STRING,
INTEGER,
FLOAT,
OBJECT,
ARRAY,
TRUE,
FALSE,
NULL
}
/**
JSON value node
*/
struct JSONValue {
union {
string str;
long integer;
real floating;
JSONValue[string] object;
JSONValue[] array;
}
JSON_TYPE type;
}
/**
Parses a serialized string and returns a tree of JSON values.
*/
JSONValue parseJSON(T)(in T json, int maxDepth = -1) if(isInputRange!T) {
JSONValue root = void;
root.type = JSON_TYPE.NULL;
if(json.empty()) return root;
int depth = -1;
char next = 0;
int line = 1, pos = 1;
void error(string msg) {
throw new JSONException(msg, line, pos);
}
char peekChar() {
if(!next) {
if(json.empty()) error("Unexpected end of data.");
next = json.front();
json.popFront();
}
return next;
}
void skipWhitespace() {
while(isSpace(peekChar())) next = 0;
}
char getChar(bool SkipWhitespace = false)() {
static if(SkipWhitespace) skipWhitespace();
char 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();
char c2 = getChar();
static if(!CaseSensitive) c2 = cast(char)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();
char c2 = peekChar();
static if(!CaseSensitive) c2 = cast(char)toLower(c2);
if(c2 != c) return false;
getChar();
return true;
}
string parseString() {
Appender!string str;
Next:
switch(peekChar()) {
case '"':
getChar();
break;
case '\\':
getChar();
char c = getChar();
switch(c) {
case '"': str ~= '"'; break;
case '\\': str ~= '\\'; break;
case '/': str ~= '/'; break;
case 'b': str ~= '\b'; break;
case 'f': str ~= '\f'; break;
case 'n': str ~= '\n'; break;
case 'r': str ~= '\r'; break;
case 't': str ~= '\t'; break;
case 'u':
dchar val = 0;
foreach_reverse(i; 0 .. 4) {
char hex = cast(char)toUpper(getChar());
if(!isXDigit(hex)) error("Expecting hex character");
val += (isDigit(hex) ? hex - '0' : hex - 'A') << (4 * i);
}
toUTF8(val, str);
break;
default:
error(text("Invalid escape sequence '\\", c, "'."));
}
goto Next;
default:
char c = getChar();
appendJSONChar(&str, c, getChar(), &error);
goto Next;
}
return str.data;
}
void parseValue(JSONValue* value) {
depth++;
if(maxDepth != -1 && depth > maxDepth) error("Nesting too deep.");
char 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 '-':
Appender!string number;
bool isFloat;
void readInteger() {
if(!isDigit(c)) error("Digit expected");
Next: number ~= c;
if(isDigit(peekChar())) {
c = getChar();
goto Next;
}
}
if(c == '-') {
number ~= '-';
c = getChar();
}
readInteger();
if(testChar('.')) {
isFloat = true;
number ~= '.';
c = getChar();
readInteger();
}
if(testChar!(false, false)('e')) {
isFloat = true;
number ~= 'e';
if(testChar('+')) number ~= '+';
else if(testChar('-')) number ~= '-';
c = getChar();
readInteger();
}
string data = number.data;
size_t offset;
if(isFloat) {
value.type = JSON_TYPE.FLOAT;
value.floating = parse!real(data, offset);
}
else {
value.type = JSON_TYPE.INTEGER;
value.integer = parse!long(data, offset);
}
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) {
Appender!string json;
void toString(string str) {
json ~= '"';
for(int i; i != str.length; i++) {
switch(str[i]) {
case '"': json ~= "\\\""; break;
case '\\': json ~= "\\\\"; break;
case '\b': json ~= "\\b"; break;
case '\f': json ~= "\\f"; break;
case '\n': json ~= "\\n"; break;
case '\r': json ~= "\\r"; break;
case '\t': json ~= "\\t"; break;
default:
appendJSONChar(&json, str[i], str[++i],
(string msg){throw new JSONException(msg);});
}
}
json ~= '"';
}
void toValue(in JSONValue* value) {
final switch(value.type) {
case JSON_TYPE.OBJECT:
json ~= '{';
bool first = true;
foreach(name, member; value.object) {
if(first) first = false;
else json ~= ',';
toString(name);
json ~= ':';
toValue(&member);
}
json ~= '}';
break;
case JSON_TYPE.ARRAY:
json ~= '[';
int length = value.array.length;
for(int i; i != length; i++) {
if(i) json ~= ',';
toValue(&value.array[i]);
}
json ~= ']';
break;
case JSON_TYPE.STRING:
toString(value.str);
break;
case JSON_TYPE.INTEGER:
json ~= to!string(value.integer);
break;
case JSON_TYPE.FLOAT:
json ~= to!string(value.floating);
break;
case JSON_TYPE.TRUE:
json ~= "true";
break;
case JSON_TYPE.FALSE:
json ~= "false";
break;
case JSON_TYPE.NULL:
json ~= "null";
break;
}
}
toValue(root);
return json.data;
}
private void appendJSONChar(Appender!string* dst, char c, lazy char next,
scope void delegate(string) error)
{
int stride = UTFStride(c);
if(stride == 1) {
if(isCntrl(c)) error("Illegal control character.");
*dst ~= c;
}
else {
char[6] utf = void;
utf[0] = c;
foreach(i; 1 .. stride) utf[i] = next;
size_t index = 0;
if(isUniControl(toUnicode(utf[0 .. stride], index)))
error("Illegal control character");
*dst ~= 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);
}
}