neo4j-fiber
Version:
Most advanced and efficient Neo4j REST API Driver, with support of https and GrapheneDB
377 lines (342 loc) • 12.1 kB
JavaScript
(function () {
'use strict';
var _ = require('./helpers')._;
/**
* @url http://docs.meteor.com/#/full/check
* @summary Check that a value matches a [pattern](#matchpatterns).
* If the value does not match the pattern, throw a `Error`.
*
* Particularly useful to assert that arguments to a function have the right
* types and structure.
* @locus Anywhere
* @param {Any} value The value to check
* @param {MatchPattern} pattern The pattern to match
* `value` against
*/
var check = function (value, pattern) {
// Record that check got called, if somebody cared.
//
// We use getOrNullIfOutsideFiber so that it's OK to call check()
// from non-Fiber server contexts; the downside is that if you forget to
// bindEnvironment on some random callback in your method/publisher,
// it might not find the argumentChecker and you'll get an error about
// not checking an argument that it looks like you're checking (instead
// of just getting a 'Node code must run in a Fiber' error).
try {
checkSubtree(value, pattern);
} catch (err) {
if ((err instanceof Error) && err.path) {
err.message += ' in field ' + err.path;
}
throw err;
}
};
module.exports.check = check;
/**
* @namespace Match
* @summary The namespace for all Match types and methods.
*/
var Match = {
Optional: function (pattern) {
return new Optional(pattern);
},
OneOf: function (/*arguments*/) {
return new OneOf(_.toArray(arguments));
},
Any: ['__any__'],
Where: function (condition) {
return new Where(condition);
},
ObjectIncluding: function (pattern) {
return new ObjectIncluding(pattern);
},
ObjectWithValues: function (pattern) {
return new ObjectWithValues(pattern);
},
// Matches only signed 32-bit integers
Integer: ['__integer__'],
// XXX matchers should know how to describe themselves for errors
Error: new Error('Error', function (msg) {
this.message = 'Match error: ' + msg;
// The path of the value that failed to match. Initially empty, this gets
// populated by catching and rethrowing the exception as it goes back up the
// stack.
// E.g.: 'vals[3].entity.created'
this.path = '';
// If this gets sent over DDP, don't give full internal details but at least
// provide something better than 500 Internal server error.
this.sanitizedError = new Error(400, 'Match failed');
}),
/**
* @summary Returns true if the value matches the pattern.
* @locus Anywhere
* @param {Any} value The value to check
* @param {MatchPattern} pattern The pattern to match `value` against
*/
test: function (value, pattern) {
try {
checkSubtree(value, pattern);
return true;
} catch (e) {
if (e instanceof Error) {
return false;
}
// Rethrow other errors.
throw e;
}
}
};
module.exports.Match = Match;
var Optional = function (pattern) {
this.pattern = pattern;
};
var OneOf = function (choices) {
if (_.isEmpty(choices)) {
throw new Error('Must provide at least one choice to Match.OneOf');
}
this.choices = choices;
};
var Where = function (condition) {
this.condition = condition;
};
var ObjectIncluding = function (pattern) {
this.pattern = pattern;
};
var ObjectWithValues = function (pattern) {
this.pattern = pattern;
};
var typeofChecks = [
[String, 'string'],
[Number, 'number'],
[Boolean, 'boolean'],
// arguments with OneOf.
[undefined, 'undefined']
];
var checkSubtree = function (value, pattern) {
// Match anything!
if (pattern === Match.Any) {
return;
}
// Basic atomic types.
// Do not match boxed objects (e.g. String, Boolean)
for (var i = 0; i < typeofChecks.length; ++i) {
if (pattern === typeofChecks[i][0]) {
if (typeof value === typeofChecks[i][1]) {
return;
}
throw new Error('Expected ' + typeofChecks[i][1] + ', got ' + typeof value);
}
}
if (pattern === null) {
if (value === null) {
return;
}
throw new Error('Expected null, got ' + value.toString());
}
// Strings and numbers match literally. Goes well with Match.OneOf.
if (typeof pattern === 'string' || typeof pattern === 'number') {
if (value === pattern) {
return;
}
throw new Error('Expected ' + pattern + ', got ' + value.toString());
}
// Match.Integer is special type encoded with array
if (pattern === Match.Integer) {
// There is no consistent and reliable way to check if variable is a 64-bit
// integer. One of the popular solutions is to get reminder of division by 1
// but this method fails on really large floats with big precision.
// E.g.: 1.348192308491824e+23 % 1 === 0 in V8
// Bitwise operators work consistantly but always cast variable to 32-bit
// signed integer according to JavaScript specs.
if (typeof value === 'number' && (value | 0) === value) {
return;
}
throw new Error('Expected Integer, got ' + (value instanceof Object ? value.toString() : value));
}
// 'Object' is shorthand for Match.ObjectIncluding({});
if (pattern === Object) {
if (typeof value === 'object') {
return;
}
throw new Error('Expected object, got ' + typeof value);
}
// Array (checked AFTER Any, which is implemented as an Array).
if (pattern instanceof Array) {
if (pattern.length !== 1) {
throw Error('Bad pattern: arrays must have one type element' + pattern.toString());
}
if (!_.isArray(value) && !_.isArguments(value)) {
throw new Error('Expected array, got ' + value.toString());
}
_.each(value, function (valueElement, index) {
try {
checkSubtree(valueElement, pattern[0]);
} catch (err) {
if (err instanceof Error) {
err.path = _prependPath(index, err.path);
}
throw err;
}
});
return;
}
// Arbitrary validation checks. The condition can return false or throw a
// Error (ie, it can internally use check()) to fail.
if (pattern instanceof Where) {
if (pattern.condition(value)) {
return;
}
// XXX this error is terrible
throw new Error('Failed Match.Where validation');
}
if (pattern instanceof Optional) {
pattern = Match.OneOf(undefined, pattern.pattern);
}
if (pattern instanceof OneOf) {
for (var j = 0; j < pattern.choices.length; ++j) {
try {
checkSubtree(value, pattern.choices[j]);
// No error? Yay, return.
return;
} catch (err) {
// Other errors should be thrown. Match errors just mean try another
// choice.
if (!(err instanceof Error)) {
throw err;
}
}
}
// XXX this error is terrible
throw new Error('Failed Match.OneOf or Match.Optional validation');
}
// A function that isn't something we special-case is assumed to be a
// constructor.
if (pattern instanceof Function) {
if (value instanceof pattern) {
return;
}
throw new Error('Expected ' + (pattern.name || 'particular constructor'));
}
var unknownKeysAllowed = false;
var unknownKeyPattern;
if (pattern instanceof ObjectIncluding) {
unknownKeysAllowed = true;
pattern = pattern.pattern;
}
if (pattern instanceof ObjectWithValues) {
unknownKeysAllowed = true;
unknownKeyPattern = [pattern.pattern];
pattern = {}; // no required keys
}
if (typeof pattern !== 'object') {
throw Error('Bad pattern: unknown pattern type');
}
// An object, with required and optional keys. Note that this does NOT do
// structural matches against objects of special types that happen to match
// the pattern: this really needs to be a plain old {Object}!
if (typeof value !== 'object') {
throw new Error('Expected object, got ' + typeof value);
}
if (value === null) {
throw new Error('Expected object, got null');
}
if (value.constructor !== Object) {
throw new Error('Expected plain object');
}
var requiredPatterns = {};
var optionalPatterns = {};
_.each(pattern, function (subPattern, key) {
if (subPattern instanceof Optional) {
optionalPatterns[key] = subPattern.pattern;
} else {
requiredPatterns[key] = subPattern;
}
});
_.each(value, function (subValue, key) {
try {
if (_.has(requiredPatterns, key)) {
checkSubtree(subValue, requiredPatterns[key]);
delete requiredPatterns[key];
} else if (_.has(optionalPatterns, key)) {
checkSubtree(subValue, optionalPatterns[key]);
} else {
if (!unknownKeysAllowed) {
throw new Error('Unknown key');
}
if (unknownKeyPattern) {
checkSubtree(subValue, unknownKeyPattern[0]);
}
}
} catch (err) {
if (err instanceof Error) {
err.path = _prependPath(key, err.path);
}
throw err;
}
});
_.each(requiredPatterns, function (subPattern, key) {
throw new Error('Missing key "' + key + '"');
});
};
var ArgumentChecker = function (args, description) {
// Make a SHALLOW copy of the arguments. (We'll be doing identity checks
// against its contents.)
this.args = _.clone(args);
// Since the common case will be to check arguments in order, and we splice
// out arguments when we check them, make it so we splice out from the end
// rather than the beginning.
this.args.reverse();
this.description = description;
};
_.extend(ArgumentChecker.prototype, {
checking: function (value) {
if (this._checkingOneValue(value)) {
return;
}
// Allow check(arguments, [String]) or check(arguments.slice(1), [String])
// or check([foo, bar], [String]) to count... but only if value wasn't
// itself an argument.
if (_.isArray(value) || _.isArguments(value)) {
_.each(value, _.bind(this._checkingOneValue, this));
}
},
_checkingOneValue: function (value) {
for (var i = 0; i < this.args.length; ++i) {
// Is this value one of the arguments? (This can have a false positive if
// the argument is an interned primitive, but it's still a good enough
// check.)
// (NaN is not === to itself, so we have to check specially.)
if (value === this.args[i] || (_.isNaN(value) && _.isNaN(this.args[i]))) {
this.args.splice(i, 1);
return true;
}
}
return false;
},
throwUnlessAllArgumentsHaveBeenChecked: function () {
if (!_.isEmpty(this.args)) {
throw new Error('Did not check() all arguments during ' + this.description);
}
}
});
var _jsKeywords = ['do', 'if', 'in', 'for', 'let', 'new', 'try', 'var', 'case',
'else', 'enum', 'eval', 'false', 'null', 'this', 'true', 'void', 'with',
'break', 'catch', 'class', 'const', 'super', 'throw', 'while', 'yield',
'delete', 'export', 'import', 'public', 'return', 'static', 'switch',
'typeof', 'default', 'extends', 'finally', 'package', 'private', 'continue',
'debugger', 'function', 'arguments', 'interface', 'protected', 'implements',
'instanceof'];
// Assumes the base of path is already escaped properly
// returns key + base
var _prependPath = function (key, base) {
if ((typeof key) === 'number' || key.match(/^[0-9]+$/)) {
key = '[' + key + ']';
} else if (!key.match(/^[a-z_$][0-9a-z_$]*$/i) || _.contains(_jsKeywords, key)) {
key = JSON.stringify([key]);
}
if (base && base[0] !== '[') {
return key + '.' + base;
}
return key + base;
};
})();