eventual-schema
Version:
Combine objects into eventual schemas. Supply rules on when to freeze into a generalised schema
216 lines (174 loc) • 6.14 kB
JavaScript
"use strict";
var utils = require('./lib/utils'),
extend = utils.extend,
forEach = utils.forEach,
isArray = utils.isArray,
isObject = utils.isObject,
isEnumerable = utils.isEnumerable,
isArrayOfObjects = utils.isArrayOfObjects,
isArrayOfFunctions = utils.isArrayOfFunctions,
isArrayOfNonEnumerables = utils.isArrayOfNonEnumerables;
// An eventual schema exists for a key-value object.
var EventualSchema = function (rules) {
this._instantiatedDate = new Date();
this._instanceCount = 0;
this._propertyCount = 0;
this._collatedInstances = null;
this._rules = (rules && this._checkRules(rules)) || [];
this.initEventualSchema();
};
EventualSchema.prototype._checkRules = function (rules) {
var isRulesListOfFunctions = isArrayOfFunctions(rules);
if (!isRulesListOfFunctions) {
throw new Error("EventualSchema's rules argument should only be passed a list of functions.");
}
return rules;
};
EventualSchema.prototype.initEventualSchema = function (eventualSchema, isFrozen) {
this._eventualSchema = null;
this.frozen = false;
if (isFrozen) {
this._eventualSchema = eventualSchema || {};
this.frozen = isFrozen;
}
};
EventualSchema.prototype.get = function () {
if (!this.frozen) {
throw new Error("You cannot get the _eventualSchema until the necessary rules have been passed.");
}
return this._eventualSchema;
};
// It does have to recurse when adding.
//
// Given three of these:
// ```json
// {
// a: {
// num: 7,
// arr: []
// },
// b: {
// arr: [
// {
// name: 'hello world'
// }
// ],
// value: {
// type: 'person',
// name: 'gilly'
// }
// },
// c: {
// arr: []
// }
// }
// ```
// Create this:
// ```json
// {
// a: {
// _propertyCount: 3,
// num: { _propertyCount: 3 },
// arr: { _propertyCount: 3 }
// },
// b: {
// _propertyCount: 3,
// arr: [
// {
// name: { _propertyCount: 3 }
// }
// ],
// value: {
// _propertyCount: 3,
// type: { _propertyCount: 3 },
// name: { _propertyCount: 3 }
// }
// },
// c: {
// _propertyCount: 3,
// arr: { _propertyCount: 3 }
// }
// }
// ```
// I chose this representation because even-though I need to recurse to create it,
// when I try to recurse later on to simplify the structure I can do so easily.
// It also allows more information and does not impart a syntax onto the idea of deep
// property hierarchies and is therefore generalised to more use-cases.
EventualSchema.prototype.add = function (instance) {
this._checkIfFrozen();
this._collatedInstances = this._addInstance(this._collatedInstances, instance);
this._instanceCount += 1;
if (this._isReadyToFreeze()) {
this._freeze();
}
return this;
};
EventualSchema.prototype.freeze = function () {
this._checkIfFrozen();
this._freeze();
return this;
};
EventualSchema.prototype._checkIfFrozen = function () {
if (this.frozen) {
throw new Error("Once frozen EventualSchema#get() is the only callable method.");
}
};
EventualSchema.prototype._addInstance = function (collatedInstances, instance, increaseCount) {
var self = this;
increaseCount = increaseCount === undefined ? true : increaseCount;
// If we are operating upon just a value, then we just return this.
if (!isEnumerable(instance) || isArrayOfNonEnumerables(instance)) {
collatedInstances = collatedInstances || {};
return collatedInstances;
}
// If what we are operating upon is an object or an array then we execute this.
if (isObject(instance)) {
collatedInstances = collatedInstances || {};
forEach(instance, function (instanceValue, instanceKey) {
var currentEventualSchemaLevel = collatedInstances[instanceKey] || {};
var keyAddedForFirstTime = !(instanceKey in collatedInstances);
increaseCount = !keyAddedForFirstTime;
currentEventualSchemaLevel = self._addInstance(currentEventualSchemaLevel, instanceValue, true);
currentEventualSchemaLevel._propertyCount = (increaseCount && currentEventualSchemaLevel._propertyCount) ? currentEventualSchemaLevel._propertyCount + 1 : 1;
if (keyAddedForFirstTime) {
self._propertyCount += 1;
}
collatedInstances[instanceKey] = currentEventualSchemaLevel;
});
return collatedInstances;
}
if (isArrayOfObjects(instance)) {
collatedInstances = collatedInstances || {};
forEach(instance, function (instanceValue, instanceKey) {
var currentEventualSchemaLevel = {};
currentEventualSchemaLevel._arrayObjects = self._addInstance(extend({}, collatedInstances._arrayObjects), instanceValue, false);
currentEventualSchemaLevel._propertyCount = collatedInstances._propertyCount ? collatedInstances._propertyCount : 0; // @todo: the reason for it being set to 0 here is that it wil be increased immediately afterwards.
collatedInstances = currentEventualSchemaLevel;
});
// @todo: BUG: the array of objects code is causing the property to be counted wrongly in two places.
return collatedInstances;
}
// The only point we reach this line is if we've finished recursing and are on the first level of the instance.
return collatedInstances;
};
// If any of the rules return `true` we are ready to freeze.
// No rules: no freezing.
EventualSchema.prototype._isReadyToFreeze = function (ctx) {
var self = this;
ctx = ctx || {};
// If *any* of these rules is true, then it shall freeze is true, otherwise it will be false.
var isReadyToFreeze = this._rules.length > 0 ? this._rules.reduce(function (acc, fn) {
return acc || fn.call(self, ctx);
}, false) : false;
return isReadyToFreeze;
};
EventualSchema.prototype._freeze = function () {
this._generateEventualSchema(this._collatedInstances);
this.frozen = true;
return this.frozen;
};
EventualSchema.prototype._generateEventualSchema = function (collatedInstances) {
this._eventualSchema = collatedInstances;
return this._eventualSchema;
};
exports = module.exports = EventualSchema;