Index: sdk/lib/convert/json.dart |
diff --git a/sdk/lib/convert/json.dart b/sdk/lib/convert/json.dart |
index 9780200836a060e892e8ef7644df49c4dda5e5b2..be503b0b92d5c9abc13f3a4bd765b7edd33b7b7e 100644 |
--- a/sdk/lib/convert/json.dart |
+++ b/sdk/lib/convert/json.dart |
@@ -17,16 +17,16 @@ part of dart.convert; |
class JsonUnsupportedObjectError extends Error { |
/** The object that could not be serialized. */ |
final unsupportedObject; |
- /** The exception thrown by object's [:toJson:] method, if any. */ |
+ /** The exception thrown when trying to convert the object. */ |
final cause; |
JsonUnsupportedObjectError(this.unsupportedObject, { this.cause }); |
String toString() { |
if (cause != null) { |
- return "Calling toJson method on object failed."; |
+ return "Converting object to an encodable object failed."; |
} else { |
- return "Object toJson method returns non-serializable value."; |
+ return "Converting object did not return an encodable object."; |
} |
} |
} |
@@ -86,8 +86,9 @@ class JsonCodec extends Codec<Object, String> { |
* |
* The default [reviver] (when not provided) is the identity function. |
*/ |
- Object decode(String str, {reviver(var key, var value)}) { |
- return new JsonDecoder(reviver).convert(str); |
+ Object decode(String source, {reviver(var key, var value)}) { |
+ if (reviver == null) return decoder.convert(source); |
+ return new JsonDecoder(reviver).convert(source); |
} |
/** |
@@ -102,11 +103,12 @@ class JsonCodec extends Codec<Object, String> { |
* unencodable object. |
*/ |
Object encode(Object value, {toEncodable(var object)}) { |
+ if (toEncodable == null) return encoder.convert(value); |
return new JsonEncoder(toEncodable).convert(value); |
} |
- JsonEncoder get encoder => new JsonEncoder(); |
- JsonDecoder get decoder => new JsonDecoder(null); |
+ JsonEncoder get encoder => const JsonEncoder(); |
+ JsonDecoder get decoder => const JsonDecoder(null); |
} |
typedef _Reviver(var key, var value); |
@@ -115,9 +117,9 @@ class _ReviverJsonCodec extends JsonCodec { |
final _Reviver _reviver; |
_ReviverJsonCodec(this._reviver); |
- Object decode(String str, {reviver(var key, var value)}) { |
+ Object decode(String source, {reviver(var key, var value)}) { |
if (reviver == null) reviver = _reviver; |
- return new JsonDecoder(reviver).convert(str); |
+ return new JsonDecoder(reviver).convert(source); |
} |
JsonDecoder get decoder => new JsonDecoder(_reviver); |
@@ -141,7 +143,7 @@ class JsonEncoder extends Converter<Object, String> { |
* If [toEncodable] is omitted, it defaults to calling `.toJson()` on |
* the object. |
*/ |
- JsonEncoder([Object toEncodable(Object nonSerializable)]) |
+ const JsonEncoder([Object toEncodable(Object nonSerializable)]) |
: this._toEncodableFunction = toEncodable; |
/** |
@@ -171,7 +173,8 @@ class JsonEncoder extends Converter<Object, String> { |
* 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. |
*/ |
- String convert(Object o) => OLD_JSON_LIB.stringify(o, _toEncodableFunction); |
+ String convert(Object o) => |
+ _JsonStringifier.stringify(o, _toEncodableFunction); |
/** |
* Starts a chunked conversion. |
@@ -219,7 +222,7 @@ class _JsonEncoderSink extends ChunkedConversionSink<Object> { |
} |
_isDone = true; |
ClosableStringSink stringSink = _sink.asStringSink(); |
- OLD_JSON_LIB.printOn(o, stringSink, _toEncodableFunction); |
+ _JsonStringifier.printOn(o, stringSink, _toEncodableFunction); |
stringSink.close(); |
} |
@@ -236,7 +239,7 @@ class JsonDecoder extends Converter<String, Object> { |
* |
* The [reviver] may be `null`. |
*/ |
- JsonDecoder(reviver(var key, var value)) : this._reviver = reviver; |
+ const JsonDecoder(reviver(var key, var value)) : this._reviver = reviver; |
/** |
* Converts the given JSON-string [input] to its corresponding object. |
@@ -297,3 +300,185 @@ class _JsonDecoderSink extends _StringSinkConversionSink { |
// Internal optimized JSON parsing implementation. |
external _parseJson(String source, reviver(key, value)); |
+ |
+ |
+// Implementation of encoder/stringifier. |
+ |
+Object _defaultToEncodable(object) => object.toJson(); |
+ |
+class _JsonStringifier { |
+ // Character code constants. |
+ static const int BACKSPACE = 0x08; |
+ static const int TAB = 0x09; |
+ static const int NEWLINE = 0x0a; |
+ static const int CARRIAGE_RETURN = 0x0d; |
+ static const int FORM_FEED = 0x0c; |
+ static const int QUOTE = 0x22; |
+ static const int BACKSLASH = 0x5c; |
+ static const int CHAR_b = 0x62; |
+ static const int CHAR_f = 0x66; |
+ static const int CHAR_n = 0x6e; |
+ static const int CHAR_r = 0x72; |
+ static const int CHAR_t = 0x74; |
+ static const int CHAR_u = 0x75; |
+ |
+ final Function toEncodable; |
+ final StringSink sink; |
+ final Set<Object> seen; |
+ |
+ _JsonStringifier(this.sink, this.toEncodable) |
+ : this.seen = new HashSet.identity(); |
+ |
+ static String stringify(final object, toEncodable(object)) { |
+ if (toEncodable == null) toEncodable = _defaultToEncodable; |
+ StringBuffer output = new StringBuffer(); |
+ _JsonStringifier stringifier = new _JsonStringifier(output, toEncodable); |
+ stringifier.stringifyValue(object); |
+ return output.toString(); |
+ } |
+ |
+ static void printOn(final object, StringSink output, toEncodable(object)) { |
+ _JsonStringifier stringifier = new _JsonStringifier(output, toEncodable); |
+ stringifier.stringifyValue(object); |
+ } |
+ |
+ static String numberToString(num x) { |
+ return x.toString(); |
+ } |
+ |
+ // ('0' + x) or ('a' + x - 10) |
+ static int hexDigit(int x) => x < 10 ? 48 + x : 87 + x; |
+ |
+ static void escape(StringSink sb, String s) { |
+ final int length = s.length; |
+ bool needsEscape = false; |
+ final charCodes = new List<int>(); |
+ for (int i = 0; i < length; i++) { |
+ int charCode = s.codeUnitAt(i); |
+ if (charCode < 32) { |
+ needsEscape = true; |
+ charCodes.add(BACKSLASH); |
+ switch (charCode) { |
+ case BACKSPACE: |
+ charCodes.add(CHAR_b); |
+ break; |
+ case TAB: |
+ charCodes.add(CHAR_t); |
+ break; |
+ case NEWLINE: |
+ charCodes.add(CHAR_n); |
+ break; |
+ case FORM_FEED: |
+ charCodes.add(CHAR_f); |
+ break; |
+ case CARRIAGE_RETURN: |
+ charCodes.add(CHAR_r); |
+ break; |
+ default: |
+ charCodes.add(CHAR_u); |
+ charCodes.add(hexDigit((charCode >> 12) & 0xf)); |
+ charCodes.add(hexDigit((charCode >> 8) & 0xf)); |
+ charCodes.add(hexDigit((charCode >> 4) & 0xf)); |
+ charCodes.add(hexDigit(charCode & 0xf)); |
+ break; |
+ } |
+ } else if (charCode == QUOTE || charCode == BACKSLASH) { |
+ needsEscape = true; |
+ charCodes.add(BACKSLASH); |
+ charCodes.add(charCode); |
+ } else { |
+ charCodes.add(charCode); |
+ } |
+ } |
+ sb.write(needsEscape ? new String.fromCharCodes(charCodes) : s); |
+ } |
+ |
+ void checkCycle(final object) { |
+ if (seen.contains(object)) { |
+ throw new JsonCyclicError(object); |
+ } |
+ seen.add(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); |
+ try { |
+ var customJson = toEncodable(object); |
+ if (!stringifyJsonValue(customJson)) { |
+ throw new JsonUnsupportedObjectError(object); |
+ } |
+ seen.remove(object); |
+ } catch (e) { |
+ throw new JsonUnsupportedObjectError(object, cause: e); |
+ } |
+ } |
+ } |
+ |
+ /** |
+ * 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. |
+ sink.write(numberToString(object)); |
+ return true; |
+ } else if (identical(object, true)) { |
+ sink.write('true'); |
+ return true; |
+ } else if (identical(object, false)) { |
+ sink.write('false'); |
+ return true; |
+ } else if (object == null) { |
+ sink.write('null'); |
+ return true; |
+ } else if (object is String) { |
+ sink.write('"'); |
+ escape(sink, object); |
+ sink.write('"'); |
+ return true; |
+ } else if (object is List) { |
+ checkCycle(object); |
+ List a = object; |
+ sink.write('['); |
+ if (a.length > 0) { |
+ stringifyValue(a[0]); |
+ // TODO: switch to Iterables. |
+ for (int i = 1; i < a.length; i++) { |
+ sink.write(','); |
+ stringifyValue(a[i]); |
+ } |
+ } |
+ sink.write(']'); |
+ seen.remove(object); |
+ return true; |
+ } else if (object is Map) { |
+ checkCycle(object); |
+ Map<String, Object> m = object; |
+ sink.write('{'); |
+ bool first = true; |
+ m.forEach((String key, Object value) { |
+ if (!first) { |
+ sink.write(',"'); |
+ } else { |
+ sink.write('"'); |
+ } |
+ escape(sink, key); |
+ sink.write('":'); |
+ stringifyValue(value); |
+ first = false; |
+ }); |
+ sink.write('}'); |
+ seen.remove(object); |
+ return true; |
+ } else { |
+ return false; |
+ } |
+ } |
+} |