UNPKG

neo4j-fiber

Version:

Most advanced and efficient Neo4j REST API Driver, with support of https and GrapheneDB

377 lines (342 loc) 12.1 kB
(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; }; })();