mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
517 lines
15 KiB
JavaScript
517 lines
15 KiB
JavaScript
/**
|
|
* @namespace
|
|
* @summary Namespace for EJSON functions
|
|
*/
|
|
EJSON = {};
|
|
EJSONTest = {};
|
|
|
|
|
|
|
|
// Custom type interface definition
|
|
/**
|
|
* @class CustomType
|
|
* @instanceName customType
|
|
* @memberOf EJSON
|
|
* @summary The interface that a class must satisfy to be able to become an
|
|
* EJSON custom type via EJSON.addType.
|
|
*/
|
|
|
|
/**
|
|
* @function typeName
|
|
* @memberOf EJSON.CustomType
|
|
* @summary Return the tag used to identify this type. This must match the tag used to register this type with [`EJSON.addType`](#ejson_add_type).
|
|
* @locus Anywhere
|
|
* @instance
|
|
*/
|
|
|
|
/**
|
|
* @function toJSONValue
|
|
* @memberOf EJSON.CustomType
|
|
* @summary Serialize this instance into a JSON-compatible value.
|
|
* @locus Anywhere
|
|
* @instance
|
|
*/
|
|
|
|
/**
|
|
* @function clone
|
|
* @memberOf EJSON.CustomType
|
|
* @summary Return a value `r` such that `this.equals(r)` is true, and modifications to `r` do not affect `this` and vice versa.
|
|
* @locus Anywhere
|
|
* @instance
|
|
*/
|
|
|
|
/**
|
|
* @function equals
|
|
* @memberOf EJSON.CustomType
|
|
* @summary Return `true` if `other` has a value equal to `this`; `false` otherwise.
|
|
* @locus Anywhere
|
|
* @param {Object} other Another object to compare this to.
|
|
* @instance
|
|
*/
|
|
|
|
|
|
var customTypes = {};
|
|
// Add a custom type, using a method of your choice to get to and
|
|
// from a basic JSON-able representation. The factory argument
|
|
// is a function of JSON-able --> your object
|
|
// The type you add must have:
|
|
// - 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.
|
|
/**
|
|
* @summary Add a custom datatype to EJSON.
|
|
* @locus Anywhere
|
|
* @param {String} name A tag for your custom type; must be unique among custom data types defined in your project, and must match the result of your type's `typeName` method.
|
|
* @param {Function} factory A function that deserializes a JSON-compatible value into an instance of your type. This should match the serialization performed by your type's `toJSONValue` method.
|
|
*/
|
|
EJSON.addType = function (name, factory) {
|
|
if (_.has(customTypes, name))
|
|
throw new Error("Type " + name + " already present");
|
|
customTypes[name] = factory;
|
|
};
|
|
|
|
var isInfOrNan = function (obj) {
|
|
return _.isNaN(obj) || obj === Infinity || obj === -Infinity;
|
|
};
|
|
|
|
var builtinConverters = [
|
|
{ // Date
|
|
matchJSONValue: function (obj) {
|
|
return _.has(obj, '$date') && _.size(obj) === 1;
|
|
},
|
|
matchObject: function (obj) {
|
|
return obj instanceof Date;
|
|
},
|
|
toJSONValue: function (obj) {
|
|
return {$date: obj.getTime()};
|
|
},
|
|
fromJSONValue: function (obj) {
|
|
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;
|
|
},
|
|
matchObject: function (obj) {
|
|
return typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array
|
|
|| (obj && _.has(obj, '$Uint8ArrayPolyfill'));
|
|
},
|
|
toJSONValue: function (obj) {
|
|
return {$binary: Base64.encode(obj)};
|
|
},
|
|
fromJSONValue: function (obj) {
|
|
return Base64.decode(obj.$binary);
|
|
}
|
|
},
|
|
{ // Escaping one level
|
|
matchJSONValue: function (obj) {
|
|
return _.has(obj, '$escape') && _.size(obj) === 1;
|
|
},
|
|
matchObject: function (obj) {
|
|
if (_.isEmpty(obj) || _.size(obj) > 2) {
|
|
return false;
|
|
}
|
|
return _.any(builtinConverters, function (converter) {
|
|
return converter.matchJSONValue(obj);
|
|
});
|
|
},
|
|
toJSONValue: function (obj) {
|
|
var newObj = {};
|
|
_.each(obj, function (value, key) {
|
|
newObj[key] = EJSON.toJSONValue(value);
|
|
});
|
|
return {$escape: newObj};
|
|
},
|
|
fromJSONValue: function (obj) {
|
|
var newObj = {};
|
|
_.each(obj.$escape, function (value, key) {
|
|
newObj[key] = EJSON.fromJSONValue(value);
|
|
});
|
|
return newObj;
|
|
}
|
|
},
|
|
{ // Custom
|
|
matchJSONValue: function (obj) {
|
|
return _.has(obj, '$type') && _.has(obj, '$value') && _.size(obj) === 2;
|
|
},
|
|
matchObject: function (obj) {
|
|
return EJSON._isCustomType(obj);
|
|
},
|
|
toJSONValue: function (obj) {
|
|
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 Meteor._noYieldsAllowed(function () {
|
|
return converter(obj.$value);
|
|
});
|
|
}
|
|
}
|
|
];
|
|
|
|
EJSON._isCustomType = function (obj) {
|
|
return obj &&
|
|
typeof obj.toJSONValue === 'function' &&
|
|
typeof obj.typeName === 'function' &&
|
|
_.has(customTypes, obj.typeName());
|
|
};
|
|
|
|
EJSON._getTypes = function () {
|
|
return customTypes;
|
|
};
|
|
|
|
EJSON._getConverters = function () {
|
|
return builtinConverters;
|
|
};
|
|
|
|
// 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 &&
|
|
!isInfOrNan(value))
|
|
return; // continue
|
|
|
|
var changed = toJSONValueHelper(value);
|
|
if (changed) {
|
|
obj[key] = changed;
|
|
return; // on to the next key
|
|
}
|
|
// if we get here, value is an object but not adjustable
|
|
// at this level. recurse.
|
|
adjustTypesToJSONValue(value);
|
|
});
|
|
return obj;
|
|
};
|
|
|
|
// Either return the JSON-compatible version of the argument, or undefined (if
|
|
// the item isn't itself replaceable, but maybe some fields in it are)
|
|
var toJSONValueHelper = function (item) {
|
|
for (var i = 0; i < builtinConverters.length; i++) {
|
|
var converter = builtinConverters[i];
|
|
if (converter.matchObject(item)) {
|
|
return converter.toJSONValue(item);
|
|
}
|
|
}
|
|
return undefined;
|
|
};
|
|
|
|
/**
|
|
* @summary Serialize an EJSON-compatible value into its plain JSON representation.
|
|
* @locus Anywhere
|
|
* @param {EJSON} val A value to serialize to plain JSON.
|
|
*/
|
|
EJSON.toJSONValue = function (item) {
|
|
var changed = toJSONValueHelper(item);
|
|
if (changed !== undefined)
|
|
return changed;
|
|
if (typeof item === 'object') {
|
|
item = EJSON.clone(item);
|
|
adjustTypesToJSONValue(item);
|
|
}
|
|
return item;
|
|
};
|
|
|
|
// for both arrays and objects. Tries its best to just
|
|
// use the object you hand it, but may return something
|
|
// different if the object you hand it itself needs changing.
|
|
//
|
|
var adjustTypesFromJSONValue =
|
|
EJSON._adjustTypesFromJSONValue = function (obj) {
|
|
if (obj === null)
|
|
return null;
|
|
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);
|
|
if (value !== changed) {
|
|
obj[key] = changed;
|
|
return;
|
|
}
|
|
// if we get here, value is an object but not adjustable
|
|
// at this level. recurse.
|
|
adjustTypesFromJSONValue(value);
|
|
}
|
|
});
|
|
return obj;
|
|
};
|
|
|
|
// Either return the argument changed to have the non-json
|
|
// rep of itself (the Object version) or the argument itself.
|
|
|
|
// DOES NOT RECURSE. For actually getting the fully-changed value, use
|
|
// EJSON.fromJSONValue
|
|
var fromJSONValueHelper = function (value) {
|
|
if (typeof value === 'object' && value !== null) {
|
|
if (_.size(value) <= 2
|
|
&& _.all(value, function (v, k) {
|
|
return typeof k === 'string' && k.substr(0, 1) === '$';
|
|
})) {
|
|
for (var i = 0; i < builtinConverters.length; i++) {
|
|
var converter = builtinConverters[i];
|
|
if (converter.matchJSONValue(value)) {
|
|
return converter.fromJSONValue(value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return value;
|
|
};
|
|
|
|
/**
|
|
* @summary Deserialize an EJSON value from its plain JSON representation.
|
|
* @locus Anywhere
|
|
* @param {JSONCompatible} val A value to deserialize into EJSON.
|
|
*/
|
|
EJSON.fromJSONValue = function (item) {
|
|
var changed = fromJSONValueHelper(item);
|
|
if (changed === item && typeof item === 'object') {
|
|
item = EJSON.clone(item);
|
|
adjustTypesFromJSONValue(item);
|
|
return item;
|
|
} else {
|
|
return changed;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @summary Serialize a value to a string.
|
|
|
|
For EJSON values, the serialization fully represents the value. For non-EJSON values, serializes the same way as `JSON.stringify`.
|
|
* @locus Anywhere
|
|
* @param {EJSON} val A value to stringify.
|
|
* @param {Object} [options]
|
|
* @param {Boolean | Integer | String} options.indent Indents objects and arrays for easy readability. When `true`, indents by 2 spaces; when an integer, indents by that number of spaces; and when a string, uses the string as the indentation pattern.
|
|
* @param {Boolean} options.canonical When `true`, stringifies keys in an object in sorted order.
|
|
*/
|
|
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);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @summary Parse a string into an EJSON value. Throws an error if the string is not valid EJSON.
|
|
* @locus Anywhere
|
|
* @param {String} str A string to parse into an EJSON value.
|
|
*/
|
|
EJSON.parse = function (item) {
|
|
if (typeof item !== 'string')
|
|
throw new Error("EJSON.parse argument should be a string");
|
|
return EJSON.fromJSONValue(JSON.parse(item));
|
|
};
|
|
|
|
/**
|
|
* @summary Returns true if `x` is a buffer of binary data, as returned from [`EJSON.newBinary`](#ejson_new_binary).
|
|
* @param {Object} x The variable to check.
|
|
* @locus Anywhere
|
|
*/
|
|
EJSON.isBinary = function (obj) {
|
|
return !!((typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array) ||
|
|
(obj && obj.$Uint8ArrayPolyfill));
|
|
};
|
|
|
|
/**
|
|
* @summary Return true if `a` and `b` are equal to each other. Return false otherwise. Uses the `equals` method on `a` if present, otherwise performs a deep comparison.
|
|
* @locus Anywhere
|
|
* @param {EJSON} a
|
|
* @param {EJSON} b
|
|
* @param {Object} [options]
|
|
* @param {Boolean} options.keyOrderSensitive Compare in key sensitive order, if supported by the JavaScript implementation. For example, `{a: 1, b: 2}` is equal to `{b: 2, a: 1}` only when `keyOrderSensitive` is `false`. The default is `false`.
|
|
*/
|
|
EJSON.equals = function (a, b, options) {
|
|
var i;
|
|
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'))
|
|
return false;
|
|
if (a instanceof Date && b instanceof Date)
|
|
return a.valueOf() === b.valueOf();
|
|
if (EJSON.isBinary(a) && EJSON.isBinary(b)) {
|
|
if (a.length !== b.length)
|
|
return false;
|
|
for (i = 0; i < a.length; i++) {
|
|
if (a[i] !== b[i])
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
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;
|
|
if (a.length !== b.length)
|
|
return false;
|
|
for (i = 0; i < a.length; i++) {
|
|
if (!EJSON.equals(a[i], b[i], options))
|
|
return false;
|
|
}
|
|
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) {
|
|
var bKeys = [];
|
|
_.each(b, function (val, x) {
|
|
bKeys.push(x);
|
|
});
|
|
i = 0;
|
|
ret = _.all(a, function (val, x) {
|
|
if (i >= bKeys.length) {
|
|
return false;
|
|
}
|
|
if (x !== bKeys[i]) {
|
|
return false;
|
|
}
|
|
if (!EJSON.equals(val, b[bKeys[i]], options)) {
|
|
return false;
|
|
}
|
|
i++;
|
|
return true;
|
|
});
|
|
return ret && i === bKeys.length;
|
|
} else {
|
|
i = 0;
|
|
ret = _.all(a, function (val, key) {
|
|
if (!_.has(b, key)) {
|
|
return false;
|
|
}
|
|
if (!EJSON.equals(val, b[key], options)) {
|
|
return false;
|
|
}
|
|
i++;
|
|
return true;
|
|
});
|
|
return ret && _.size(b) === i;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @summary Return a deep copy of `val`.
|
|
* @locus Anywhere
|
|
* @param {EJSON} val A value to copy.
|
|
*/
|
|
EJSON.clone = function (v) {
|
|
var ret;
|
|
if (typeof v !== "object")
|
|
return v;
|
|
if (v === null)
|
|
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++) {
|
|
ret[i] = v[i];
|
|
}
|
|
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).
|
|
ret = [];
|
|
for (i = 0; i < v.length; i++)
|
|
ret[i] = EJSON.clone(v[i]);
|
|
return ret;
|
|
}
|
|
// handle general user-defined typed Objects if they have a clone method
|
|
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) {
|
|
ret[key] = EJSON.clone(value);
|
|
});
|
|
return ret;
|
|
};
|
|
|
|
/**
|
|
* @summary Allocate a new buffer of binary data that EJSON can serialize.
|
|
* @locus Anywhere
|
|
* @param {Number} size The number of bytes of binary data to allocate.
|
|
*/
|
|
// EJSON.newBinary is the public documented API for this functionality,
|
|
// but the implementation is in the 'base64' package to avoid
|
|
// introducing a circular dependency. (If the implementation were here,
|
|
// then 'base64' would have to use EJSON.newBinary, and 'ejson' would
|
|
// also have to use 'base64'.)
|
|
EJSON.newBinary = Base64.newBinary;
|