(function () { /* Imports */ var Meteor = Package.meteor.Meteor; var _ = Package.underscore._; var EJSON = Package.ejson.EJSON; /* Package-scope variables */ var check, Match; (function(){ //////////////////////////////////////////////////////////////////////////////////////////////////////// // // // packages/check/match.js // // // //////////////////////////////////////////////////////////////////////////////////////////////////////// // // XXX docs // 1 // 2 // Things we explicitly do NOT support: // 3 // - heterogenous arrays // 4 // 5 var currentArgumentChecker = new Meteor.EnvironmentVariable; // 6 // 7 /** // 8 * @summary Check that a value matches a [pattern](#matchpatterns). // 9 * If the value does not match the pattern, throw a `Match.Error`. // 10 * // 11 * Particularly useful to assert that arguments to a function have the right // 12 * types and structure. // 13 * @locus Anywhere // 14 * @param {Any} value The value to check // 15 * @param {MatchPattern} pattern The pattern to match // 16 * `value` against // 17 */ // 18 check = function (value, pattern) { // 19 // Record that check got called, if somebody cared. // 20 // // 21 // We use getOrNullIfOutsideFiber so that it's OK to call check() // 22 // from non-Fiber server contexts; the downside is that if you forget to // 23 // bindEnvironment on some random callback in your method/publisher, // 24 // it might not find the argumentChecker and you'll get an error about // 25 // not checking an argument that it looks like you're checking (instead // 26 // of just getting a "Node code must run in a Fiber" error). // 27 var argChecker = currentArgumentChecker.getOrNullIfOutsideFiber(); // 28 if (argChecker) // 29 argChecker.checking(value); // 30 var result = testSubtree(value, pattern); // 31 if (result) { // 32 var err = new Match.Error(result.message); // 33 if (result.path) { // 34 err.message += " in field " + result.path; // 35 err.path = result.path; // 36 } // 37 throw err; // 38 } // 39 }; // 40 // 41 /** // 42 * @namespace Match // 43 * @summary The namespace for all Match types and methods. // 44 */ // 45 Match = { // 46 Optional: function (pattern) { // 47 return new Optional(pattern); // 48 }, // 49 OneOf: function (/*arguments*/) { // 50 return new OneOf(_.toArray(arguments)); // 51 }, // 52 Any: ['__any__'], // 53 Where: function (condition) { // 54 return new Where(condition); // 55 }, // 56 ObjectIncluding: function (pattern) { // 57 return new ObjectIncluding(pattern); // 58 }, // 59 ObjectWithValues: function (pattern) { // 60 return new ObjectWithValues(pattern); // 61 }, // 62 // Matches only signed 32-bit integers // 63 Integer: ['__integer__'], // 64 // 65 // XXX matchers should know how to describe themselves for errors // 66 Error: Meteor.makeErrorType("Match.Error", function (msg) { // 67 this.message = "Match error: " + msg; // 68 // The path of the value that failed to match. Initially empty, this gets // 69 // populated by catching and rethrowing the exception as it goes back up the // 70 // stack. // 71 // E.g.: "vals[3].entity.created" // 72 this.path = ""; // 73 // If this gets sent over DDP, don't give full internal details but at least // 74 // provide something better than 500 Internal server error. // 75 this.sanitizedError = new Meteor.Error(400, "Match failed"); // 76 }), // 77 // 78 // Tests to see if value matches pattern. Unlike check, it merely returns true // 79 // or false (unless an error other than Match.Error was thrown). It does not // 80 // interact with _failIfArgumentsAreNotAllChecked. // 81 // XXX maybe also implement a Match.match which returns more information about // 82 // failures but without using exception handling or doing what check() // 83 // does with _failIfArgumentsAreNotAllChecked and Meteor.Error conversion // 84 // 85 /** // 86 * @summary Returns true if the value matches the pattern. // 87 * @locus Anywhere // 88 * @param {Any} value The value to check // 89 * @param {MatchPattern} pattern The pattern to match `value` against // 90 */ // 91 test: function (value, pattern) { // 92 return !testSubtree(value, pattern); // 93 }, // 94 // 95 // Runs `f.apply(context, args)`. If check() is not called on every element of // 96 // `args` (either directly or in the first level of an array), throws an error // 97 // (using `description` in the message). // 98 // // 99 _failIfArgumentsAreNotAllChecked: function (f, context, args, description) { // 100 var argChecker = new ArgumentChecker(args, description); // 101 var result = currentArgumentChecker.withValue(argChecker, function () { // 102 return f.apply(context, args); // 103 }); // 104 // If f didn't itself throw, make sure it checked all of its arguments. // 105 argChecker.throwUnlessAllArgumentsHaveBeenChecked(); // 106 return result; // 107 } // 108 }; // 109 // 110 var Optional = function (pattern) { // 111 this.pattern = pattern; // 112 }; // 113 // 114 var OneOf = function (choices) { // 115 if (_.isEmpty(choices)) // 116 throw new Error("Must provide at least one choice to Match.OneOf"); // 117 this.choices = choices; // 118 }; // 119 // 120 var Where = function (condition) { // 121 this.condition = condition; // 122 }; // 123 // 124 var ObjectIncluding = function (pattern) { // 125 this.pattern = pattern; // 126 }; // 127 // 128 var ObjectWithValues = function (pattern) { // 129 this.pattern = pattern; // 130 }; // 131 // 132 var typeofChecks = [ // 133 [String, "string"], // 134 [Number, "number"], // 135 [Boolean, "boolean"], // 136 // While we don't allow undefined in EJSON, this is good for optional // 137 // arguments with OneOf. // 138 [undefined, "undefined"] // 139 ]; // 140 // 141 // Return `false` if it matches. Otherwise, return an object with a `message` and a `path` field. // 142 var testSubtree = function (value, pattern) { // 143 // Match anything! // 144 if (pattern === Match.Any) // 145 return false; // 146 // 147 // Basic atomic types. // 148 // Do not match boxed objects (e.g. String, Boolean) // 149 for (var i = 0; i < typeofChecks.length; ++i) { // 150 if (pattern === typeofChecks[i][0]) { // 151 if (typeof value === typeofChecks[i][1]) // 152 return false; // 153 return { // 154 message: "Expected " + typeofChecks[i][1] + ", got " + typeof value, // 155 path: "" // 156 }; // 157 } // 158 } // 159 if (pattern === null) { // 160 if (value === null) // 161 return false; // 162 return { // 163 message: "Expected null, got " + EJSON.stringify(value), // 164 path: "" // 165 }; // 166 } // 167 // 168 // Strings, numbers, and booleans match literally. Goes well with Match.OneOf. // 169 if (typeof pattern === "string" || typeof pattern === "number" || typeof pattern === "boolean") { // 170 if (value === pattern) // 171 return false; // 172 return { // 173 message: "Expected " + pattern + ", got " + EJSON.stringify(value), // 174 path: "" // 175 }; // 176 } // 177 // 178 // Match.Integer is special type encoded with array // 179 if (pattern === Match.Integer) { // 180 // There is no consistent and reliable way to check if variable is a 64-bit // 181 // integer. One of the popular solutions is to get reminder of division by 1 // 182 // but this method fails on really large floats with big precision. // 183 // E.g.: 1.348192308491824e+23 % 1 === 0 in V8 // 184 // Bitwise operators work consistantly but always cast variable to 32-bit // 185 // signed integer according to JavaScript specs. // 186 if (typeof value === "number" && (value | 0) === value) // 187 return false; // 188 return { // 189 message: "Expected Integer, got " + (value instanceof Object ? EJSON.stringify(value) : value), path: "" // 191 }; // 192 } // 193 // 194 // "Object" is shorthand for Match.ObjectIncluding({}); // 195 if (pattern === Object) // 196 pattern = Match.ObjectIncluding({}); // 197 // 198 // Array (checked AFTER Any, which is implemented as an Array). // 199 if (pattern instanceof Array) { // 200 if (pattern.length !== 1) { // 201 return { // 202 message: "Bad pattern: arrays must have one type element" + EJSON.stringify(pattern), // 203 path: "" // 204 }; // 205 } // 206 if (!_.isArray(value) && !_.isArguments(value)) { // 207 return { // 208 message: "Expected array, got " + EJSON.stringify(value), // 209 path: "" // 210 }; // 211 } // 212 // 213 for (var i = 0, length = value.length; i < length; i++) { // 214 var result = testSubtree(value[i], pattern[0]); // 215 if (result) { // 216 result.path = _prependPath(i, result.path); // 217 return result; // 218 } // 219 } // 220 return false; // 221 } // 222 // 223 // Arbitrary validation checks. The condition can return false or throw a // 224 // Match.Error (ie, it can internally use check()) to fail. // 225 if (pattern instanceof Where) { // 226 var result; // 227 try { // 228 result = pattern.condition(value); // 229 } catch (err) { // 230 if (!(err instanceof Match.Error)) // 231 throw err; // 232 return { // 233 message: err.message, // 234 path: err.path // 235 }; // 236 } // 237 if (pattern.condition(value)) // 238 return false; // 239 // XXX this error is terrible // 240 return { // 241 message: "Failed Match.Where validation", // 242 path: "" // 243 }; // 244 } // 245 // 246 // 247 if (pattern instanceof Optional) // 248 pattern = Match.OneOf(undefined, pattern.pattern); // 249 // 250 if (pattern instanceof OneOf) { // 251 for (var i = 0; i < pattern.choices.length; ++i) { // 252 var result = testSubtree(value, pattern.choices[i]); // 253 if (!result) { // 254 // No error? Yay, return. // 255 return false; // 256 } // 257 // Match errors just mean try another choice. // 258 } // 259 // XXX this error is terrible // 260 return { // 261 message: "Failed Match.OneOf or Match.Optional validation", // 262 path: "" // 263 }; // 264 } // 265 // 266 // A function that isn't something we special-case is assumed to be a // 267 // constructor. // 268 if (pattern instanceof Function) { // 269 if (value instanceof pattern) // 270 return false; // 271 return { // 272 message: "Expected " + (pattern.name ||"particular constructor"), // 273 path: "" // 274 }; // 275 } // 276 // 277 var unknownKeysAllowed = false; // 278 var unknownKeyPattern; // 279 if (pattern instanceof ObjectIncluding) { // 280 unknownKeysAllowed = true; // 281 pattern = pattern.pattern; // 282 } // 283 if (pattern instanceof ObjectWithValues) { // 284 unknownKeysAllowed = true; // 285 unknownKeyPattern = [pattern.pattern]; // 286 pattern = {}; // no required keys // 287 } // 288 // 289 if (typeof pattern !== "object") { // 290 return { // 291 message: "Bad pattern: unknown pattern type", // 292 path: "" // 293 }; // 294 } // 295 // 296 // An object, with required and optional keys. Note that this does NOT do // 297 // structural matches against objects of special types that happen to match // 298 // the pattern: this really needs to be a plain old {Object}! // 299 if (typeof value !== 'object') { // 300 return { // 301 message: "Expected object, got " + typeof value, // 302 path: "" // 303 }; // 304 } // 305 if (value === null) { // 306 return { // 307 message: "Expected object, got null", // 308 path: "" // 309 }; // 310 } // 311 if (value.constructor !== Object) { // 312 return { // 313 message: "Expected plain object", // 314 path: "" // 315 }; // 316 } // 317 // 318 var requiredPatterns = {}; // 319 var optionalPatterns = {}; // 320 _.each(pattern, function (subPattern, key) { // 321 if (subPattern instanceof Optional) // 322 optionalPatterns[key] = subPattern.pattern; // 323 else // 324 requiredPatterns[key] = subPattern; // 325 }); // 326 // 327 for (var keys = _.keys(value), i = 0, length = keys.length; i < length; i++) { // 328 var key = keys[i]; // 329 var subValue = value[key]; // 330 if (_.has(requiredPatterns, key)) { // 331 var result = testSubtree(subValue, requiredPatterns[key]); // 332 if (result) { // 333 result.path = _prependPath(key, result.path); // 334 return result; // 335 } // 336 delete requiredPatterns[key]; // 337 } else if (_.has(optionalPatterns, key)) { // 338 var result = testSubtree(subValue, optionalPatterns[key]); // 339 if (result) { // 340 result.path = _prependPath(key, result.path); // 341 return result; // 342 } // 343 } else { // 344 if (!unknownKeysAllowed) { // 345 return { // 346 message: "Unknown key", // 347 path: key // 348 }; // 349 } // 350 if (unknownKeyPattern) { // 351 var result = testSubtree(subValue, unknownKeyPattern[0]); // 352 if (result) { // 353 result.path = _prependPath(key, result.path); // 354 return result; // 355 } // 356 } // 357 } // 358 } // 359 // 360 var keys = _.keys(requiredPatterns); // 361 if (keys.length) { // 362 return { // 363 message: "Missing key '" + keys[0] + "'", // 364 path: "" // 365 }; // 366 } // 367 }; // 368 // 369 var ArgumentChecker = function (args, description) { // 370 var self = this; // 371 // Make a SHALLOW copy of the arguments. (We'll be doing identity checks // 372 // against its contents.) // 373 self.args = _.clone(args); // 374 // Since the common case will be to check arguments in order, and we splice // 375 // out arguments when we check them, make it so we splice out from the end // 376 // rather than the beginning. // 377 self.args.reverse(); // 378 self.description = description; // 379 }; // 380 // 381 _.extend(ArgumentChecker.prototype, { // 382 checking: function (value) { // 383 var self = this; // 384 if (self._checkingOneValue(value)) // 385 return; // 386 // Allow check(arguments, [String]) or check(arguments.slice(1), [String]) // 387 // or check([foo, bar], [String]) to count... but only if value wasn't // 388 // itself an argument. // 389 if (_.isArray(value) || _.isArguments(value)) { // 390 _.each(value, _.bind(self._checkingOneValue, self)); // 391 } // 392 }, // 393 _checkingOneValue: function (value) { // 394 var self = this; // 395 for (var i = 0; i < self.args.length; ++i) { // 396 // Is this value one of the arguments? (This can have a false positive if // 397 // the argument is an interned primitive, but it's still a good enough // 398 // check.) // 399 // (NaN is not === to itself, so we have to check specially.) // 400 if (value === self.args[i] || (_.isNaN(value) && _.isNaN(self.args[i]))) { // 401 self.args.splice(i, 1); // 402 return true; // 403 } // 404 } // 405 return false; // 406 }, // 407 throwUnlessAllArgumentsHaveBeenChecked: function () { // 408 var self = this; // 409 if (!_.isEmpty(self.args)) // 410 throw new Error("Did not check() all arguments during " + // 411 self.description); // 412 } // 413 }); // 414 // 415 var _jsKeywords = ["do", "if", "in", "for", "let", "new", "try", "var", "case", // 416 "else", "enum", "eval", "false", "null", "this", "true", "void", "with", // 417 "break", "catch", "class", "const", "super", "throw", "while", "yield", // 418 "delete", "export", "import", "public", "return", "static", "switch", // 419 "typeof", "default", "extends", "finally", "package", "private", "continue", // 420 "debugger", "function", "arguments", "interface", "protected", "implements", // 421 "instanceof"]; // 422 // 423 // Assumes the base of path is already escaped properly // 424 // returns key + base // 425 var _prependPath = function (key, base) { // 426 if ((typeof key) === "number" || key.match(/^[0-9]+$/)) // 427 key = "[" + key + "]"; // 428 else if (!key.match(/^[a-z_$][0-9a-z_$]*$/i) || _.contains(_jsKeywords, key)) // 429 key = JSON.stringify([key]); // 430 // 431 if (base && base[0] !== "[") // 432 return key + '.' + base; // 433 return key + base; // 434 }; // 435 // 436 // 437 //////////////////////////////////////////////////////////////////////////////////////////////////////// }).call(this); /* Exports */ if (typeof Package === 'undefined') Package = {}; Package.check = { check: check, Match: Match }; })(); //# sourceMappingURL=check.js.map