├── .editorconfig ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── match.js ├── package.json └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | tab_width = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ingvar Stepanyan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xmatch 2 | 3 | Simple pattern matching for ES6 (no transpilation!) 4 | 5 | ## Property matching 6 | 7 | ```javascript 8 | const { match } = require('xmatch'); 9 | 10 | match(obj, [ 11 | ({ x }) => console.log('x', x), 12 | ({ y }) => console.log('y', y), 13 | ({ z }) => console.log('z', z), 14 | // Exhaustive match; will throw `xmatch.UnmatchedPatternError` unless uncommented: 15 | // other => console.error('Something else', other), 16 | ]); 17 | ``` 18 | 19 | ## Iterable matching 20 | 21 | ```javascript 22 | const { match } = require('xmatch'); 23 | 24 | match(arr, [ 25 | ([]) => 'empty', 26 | ([x]) => `x=${x}`, 27 | ([x, y]) => `x=${x},y=${y}`, 28 | ([x, y, ...{ length }]) => `x=${x},y=${y},rest.length=${length}`, 29 | ]); 30 | ``` 31 | 32 | ## Custom guards 33 | 34 | ```javascript 35 | const { match, guard } = require('xmatch'); 36 | 37 | match(obj, [ 38 | ({ command }) => { 39 | // When you want to match simple values: 40 | guard(command === 'ignore'); 41 | /* Do nothing */ 42 | }, 43 | ({ command }) => { 44 | // Or, say, match result of regex: 45 | let [, name, args] = guard(command.match(/^(\w+):(.*)$/)); 46 | console.log({ name, args }); 47 | }, 48 | ({ command }) => { 49 | throw new Error(`Invalid command: ${command}`); 50 | }, 51 | ]); 52 | ``` 53 | 54 | ## Shape assertions 55 | 56 | ```javascript 57 | const { guard } = require('xmatch'); 58 | 59 | const { x, y } = guard({ x: 1, y: 2 }); // OK 60 | const { x, y } = guard({ x: 1, z: 2 }); // throws `xmatch.UnmatchedPatternError` 61 | ``` 62 | 63 | ## Known issues 64 | 65 | * You can't use literals directly in patterns (this is limitation of ES6 syntax, can be fixed as part of https://github.com/tc39/proposal-pattern-matching). 66 | * You can't use default values for parameters. This is limitation of the way matching is implemented, and you'll have to resolve defaults yourself if that's what you want. 67 | * Trying to further destructure or access undefined properties of an object will also trigger the match guard ([#1](https://github.com/RReverser/xmatch/issues/1)). This is tricky to workaround without changing the syntax, but I'll look into it (and happy to hear any suggestions). 68 | * This uses dynamic metaprogramming via [`Proxy`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) which might have undesirable performance effect on hot code paths. If your benchmarks suggest it's causing critical performance issues, consider using transpiler plugins instead. 69 | * `Proxy` is not implemented in pre-ES6 browsers and can't be polyfilled, so use this only if you're okay with the supported target set: https://caniuse.com/#feat=proxy 70 | -------------------------------------------------------------------------------- /match.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let currentUnmatchedDepth = 0; 4 | let unmatchedErrorDepth = new WeakMap(); 5 | 6 | class UnmatchedPatternError extends Error { 7 | constructor() { 8 | super('Unmatched pattern'); 9 | unmatchedErrorDepth.set(this, currentUnmatchedDepth); 10 | } 11 | } 12 | 13 | function guard(condition) { 14 | if (!condition) { 15 | throw new UnmatchedPatternError(); 16 | } 17 | // in case you want to further destructure 18 | return wrap(condition); 19 | } 20 | 21 | const TRAPS = { 22 | get(target, prop, receiver) { 23 | let result = Reflect.get(target, prop, receiver); 24 | guard(result !== undefined); 25 | if (prop === Symbol.iterator) { 26 | return function() { 27 | let iter = result.apply(this, arguments); 28 | let done = false; 29 | return { 30 | next() { 31 | let value; 32 | if (!done) { 33 | ({ value, done } = iter.next()); 34 | } 35 | return { value, done }; 36 | }, 37 | return(value) { 38 | if (!done) { 39 | done = iter.next().done; 40 | if (!done) { 41 | // tried to bail early even though we have elements 42 | throw new UnmatchedPatternError(); 43 | } 44 | } 45 | return { value, done }; 46 | }, 47 | }; 48 | }; 49 | } 50 | return wrap(result); 51 | }, 52 | }; 53 | 54 | let wrapCache = new WeakMap(); 55 | 56 | function wrap(obj) { 57 | if (typeof obj !== 'object' || obj === null) { 58 | return obj; 59 | } 60 | let proxy = wrapCache.get(obj); 61 | if (proxy === undefined) { 62 | wrapCache.set(obj, (proxy = new Proxy(obj, TRAPS))); 63 | } 64 | return proxy; 65 | } 66 | 67 | function match(obj, matchers) { 68 | try { 69 | let depth = ++currentUnmatchedDepth; 70 | for (let matcher of matchers) { 71 | try { 72 | return matcher(wrap(obj)); 73 | } catch (e) { 74 | if (unmatchedErrorDepth.get(e) !== depth) { 75 | // either another error or a deeper UnmatchedPatternError 76 | throw e; 77 | } 78 | } 79 | } 80 | throw new UnmatchedPatternError(); 81 | } finally { 82 | currentUnmatchedDepth--; 83 | } 84 | } 85 | 86 | module.exports = { 87 | match, 88 | guard, 89 | UnmatchedPatternError, 90 | }; 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xmatch", 3 | "version": "1.3.0", 4 | "description": "Simple pattern matching for ES6 (no transpilation!)", 5 | "main": "match.js", 6 | "scripts": { 7 | "test": "node test.js" 8 | }, 9 | "keywords": ["pattern", "matching", "match", "es6", "proxy"], 10 | "author": "Ingvar Stepanyan (https://rreverser.com/)", 11 | "license": "MIT" 12 | } 13 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { match, guard, UnmatchedPatternError } = require('./'); 4 | const assert = require('assert'); 5 | 6 | function test1(obj) { 7 | return match(obj, [ 8 | ({ foo }) => `foo ${foo}`, 9 | ({ bar: { x } }) => `bar with x ${x}`, 10 | ({ bar: { answer } }) => { 11 | guard(answer !== 42); 12 | return `bar without correct answer ${answer}`; 13 | }, 14 | ({ bar: { answer } }) => `bar with correct answer ${answer}`, 15 | ]); 16 | } 17 | 18 | assert.strictEqual(test1({ foo: 10 }), 'foo 10'); 19 | 20 | assert.strictEqual(test1({ bar: { x: 20 } }), 'bar with x 20'); 21 | 22 | assert.strictEqual( 23 | test1({ bar: { answer: 30 } }), 24 | 'bar without correct answer 30' 25 | ); 26 | 27 | assert.strictEqual( 28 | test1({ bar: { answer: 40 } }), 29 | 'bar without correct answer 40' 30 | ); 31 | 32 | assert.throws(() => test1({ other: 50 }), UnmatchedPatternError); 33 | 34 | function test2(obj) { 35 | return match(obj, [ 36 | ({ command }) => { 37 | // When you want to match simple values: 38 | guard(command === 'ignore'); 39 | /* Do nothing */ 40 | }, 41 | ({ command }) => { 42 | // Or, say, match result of regex: 43 | let [, name, arg] = guard(command.match(/^(\w+):(.*)$/)); 44 | return { 45 | name, 46 | arg, 47 | }; 48 | }, 49 | ({ command }) => { 50 | throw new Error(`Invalid command: ${command}`); 51 | }, 52 | () => { 53 | throw new Error(`Invalid object`); 54 | }, 55 | ]); 56 | } 57 | 58 | assert.strictEqual(test2({ command: 'ignore' }), undefined); 59 | 60 | assert.deepStrictEqual(test2({ command: 'abc:123' }), { 61 | name: 'abc', 62 | arg: '123', 63 | }); 64 | 65 | assert.throws( 66 | () => test2({ command: 'whatever' }), 67 | Error, 68 | 'Invalid command: whatever' 69 | ); 70 | 71 | assert.throws(() => test2({}), Error, 'Invalid object'); 72 | 73 | assert.doesNotThrow(() => { 74 | let { x, y } = guard({ x: 10, y: 20 }); 75 | }); 76 | 77 | assert.throws(() => { 78 | let { x, y } = guard({ x: 10, z: 20 }); 79 | }, UnmatchedPatternError); 80 | 81 | function test3(obj) { 82 | return match(obj, [ 83 | ([]) => 'empty', 84 | ([x]) => `x=${x}`, 85 | ([x, y]) => `x=${x},y=${y}`, 86 | ([x, y, ...{ length }]) => `x=${x},y=${y},rest.length=${length}`, 87 | ]); 88 | } 89 | 90 | assert.strictEqual(test3([]), 'empty'); 91 | assert.strictEqual(test3([10]), 'x=10'); 92 | assert.strictEqual(test3([10,20]), 'x=10,y=20'); 93 | assert.strictEqual(test3([10,20,30,40]), 'x=10,y=20,rest.length=2'); 94 | assert.throws(() => test3({}), UnmatchedPatternError); 95 | 96 | assert.throws(() => match({ x: 1 }, [ 97 | any => { 98 | match({ y: 2 }, []); 99 | }, 100 | otherAny => { 101 | assert.fail('otherAny', 'any', 'Inner match failure should not cause invocation of the next branch', '!='); 102 | } 103 | ]), UnmatchedPatternError); 104 | --------------------------------------------------------------------------------