diff --git a/build.js b/build.js index 77d6d71..ceaf49d 100644 --- a/build.js +++ b/build.js @@ -26,4 +26,4 @@ var source = [ '}).call(this);' ].join('\n'); -fs.writeFileSync(__dirname + '/ejson.js', source); +fs.writeFileSync(__dirname + '/index.js', source); diff --git a/index.js b/index.js index 07077ac..8e5c4d7 100644 --- a/index.js +++ b/index.js @@ -1 +1,460 @@ -module.exports = require('./ejson.js'); \ No newline at end of file +module.exports = (function () { +"use strict"; +var EJSON, EJSONTest, i, base64Encode, base64Decode, root = {}; +var _ = require("underscore"); +EJSON = {}; +EJSONTest = {}; + +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 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.addType = function (name, factory) { + if (_.has(customTypes, name)) + throw new Error("Type " + name + " already present"); + customTypes[name] = factory; +}; + +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); + } + }, + { // 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: base64Encode(obj)}; + }, + fromJSONValue: function (obj) { + return base64Decode(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) { + return {$type: obj.typeName(), $value: obj.toJSONValue()}; + }, + fromJSONValue: function (obj) { + var typeName = obj.$type; + var converter = customTypes[typeName]; + return converter(obj.$value); + } + } +]; + +EJSON._isCustomType = function (obj) { + return obj && + typeof obj.toJSONValue === 'function' && + typeof obj.typeName === 'function' && + _.has(customTypes, obj.typeName()); +}; + + +// for both arrays and objects, in-place modification. +var adjustTypesToJSONValue = +EJSON._adjustTypesToJSONValue = function (obj) { + if (obj === null) + return null; + var maybeChanged = toJSONValueHelper(obj); + if (maybeChanged !== undefined) + return maybeChanged; + _.each(obj, function (value, key) { + if (typeof value !== 'object' && value !== undefined) + 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; +}; + +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; + _.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; +}; + +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; + } +}; + +EJSON.stringify = function (item) { + return JSON.stringify(EJSON.toJSONValue(item)); +}; + +EJSON.parse = function (item) { + return EJSON.fromJSONValue(JSON.parse(item)); +}; + +EJSON.isBinary = function (obj) { + return !!((typeof Uint8Array !== 'undefined' && obj instanceof Uint8Array) || + (obj && obj.$Uint8ArrayPolyfill)); +}; + +EJSON.equals = function (a, b, options) { + var i; + var keyOrderSensitive = !!(options && options.keyOrderSensitive); + if (a === b) + return true; + 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 (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; + } + // 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; + } +}; + +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()); + if (EJSON.isBinary(v)) { + ret = EJSON.newBinary(v.length); + for (var i = 0; i < v.length; i++) { + ret[i] = v[i]; + } + return ret; + } + 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 objects + ret = {}; + _.each(v, function (value, key) { + ret[key] = EJSON.clone(value); + }); + return ret; +}; + +// Base 64 encoding +var BASE_64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +var BASE_64_VALS = {}; + +for (var i = 0; i < BASE_64_CHARS.length; i++) { + BASE_64_VALS[BASE_64_CHARS.charAt(i)] = i; +}; + +base64Encode = function (array) { + var answer = []; + var a = null; + var b = null; + var c = null; + var d = null; + for (var i = 0; i < array.length; i++) { + switch (i % 3) { + case 0: + a = (array[i] >> 2) & 0x3F; + b = (array[i] & 0x03) << 4; + break; + case 1: + b = b | (array[i] >> 4) & 0xF; + c = (array[i] & 0xF) << 2; + break; + case 2: + c = c | (array[i] >> 6) & 0x03; + d = array[i] & 0x3F; + answer.push(getChar(a)); + answer.push(getChar(b)); + answer.push(getChar(c)); + answer.push(getChar(d)); + a = null; + b = null; + c = null; + d = null; + break; + } + } + if (a != null) { + answer.push(getChar(a)); + answer.push(getChar(b)); + if (c == null) + answer.push('='); + else + answer.push(getChar(c)); + if (d == null) + answer.push('='); + } + return answer.join(""); +}; + +var getChar = function (val) { + return BASE_64_CHARS.charAt(val); +}; + +var getVal = function (ch) { + if (ch === '=') { + return -1; + } + return BASE_64_VALS[ch]; +}; + +EJSON.newBinary = function (len) { + if (typeof Uint8Array === 'undefined' || typeof ArrayBuffer === 'undefined') { + var ret = []; + for (var i = 0; i < len; i++) { + ret.push(0); + } + ret.$Uint8ArrayPolyfill = true; + return ret; + } + return new Uint8Array(new ArrayBuffer(len)); +}; + +base64Decode = function (str) { + var len = Math.floor((str.length*3)/4); + if (str.charAt(str.length - 1) == '=') { + len--; + if (str.charAt(str.length - 2) == '=') + len--; + } + var arr = EJSON.newBinary(len); + + var one = null; + var two = null; + var three = null; + + var j = 0; + + for (var i = 0; i < str.length; i++) { + var c = str.charAt(i); + var v = getVal(c); + switch (i % 4) { + case 0: + if (v < 0) + throw new Error('invalid base64 string'); + one = v << 2; + break; + case 1: + if (v < 0) + throw new Error('invalid base64 string'); + one = one | (v >> 4); + arr[j++] = one; + two = (v & 0x0F) << 4; + break; + case 2: + if (v >= 0) { + two = two | (v >> 2); + arr[j++] = two; + three = (v & 0x03) << 6; + } + break; + case 3: + if (v >= 0) { + arr[j++] = three | v; + } + break; + } + } + return arr; +}; + +EJSONTest.base64Encode = base64Encode; + +EJSONTest.base64Decode = base64Decode; + + return EJSON; +}).call(this); \ No newline at end of file diff --git a/package.json b/package.json index d16f342..ad3d9ba 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "e-json", - "version": "0.0.4", - "description": "EJSON - Extended and Extensible JSON library from Meteor made compatible for Primus", + "name": "ejson", + "version": "1.0.0", + "description": "EJSON - Extended and Extensible JSON library from Meteor made compatible for Nodejs and Browserif", "main": "index.js", "scripts": { "prepublish": "node build.js",