From f7381ae9b1cfefbb55a4cee397152eace55c7ec9 Mon Sep 17 00:00:00 2001 From: 3rd-Eden Date: Wed, 4 Jun 2014 18:16:20 +0200 Subject: [PATCH] [major] Upgrade to the latest version of EJSON --- index.js | 89 ++++++++++++++++++++++++++++++++++++++++++++---- vendor/base64.js | 1 + vendor/ejson.js | 88 +++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 164 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index 8e5c4d7..29bc037 100644 --- a/index.js +++ b/index.js @@ -10,11 +10,14 @@ var customTypes = {}; // from a basic JSON-able representation. The factory argument // is a function of JSON-able --> your object // The type you add must have: -// - A clone() method, so that Meteor can deep-copy it when necessary. -// - A equals() method, so that Meteor can compare it // - A toJSONValue() method, so that Meteor can serialize it // - a typeName() method, to show how to look it up in our type table. // It is okay if these methods are monkey-patched on. +// EJSON.clone will use toJSONValue and the given factory to produce +// a clone, but you may specify a method clone() that will be +// used instead. +// Similarly, EJSON.equals will use toJSONValue to make comparisons, +// but you may provide a method equals() instead. // EJSON.addType = function (name, factory) { if (_.has(customTypes, name)) @@ -22,6 +25,10 @@ EJSON.addType = function (name, factory) { customTypes[name] = factory; }; +var isInfOrNan = function (obj) { + return _.isNaN(obj) || obj === Infinity || obj === -Infinity; +}; + var builtinConverters = [ { // Date matchJSONValue: function (obj) { @@ -37,6 +44,26 @@ var builtinConverters = [ return new Date(obj.$date); } }, + { // NaN, Inf, -Inf. (These are the only objects with typeof !== 'object' + // which we match.) + matchJSONValue: function (obj) { + return _.has(obj, '$InfNaN') && _.size(obj) === 1; + }, + matchObject: isInfOrNan, + toJSONValue: function (obj) { + var sign; + if (_.isNaN(obj)) + sign = 0; + else if (obj === Infinity) + sign = 1; + else + sign = -1; + return {$InfNaN: sign}; + }, + fromJSONValue: function (obj) { + return obj.$InfNaN/0; + } + }, { // Binary matchJSONValue: function (obj) { return _.has(obj, '$binary') && _.size(obj) === 1; @@ -87,12 +114,19 @@ var builtinConverters = [ return EJSON._isCustomType(obj); }, toJSONValue: function (obj) { - return {$type: obj.typeName(), $value: obj.toJSONValue()}; + var jsonValue = Meteor._noYieldsAllowed(function () { + return obj.toJSONValue(); + }); + return {$type: obj.typeName(), $value: jsonValue}; }, fromJSONValue: function (obj) { var typeName = obj.$type; + if (!_.has(customTypes, typeName)) + throw new Error("Custom EJSON type " + typeName + " is not defined"); var converter = customTypes[typeName]; - return converter(obj.$value); + return Meteor._noYieldsAllowed(function () { + return converter(obj.$value); + }); } } ]; @@ -108,14 +142,23 @@ EJSON._isCustomType = function (obj) { // for both arrays and objects, in-place modification. var adjustTypesToJSONValue = EJSON._adjustTypesToJSONValue = function (obj) { + // Is it an atom that we need to adjust? if (obj === null) return null; var maybeChanged = toJSONValueHelper(obj); if (maybeChanged !== undefined) return maybeChanged; + + // Other atoms are unchanged. + if (typeof obj !== 'object') + return obj; + + // Iterate over array or object structure. _.each(obj, function (value, key) { - if (typeof value !== 'object' && value !== undefined) + if (typeof value !== 'object' && value !== undefined && + !isInfOrNan(value)) return; // continue + var changed = toJSONValueHelper(value); if (changed) { obj[key] = changed; @@ -162,6 +205,11 @@ EJSON._adjustTypesFromJSONValue = function (obj) { var maybeChanged = fromJSONValueHelper(obj); if (maybeChanged !== obj) return maybeChanged; + + // Other atoms are unchanged. + if (typeof obj !== 'object') + return obj; + _.each(obj, function (value, key) { if (typeof value === 'object') { var changed = fromJSONValueHelper(value); @@ -210,11 +258,18 @@ EJSON.fromJSONValue = function (item) { } }; -EJSON.stringify = function (item) { - return JSON.stringify(EJSON.toJSONValue(item)); +EJSON.stringify = function (item, options) { + var json = EJSON.toJSONValue(item); + if (options && (options.canonical || options.indent)) { + return EJSON._canonicalStringify(json, options); + } else { + return JSON.stringify(json); + } }; EJSON.parse = function (item) { + if (typeof item !== 'string') + throw new Error("EJSON.parse argument should be a string"); return EJSON.fromJSONValue(JSON.parse(item)); }; @@ -228,6 +283,9 @@ EJSON.equals = function (a, b, options) { var keyOrderSensitive = !!(options && options.keyOrderSensitive); if (a === b) return true; + if (_.isNaN(a) && _.isNaN(b)) + return true; // This differs from the IEEE spec for NaN equality, b/c we don't want + // anything ever with a NaN to be poisoned from becoming equal to anything. if (!a || !b) // if either one is falsy, they'd have to be === to be equal return false; if (!(typeof a === 'object' && typeof b === 'object')) @@ -245,6 +303,8 @@ EJSON.equals = function (a, b, options) { } if (typeof (a.equals) === 'function') return a.equals(b, options); + if (typeof (b.equals) === 'function') + return b.equals(a, options); if (a instanceof Array) { if (!(b instanceof Array)) return false; @@ -256,6 +316,11 @@ EJSON.equals = function (a, b, options) { } return true; } + // fallback for custom types that don't implement their own equals + switch (EJSON._isCustomType(a) + EJSON._isCustomType(b)) { + case 1: return false; + case 2: return EJSON.equals(EJSON.toJSONValue(a), EJSON.toJSONValue(b)); + } // fall back to structural equality of objects var ret; if (keyOrderSensitive) { @@ -302,6 +367,10 @@ EJSON.clone = function (v) { return null; // null has typeof "object" if (v instanceof Date) return new Date(v.getTime()); + // RegExps are not really EJSON elements (eg we don't define a serialization + // for them), but they're immutable anyway, so we can support them in clone. + if (v instanceof RegExp) + return v; if (EJSON.isBinary(v)) { ret = EJSON.newBinary(v.length); for (var i = 0; i < v.length; i++) { @@ -309,6 +378,7 @@ EJSON.clone = function (v) { } return ret; } + // XXX: Use something better than underscore's isArray if (_.isArray(v) || _.isArguments(v)) { // For some reason, _.map doesn't work in this context on Opera (weird test // failures). @@ -321,6 +391,10 @@ EJSON.clone = function (v) { if (typeof v.clone === 'function') { return v.clone(); } + // handle other custom types + if (EJSON._isCustomType(v)) { + return EJSON.fromJSONValue(EJSON.clone(EJSON.toJSONValue(v)), true); + } // handle other objects ret = {}; _.each(v, function (value, key) { @@ -330,6 +404,7 @@ EJSON.clone = function (v) { }; // Base 64 encoding + var BASE_64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; var BASE_64_VALS = {}; diff --git a/vendor/base64.js b/vendor/base64.js index 22c5e27..795050b 100644 --- a/vendor/base64.js +++ b/vendor/base64.js @@ -1,4 +1,5 @@ // Base 64 encoding + var BASE_64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; var BASE_64_VALS = {}; diff --git a/vendor/ejson.js b/vendor/ejson.js index 3bce3f8..798fb38 100644 --- a/vendor/ejson.js +++ b/vendor/ejson.js @@ -6,11 +6,14 @@ var customTypes = {}; // from a basic JSON-able representation. The factory argument // is a function of JSON-able --> your object // The type you add must have: -// - A clone() method, so that Meteor can deep-copy it when necessary. -// - A equals() method, so that Meteor can compare it // - A toJSONValue() method, so that Meteor can serialize it // - a typeName() method, to show how to look it up in our type table. // It is okay if these methods are monkey-patched on. +// EJSON.clone will use toJSONValue and the given factory to produce +// a clone, but you may specify a method clone() that will be +// used instead. +// Similarly, EJSON.equals will use toJSONValue to make comparisons, +// but you may provide a method equals() instead. // EJSON.addType = function (name, factory) { if (_.has(customTypes, name)) @@ -18,6 +21,10 @@ EJSON.addType = function (name, factory) { customTypes[name] = factory; }; +var isInfOrNan = function (obj) { + return _.isNaN(obj) || obj === Infinity || obj === -Infinity; +}; + var builtinConverters = [ { // Date matchJSONValue: function (obj) { @@ -33,6 +40,26 @@ var builtinConverters = [ return new Date(obj.$date); } }, + { // NaN, Inf, -Inf. (These are the only objects with typeof !== 'object' + // which we match.) + matchJSONValue: function (obj) { + return _.has(obj, '$InfNaN') && _.size(obj) === 1; + }, + matchObject: isInfOrNan, + toJSONValue: function (obj) { + var sign; + if (_.isNaN(obj)) + sign = 0; + else if (obj === Infinity) + sign = 1; + else + sign = -1; + return {$InfNaN: sign}; + }, + fromJSONValue: function (obj) { + return obj.$InfNaN/0; + } + }, { // Binary matchJSONValue: function (obj) { return _.has(obj, '$binary') && _.size(obj) === 1; @@ -83,12 +110,19 @@ var builtinConverters = [ return EJSON._isCustomType(obj); }, toJSONValue: function (obj) { - return {$type: obj.typeName(), $value: obj.toJSONValue()}; + var jsonValue = Meteor._noYieldsAllowed(function () { + return obj.toJSONValue(); + }); + return {$type: obj.typeName(), $value: jsonValue}; }, fromJSONValue: function (obj) { var typeName = obj.$type; + if (!_.has(customTypes, typeName)) + throw new Error("Custom EJSON type " + typeName + " is not defined"); var converter = customTypes[typeName]; - return converter(obj.$value); + return Meteor._noYieldsAllowed(function () { + return converter(obj.$value); + }); } } ]; @@ -104,14 +138,23 @@ EJSON._isCustomType = function (obj) { // for both arrays and objects, in-place modification. var adjustTypesToJSONValue = EJSON._adjustTypesToJSONValue = function (obj) { + // Is it an atom that we need to adjust? if (obj === null) return null; var maybeChanged = toJSONValueHelper(obj); if (maybeChanged !== undefined) return maybeChanged; + + // Other atoms are unchanged. + if (typeof obj !== 'object') + return obj; + + // Iterate over array or object structure. _.each(obj, function (value, key) { - if (typeof value !== 'object' && value !== undefined) + if (typeof value !== 'object' && value !== undefined && + !isInfOrNan(value)) return; // continue + var changed = toJSONValueHelper(value); if (changed) { obj[key] = changed; @@ -158,6 +201,11 @@ EJSON._adjustTypesFromJSONValue = function (obj) { var maybeChanged = fromJSONValueHelper(obj); if (maybeChanged !== obj) return maybeChanged; + + // Other atoms are unchanged. + if (typeof obj !== 'object') + return obj; + _.each(obj, function (value, key) { if (typeof value === 'object') { var changed = fromJSONValueHelper(value); @@ -206,11 +254,18 @@ EJSON.fromJSONValue = function (item) { } }; -EJSON.stringify = function (item) { - return JSON.stringify(EJSON.toJSONValue(item)); +EJSON.stringify = function (item, options) { + var json = EJSON.toJSONValue(item); + if (options && (options.canonical || options.indent)) { + return EJSON._canonicalStringify(json, options); + } else { + return JSON.stringify(json); + } }; EJSON.parse = function (item) { + if (typeof item !== 'string') + throw new Error("EJSON.parse argument should be a string"); return EJSON.fromJSONValue(JSON.parse(item)); }; @@ -224,6 +279,9 @@ EJSON.equals = function (a, b, options) { var keyOrderSensitive = !!(options && options.keyOrderSensitive); if (a === b) return true; + if (_.isNaN(a) && _.isNaN(b)) + return true; // This differs from the IEEE spec for NaN equality, b/c we don't want + // anything ever with a NaN to be poisoned from becoming equal to anything. if (!a || !b) // if either one is falsy, they'd have to be === to be equal return false; if (!(typeof a === 'object' && typeof b === 'object')) @@ -241,6 +299,8 @@ EJSON.equals = function (a, b, options) { } if (typeof (a.equals) === 'function') return a.equals(b, options); + if (typeof (b.equals) === 'function') + return b.equals(a, options); if (a instanceof Array) { if (!(b instanceof Array)) return false; @@ -252,6 +312,11 @@ EJSON.equals = function (a, b, options) { } return true; } + // fallback for custom types that don't implement their own equals + switch (EJSON._isCustomType(a) + EJSON._isCustomType(b)) { + case 1: return false; + case 2: return EJSON.equals(EJSON.toJSONValue(a), EJSON.toJSONValue(b)); + } // fall back to structural equality of objects var ret; if (keyOrderSensitive) { @@ -298,6 +363,10 @@ EJSON.clone = function (v) { return null; // null has typeof "object" if (v instanceof Date) return new Date(v.getTime()); + // RegExps are not really EJSON elements (eg we don't define a serialization + // for them), but they're immutable anyway, so we can support them in clone. + if (v instanceof RegExp) + return v; if (EJSON.isBinary(v)) { ret = EJSON.newBinary(v.length); for (var i = 0; i < v.length; i++) { @@ -305,6 +374,7 @@ EJSON.clone = function (v) { } return ret; } + // XXX: Use something better than underscore's isArray if (_.isArray(v) || _.isArguments(v)) { // For some reason, _.map doesn't work in this context on Opera (weird test // failures). @@ -317,6 +387,10 @@ EJSON.clone = function (v) { if (typeof v.clone === 'function') { return v.clone(); } + // handle other custom types + if (EJSON._isCustomType(v)) { + return EJSON.fromJSONValue(EJSON.clone(EJSON.toJSONValue(v)), true); + } // handle other objects ret = {}; _.each(v, function (value, key) {