Skip to content
This repository has been archived by the owner on Apr 12, 2024. It is now read-only.

Commit

Permalink
refactor(toJson): use native JSON.stringify
Browse files Browse the repository at this point in the history
Instead of using our custom serializer we now use the native one and
use the replacer function to customize the serialization to preserve
most of the previous behavior (ignore $ and $$ properties as well
as window, document and scope instances).
  • Loading branch information
IgorMinar committed Mar 28, 2012
1 parent 87f5c6e commit 35125d2
Show file tree
Hide file tree
Showing 5 changed files with 51 additions and 247 deletions.
149 changes: 16 additions & 133 deletions src/JSON.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
'use strict';

var jsonReplacer = function(key, value) {
var val = value;
if (/^\$+/.test(key)) {
val = undefined;
} else if (isWindow(value)) {
val = '$WINDOW';
} else if (value && document === value) {
val = '$DOCUMENT';
} else if (isScope(value)) {
val = '$SCOPE';
}

return val;
};

/**
* @ngdoc function
* @name angular.toJson
Expand All @@ -13,9 +28,7 @@
* @returns {string} Jsonified string representing `obj`.
*/
function toJson(obj, pretty) {
var buf = [];
toJsonArray(buf, obj, pretty ? "\n " : null, []);
return buf.join('');
return JSON.stringify(obj, jsonReplacer, pretty ? ' ' : null);
}

