angular-meteor
Version:
Combining the simplicity and power of AngularJS and Meteor
413 lines (409 loc) • 1.67 MB
JavaScript
(function () {
/* Imports */
var Meteor = Package.meteor.Meteor;
var global = Package.meteor.global;
var meteorEnv = Package.meteor.meteorEnv;
var _ = Package.underscore._;
var Base64 = Package.base64.Base64;
/* Package-scope variables */
var EJSON, EJSONTest;
(function(){
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// //
// packages/ejson/ejson.js //
// //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
/** // 1
* @namespace // 2
* @summary Namespace for EJSON functions // 3
*/ // 4
EJSON = {}; // 5
EJSONTest = {}; // 6
// 7
// 8
// 9
// Custom type interface definition // 10
/** // 11
* @class CustomType // 12
* @instanceName customType // 13
* @memberOf EJSON // 14
* @summary The interface that a class must satisfy to be able to become an // 15
* EJSON custom type via EJSON.addType. // 16
*/ // 17
// 18
/** // 19
* @function typeName // 20
* @memberOf EJSON.CustomType // 21
* @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 // 23
* @instance // 24
*/ // 25
// 26
/** // 27
* @function toJSONValue // 28
* @memberOf EJSON.CustomType // 29
* @summary Serialize this instance into a JSON-compatible value. // 30
* @locus Anywhere // 31
* @instance // 32
*/ // 33
// 34
/** // 35
* @function clone // 36
* @memberOf EJSON.CustomType // 37
* @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 // 39
* @instance // 40
*/ // 41
// 42
/** // 43
* @function equals // 44
* @memberOf EJSON.CustomType // 45
* @summary Return `true` if `other` has a value equal to `this`; `false` otherwise. // 46
* @locus Anywhere // 47
* @param {Object} other Another object to compare this to. // 48
* @instance // 49
*/ // 50
// 51
// 52
var customTypes = {}; // 53
// Add a custom type, using a method of your choice to get to and // 54
// from a basic JSON-able representation. The factory argument // 55
// is a function of JSON-able --> your object // 56
// The type you add must have: // 57
// - A toJSONValue() method, so that Meteor can serialize it // 58
// - a typeName() method, to show how to look it up in our type table. // 59
// It is okay if these methods are monkey-patched on. // 60
// EJSON.clone will use toJSONValue and the given factory to produce // 61
// a clone, but you may specify a method clone() that will be // 62
// used instead. // 63
// Similarly, EJSON.equals will use toJSONValue to make comparisons, // 64
// but you may provide a method equals() instead. // 65
/** // 66
* @summary Add a custom datatype to EJSON. // 67
* @locus Anywhere // 68
* @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.
*/ // 71
EJSON.addType = function (name, factory) { // 72
if (_.has(customTypes, name)) // 73
throw new Error("Type " + name + " already present"); // 74
customTypes[name] = factory; // 75
}; // 76
// 77
var isInfOrNan = function (obj) { // 78
return _.isNaN(obj) || obj === Infinity || obj === -Infinity; // 79
}; // 80
// 81
var builtinConverters = [ // 82
{ // Date // 83
matchJSONValue: function (obj) { // 84
return _.has(obj, '$date') && _.size(obj) === 1; // 85
}, // 86
matchObject: function (obj) { // 87
return obj instanceof Date; // 88
}, // 89
toJSONValue: function (obj) { // 90
return {$date: obj.getTime()}; // 91
}, // 92
fromJSONValue: function (obj) { // 93
return new Date(obj.$date); // 94
} // 95
}, // 96
{ // NaN, Inf, -Inf. (These are the only objects with typeof !== 'object' // 97
// which we match.) // 98
matchJSONValue: function (obj) { // 99
return _.has(obj, '$InfNaN') && _.size(obj) === 1; // 100
}, // 101
matchObject: isInfOrNan, // 102
toJSONValue: function (obj) { // 103
var sign; // 104
if (_.isNaN(obj)) // 105
sign = 0; // 106
else if (obj === Infinity) // 107
sign = 1; // 108
else // 109
sign = -1; // 110
return {$InfNaN: sign}; // 111
}, // 112
fromJSONValue: function (obj) { // 113
return obj.$InfNaN/0; // 114
} // 115
}, // 116
{ // Binary // 117
matchJSONValue: function (obj) { // 118
return _.has(obj, '$binary') && _.size(obj) === 1; // 119
}, // 120
matchObject: function (obj) { // 121
return typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array // 122
|| (obj && _.has(obj, '$Uint8ArrayPolyfill')); // 123
}, // 124
toJSONValue: function (obj) { // 125
return {$binary: Base64.encode(obj)}; // 126
}, // 127
fromJSONValue: function (obj) { // 128
return Base64.decode(obj.$binary); // 129
} // 130
}, // 131
{ // Escaping one level // 132
matchJSONValue: function (obj) { // 133
return _.has(obj, '$escape') && _.size(obj) === 1; // 134
}, // 135
matchObject: function (obj) { // 136
if (_.isEmpty(obj) || _.size(obj) > 2) { // 137
return false; // 138
} // 139
return _.any(builtinConverters, function (converter) { // 140
return converter.matchJSONValue(obj); // 141
}); // 142
}, // 143
toJSONValue: function (obj) { // 144
var newObj = {}; // 145
_.each(obj, function (value, key) { // 146
newObj[key] = EJSON.toJSONValue(value); // 147
}); // 148
return {$escape: newObj}; // 149
}, // 150
fromJSONValue: function (obj) { // 151
var newObj = {}; // 152
_.each(obj.$escape, function (value, key) { // 153
newObj[key] = EJSON.fromJSONValue(value); // 154
}); // 155
return newObj; // 156
} // 157
}, // 158
{ // Custom // 159
matchJSONValue: function (obj) { // 160
return _.has(obj, '$type') && _.has(obj, '$value') && _.size(obj) === 2; // 161
}, // 162
matchObject: function (obj) { // 163
return EJSON._isCustomType(obj); // 164
}, // 165
toJSONValue: function (obj) { // 166
var jsonValue = Meteor._noYieldsAllowed(function () { // 167
return obj.toJSONValue(); // 168
}); // 169
return {$type: obj.typeName(), $value: jsonValue}; // 170
}, // 171
fromJSONValue: function (obj) { // 172
var typeName = obj.$type; // 173
if (!_.has(customTypes, typeName)) // 174
throw new Error("Custom EJSON type " + typeName + " is not defined"); // 175
var converter = customTypes[typeName]; // 176
return Meteor._noYieldsAllowed(function () { // 177
return converter(obj.$value); // 178
}); // 179
} // 180
} // 181
]; // 182
// 183
EJSON._isCustomType = function (obj) { // 184
return obj && // 185
typeof obj.toJSONValue === 'function' && // 186
typeof obj.typeName === 'function' && // 187
_.has(customTypes, obj.typeName()); // 188
}; // 189
// 190
EJSON._getTypes = function () { // 191
return customTypes; // 192
}; // 193
// 194
EJSON._getConverters = function () { // 195
return builtinConverters; // 196
}; // 197
// 198
// for both arrays and objects, in-place modification. // 199
var adjustTypesToJSONValue = // 200
EJSON._adjustTypesToJSONValue = function (obj) { // 201
// Is it an atom that we need to adjust? // 202
if (obj === null) // 203
return null; // 204
var maybeChanged = toJSONValueHelper(obj); // 205
if (maybeChanged !== undefined) // 206
return maybeChanged; // 207
// 208
// Other atoms are unchanged. // 209
if (typeof obj !== 'object') // 210
return obj; // 211
// 212
// Iterate over array or object structure. // 213
_.each(obj, function (value, key) { // 214
if (typeof value !== 'object' && value !== undefined && // 215
!isInfOrNan(value)) // 216
return; // continue // 217
// 218
var changed = toJSONValueHelper(value); // 219
if (changed) { // 220
obj[key] = changed; // 221
return; // on to the next key // 222
} // 223
// if we get here, value is an object but not adjustable // 224
// at this level. recurse. // 225
adjustTypesToJSONValue(value); // 226
}); // 227
return obj; // 228
}; // 229
// 230
// Either return the JSON-compatible version of the argument, or undefined (if // 231
// the item isn't itself replaceable, but maybe some fields in it are) // 232
var toJSONValueHelper = function (item) { // 233
for (var i = 0; i < builtinConverters.length; i++) { // 234
var converter = builtinConverters[i]; // 235
if (converter.matchObject(item)) { // 236
return converter.toJSONValue(item); // 237
} // 238
} // 239
return undefined; // 240
}; // 241
// 242
/** // 243
* @summary Serialize an EJSON-compatible value into its plain JSON representation. // 244
* @locus Anywhere // 245
* @param {EJSON} val A value to serialize to plain JSON. // 246
*/ // 247
EJSON.toJSONValue = function (item) { // 248
var changed = toJSONValueHelper(item); // 249
if (changed !== undefined) // 250
return changed; // 251
if (typeof item === 'object') { // 252
item = EJSON.clone(item); // 253
adjustTypesToJSONValue(item); // 254
} // 255
return item; // 256
}; // 257
// 258
// for both arrays and objects. Tries its best to just // 259
// use the object you hand it, but may return something // 260
// different if the object you hand it itself needs changing. // 261
// // 262
var adjustTypesFromJSONValue = // 263
EJSON._adjustTypesFromJSONValue = function (obj) { // 264
if (obj === null) // 265
return null; // 266
var maybeChanged = fromJSONValueHelper(obj); // 267
if (maybeChanged !== obj) // 268
return maybeChanged; // 269
// 270
// Other atoms are unchanged. // 271
if (typeof obj !== 'object') // 272
return obj; // 273
// 274
_.each(obj, function (value, key) { // 275
if (typeof value === 'object') { // 276
var changed = fromJSONValueHelper(value); // 277
if (value !== changed) { // 278
obj[key] = changed; // 279
return; // 280
} // 281
// if we get here, value is an object but not adjustable // 282
// at this level. recurse. // 283
adjustTypesFromJSONValue(value); // 284
} // 285
}); // 286
return obj; // 287
}; // 288
// 289
// Either return the argument changed to have the non-json // 290
// rep of itself (the Object version) or the argument itself. // 291
// 292
// DOES NOT RECURSE. For actually getting the fully-changed value, use // 293
// EJSON.fromJSONValue // 294
var fromJSONValueHelper = function (value) { // 295
if (typeof value === 'object' && value !== null) { // 296
if (_.size(value) <= 2 // 297
&& _.all(value, function (v, k) { // 298
return typeof k === 'string' && k.substr(0, 1) === '$'; // 299
})) { // 300
for (var i = 0; i < builtinConverters.length; i++) { // 301
var converter = builtinConverters[i]; // 302
if (converter.matchJSONValue(value)) { // 303
return converter.fromJSONValue(value); // 304
} // 305
} // 306
} // 307
} // 308
return value; // 309
}; // 310
// 311
/** // 312
* @summary Deserialize an EJSON value from its plain JSON representation. // 313
* @locus Anywhere // 314
* @param {JSONCompatible} val A value to deserialize into EJSON. // 315
*/ // 316
EJSON.fromJSONValue = function (item) { // 317
var changed = fromJSONValueHelper(item); // 318
if (changed === item && typeof item === 'object') { // 319
item = EJSON.clone(item); // 320
adjustTypesFromJSONValue(item); // 321
return item; // 322
} else { // 323
return changed; // 324
} // 325
}; // 326
// 327
/** // 328
* @summary Serialize a value to a string. // 329
// 330
For EJSON values, the serialization fully represents the value. For non-EJSON values, serializes the same way as `JSON.stringify`.
* @locus Anywhere // 332
* @param {EJSON} val A value to stringify. // 333
* @param {Object} [options] // 334
* @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. // 336
*/ // 337
EJSON.stringify = function (item, options) { // 338
var json = EJSON.toJSONValue(item); // 339
if (options && (options.canonical || options.indent)) { // 340
return EJSON._canonicalStringify(json, options); // 341
} else { // 342
return JSON.stringify(json); // 343
} // 344
}; // 345
// 346
/** // 347
* @summary Parse a string into an EJSON value. Throws an error if the string is not valid EJSON. // 348
* @locus Anywhere // 349
* @param {String} str A string to parse into an EJSON value. // 350
*/ // 351
EJSON.parse = function (item) { // 352
if (typeof item !== 'string') // 353
throw new Error("EJSON.parse argument should be a string"); // 354
return EJSON.fromJSONValue(JSON.parse(item)); // 355
}; // 356
// 357
/** // 358
* @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. // 360
* @locus Anywhere // 361
*/ // 362
EJSON.isBinary = function (obj) { // 363
return !!((typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array) || // 364
(obj && obj.$Uint8ArrayPolyfill)); // 365
}; // 366
// 367
/** // 368
* @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 // 370
* @param {EJSON} a // 371
* @param {EJSON} b // 372
* @param {Object} [options] // 373
* @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`.
*/ // 375
EJSON.equals = function (a, b, options) { // 376
var i; // 377
var keyOrderSensitive = !!(options && options.keyOrderSensitive); // 378
if (a === b) // 379
return true; // 380
if (_.isNaN(a) && _.isNaN(b)) // 381
return true; // This differs from the IEEE spec for NaN equality, b/c we don't want // 382
// anything ever with a NaN to be poisoned from becoming equal to anything. // 383
if (!a || !b) // if either one is falsy, they'd have to be === to be equal // 384
return false; // 385
if (!(typeof a === 'object' && typeof b === 'object')) // 386
return false; // 387
if (a instanceof Date && b instanceof Date) // 388
return a.valueOf() === b.valueOf(); // 389
if (EJSON.isBinary(a) && EJSON.isBinary(b)) { // 390
if (a.length !== b.length) // 391
return false; // 392
for (i = 0; i < a.length; i++) {