// |reftest| skip // A little pattern-matching library. var 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: function(act) { return match(act, this.template); }, matches: function(act) { try { return this.match(act); } catch (e) { if (!(e instanceof MatchError)) throw e; return false; } }, assert: function(act, message) { try { return this.match(act); } catch (e) { if (!(e instanceof MatchError)) throw e; throw new Error((message || "failed match") + ": " + e.message); } }, 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)); } } class ObjectWithExactly extends Pattern { constructor(template) { super(template); } match(actual) { return matchObjectWithExactly(actual, this.template) } } Pattern.OBJECT_WITH_EXACTLY = function (template) { return new ObjectWithExactly(template); } var quote = JSON.stringify; class MatchError extends Error { toString() { return "match error: " + this.message; } }; Pattern.MatchError = MatchError; function isAtom(x) { return (typeof x === "number") || (typeof x === "string") || (typeof x === "boolean") || (x === null) || (x === undefined) || (typeof x === "object" && x instanceof RegExp) || (typeof x === "bigint"); } 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": case "undefined": if (act !== exp) throw new MatchError("expected " + quote(exp) + ", got " + quote(act)); return true; case "boolean": case "number": case "bigint": if (exp !== act) throw new MatchError("expected " + exp + ", got " + quote(act)); return true; } throw new Error("bad pattern: " + JSON.stringify(exp)); } // Match an object having at least the expected properties. function matchObjectWithAtLeast(act, exp) { if (!isObject(act)) throw new MatchError("expected object, got " + quote(act)); for (var key in exp) { if (!(key in act)) throw new MatchError("expected property " + quote(key) + " not found in " + quote(act)); try { match(act[key], exp[key]); } catch (inner) { if (!(inner instanceof MatchError)) { throw inner; } inner.message = `matching property "${String(key)}":\n${inner.message}`; throw inner; } } return true; } // Match an object having all the expected properties and no more. function matchObjectWithExactly(act, exp) { matchObjectWithAtLeast(act, exp); for (var key in act) { if (!(key in exp)) { throw new MatchError("unexpected property " + quote(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)); var length = exp.length; if (act.length !== exp.length) throw new MatchError("expected array-like object of length " + length + ", got " + quote(act)); for (var 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)); try { match(act[i], exp[i]); } catch (inner) { if (!(inner instanceof MatchError)) { throw inner; } inner.message = `matching array element [${i}]:\n${inner.message}`; throw inner; } } } 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); if (isObject(exp)) return matchObjectWithAtLeast(act, exp); throw new Error("bad pattern: " + JSON.stringify(exp)); } return { Pattern: Pattern, MatchError: MatchError }; })();