// A little pattern-matching library. // // Ported from js/src/tests/js1_8_5/reflect-parse/Match.js for use with devtools // server xpcshell tests. export const Match = (function() { function Pattern(template) { // act like a constructor even as a function if (!(this instanceof Pattern)) { return new Pattern(template); } this.template = template; } Pattern.prototype = { match(act) { return match(act, this.template); }, matches(act) { try { return this.match(act); } catch (e) { if (e instanceof MatchError) { return false; } } return false; }, assert(act, message) { try { return this.match(act); } catch (e) { if (e instanceof MatchError) { throw new Error((message || "failed match") + ": " + e.message); } } return false; }, toString: () => "[object Pattern]", }; Pattern.ANY = new Pattern(); Pattern.ANY.template = Pattern.ANY; Pattern.NUMBER = new Pattern(); Pattern.NUMBER.match = function(act) { if (typeof act !== "number") { throw new MatchError("Expected number, got: " + quote(act)); } }; Pattern.NATURAL = new Pattern(); Pattern.NATURAL.match = function(act) { if (typeof act !== "number" || act !== Math.floor(act) || act < 0) { throw new MatchError("Expected natural number, got: " + quote(act)); } }; const quote = uneval; function MatchError(msg) { this.message = msg; } MatchError.prototype = { toString() { return "match error: " + this.message; }, }; function isAtom(x) { return ( typeof x === "number" || typeof x === "string" || typeof x === "boolean" || x === null || (typeof x === "object" && x instanceof RegExp) ); } function isObject(x) { return x !== null && typeof x === "object"; } function isFunction(x) { return typeof x === "function"; } function isArrayLike(x) { return isObject(x) && "length" in x; } function matchAtom(act, exp) { if (typeof exp === "number" && isNaN(exp)) { if (typeof act !== "number" || !isNaN(act)) { throw new MatchError("expected NaN, got: " + quote(act)); } return true; } if (exp === null) { if (act !== null) { throw new MatchError("expected null, got: " + quote(act)); } return true; } if (exp instanceof RegExp) { if (!(act instanceof RegExp) || exp.source !== act.source) { throw new MatchError("expected " + quote(exp) + ", got: " + quote(act)); } return true; } switch (typeof exp) { case "string": if (act !== exp) { throw new MatchError( "expected " + quote(exp) + ", got " + quote(act) ); } return true; case "boolean": case "number": if (exp !== act) { throw new MatchError("expected " + exp + ", got " + quote(act)); } return true; } throw new Error("bad pattern: " + exp.toSource()); } function matchObject(act, exp) { if (!isObject(act)) { throw new MatchError("expected object, got " + quote(act)); } for (const key in exp) { if (!(key in act)) { throw new MatchError( "expected property " + quote(key) + " not found in " + quote(act) ); } match(act[key], exp[key]); } return true; } function matchFunction(act, exp) { if (!isFunction(act)) { throw new MatchError("expected function, got " + quote(act)); } if (act !== exp) { throw new MatchError( "expected function: " + exp + "\nbut got different function: " + act ); } } function matchArray(act, exp) { if (!isObject(act) || !("length" in act)) { throw new MatchError("expected array-like object, got " + quote(act)); } const length = exp.length; if (act.length !== exp.length) { throw new MatchError( "expected array-like object of length " + length + ", got " + quote(act) ); } for (let i = 0; i < length; i++) { if (i in exp) { if (!(i in act)) { throw new MatchError( "expected array property " + i + " not found in " + quote(act) ); } match(act[i], exp[i]); } } return true; } function match(act, exp) { if (exp === Pattern.ANY) { return true; } if (exp instanceof Pattern) { return exp.match(act); } if (isAtom(exp)) { return matchAtom(act, exp); } if (isArrayLike(exp)) { return matchArray(act, exp); } if (isFunction(exp)) { return matchFunction(act, exp); } return matchObject(act, exp); } return { Pattern, MatchError }; })();