Match.js (6794B)
1 // |reftest| skip 2 3 // A little pattern-matching library. 4 var Match = 5 6 (function() { 7 8 function Pattern(template) { 9 // act like a constructor even as a function 10 if (!(this instanceof Pattern)) 11 return new Pattern(template); 12 13 this.template = template; 14 } 15 16 Pattern.prototype = { 17 match: function(act) { 18 return match(act, this.template); 19 }, 20 21 matches: function(act) { 22 try { 23 return this.match(act); 24 } 25 catch (e) { 26 if (!(e instanceof MatchError)) 27 throw e; 28 return false; 29 } 30 }, 31 32 assert: function(act, message) { 33 try { 34 return this.match(act); 35 } 36 catch (e) { 37 if (!(e instanceof MatchError)) 38 throw e; 39 throw new Error((message || "failed match") + ": " + e.message); 40 } 41 }, 42 43 toString: () => "[object Pattern]" 44 }; 45 46 Pattern.ANY = new Pattern; 47 Pattern.ANY.template = Pattern.ANY; 48 49 Pattern.NUMBER = new Pattern; 50 Pattern.NUMBER.match = function (act) { 51 if (typeof act !== 'number') { 52 throw new MatchError("Expected number, got: " + quote(act)); 53 } 54 } 55 56 Pattern.NATURAL = new Pattern 57 Pattern.NATURAL.match = function (act) { 58 if (typeof act !== 'number' || act !== Math.floor(act) || act < 0) { 59 throw new MatchError("Expected natural number, got: " + quote(act)); 60 } 61 } 62 63 class ObjectWithExactly extends Pattern { 64 constructor(template) { 65 super(template); 66 } 67 68 match(actual) { 69 return matchObjectWithExactly(actual, this.template) 70 } 71 } 72 73 Pattern.OBJECT_WITH_EXACTLY = function (template) { 74 return new ObjectWithExactly(template); 75 } 76 77 var quote = JSON.stringify; 78 79 class MatchError extends Error { 80 toString() { 81 return "match error: " + this.message; 82 } 83 }; 84 85 Pattern.MatchError = MatchError; 86 87 function isAtom(x) { 88 return (typeof x === "number") || 89 (typeof x === "string") || 90 (typeof x === "boolean") || 91 (x === null) || 92 (x === undefined) || 93 (typeof x === "object" && x instanceof RegExp) || 94 (typeof x === "bigint"); 95 } 96 97 function isObject(x) { 98 return (x !== null) && (typeof x === "object"); 99 } 100 101 function isFunction(x) { 102 return typeof x === "function"; 103 } 104 105 function isArrayLike(x) { 106 return isObject(x) && ("length" in x); 107 } 108 109 function matchAtom(act, exp) { 110 if ((typeof exp) === "number" && isNaN(exp)) { 111 if ((typeof act) !== "number" || !isNaN(act)) 112 throw new MatchError("expected NaN, got: " + quote(act)); 113 return true; 114 } 115 116 if (exp === null) { 117 if (act !== null) 118 throw new MatchError("expected null, got: " + quote(act)); 119 return true; 120 } 121 122 if (exp instanceof RegExp) { 123 if (!(act instanceof RegExp) || exp.source !== act.source) 124 throw new MatchError("expected " + quote(exp) + ", got: " + quote(act)); 125 return true; 126 } 127 128 switch (typeof exp) { 129 case "string": 130 case "undefined": 131 if (act !== exp) 132 throw new MatchError("expected " + quote(exp) + ", got " + quote(act)); 133 return true; 134 case "boolean": 135 case "number": 136 case "bigint": 137 if (exp !== act) 138 throw new MatchError("expected " + exp + ", got " + quote(act)); 139 return true; 140 } 141 142 throw new Error("bad pattern: " + JSON.stringify(exp)); 143 } 144 145 // Match an object having at least the expected properties. 146 function matchObjectWithAtLeast(act, exp) { 147 if (!isObject(act)) 148 throw new MatchError("expected object, got " + quote(act)); 149 150 for (var key in exp) { 151 if (!(key in act)) 152 throw new MatchError("expected property " + quote(key) + " not found in " + quote(act)); 153 try { 154 match(act[key], exp[key]); 155 } catch (inner) { 156 if (!(inner instanceof MatchError)) { 157 throw inner; 158 } 159 inner.message = `matching property "${String(key)}":\n${inner.message}`; 160 throw inner; 161 } 162 } 163 164 return true; 165 } 166 167 // Match an object having all the expected properties and no more. 168 function matchObjectWithExactly(act, exp) { 169 matchObjectWithAtLeast(act, exp); 170 171 for (var key in act) { 172 if (!(key in exp)) { 173 throw new MatchError("unexpected property " + quote(key)); 174 } 175 } 176 177 return true; 178 } 179 180 function matchFunction(act, exp) { 181 if (!isFunction(act)) 182 throw new MatchError("expected function, got " + quote(act)); 183 184 if (act !== exp) 185 throw new MatchError("expected function: " + exp + 186 "\nbut got different function: " + act); 187 } 188 189 function matchArray(act, exp) { 190 if (!isObject(act) || !("length" in act)) 191 throw new MatchError("expected array-like object, got " + quote(act)); 192 193 var length = exp.length; 194 if (act.length !== exp.length) 195 throw new MatchError("expected array-like object of length " + length + ", got " + quote(act)); 196 197 for (var i = 0; i < length; i++) { 198 if (i in exp) { 199 if (!(i in act)) 200 throw new MatchError("expected array property " + i + " not found in " + quote(act)); 201 try { 202 match(act[i], exp[i]); 203 } catch (inner) { 204 if (!(inner instanceof MatchError)) { 205 throw inner; 206 } 207 inner.message = `matching array element [${i}]:\n${inner.message}`; 208 throw inner; 209 } 210 } 211 } 212 213 return true; 214 } 215 216 function match(act, exp) { 217 if (exp === Pattern.ANY) 218 return true; 219 220 if (exp instanceof Pattern) 221 return exp.match(act); 222 223 if (isAtom(exp)) 224 return matchAtom(act, exp); 225 226 if (isArrayLike(exp)) 227 return matchArray(act, exp); 228 229 if (isFunction(exp)) 230 return matchFunction(act, exp); 231 232 if (isObject(exp)) 233 return matchObjectWithAtLeast(act, exp); 234 235 throw new Error("bad pattern: " + JSON.stringify(exp)); 236 } 237 238 return { Pattern: Pattern, 239 MatchError: MatchError }; 240 241 })();