Source code
Revision control
Copy as Markdown
Other Tools
// |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 };
})();