mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
383 lines
12 KiB
JavaScript
383 lines
12 KiB
JavaScript
// XXX docs
|
|
|
|
// Things we explicitly do NOT support:
|
|
// - heterogenous arrays
|
|
|
|
var currentArgumentChecker = new Meteor.EnvironmentVariable;
|
|
|
|
/**
|
|
* @summary Check that a value matches a [pattern](#matchpatterns).
|
|
* If the value does not match the pattern, throw a `Match.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
|
|
*/
|
|
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).
|
|
var argChecker = currentArgumentChecker.getOrNullIfOutsideFiber();
|
|
if (argChecker)
|
|
argChecker.checking(value);
|
|
try {
|
|
checkSubtree(value, pattern);
|
|
} catch (err) {
|
|
if ((err instanceof Match.Error) && err.path)
|
|
err.message += " in field " + err.path;
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* @namespace Match
|
|
* @summary The namespace for all Match types and methods.
|
|
*/
|
|
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: Meteor.makeErrorType("Match.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 Meteor.Error(400, "Match failed");
|
|
}),
|
|
|
|
// Tests to see if value matches pattern. Unlike check, it merely returns true
|
|
// or false (unless an error other than Match.Error was thrown). It does not
|
|
// interact with _failIfArgumentsAreNotAllChecked.
|
|
// XXX maybe also implement a Match.match which returns more information about
|
|
// failures but without using exception handling or doing what check()
|
|
// does with _failIfArgumentsAreNotAllChecked and Meteor.Error conversion
|
|
|
|
/**
|
|
* @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 Match.Error)
|
|
return false;
|
|
// Rethrow other errors.
|
|
throw e;
|
|
}
|
|
},
|
|
|
|
// Runs `f.apply(context, args)`. If check() is not called on every element of
|
|
// `args` (either directly or in the first level of an array), throws an error
|
|
// (using `description` in the message).
|
|
//
|
|
_failIfArgumentsAreNotAllChecked: function (f, context, args, description) {
|
|
var argChecker = new ArgumentChecker(args, description);
|
|
var result = currentArgumentChecker.withValue(argChecker, function () {
|
|
return f.apply(context, args);
|
|
});
|
|
// If f didn't itself throw, make sure it checked all of its arguments.
|
|
argChecker.throwUnlessAllArgumentsHaveBeenChecked();
|
|
return result;
|
|
}
|
|
};
|
|
|
|
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"],
|
|
// While we don't allow undefined in EJSON, this is good for optional
|
|
// 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 Match.Error("Expected " + typeofChecks[i][1] + ", got " +
|
|
typeof value);
|
|
}
|
|
}
|
|
if (pattern === null) {
|
|
if (value === null)
|
|
return;
|
|
throw new Match.Error("Expected null, got " + EJSON.stringify(value));
|
|
}
|
|
|
|
// Strings and numbers match literally. Goes well with Match.OneOf.
|
|
if (typeof pattern === "string" || typeof pattern === "number") {
|
|
if (value === pattern)
|
|
return;
|
|
throw new Match.Error("Expected " + pattern + ", got " +
|
|
EJSON.stringify(value));
|
|
}
|
|
|
|
// 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 Match.Error("Expected Integer, got "
|
|
+ (value instanceof Object ? EJSON.stringify(value) : value));
|
|
}
|
|
|
|
// "Object" is shorthand for Match.ObjectIncluding({});
|
|
if (pattern === Object)
|
|
pattern = Match.ObjectIncluding({});
|
|
|
|
// 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" +
|
|
EJSON.stringify(pattern));
|
|
if (!_.isArray(value) && !_.isArguments(value)) {
|
|
throw new Match.Error("Expected array, got " + EJSON.stringify(value));
|
|
}
|
|
|
|
_.each(value, function (valueElement, index) {
|
|
try {
|
|
checkSubtree(valueElement, pattern[0]);
|
|
} catch (err) {
|
|
if (err instanceof Match.Error) {
|
|
err.path = _prependPath(index, err.path);
|
|
}
|
|
throw err;
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Arbitrary validation checks. The condition can return false or throw a
|
|
// Match.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 Match.Error("Failed Match.Where validation");
|
|
}
|
|
|
|
|
|
if (pattern instanceof Optional)
|
|
pattern = Match.OneOf(undefined, pattern.pattern);
|
|
|
|
if (pattern instanceof OneOf) {
|
|
for (var i = 0; i < pattern.choices.length; ++i) {
|
|
try {
|
|
checkSubtree(value, pattern.choices[i]);
|
|
// No error? Yay, return.
|
|
return;
|
|
} catch (err) {
|
|
// Other errors should be thrown. Match errors just mean try another
|
|
// choice.
|
|
if (!(err instanceof Match.Error))
|
|
throw err;
|
|
}
|
|
}
|
|
// XXX this error is terrible
|
|
throw new Match.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;
|
|
// XXX what if .name isn't defined
|
|
throw new Match.Error("Expected " + pattern.name);
|
|
}
|
|
|
|
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 Match.Error("Expected object, got " + typeof value);
|
|
if (value === null)
|
|
throw new Match.Error("Expected object, got null");
|
|
if (value.constructor !== Object)
|
|
throw new Match.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 Match.Error("Unknown key");
|
|
if (unknownKeyPattern) {
|
|
checkSubtree(subValue, unknownKeyPattern[0]);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
if (err instanceof Match.Error)
|
|
err.path = _prependPath(key, err.path);
|
|
throw err;
|
|
}
|
|
});
|
|
|
|
_.each(requiredPatterns, function (subPattern, key) {
|
|
throw new Match.Error("Missing key '" + key + "'");
|
|
});
|
|
};
|
|
|
|
var ArgumentChecker = function (args, description) {
|
|
var self = this;
|
|
// Make a SHALLOW copy of the arguments. (We'll be doing identity checks
|
|
// against its contents.)
|
|
self.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.
|
|
self.args.reverse();
|
|
self.description = description;
|
|
};
|
|
|
|
_.extend(ArgumentChecker.prototype, {
|
|
checking: function (value) {
|
|
var self = this;
|
|
if (self._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(self._checkingOneValue, self));
|
|
}
|
|
},
|
|
_checkingOneValue: function (value) {
|
|
var self = this;
|
|
for (var i = 0; i < self.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 === self.args[i] || (_.isNaN(value) && _.isNaN(self.args[i]))) {
|
|
self.args.splice(i, 1);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
throwUnlessAllArgumentsHaveBeenChecked: function () {
|
|
var self = this;
|
|
if (!_.isEmpty(self.args))
|
|
throw new Error("Did not check() all arguments during " +
|
|
self.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;
|
|
};
|
|
|