dart-sdk/client/json/dart_json.dart
dgrove@google.com 4bc64b8c2b Initial checkin.
git-svn-id: https://dart.googlecode.com/svn/branches/bleeding_edge/dart@7 260f80e4-7a28-3924-810f-c04153c831b5
2011-10-05 04:52:33 +00:00

593 lines
15 KiB
Dart

// Copyright (c) 2011, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
#library("json");
// Pure Dart implementation of JSON protocol.
/**
* Utility class to parse JSON and serialize objects to JSON.
*/
class JSON {
/**
* Parses [:json:] and build the corresponding object.
*/
static parse(String json) {
return JsonParser.parse(json);
}
/**
* Serializes [:object:] into JSON string.
*/
static String stringify(Object object) {
return JsonStringifier.stringify(object);
}
}
//// Implementation ///////////////////////////////////////////////////////////
/**
* Union-like class for JSON tokens.
*/
class JsonToken {
static final int STRING = 0;
static final int NUMBER = 1;
static final int NULL = 2;
static final int FALSE = 3;
static final int TRUE = 4;
static final int RBRACKET = 5;
static final int LBRACKET = 6;
static final int RBRACE = 7;
static final int LBRACE = 8;
static final int COLON = 9;
static final int COMMA = 10;
final int kind;
final String _s;
final num _n;
String get str() {
assert(kind == STRING);
return _s;
}
num get number() {
assert(kind == NUMBER);
return _n;
}
const JsonToken._internal(this.kind, this._s, this._n);
factory JsonToken.string(String s) {
return new JsonToken._internal(STRING, s, 0);
}
factory JsonToken.number(num n) {
return new JsonToken._internal(NUMBER, '', n);
}
factory JsonToken.atom(int kind) {
return new JsonToken._internal(kind, '', 0);
}
String toString() {
switch (kind) {
case STRING:
return 'STRING(${str})';
case NUMBER:
return 'NUMBER(${number})';
case NULL:
return 'ATOM(null)';
case FALSE:
return 'ATOM(false)';
case TRUE:
return 'ATOM(true)';
case RBRACKET:
return 'ATOM(])';
case LBRACKET:
return 'ATOM([)';
case RBRACE:
return 'ATOM(})';
case LBRACE:
return 'ATOM({)';
case COLON:
return 'ATOM(:)';
case COMMA:
return 'ATOM(,)';
}
}
}
typedef bool Predicate(int c);
class JsonTokenizer {
static final int BACKSPACE = 8; // '\b'.charCodeAt(0)
static final int TAB = 9; // '\t'.charCodeAt(0)
static final int NEW_LINE = 10; // '\n'.charCodeAt(0)
static final int FORM_FEED = 12; // '\f'.charCodeAt(0)
static final int LINE_FEED = 13; // '\r'.charCodeAt(0)
static final int SPACE = 32; // ' '.charCodeAt(0)
static final int QUOTE = 34; // '"'.charCodeAt(0)
static final int PLUS = 43; // '+'.charCodeAt(0)
static final int COMMA = 44; // ','.charCodeAt(0)
static final int MINUS = 45; // '-'.charCodeAt(0)
static final int DOT = 46; // '.'.charCodeAt(0)
static final int SLASH = 47; // '/'.charCodeAt(0)
static final int ZERO = 48; // '0'.charCodeAt(0)
static final int NINE = 57; // '9'.charCodeAt(0)
static final int COLON = 58; // ':'.charCodeAt(0)
static final int A_BIG = 65; // 'A'.charCodeAt(0)
static final int E_BIG = 69; // 'E'.charCodeAt(0)
static final int Z_BIG = 90; // 'Z'.charCodeAt(0)
static final int LBRACKET = 91; // '['.charCodeAt(0)
static final int BACKSLASH = 92; // '\\'.charCodeAt(0)
static final int RBRACKET = 93; // ']'.charCodeAt(0)
static final int A_SMALL = 97; // 'a'.charCodeAt(0)
static final int B_SMALL = 98; // 'b'.charCodeAt(0)
static final int E_SMALL = 101; // 'e'.charCodeAt(0)
static final int Z_SMALL = 122; // 'z'.charCodeAt(0)
static final int LBRACE = 123; // '{'.charCodeAt(0)
static final int RBRACE = 125; // '}'.charCodeAt(0)
JsonTokenizer(String s) : _s = s + ' ', _pos = 0, _len = s.length + 1 {}
/**
* Fetches next token or [:null:] if the stream has been exhausted.
*/
JsonToken next() {
while (_pos < _len && isWhitespace(_s.charCodeAt(_pos))) {
_pos++;
}
if (_pos == _len) {
return null;
}
final int cur = _s.charCodeAt(_pos);
switch (true) {
case cur == QUOTE:
_pos++;
List<int> charCodes = new List<int>();
while (_pos < _len) {
int c = _s.charCodeAt(_pos);
if (c == QUOTE) {
break;
}
if (c == BACKSLASH) {
_pos++;
if (_pos == _len) {
throw '\\ at the end';
}
switch (_s[_pos]) {
case '"':
c = QUOTE;
break;
case '\\':
c = BACKSLASH;
break;
case '/':
c = SLASH;
break;
case 'b':
c = BACKSPACE;
break;
case 'n':
c = NEW_LINE;
break;
case 'r':
c = LINE_FEED;
break;
case 'f':
c = FORM_FEED;
break;
case 't':
c = TAB;
break;
case 'u':
if (_pos + 5 > _len) {
throw 'Invalid unicode esacape sequence: \\' +
_s.substring(_pos, _len);
}
final codeString = _s.substring(_pos + 1, _pos + 5);
c = Math.parseInt('0x' + codeString);
if (c >= 128) {
// TODO(jmessery): the VM doesn't support 2-byte strings yet
// see runtime/lib/string.cc:49
// So instead we replace these characters with '?'
c = '?'.charCodeAt(0);
}
_pos += 4;
break;
default:
throw 'Invalid esacape sequence: \\' + _s[_pos];
}
}
charCodes.add(c);
_pos++;
}
if (_pos == _len) {
throw 'Unmatched quote';
}
final String body = new String.fromCharCodes(charCodes);
_pos++;
return new JsonToken.string(body);
case cur == MINUS || isDigit(cur):
skipDigits() {
_scanWhile((int c) => isDigit(c), 'Invalid number');
}
int c = cur;
final int startPos = _pos;
int value = 0;
bool isNegative = false;
if (c == MINUS) {
isNegative = true;
_pos++;
c = _s.charCodeAt(_pos);
}
while (isDigit(c)) {
value = value * 10 + c - ZERO;
_pos++;
c = _s.charCodeAt(_pos);
}
if (c != DOT) {
if (c != E_SMALL && cur != E_BIG) {
if (isNegative) value = -value;
return new JsonToken.number(value);
}
} else {
_pos++;
skipDigits();
c = _s.charCodeAt(_pos);
}
if (c == E_SMALL || c == E_BIG) {
// TODO: consider keeping E+ as an integer.
_pos++;
c = _s.charCodeAt(_pos);
if (c == PLUS || c == MINUS) {
_pos++;
}
skipDigits();
}
final String body = _s.substring(startPos, _pos);
return new JsonToken.number(Math.parseDouble(body));
case cur == LBRACE:
_pos++;
return new JsonToken.atom(JsonToken.LBRACE);
case cur == RBRACE:
_pos++;
return new JsonToken.atom(JsonToken.RBRACE);
case cur == LBRACKET:
_pos++;
return new JsonToken.atom(JsonToken.LBRACKET);
case cur == RBRACKET:
_pos++;
return new JsonToken.atom(JsonToken.RBRACKET);
case cur == COMMA:
_pos++;
return new JsonToken.atom(JsonToken.COMMA);
case cur == COLON:
_pos++;
return new JsonToken.atom(JsonToken.COLON);
case isLetter(cur):
final int startPos = _pos;
_pos++;
while (_pos < _len && isLetter(_s.charCodeAt(_pos))) {
_pos++;
}
final String body = _s.substring(startPos, _pos);
switch (body) {
case 'null':
return new JsonToken.atom(JsonToken.NULL);
case 'false':
return new JsonToken.atom(JsonToken.FALSE);
case 'true':
return new JsonToken.atom(JsonToken.TRUE);
default:
throw 'Unexpected sequence ${body}';
}
// TODO: Bogous, to please DartVM.
return null;
default:
throw 'Invalid token';
}
}
final String _s;
int _pos;
final int _len;
void _scanWhile(Predicate predicate, String errorMsg) {
while (_pos < _len && predicate(_s.charCodeAt(_pos))) {
_pos++;
}
if (_pos == _len) {
throw errorMsg;
}
}
// TODO other kind of whitespace.
static bool isWhitespace(int c) {
return c == SPACE || c == TAB || c == NEW_LINE || c == LINE_FEED;
}
static bool isDigit(int c) {
return (ZERO <= c) && (c <= NINE);
}
static bool isLetter(int c) {
return ((A_SMALL <= c) && (c <= Z_SMALL)) || ((A_BIG <= c) && (c <= Z_BIG));
}
}
class JsonParser {
static parse(String json) {
return new JsonParser._internal(json)._parseToplevel();
}
final JsonTokenizer _tokenizer;
JsonParser._internal(String json) : _tokenizer = new JsonTokenizer(json) {}
_parseToplevel() {
JsonToken token = _tokenizer.next();
final result = _parseValue(token);
token = _tokenizer.next();
if (token !== null) {
throw 'Junk at the end';
}
return result;
}
_parseValue(final JsonToken token) {
if (token === null) {
throw 'Nothing to parse';
}
switch (token.kind) {
case JsonToken.STRING:
return token.str;
case JsonToken.NUMBER:
return token.number;
case JsonToken.NULL:
return null;
case JsonToken.FALSE:
return false;
case JsonToken.TRUE:
return true;
case JsonToken.LBRACE:
return _parseObject();
case JsonToken.LBRACKET:
return _parseList();
default:
throw 'Unexpected token: ${token}';
}
}
_parseObject() {
Map<String, Object> object = new Map<String, Object>();
_parseSequence(JsonToken.RBRACE, (JsonToken token) {
_assertTokenKind(token, JsonToken.STRING);
final String key = token.str;
token = _tokenizer.next();
_assertTokenKind(token, JsonToken.COLON);
token = _tokenizer.next();
final value = _parseValue(token);
object[key] = value;
});
return object;
}
_parseList() {
List<Object> list = new List<Object>();
_parseSequence(JsonToken.RBRACKET, (JsonToken token) {
final value = _parseValue(token);
list.add(value);
});
return list;
}
void _parseSequence(int endTokenKind, void parseElement(JsonToken token)) {
JsonToken token = _tokenizer.next();
if (token === null) {
throw 'Unexpected end of stream';
}
if (token.kind == endTokenKind) {
return;
}
parseElement(token);
token = _tokenizer.next();
if (token === null) {
throw 'Expected either comma or terminator';
}
while (token.kind != endTokenKind) {
_assertTokenKind(token, JsonToken.COMMA);
token = _tokenizer.next();
parseElement(token);
token = _tokenizer.next();
}
}
void _assertTokenKind(JsonToken token, int kind) {
if (token === null || token.kind != kind) {
throw 'Unexpected token kind: token = ${token}, expected kind = ${kind}';
}
}
// TODO: consider factor out error throwing code and build more complicated
// data structure to provide more info for a caller.
}
// TODO: proper base class.
class JsonUnsupportedObjectType {
const JsonUnsupportedObjectType();
}
class JsonStringifier {
static String stringify(final object) {
JsonStringifier stringifier = new JsonStringifier._internal();
stringifier._stringify(object);
return stringifier._result;
}
JsonStringifier._internal()
: _sb = new StringBuffer(), _seen = new List<Object>() {}
StringBuffer _sb;
List<Object> _seen; // TODO: that should be identity set.
String get _result() { return _sb.toString(); }
static String _numberToString(num x) {
// TODO: need some more investigation what to do with precision
// of double values.
switch (true) {
case x is int:
return x.toString();
case x is double:
return x.toString();
default:
return x.toDouble().toString();
}
}
// TODO: add others.
static bool _needsEscape(int charCode) {
return JsonTokenizer.QUOTE == charCode || JsonTokenizer.BACKSLASH == charCode;
}
static void _escape(StringBuffer sb, String s) {
// TODO: support \u code points.
// TODO: use writeCodePoint when implemented.
// TODO: use for each if implemented.
final int length = s.length;
bool needsEscape = false;
final charCodes = new List<int>();
for (int i = 0; i < length; i++) {
final int charCode = s.charCodeAt(i);
if (_needsEscape(charCode)) {
charCodes.add(JsonTokenizer.BACKSLASH);
needsEscape = true;
}
charCodes.add(charCode);
}
sb.add(needsEscape ? new String.fromCharCodes(charCodes) : s);
}
void _checkCycle(final object) {
// TODO: use Iterables.
for (int i = 0; i < _seen.length; i++) {
if (_seen[i] === object) {
throw 'Cyclic structure';
}
}
_seen.add(object);
}
void _stringify(final object) {
switch (true) {
case object is num:
// TODO: use writeOn.
_sb.add(_numberToString(object));
return;
case object === true:
_sb.add('true');
return;
case object === false:
_sb.add('false');
return;
case object === null:
_sb.add('null');
return;
case object is String:
_sb.add('"');
_escape(_sb, object);
_sb.add('"');
return;
case object is List:
_checkCycle(object);
List a = object;
_sb.add('[');
if (a.length > 0) {
_stringify(a[0]);
// TODO: switch to Iterables.
for (int i = 1; i < a.length; i++) {
_sb.add(',');
_stringify(a[i]);
}
}
_sb.add(']');
_seen.removeLast();
return;
case object is Map:
_checkCycle(object);
Map<String, Object> m = object;
_sb.add('{');
bool first = true;
m.forEach((String key, Object value) {
if (!first) {
_sb.add(',"');
} else {
_sb.add('"');
}
_escape(_sb, key);
_sb.add('":');
_stringify(value);
first = false;
});
_sb.add('}');
_seen.removeLast();
return;
default:
throw const JsonUnsupportedObjectType();
}
}
}