├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── package.json ├── src └── index.js └── test └── index.js /.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 | lib/ 61 | package-lock.json 62 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - node 4 | deploy: 5 | provider: npm 6 | email: christopher.andrejewski@gmail.com 7 | api_key: 8 | secure: FFex/HtSaCaBwRWoqkbn9Wx5e76bCIZxo73dewfbLswgj39hrypJ3BBpWFJTp8TiEfuUwJGKAk//Hn0iJGKHqBeZRCuCfMAHVW0vqh1dZCbaSGFal73nD7TrsCqYt3rG15go098vyJxnro1XgTdCXfcz/kFT+4tmEPx+ggHWs6chzD+zDiTicWAetk35/24ffUlrmNQWHcFD2HZi6arChmcocIisBGZaYf2SdGmUIj2jWN5ObEiweyJ6S4lwvkGP8qA+vzc850ZLf+PTJQKyC/Dr/QPFoozZV2y6repeWM5a4LsM2XgjIF803whGiclNIB2HUAHTpRB1fP6GQ3F+AmFcdi4X3K/8Zsvp6P92hjKTHg0BTw++bjlYF9NjC02echwcE+5w5GEEgkfJ7egcqr7cSAeg8qJthhDolfMsoYUNcw0S5F6LVdpZdcByUJpjDyj+1PW44QDPrWA1YXZ9zRvz+9jHAeAg7HBRvwJD+bFDc4XhRVCxHdnWVFsMstGK9w5saMu6oyb3hoZ607pga/jwC51+4ftYo/cRLiPHxHqikUObZzA8gco/GC4lNQ1+Jpykfg4+9+lG1B9GxC3/0JIo1xiunNkQDFB/6dmpqbk5jM8xyEN2o+AaQxr6Y9Sa+cGVWRRlY3eQ1CeVK6EvQVgUq44DkJ7ctse11JJX+Rw= 9 | on: 10 | tags: true 11 | repo: andrejewski/tagmeme 12 | skip_cleanup: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Chris Andrejewski 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 | # Tagmeme 2 | > Tagged unions 3 | 4 | ```sh 5 | npm install tagmeme 6 | ``` 7 | 8 | [![npm](https://img.shields.io/npm/v/tagmeme.svg)](https://www.npmjs.com/package/tagmeme) 9 | [![Build Status](https://travis-ci.org/andrejewski/tagmeme.svg?branch=master)](https://travis-ci.org/andrejewski/tagmeme) 10 | [![Greenkeeper badge](https://badges.greenkeeper.io/andrejewski/tagmeme.svg)](https://greenkeeper.io/) 11 | 12 | Tagmeme is a library for building tagged unions. 13 | This project offers: 14 | 15 | - A concise API for pattern matching and variant constructors 16 | - Data-oriented tags that can be serialized and namespaced 17 | - Errors in development to ensure exhaustive pattern matching 18 | - Small size and zero-dependencies in production using dead code elimination 19 | 20 | Let's check it out. 21 | 22 | ## Examples 23 | 24 | ```js 25 | import assert from 'assert' 26 | import { union } from 'tagmeme' 27 | 28 | const Result = union(['Ok', 'Err']) 29 | 30 | const err = Result.Err(new Error('My error')) 31 | 32 | const message = Result.match(err, { 33 | Ok: () => 'No error', 34 | Err: error => error.message 35 | }) 36 | 37 | assert(message === 'My error') 38 | 39 | const handle = Result.matcher({ 40 | Ok: () => 'No error', 41 | Err: error => error.message 42 | }) 43 | 44 | assert(handle(err) === 'My error') 45 | 46 | const isError = Result.matches(err, Result.Err) 47 | 48 | assert(isError) 49 | ``` 50 | 51 | ## Documentation 52 | 53 | This package includes: 54 | 55 | - [`union(types, options)`](#union) 56 | - [`Union[type](data)`](#uniontype) 57 | - [`Union.match(tag, handlers, catchAll)`](#unionmatch) 58 | - [`Union.matcher(handlers, catchAll)`](#unionmatcher) 59 | - [`Union.matches(tag, variant)`](#unionmatches) 60 | - [`safeUnion(types, options)`](#safeunion) 61 | 62 | #### `union` 63 | > `union(types: Array[, options: { prefix: String }]): Union` 64 | 65 | Create a tagged union. Throws if: 66 | - `types` is not an array of unique strings 67 | - any `types` are named "match", "matcher", or "matches" 68 | 69 | See [`safeUnion`](#safeunion) if using arbitrary strings. 70 | 71 | #### `Union[type]` 72 | > `Union[type](data: any): ({ type, data })` 73 | 74 | Create a tag of the union containing `data` which can be retrieved via `Union.match`. 75 | 76 | ```js 77 | import assert from 'assert' 78 | import { union } from 'tagmeme' 79 | 80 | const Result = union(['Ok', 'Err']) 81 | const result = Result.Ok('good stuff') 82 | 83 | assert(result.type === 'Ok') 84 | assert(result.data === 'good stuff') 85 | ``` 86 | 87 | #### `Union.match` 88 | > `Union.match(tag, handlers[, catchAll: function])` 89 | 90 | Pattern match on `tag` with a hashmap of `handlers` where keys are kinds and values are functions, with an optional `catchAll` if no handler matches the value. 91 | Throws if: 92 | - `tag` is not of any of the union types 93 | - `tag` does not match any type and there is no `catchAll` 94 | - any `handlers` key is not a kind in the union 95 | - any `handlers` value is not a function 96 | - it handles all cases and there is a useless `catchAll` 97 | - it does not handle all cases and there is no `catchAll` 98 | 99 | ```js 100 | import assert from 'assert' 101 | import { union } from 'tagmeme' 102 | 103 | const Result = union(['Ok', 'Err']) 104 | const result = Result.Err('Request failed') 105 | const status = Result.match( 106 | result, 107 | { Err: () => 400 }, 108 | () => 200 109 | }) 110 | 111 | assert(status === 400) 112 | ``` 113 | 114 | #### `Union.matcher` 115 | > `Union.matcher(handlers[, catchAll: function])` 116 | 117 | Create a matching function which will take `tag` and `context` arguments. 118 | This reduces the boilerplate of a function that delegates to `Union.match` with static handlers. 119 | This is also a bit faster than `match` because the handler functions only need to be created once. 120 | 121 | Unlike with `match`, the second argument to handlers will be `context` to avoid the need for a closure. 122 | 123 | ```js 124 | import assert from 'assert' 125 | import { union } from 'tagmeme' 126 | 127 | const Result = union(['Ok', 'Err']) 128 | const collectErrors = Result.matcher({ 129 | Ok: (_, errors) => errors, 130 | Err: (error, errors) => errors.concat(error) 131 | }) 132 | 133 | const errors = collectErrors(Result.Err('Bad'), []) 134 | assert.deepEqual(errors, ['Bad']) 135 | ``` 136 | 137 | #### `Union.matches` 138 | > `Union.matches(tag, variant: Variant): Boolean` 139 | 140 | Determine whether a given `tag` is of `variant`. 141 | 142 | ```js 143 | import assert from 'assert' 144 | import { union } from 'tagmeme' 145 | 146 | const Result = union(['Ok', 'Err']) 147 | const okTag = Result.Ok(1) 148 | 149 | assert(Result.matches(okTag, Result.Ok)) 150 | ``` 151 | 152 | #### `safeUnion` 153 | > `safeUnion(types: Array[, options: { prefix: String }]): { methods, variants }` 154 | 155 | For library authors accepting arbitrary strings for type names, `safeUnion` is `union` but returns distinct collections of methods and type variants. 156 | This will not throw if a type is "match", "matcher", or "matches". 157 | 158 | ## Name 159 | 160 | > tagmeme |ˈtaɡmiːm|: a slot in a syntactic frame which may be filled by any member of a set of appropriate linguistic items. 161 | 162 | This name is kind of fitting for a tagged union library. 163 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tagmeme", 3 | "description": "Simple tagged unions", 4 | "version": "0.0.10", 5 | "author": "Chris Andrejewski ", 6 | "babel": { 7 | "presets": [ 8 | "es2015", 9 | "stage-3" 10 | ] 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/andrejewski/tagmeme/issues" 14 | }, 15 | "dependencies": { 16 | "invariant": "^2.2.4" 17 | }, 18 | "devDependencies": { 19 | "ava": "^0.25.0", 20 | "babel-cli": "^6.24.1", 21 | "babel-core": "^6.26.3", 22 | "babel-preset-es2015": "^6.24.1", 23 | "babel-preset-stage-3": "^6.24.1", 24 | "fixpack": "^2.3.1", 25 | "standard": "^12.0.1" 26 | }, 27 | "homepage": "https://github.com/andrejewski/tagmeme#readme", 28 | "keywords": [ 29 | "adt", 30 | "match", 31 | "pattern", 32 | "tag", 33 | "union" 34 | ], 35 | "license": "MIT", 36 | "main": "lib/index.js", 37 | "repository": { 38 | "type": "git", 39 | "url": "git+https://github.com/andrejewski/tagmeme.git" 40 | }, 41 | "scripts": { 42 | "build": "babel src --out-dir lib", 43 | "lint": "fixpack && standard --fix", 44 | "prepublishOnly": "npm run build", 45 | "test": "npm run lint && ava" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const invariant = require('invariant') 2 | const hasOwnProperty = Object.prototype.hasOwnProperty 3 | 4 | function checkTypes (types) { 5 | invariant(Array.isArray(types), 'types must be an array') 6 | 7 | const seen = Object.create(null) 8 | for (let i = 0; i < types.length; i++) { 9 | const type = types[i] 10 | invariant(typeof type === 'string', 'Tag type must be a string') 11 | invariant(!seen[type], `Duplicate tag type "${type}". Types must be unique`) 12 | seen[type] = true 13 | } 14 | } 15 | 16 | function checkMatch (handlers, catchAll, types) { 17 | const seenTypes = [] 18 | for (const key in handlers) { 19 | invariant( 20 | types.includes(key), 21 | `Key "${key}" is not a tag type of the union` 22 | ) 23 | const handler = handlers[key] 24 | invariant( 25 | typeof handler === 'function', 26 | `Key "${key}" value must be a function` 27 | ) 28 | seenTypes.push(key) 29 | } 30 | 31 | if (catchAll) { 32 | invariant( 33 | types.length !== seenTypes.length, 34 | 'All types are handled; remove unnecessary catch-all' 35 | ) 36 | invariant(typeof catchAll === 'function', 'catch-all must be a function') 37 | } else { 38 | const missingTypes = types.filter(type => !seenTypes.includes(type)) 39 | invariant( 40 | types.length === seenTypes.length, 41 | `All types are not handled; add a catch-all. Missing types: ${missingTypes.join(', ')}` 42 | ) 43 | } 44 | } 45 | 46 | function checkTag (tag, tagType, types) { 47 | invariant(typeof tag === 'object', 'Tag must be an object') 48 | invariant(typeof tag.type === 'string', 'Tag type must be a string') 49 | invariant(typeof tagType === 'string', 'Tag type must be prefixed') 50 | invariant(types.includes(tagType), `Tag must be a tag of the union`) 51 | } 52 | 53 | function checkType (type, tagUnion) { 54 | invariant(type, 'Type must be provided') 55 | 56 | for (const key in tagUnion) { 57 | if (tagUnion[key] === type) { 58 | return 59 | } 60 | } 61 | 62 | invariant(false, `Type must be a type of the union`) 63 | } 64 | 65 | function safeUnion (types, options) { 66 | if (process.env.NODE_ENV !== 'production') { 67 | checkTypes(types) 68 | } 69 | 70 | const prefix = (options && options.prefix) || '' 71 | const prefixSize = prefix.length 72 | const stripPrefix = prefixSize 73 | ? tag => 74 | tag && 75 | tag.type && 76 | tag.type.startsWith(prefix) && 77 | tag.type.slice(prefixSize) 78 | : x => x && x.type 79 | 80 | const matcher = (handlers, catchAll) => { 81 | if (process.env.NODE_ENV !== 'production') { 82 | checkMatch(handlers, catchAll, types) 83 | } 84 | 85 | return function _matcher (tag, context) { 86 | const tagType = stripPrefix(tag) 87 | if (process.env.NODE_ENV !== 'production') { 88 | checkTag(tag, tagType, types) 89 | } 90 | 91 | const match = hasOwnProperty.call(handlers, tagType) && handlers[tagType] 92 | return match ? match(tag.data, context) : catchAll(context) 93 | } 94 | } 95 | 96 | const methods = { 97 | match (tag, handlers, catchAll) { 98 | return matcher(handlers, catchAll)(tag) 99 | }, 100 | matcher, 101 | matches (tag, type) { 102 | const tagType = stripPrefix(tag) 103 | if (process.env.NODE_ENV !== 'production') { 104 | checkTag(tag, tagType, types) 105 | checkType(type, variants) 106 | } 107 | 108 | return !!(typeof tagType === 'string' && variants[tagType] === type) 109 | } 110 | } 111 | 112 | const variants = Object.create(null) 113 | for (let i = 0; i < types.length; i++) { 114 | const type = types[i] 115 | const prefixedType = prefix + type 116 | variants[type] = data => ({ type: prefixedType, data }) 117 | } 118 | 119 | return { variants, methods } 120 | } 121 | 122 | function union (types, options) { 123 | const { variants, methods } = safeUnion(types, options) 124 | for (const key in methods) { 125 | if (process.env.NODE_ENV !== 'production') { 126 | invariant( 127 | !hasOwnProperty.call(variants, key), 128 | `Tag type cannot be "${key}"` 129 | ) 130 | } 131 | variants[key] = methods[key] 132 | } 133 | return variants 134 | } 135 | 136 | exports.union = union 137 | exports.safeUnion = safeUnion 138 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | import { union, safeUnion } from '../src/' 3 | 4 | test('union() should return an object with match method', t => { 5 | const Msg = union([]) 6 | t.is(typeof Msg.match, 'function') 7 | }) 8 | 9 | test('union() should return an object with [kind] constructors', t => { 10 | const Msg = union(['Foo']) 11 | t.is(typeof Msg.Foo, 'function') 12 | }) 13 | 14 | test('union() should throw if kinds if not an array', t => { 15 | t.throws(() => union(4), /must be an array/) 16 | }) 17 | 18 | test('union() should throw if kinds is not all strings', t => { 19 | t.throws(() => union(['A', 4]), /must be a string/) 20 | }) 21 | 22 | test('union() should throw if there are duplicate kinds', t => { 23 | t.throws(() => union(['A', 'A']), /must be unique/) 24 | }) 25 | 26 | test('union() should throw if there is a kind "match"', t => { 27 | t.throws(() => union(['match']), /cannot be "match"/) 28 | }) 29 | 30 | test('union() should throw if there is a kind "matcher"', t => { 31 | t.throws(() => union(['matcher']), /cannot be "matcher"/) 32 | }) 33 | 34 | test('union() should throw if there is a kind "matches"', t => { 35 | t.throws(() => union(['matches']), /cannot be "matches"/) 36 | }) 37 | 38 | test('union() prefix should be added to types', t => { 39 | const A = union(['Foo'], { prefix: 'a/' }) 40 | t.is(A.Foo().type, 'a/Foo') 41 | }) 42 | 43 | test('union() prefix should prevent name conflicts', t => { 44 | const A = union(['Foo'], { prefix: 'a/' }) 45 | const B = union(['Foo'], { prefix: 'b/' }) 46 | 47 | t.notDeepEqual(A.Foo(), B.Foo()) 48 | }) 49 | 50 | test('union() should not throw for Object.prototype type names', t => { 51 | const types = Object.getOwnPropertyNames(Object.prototype) 52 | t.notThrows(() => { 53 | union(types) 54 | }) 55 | }) 56 | 57 | test('union() should not have Object.prototype properties (unless specified)', t => { 58 | const props = Object.getOwnPropertyNames(Object.prototype) 59 | const A = union([]) 60 | props.forEach(prop => { 61 | t.is(A[prop], undefined, `Property ${prop} should be undefined`) 62 | }) 63 | }) 64 | 65 | test('match() should return the handler return value', t => { 66 | const Msg = union(['Foo']) 67 | const val = Msg.Foo() 68 | 69 | t.is(Msg.match(val, { Foo: () => 1 }), 1) 70 | }) 71 | 72 | test('match() should call catchAll if a handler is not found', t => { 73 | const Msg = union(['Foo', 'Bar']) 74 | const val = Msg.Bar() 75 | 76 | t.is( 77 | Msg.match( 78 | val, 79 | { 80 | Foo: () => 1 81 | }, 82 | () => 2 83 | ), 84 | 2 85 | ) 86 | }) 87 | 88 | test('match() should throw if tag if not of the union', t => { 89 | const A = union(['Foo']) 90 | const B = union(['Bar']) 91 | 92 | const tag = B.Bar() 93 | 94 | t.throws(() => { 95 | A.match(tag, {}, () => {}) 96 | }, /must be a tag of the union/) 97 | }) 98 | 99 | test('match() should throw if handler[kind] is not of the union', t => { 100 | const Msg = union(['Foo']) 101 | const val = Msg.Foo() 102 | 103 | t.throws(() => { 104 | Msg.match(val, { Bar: () => 1 }) 105 | }, /not a tag type of the union/) 106 | }) 107 | 108 | test('match() should throw if handler[kind] is not a function', t => { 109 | const Msg = union(['Foo']) 110 | const val = Msg.Foo() 111 | 112 | t.throws(() => { 113 | Msg.match(val, { Foo: 4 }) 114 | }, /must be a function/) 115 | }) 116 | 117 | test('match() should throw if a provided catchAll is not a function', t => { 118 | const Msg = union(['Foo', 'Bar']) 119 | const val = Msg.Foo() 120 | 121 | t.throws(() => { 122 | Msg.match(val, { Foo: () => {} }, 4) 123 | }, /must be a function/) 124 | }) 125 | 126 | test('match() should throw if a catch-all is needed', t => { 127 | const Msg = union(['Foo', 'Bar']) 128 | const val = Msg.Foo() 129 | 130 | t.throws(() => { 131 | Msg.match(val, { Foo: () => {} }) 132 | }, /add a catch-all/) 133 | }) 134 | 135 | test('match() should throw if a catch-all is not needed', t => { 136 | const Msg = union(['Foo']) 137 | const val = Msg.Foo() 138 | 139 | t.throws(() => { 140 | Msg.match(val, { Foo: () => {} }, () => {}) 141 | }, /remove unnecessary catch-all/) 142 | }) 143 | 144 | test('match() should not treat Object.prototype properties as handlers', t => { 145 | const types = Object.getOwnPropertyNames(Object.prototype) 146 | const A = union(types) 147 | 148 | A.match(A.constructor(1), {}, () => t.pass()) 149 | }) 150 | 151 | test('match() should work for empty string', t => { 152 | const A = union(['']) 153 | A.match(A[''](1), { '': () => t.pass() }) 154 | }) 155 | 156 | test('match() should work for empty string with prefix', t => { 157 | const A = union([''], { prefix: 'abc' }) 158 | A.match(A[''](1), { '': () => t.pass() }) 159 | }) 160 | 161 | test('matcher() should work', t => { 162 | const Msg = union(['Inc', 'Dec', 'Wut']) 163 | const update = Msg.matcher( 164 | { 165 | Inc: (num, memo) => memo + num, 166 | Dec: (num, memo) => memo - num 167 | }, 168 | memo => memo * memo 169 | ) 170 | 171 | t.is(update(Msg.Inc(5), 1), 6) 172 | t.is(update(Msg.Dec(2), 5), 3) 173 | t.is(update(Msg.Wut(0), 2), 4) 174 | }) 175 | 176 | test('matches() should return whether the tag matches type', t => { 177 | const Msg = union(['Foo', 'Bar']) 178 | t.true(Msg.matches(Msg.Foo(), Msg.Foo)) 179 | t.true(Msg.matches(Msg.Bar(), Msg.Bar)) 180 | 181 | t.false(Msg.matches(Msg.Foo(), Msg.Bar)) 182 | t.false(Msg.matches(Msg.Bar(), Msg.Foo)) 183 | }) 184 | 185 | test('matches() should throw if tag is not an object', t => { 186 | const A = union(['Foo']) 187 | 188 | t.throws(() => { 189 | A.matches(8, A.Foo) 190 | }, /must be an object/) 191 | }) 192 | 193 | test('matches() should throw if tag type is not a string', t => { 194 | const A = union(['Foo']) 195 | 196 | t.throws(() => { 197 | A.matches({ type: 8 }, A.Foo) 198 | }, /type must be a string/) 199 | }) 200 | 201 | test('matches() should throw if tag is not of the union', t => { 202 | const A = union(['Foo']) 203 | const B = union(['Bar']) 204 | 205 | t.throws(() => { 206 | A.matches(B.Bar(), A.Foo) 207 | }, /must be a tag of the union/) 208 | }) 209 | 210 | test('matches() should throw if type is not provided', t => { 211 | const A = union(['Foo']) 212 | 213 | t.throws(() => { 214 | A.matches(A.Foo()) 215 | }, /must be provided/) 216 | }) 217 | 218 | test('matches() should throw if type is not of the union', t => { 219 | const A = union(['Foo']) 220 | const B = union(['Bar']) 221 | 222 | t.throws(() => { 223 | A.matches(A.Foo(), B.Bar) 224 | }, /must be a type of the union/) 225 | }) 226 | 227 | test('matches() should work for empty string', t => { 228 | const A = union(['']) 229 | t.true(A.matches(A[''](1), A[''])) 230 | }) 231 | 232 | test('tags should be de/serialize-able', t => { 233 | const Msg = union(['Foo']) 234 | const tag = Msg.Foo('cake') 235 | const tagCopy = JSON.parse(JSON.stringify(tag)) 236 | 237 | t.true( 238 | Msg.match(tagCopy, { 239 | Foo: () => true 240 | }) 241 | ) 242 | 243 | t.true(Msg.matches(tagCopy, Msg.Foo)) 244 | }) 245 | 246 | test('tags should be de/serialize-able with prefixes', t => { 247 | const Msg = union(['Foo'], { prefix: 'a/' }) 248 | const tag = Msg.Foo('cake') 249 | const tagCopy = JSON.parse(JSON.stringify(tag)) 250 | 251 | t.true( 252 | Msg.match(tagCopy, { 253 | Foo: () => true 254 | }) 255 | ) 256 | 257 | t.true(Msg.matches(tagCopy, Msg.Foo)) 258 | }) 259 | 260 | test('safeUnion() allows "match", "matcher", and "matches" types', t => { 261 | const { variants, methods } = safeUnion([ 262 | 'Foo', 263 | 'match', 264 | 'matcher', 265 | 'matches' 266 | ]) 267 | 268 | t.true(methods.matches(variants.match(), variants.match)) 269 | t.true(methods.matches(variants.matcher(), variants.matcher)) 270 | t.true(methods.matches(variants.matches(), variants.matches)) 271 | 272 | t.is( 273 | methods.match(variants.Foo(), { 274 | Foo: () => 1, 275 | match: () => 2, 276 | matcher: () => 3, 277 | matches: () => 4 278 | }), 279 | 1 280 | ) 281 | }) 282 | --------------------------------------------------------------------------------