/**
Expand All @@ -34,133 +47,3 @@ function fromJson(json) {
? JSON.parse(json)
: json;
}


function jsonDateToString(date){
if (!date) return date;
var isoString = date.toISOString ? date.toISOString() : '';
return (isoString.length==24)
? isoString
: padNumber(date.getUTCFullYear(), 4) + '-' +
padNumber(date.getUTCMonth() + 1, 2) + '-' +
padNumber(date.getUTCDate(), 2) + 'T' +
padNumber(date.getUTCHours(), 2) + ':' +
padNumber(date.getUTCMinutes(), 2) + ':' +
padNumber(date.getUTCSeconds(), 2) + '.' +
padNumber(date.getUTCMilliseconds(), 3) + 'Z';
}

function quoteUnicode(string) {
var chars = ['"'];
for ( var i = 0; i < string.length; i++) {
var code = string.charCodeAt(i);
var ch = string.charAt(i);
switch(ch) {
case '"': chars.push('\\"'); break;
case '\\': chars.push('\\\\'); break;
case '\n': chars.push('\\n'); break;
case '\f': chars.push('\\f'); break;
case '\r': chars.push(ch = '\\r'); break;
case '\t': chars.push(ch = '\\t'); break;
default:
if (32 <= code && code <= 126) {
chars.push(ch);
} else {
var encode = "000" + code.toString(16);
chars.push("\\u" + encode.substring(encode.length - 4));
}
}
}
chars.push('"');
return chars.join('');
}


function toJsonArray(buf, obj, pretty, stack) {
if (isObject(obj)) {
if (obj === window) {
buf.push('WINDOW');
return;
}

if (obj === document) {
buf.push('DOCUMENT');
return;
}

if (includes(stack, obj)) {
buf.push('RECURSION');
return;
}
stack.push(obj);
}
if (obj === null) {
buf.push('null');
} else if (obj instanceof RegExp) {
buf.push(quoteUnicode(obj.toString()));
} else if (isFunction(obj)) {
return;
} else if (isBoolean(obj)) {
buf.push('' + obj);
} else if (isNumber(obj)) {
if (isNaN(obj)) {
buf.push('null');
} else {
buf.push('' + obj);
}
} else if (isString(obj)) {
return buf.push(quoteUnicode(obj));
} else if (isObject(obj)) {
if (isArray(obj)) {
buf.push("[");
var len = obj.length;
var sep = false;
for(var i=0; i<len; i++) {
var item = obj[i];
if (sep) buf.push(",");
if (!(item instanceof RegExp) && (isFunction(item) || isUndefined(item))) {
buf.push('null');
} else {
toJsonArray(buf, item, pretty, stack);
}
sep = true;
}
buf.push("]");
} else if (isElement(obj)) {
// TODO(misko): maybe in dev mode have a better error reporting?
buf.push('DOM_ELEMENT');
} else if (isDate(obj)) {
buf.push(quoteUnicode(jsonDateToString(obj)));
} else {
buf.push("{");
if (pretty) buf.push(pretty);
var comma = false;
var childPretty = pretty ? pretty + " " : false;
var keys = [];
for(var k in obj) {
if (k!='this' && k!='$parent' && k.substring(0,2) != '$$' && obj.hasOwnProperty(k) && obj[k] !== undefined) {
keys.push(k);
}
}
keys.sort();
for ( var keyIndex = 0; keyIndex < keys.length; keyIndex++) {
var key = keys[keyIndex];
var value = obj[key];
if (!isFunction(value)) {
if (comma) {
buf.push(",");
if (pretty) buf.push(pretty);
}
buf.push(quoteUnicode(key));
buf.push(":");
toJsonArray(buf, value, childPretty, stack);
comma = true;
}
}
buf.push("}");
}
}
if (isObject(obj)) {
stack.pop();
}
}
4 changes: 2 additions & 2 deletions src/ng/directive/input.js
Original file line number Diff line number Diff line change
Expand Up @@ -671,7 +671,7 @@ function checkboxInputType(scope, element, attr, ctrl) {
</doc:source>
<doc:scenario>
it('should initialize to model', function() {
expect(binding('user')).toEqual('{"last":"visitor","name":"guest"}');
expect(binding('user')).toEqual('{"name":"guest","last":"visitor"}');
expect(binding('myForm.userName.$valid')).toEqual('true');
expect(binding('myForm.$valid')).toEqual('true');
});
Expand All @@ -685,7 +685,7 @@ function checkboxInputType(scope, element, attr, ctrl) {
it('should be valid if empty when min length is set', function() {
input('user.last').enter('');
expect(binding('user')).toEqual('{"last":"","name":"guest"}');
expect(binding('user')).toEqual('{"name":"guest","last":""}');
expect(binding('myForm.lastName.$valid')).toEqual('true');
expect(binding('myForm.$valid')).toEqual('true');
});
Expand Down
2 changes: 1 addition & 1 deletion src/ng/filter/filters.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ function dateFilter($locale) {
</doc:source>
<doc:scenario>
it('should jsonify filtered objects', function() {
expect(binding("{'name':'value'}")).toBe('{\n "name":"value"}');
expect(binding("{'name':'value'}")).toMatch(/\{\n "name": ?"value"\n}/);
});
</doc:scenario>
</doc:example>
Expand Down
10 changes: 5 additions & 5 deletions src/ng/filter/limitTo.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,24 @@
}
</script>
<div ng-controller="Ctrl">
Limit {{numbers}} to: <input type="integer" ng-model="limit"/>
<p>Output: {{ numbers | limitTo:limit | json }}</p>
Limit {{numbers}} to: <input type="integer" ng-model="limit" ng-model-instant>
<p>Output: {{ numbers | limitTo:limit }}</p>
</div>
</doc:source>
<doc:scenario>
it('should limit the numer array to first three items', function() {
expect(element('.doc-example-live input[ng-model=limit]').val()).toBe('3');
expect(binding('numbers | limitTo:limit | json')).toEqual('[1,2,3]');
expect(binding('numbers | limitTo:limit')).toEqual('[1,2,3]');
});
it('should update the output when -3 is entered', function() {
input('limit').enter(-3);
expect(binding('numbers | limitTo:limit | json')).toEqual('[7,8,9]');
expect(binding('numbers | limitTo:limit')).toEqual('[7,8,9]');
});
it('should not exceed the maximum size of input array', function() {
input('limit').enter(100);
expect(binding('numbers | limitTo:limit | json')).toEqual('[1,2,3,4,5,6,7,8,9]');
expect(binding('numbers | limitTo:limit')).toEqual('[1,2,3,4,5,6,7,8,9]');
});
</doc:scenario>
</doc:example>
Expand Down
133 changes: 27 additions & 106 deletions test/JsonSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ describe('json', function() {

describe('fromJson', function() {

it('should delegate to native parser', function() {
it('should delegate to JSON.parse', function() {
var spy = spyOn(JSON, 'parse').andCallThrough();

expect(fromJson('{}')).toEqual({});
Expand All @@ -13,125 +13,46 @@ describe('json', function() {
});


it('should serialize primitives', function() {
expect(toJson(0/0)).toEqual('null');
expect(toJson(null)).toEqual('null');
expect(toJson(true)).toEqual('true');
expect(toJson(false)).toEqual('false');
expect(toJson(123.45)).toEqual('123.45');
expect(toJson('abc')).toEqual('"abc"');
expect(toJson('a \t \n \r b \\')).toEqual('"a \\t \\n \\r b \\\\"');
});

it('should not serialize $$properties', function() {
expect(toJson({$$some:'value', 'this':1, '$parent':1}, false)).toEqual('{}');
});

it('should not serialize this or $parent', function() {
expect(toJson({'this':'value', $parent:'abc'}, false)).toEqual('{}');
});
describe('toJson', function() {

it('should serialize strings with escaped characters', function() {
expect(toJson("7\\\"7")).toEqual("\"7\\\\\\\"7\"");
});

it('should serialize objects', function() {
expect(toJson({a: 1, b: 2})).toEqual('{"a":1,"b":2}');
expect(toJson({a: {b: 2}})).toEqual('{"a":{"b":2}}');
expect(toJson({a: {b: {c: 0}}})).toEqual('{"a":{"b":{"c":0}}}');
expect(toJson({a: {b: 0/0}})).toEqual('{"a":{"b":null}}');
});

it('should format objects pretty', function() {
expect(toJson({a: 1, b: 2}, true)).toEqual('{\n "a":1,\n "b":2}');
expect(toJson({a: {b: 2}}, true)).toEqual('{\n "a":{\n "b":2}}');
});
it('should delegate to JSON.stringify', function() {
var spy = spyOn(JSON, 'stringify').andCallThrough();

it('should serialize array', function() {
expect(toJson([])).toEqual('[]');
expect(toJson([1, 'b'])).toEqual('[1,"b"]');
});

it('should serialize RegExp', function() {
expect(toJson(/foo/)).toEqual('"/foo/"');
expect(toJson([1, new RegExp('foo')])).toEqual('[1,"/foo/"]');
});

it('should ignore functions', function() {
expect(toJson([function() {},1])).toEqual('[null,1]');
expect(toJson({a:function() {}})).toEqual('{}');
});

it('should serialize array with empty items', function() {
var a = [];
a[1] = 'X';
expect(toJson(a)).toEqual('[null,"X"]');
});

it('should escape unicode', function() {
expect('\u00a0'.length).toEqual(1);
expect(toJson('\u00a0').length).toEqual(8);
expect(fromJson(toJson('\u00a0')).length).toEqual(1);
});

it('should serialize UTC dates', function() {
var date = new angular.mock.TzDate(-1, '2009-10-09T01:02:03.027Z');
expect(toJson(date)).toEqual('"2009-10-09T01:02:03.027Z"');
});

it('should prevent recursion', function() {
var obj = {a: 'b'};
obj.recursion = obj;
expect(angular.toJson(obj)).toEqual('{"a":"b","recursion":RECURSION}');
});
expect(toJson({})).toEqual('{}');
expect(spy).toHaveBeenCalled();
});

it('should serialize $ properties', function() {
var obj = {$a: 'a'};
expect(angular.toJson(obj)).toEqual('{"$a":"a"}');
});

it('should NOT serialize inherited properties', function() {
// This is what native Browser does
var obj = inherit({p:'p'});
obj.a = 'a';
expect(angular.toJson(obj)).toEqual('{"a":"a"}');
});
it('should format objects pretty', function() {
expect(toJson({a: 1, b: 2}, true)).
toBeOneOf('{\n "a": 1,\n "b": 2\n}', '{\n "a":1,\n "b":2\n}');
expect(toJson({a: {b: 2}}, true)).
toBeOneOf('{\n "a": {\n "b": 2\n }\n}', '{\n "a":{\n "b":2\n }\n}');
});

it('should serialize same objects multiple times', function() {
var obj = {a:'b'};
expect(angular.toJson({A:obj, B:obj})).toEqual('{"A":{"a":"b"},"B":{"a":"b"}}');
});

it('should not serialize undefined values', function() {
expect(angular.toJson({A:undefined})).toEqual('{}');
});
it('should not serialize properties starting with $', function() {
expect(toJson({$few: 'v', $$some:'value'}, false)).toEqual('{}');
});

it('should not serialize $window object', function() {
expect(toJson(window)).toEqual('WINDOW');
});

it('should not serialize $document object', function() {
expect(toJson(document)).toEqual('DOCUMENT');
});
it('should not serialize undefined values', function() {
expect(angular.toJson({A:undefined})).toEqual('{}');
});


describe('string', function() {
it('should quote', function() {
expect(quoteUnicode('a')).toBe('"a"');
expect(quoteUnicode('\\')).toBe('"\\\\"');
expect(quoteUnicode("'a'")).toBe('"\'a\'"');
expect(quoteUnicode('"a"')).toBe('"\\"a\\""');
expect(quoteUnicode('\n\f\r\t')).toBe('"\\n\\f\\r\\t"');
it('should not serialize $window object', function() {
expect(toJson(window)).toEqual('"$WINDOW"');
});

it('should quote slashes', function() {
expect(quoteUnicode("7\\\"7")).toBe('"7\\\\\\\"7"');
});

it('should quote unicode', function() {
expect(quoteUnicode('abc\u00A0def')).toBe('"abc\\u00a0def"');
it('should not serialize $document object', function() {
expect(toJson(document)).toEqual('"$DOCUMENT"');
});

});

it('should not serialize scope instances', inject(function($rootScope) {
expect(toJson({key: $rootScope})).toEqual('{"key":"$SCOPE"}');
}));
});
});

0 comments on commit 35125d2

Please sign in to comment.