Chromium Code Reviews| Index: lib/json/json.dart |
| diff --git a/lib/json/json.dart b/lib/json/json.dart |
| index 01b16819c2ca819a52ae097f6ea917c53d3f85fe..c51ac5cfb0ab601ad996ed0ab8f57a7221df7080 100644 |
| --- a/lib/json/json.dart |
| +++ b/lib/json/json.dart |
| @@ -13,33 +13,64 @@ |
| */ |
| class JSON { |
| /** |
| - * Parses [json] and build the corresponding object. |
| + * Parses [json] and build the corresponding parsed JSON value. |
| + * |
| + * Parsed JSON values are of the types [num], [String], [bool], [Null], |
|
Anders Johnsen
2012/08/30 14:03:42
Null -> null
Lasse Reichstein Nielsen
2012/08/31 14:29:19
"null" is not a type, "Null" is. I could rewrite i
|
| + * [List]s of parsed JSON values or [Map]s from [String] to parsed |
| + * JSON values. |
| + * |
| + * Throws [JSONParseException] if the input is not valid JSON text. |
| */ |
| static parse(String json) { |
| return _JsonParser.parse(json); |
| } |
| /** |
| - * Checks validity of JSON source in [str] and returns its text |
| - * length. Returns 0 if [str] does not begin with a valid JSON |
| - * object. |
| + * Validates a prefix of [string] as a JSON map and returns the source length. |
| + * |
| + * If this method returns [:result:], then [:string.substring(0, result):] |
| + * contains a valid JSON object that will be accepted by [parse]. |
| + * Returns 0 if [str] does not begin with a valid JSON value. |
|
Anders Johnsen
2012/08/30 14:03:42
Having this methods makes it look like we do some
Lasse Reichstein Nielsen
2012/08/31 14:29:19
Yes, it sucks. I don't know where it's used, or if
|
| */ |
| - static int length(String str) { |
| - return _JsonParser.objectLength(str); |
| + static int length(String string) { |
|
kasperl
2012/08/30 14:14:14
I really think this method should go away. It make
Lasse Reichstein Nielsen
2012/08/31 14:29:19
Agree. It would be better to have a method that re
|
| + return _JsonParser.objectLength(string); |
| } |
| /** |
| - * Serializes [object] into JSON string. |
| + * Serializes [object] into a JSON string. |
| + * |
| + * Directly serializable types are [num], [String], [bool], [Null], [List] |
| + * and [Map]. |
| + * For [List], the elements must all be serializable. |
| + * For [Map], the keys must be [String] and the values must be serializable. |
| + * If a value is any other type is attempted serialized, a "toJson()" method |
| + * is invoked on the object and the result, which must be a directly |
| + * serializable type, is serialized instead of the original value. |
| + * If the object does not support this method, the [NoSuchMethodError] thrown |
|
Anders Johnsen
2012/08/30 14:03:42
Getting NoSuchMethodError from calling stringify(m
Lasse Reichstein Nielsen
2012/08/31 14:29:19
We can't test if there is a toJson method, we can
Anders Johnsen
2012/09/11 13:15:46
Discussed at office.
Lasse Reichstein Nielsen
2012/09/12 10:19:40
Doh, forgot to change the comment. I did change th
|
| + * by the call will end serialization. If the "toJson" method returns |
| + * a value that is not directly serializable, a [JsonUnsupportedObjectType] |
| + * exception is thrown. |
| + * |
| + * Objects should not change during serialization. |
| + * If an object is serialized more than once, [stringify] is allowed to cache |
| + * the JSON text for it. I.e., if an object changes after it is first |
| + * serialized, the new values may or may not be reflected in the result. |
| */ |
| static String stringify(Object object) { |
| - return JsonStringifier.stringify(object); |
| + return _JsonStringifier.stringify(object); |
| } |
| /** |
| * Serializes [object] into [output] stream. |
| + * |
| + * Performs the same operations as [stringify] but outputs the resulting |
| + * string to an existing [StringBuffer] instead of creating a new [String]. |
| + * |
| + * If serialization fails by throwing, some data might have been added to |
| + * [output], but it won't contain valid JSON text. |
| */ |
| static void printOn(Object object, StringBuffer output) { |
| - return JsonStringifier.printOn(object, output); |
| + return _JsonStringifier.printOn(object, output); |
| } |
| } |
| @@ -111,30 +142,15 @@ class _JsonParser { |
| static parse(String json) { |
| - return new _JsonParser._internal(json)._parseToplevel(); |
| + return new _JsonParser(json).parseToplevel(); |
| } |
| - static objectLength(String str) { |
| - var p = new _JsonParser._internal(str); |
| - var firstToken = p._token(); |
| - if (firstToken != LBRACE) { |
| - return 0; |
| - } |
| - try { |
| - p._parseObject(); |
| - assert(p.position <= p.length); |
| - return p.position; |
| - } catch (e) { |
| - return 0; |
| - } |
| - } |
| - |
| - _JsonParser._internal(String json) |
| + _JsonParser(String json) |
| : json = json, |
| length = json.length { |
| if (tokens !== null) return; |
| - // Use a list as jump-table, faster then switch and if. |
| + // Use a list as jump-table. It is faster than switch and if. |
| tokens = new List<int>(LAST_ASCII + 1); |
| tokens[TAB] = WHITESPACE; |
| tokens[NEW_LINE] = WHITESPACE; |
| @@ -163,66 +179,81 @@ class _JsonParser { |
| tokens[CHAR_F] = FALSE_LITERAL; |
| } |
| - _parseToplevel() { |
| - final result = _parseValue(); |
| + static objectLength(String str) { |
| + var p = new _JsonParser(str); |
| + var firstToken = p.token(); |
| + if (firstToken != LBRACE) { |
| + return 0; |
| + } |
| + try { |
| + p.parseObject(); |
| + assert(p.position <= p.length); |
| + return p.position; |
| + } catch (e) { |
| + return 0; |
| + } |
| + } |
| + |
| + parseToplevel() { |
| + final result = parseValue(); |
| if (_token() !== null) { |
| - _error('Junk at the end of JSON input'); |
| + error('Junk at the end of JSON input'); |
| } |
| return result; |
| } |
| - _parseValue() { |
| - final int token = _token(); |
| + parseValue() { |
| + final int token = token(); |
| if (token === null) { |
| - _error('Nothing to parse'); |
| + error('Nothing to parse'); |
| } |
| switch (token) { |
| - case STRING_LITERAL: return _parseString(); |
| - case NUMBER_LITERAL: return _parseNumber(); |
| - case NULL_LITERAL: return _expectKeyword(NULL_STRING, null); |
| - case FALSE_LITERAL: return _expectKeyword(FALSE_STRING, false); |
| - case TRUE_LITERAL: return _expectKeyword(TRUE_STRING, true); |
| - case LBRACE: return _parseObject(); |
| - case LBRACKET: return _parseList(); |
| + case STRING_LITERAL: return parseString(); |
| + case NUMBER_LITERAL: return parseNumber(); |
| + case NULL_LITERAL: return expectKeyword(NULL_STRING, null); |
| + case FALSE_LITERAL: return expectKeyword(FALSE_STRING, false); |
| + case TRUE_LITERAL: return expectKeyword(TRUE_STRING, true); |
| + case LBRACE: return parseObject(); |
| + case LBRACKET: return parseList(); |
| default: |
| - _error('Unexpected token'); |
| + error('Unexpected token'); |
| } |
| } |
| - Object _expectKeyword(String word, Object value) { |
| + Object expectKeyword(String word, Object value) { |
| for (int i = 0; i < word.length; i++) { |
| - // Implicit end check in _char(). |
| - if (_char() != word.charCodeAt(i)) _error("Expected keyword '$word'"); |
| + // Implicit end check in char(). |
| + if (_char() != word.charCodeAt(i)) error("Expected keyword '$word'"); |
| position++; |
| } |
| return value; |
| } |
| - _parseObject() { |
| + parseObject() { |
| final object = {}; |
| position++; // Eat '{'. |
| if (!_isToken(RBRACE)) { |
| while (true) { |
| - final String key = _parseString(); |
| - if (!_isToken(COLON)) _error("Expected ':' when parsing object"); |
| + final String key = parseString(); |
| + if (!_isToken(COLON)) error("Expected ':' when parsing object"); |
| position++; |
| - object[key] = _parseValue(); |
| + object[key] = parseValue(); |
| if (!_isToken(COMMA)) break; |
| position++; // Skip ','. |
| }; |
| - if (!_isToken(RBRACE)) _error("Expected '}' at end of object"); |
| + if (!_isToken(RBRACE)) error("Expected '}' at end of object"); |
| } |
| position++; |
| return object; |
| } |
| - _parseList() { |
| + parseList() { |
| final list = []; |
| position++; // Eat '['. |
| @@ -235,21 +266,21 @@ class _JsonParser { |
| position++; |
| }; |
| - if (!_isToken(RBRACKET)) _error("Expected ']' at end of list"); |
| + if (!_isToken(RBRACKET)) error("Expected ']' at end of list"); |
| } |
| position++; |
| return list; |
| } |
| - String _parseString() { |
| - if (!_isToken(STRING_LITERAL)) _error("Expected string literal"); |
| + String parseString() { |
| + if (!_isToken(STRING_LITERAL)) error("Expected string literal"); |
| position++; // Eat '"'. |
| List<int> charCodes = new List<int>(); |
| while (true) { |
| - int c = _char(); |
| + int c = char(); |
| if (c == QUOTE) { |
| position++; |
| break; |
| @@ -257,7 +288,7 @@ class _JsonParser { |
| if (c == BACKSLASH) { |
| position++; |
| if (position == length) { |
| - _error('\\ at the end of input'); |
| + error('\\ at the end of input'); |
| } |
| switch (_char()) { |
| @@ -287,18 +318,18 @@ class _JsonParser { |
| break; |
| case CHAR_U: |
| if (position + 5 > length) { |
| - _error('Invalid unicode esacape sequence'); |
| + error('Invalid unicode esacape sequence'); |
| } |
| final codeString = json.substring(position + 1, position + 5); |
| try { |
| c = parseInt('0x${codeString}'); |
| } catch (e) { |
| - _error('Invalid unicode esacape sequence'); |
| + error('Invalid unicode esacape sequence'); |
| } |
| position += 4; |
| break; |
| default: |
| - _error('Invalid esacape sequence in string literal'); |
| + error('Invalid esacape sequence in string literal'); |
| } |
| } |
| charCodes.add(c); |
| @@ -308,42 +339,42 @@ class _JsonParser { |
| return new String.fromCharCodes(charCodes); |
| } |
| - num _parseNumber() { |
| - if (!_isToken(NUMBER_LITERAL)) _error('Expected number literal'); |
| + num parseNumber() { |
| + if (!_isToken(NUMBER_LITERAL)) error('Expected number literal'); |
| final int startPos = position; |
| - int char = _char(); |
| - if (char === MINUS) char = _nextChar(); |
| + int char = char(); |
| + if (char === MINUS) char = nextChar(); |
| if (char === CHAR_0) { |
| - char = _nextChar(); |
| + char = nextChar(); |
| } else if (_isDigit(char)) { |
| - char = _nextChar(); |
| - while (_isDigit(char)) char = _nextChar(); |
| + char = nextChar(); |
| + while (_isDigit(char)) char = nextChar(); |
| } else { |
| - _error('Expected digit when parsing number'); |
| + error('Expected digit when parsing number'); |
| } |
| bool isInt = true; |
| if (char === DOT) { |
| - char = _nextChar(); |
| + char = nextChar(); |
| if (_isDigit(char)) { |
| - char = _nextChar(); |
| + char = nextChar(); |
| isInt = false; |
| - while (_isDigit(char)) char = _nextChar(); |
| + while (_isDigit(char)) char = nextChar(); |
| } else { |
| - _error('Expected digit following comma'); |
| + error('Expected digit following comma'); |
| } |
| } |
| if (char === CHAR_E || char === CHAR_CAPITAL_E) { |
| - char = _nextChar(); |
| - if (char === MINUS || char === PLUS) char = _nextChar(); |
| + char = nextChar(); |
| + if (char === MINUS || char === PLUS) char = nextChar(); |
| if (_isDigit(char)) { |
| - char = _nextChar(); |
| + char = nextChar(); |
| isInt = false; |
| - while (_isDigit(char)) char = _nextChar(); |
| + while (_isDigit(char)) char = nextChar(); |
| } else { |
| - _error('Expected digit following \'e\' or \'E\''); |
| + error('Expected digit following \'e\' or \'E\''); |
| } |
| } |
| @@ -355,31 +386,31 @@ class _JsonParser { |
| } |
| } |
| - bool _isChar(int char) { |
| + bool isChar(int char) { |
| if (position >= length) return false; |
| return json.charCodeAt(position) == char; |
| } |
| - bool _isDigit(int char) { |
| + bool isDigit(int char) { |
| return char >= CHAR_0 && char <= CHAR_9; |
| } |
| - bool _isToken(int tokenKind) => _token() == tokenKind; |
| + bool isToken(int tokenKind) => token() == tokenKind; |
| - int _char() { |
| + int char() { |
| if (position >= length) { |
| - _error('Unexpected end of JSON stream'); |
| + error('Unexpected end of JSON stream'); |
| } |
| return json.charCodeAt(position); |
| } |
| - int _nextChar() { |
| + int nextChar() { |
| position++; |
| if (position >= length) return 0; |
| return json.charCodeAt(position); |
| } |
| - int _token() { |
| + int token() { |
| while (true) { |
| if (position >= length) return null; |
| int char = json.charCodeAt(position); |
| @@ -393,7 +424,7 @@ class _JsonParser { |
| } |
| } |
| - void _error(String message) { |
| + void error(String message) { |
| throw message; |
| } |
| @@ -408,41 +439,36 @@ class JsonUnsupportedObjectType { |
| const JsonUnsupportedObjectType(); |
| } |
| -class JsonStringifier { |
| +class _JsonStringifier { |
| + StringBuffer sb; |
| + List<Object> seen; // TODO: that should be identity set. |
| + |
| + _JsonStringifier(this.sb) : seen = []; |
| + |
| static String stringify(final object) { |
| StringBuffer output = new StringBuffer(); |
| - JsonStringifier stringifier = new JsonStringifier._internal(output); |
| - stringifier._stringify(object); |
| + _JsonStringifier stringifier = new _JsonStringifier(output); |
| + stringifier.stringifyValue(object); |
| return output.toString(); |
| } |
| static void printOn(final object, StringBuffer output) { |
| - JsonStringifier stringifier = new JsonStringifier._internal(output); |
| - stringifier._stringify(object); |
| + _JsonStringifier stringifier = new _JsonStringifier(output); |
| + stringifier.stringifyValue(object); |
| } |
| - JsonStringifier._internal(this._sb) |
| - : _seen = []; |
| - |
| - StringBuffer _sb; |
| - List<Object> _seen; // TODO: that should be identity set. |
| - |
| - static String _numberToString(num x) { |
| - // TODO: need some more investigation what to do with precision |
| - // of double values. |
| - if (x is int) { |
| - return x.toString(); |
| - } else if (x is double) { |
| - return x.toString(); |
| - } else { |
| - return x.toDouble().toString(); |
| - } |
| + static String numberToString(num x) { |
| + // Double values should create a representation with sufficient digits to |
|
floitsch
2012/08/30 14:48:29
This is guaranteed by toString.
Lasse Reichstein Nielsen
2012/08/31 14:29:19
I assumed that. I'll remove the comment, and check
|
| + // create the same value again. I.e., such that the original double value |
| + // is the closest representable double value to the exact mathematical |
| + // value of the string representation. |
| + return x.toString(); |
| } |
| // ('0' + x) or ('a' + x - 10) |
| - static int _hexDigit(int x) => x < 10 ? 48 + x : 87 + x; |
| + static int hexDigit(int x) => x < 10 ? 48 + x : 87 + x; |
| - static void _escape(StringBuffer sb, String s) { |
| + static void escape(StringBuffer sb, String s) { |
| final int length = s.length; |
| bool needsEscape = false; |
| final charCodes = new List<int>(); |
| @@ -452,19 +478,19 @@ class JsonStringifier { |
| needsEscape = true; |
| charCodes.add(_JsonParser.BACKSLASH); |
| switch (charCode) { |
| - case _JsonParser.BACKSPACE: |
| + case JsonParser.BACKSPACE: |
| charCodes.add(_JsonParser.CHAR_B); |
| break; |
| - case _JsonParser.TAB: |
| + case JsonParser.TAB: |
| charCodes.add(_JsonParser.CHAR_T); |
| break; |
| - case _JsonParser.NEW_LINE: |
| + case JsonParser.NEW_LINE: |
| charCodes.add(_JsonParser.CHAR_N); |
| break; |
| - case _JsonParser.FORM_FEED: |
| + case JsonParser.FORM_FEED: |
| charCodes.add(_JsonParser.CHAR_F); |
| break; |
| - case _JsonParser.CARRIAGE_RETURN: |
| + case JsonParser.CARRIAGE_RETURN: |
| charCodes.add(_JsonParser.CHAR_R); |
| break; |
| default: |
| @@ -475,8 +501,8 @@ class JsonStringifier { |
| charCodes.add(_hexDigit(charCode & 0xf)); |
| break; |
| } |
| - } else if (charCode == _JsonParser.QUOTE || |
| - charCode == _JsonParser.BACKSLASH) { |
| + } else if (charCode == JsonParser.QUOTE || |
| + charCode == JsonParser.BACKSLASH) { |
| needsEscape = true; |
| charCodes.add(_JsonParser.BACKSLASH); |
| charCodes.add(charCode); |
| @@ -487,71 +513,91 @@ class JsonStringifier { |
| sb.add(needsEscape ? new String.fromCharCodes(charCodes) : s); |
| } |
| - void _checkCycle(final object) { |
| + void checkCycle(final object) { |
| // TODO: use Iterables. |
| - for (int i = 0; i < _seen.length; i++) { |
| + for (int i = 0; i < seen.length; i++) { |
| if (_seen[i] === object) { |
| throw 'Cyclic structure'; |
| } |
| } |
| - _seen.add(object); |
| + seen.add(object); |
| } |
| - void _stringify(final object) { |
| + void stringifyValue(final object) { |
| + // Tries stringifying object directly. If it's not a simple value, List or |
| + // Map, call toJson() to get a custom representation and try serializing |
| + // that. |
| + if (!_stringifyJsonValue(object)) { |
| + checkCycle(object); |
| + var customJson = object.toJson(); |
| + if (!_stringifyJsonValue(customJson)) { |
| + throw const JsonUnsupportedObjectType(); |
| + } |
| + seen.removeLast(); |
| + } |
| + } |
| + |
| + /** |
| + * Serializes a [num], [String], [bool], [Null], [List] or [Map] value. |
| + * |
| + * Returns true if the value is one of these types, and false if not. |
| + * If a value is both a [List] and a [Map], it's serialized as a [List]. |
| + */ |
| + bool stringifyJsonValue(final object) { |
| if (object is num) { |
| // TODO: use writeOn. |
| - _sb.add(_numberToString(object)); |
| - return; |
| + sb.add(_numberToString(object)); |
| + return true; |
| } else if (object === true) { |
| - _sb.add('true'); |
| - return; |
| + sb.add('true'); |
| + return true; |
| } else if (object === false) { |
| - _sb.add('false'); |
| - return; |
| + sb.add('false'); |
| + return true; |
| } else if (object === null) { |
| - _sb.add('null'); |
| - return; |
| + sb.add('null'); |
| + return true; |
| } else if (object is String) { |
| - _sb.add('"'); |
| - _escape(_sb, object); |
| - _sb.add('"'); |
| - return; |
| + sb.add('"'); |
| + escape(_sb, object); |
| + sb.add('"'); |
| + return true; |
| } else if (object is List) { |
| - _checkCycle(object); |
| + checkCycle(object); |
| List a = object; |
| - _sb.add('['); |
| + sb.add('['); |
| if (a.length > 0) { |
| - _stringify(a[0]); |
| + stringifyValue(a[0]); |
| // TODO: switch to Iterables. |
| for (int i = 1; i < a.length; i++) { |
| - _sb.add(','); |
| - _stringify(a[i]); |
| + sb.add(','); |
| + stringifyValue(a[i]); |
| } |
| } |
| - _sb.add(']'); |
| - _seen.removeLast(); |
| - return; |
| + sb.add(']'); |
| + seen.removeLast(); |
| + return true; |
| } else if (object is Map) { |
| - _checkCycle(object); |
| + checkCycle(object); |
| Map<String, Object> m = object; |
| - _sb.add('{'); |
| + sb.add('{'); |
| bool first = true; |
| m.forEach((String key, Object value) { |
| if (!first) { |
| - _sb.add(',"'); |
| + sb.add(',"'); |
| } else { |
| - _sb.add('"'); |
| + sb.add('"'); |
| } |
| - _escape(_sb, key); |
| - _sb.add('":'); |
| - _stringify(value); |
| + escape(_sb, key); |
| + sb.add('":'); |
| + stringifyValue(value); |
| first = false; |
| }); |
| - _sb.add('}'); |
| - _seen.removeLast(); |
| - return; |
| + sb.add('}'); |
| + seen.removeLast(); |
| + return true; |
| } else { |
| - throw const JsonUnsupportedObjectType(); |
| + return false; |
| } |
| } |
| } |