Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(59)

Unified Diff: lib/json/json.dart

Issue 10914009: Make JSON.stringify call toJson() on objects that it can't serialize. (Closed) Base URL: https://dart.googlecode.com/svn/branches/bleeding_edge/dart
Patch Set: Created 8 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | tests/json/json_test.dart » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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;
}
}
}
« no previous file with comments | « no previous file | tests/json/json_test.dart » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698