From 767d84908f1558002750acecccd2c17e161caa5e Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Mon, 23 Mar 2020 02:15:34 +0100 Subject: [PATCH] Initial commit --- README.md | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++ example.js | 14 ++++++ index.js | 30 ++++++++++++ package.json | 10 ++++ 4 files changed, 184 insertions(+) create mode 100644 README.md create mode 100644 example.js create mode 100644 index.js create mode 100644 package.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..db1781c --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# match-value + +Since JS as a language does not (yet) have a match statement or expression, this package implements the same concept for the most common cases. + +You specify a mapping of source values to result values, as an object, and `match-value` maps from one to the other, throwing an error if an invalid source value is specified, optionally having a catch-all for unknown values, and with support for lazily-produced result values. + +In other words, it turns this: + +```js +let mode; + +if (flag === "r") { + mode = "readOnly"; +} else if (flag === "rw") { + mode = "readWrite"; +} else if (flag === "a") { + mode = "appendOnly"; +} else { + mode = `unknown:${someExpensiveCall(flag)}`; +} +``` + +... or this: + +```js +let mode; + +switch (flag) { + case "r": + mode = "readOnly"; + break; + case "rw": + mode = "readWrite"; + break; + case "a": + mode = "appendOnly"; + break; + default: + mode = `unknown:${someExpensiveCall(flag)}`; + break; +} +``` + +... into this: + +```js +const matchValue = require("match-value"); + +let mode = matchValue(flag, { + r: "readOnly", + rw: "readWrite", + a: "appendOnly", + _: () => `unknown:${someExpensiveCall(flag)}` +}); +``` + +Less repetition, and a much clearer intention. + +## Limitations + +- Source values may only be strings. Any input that isn't a string will always throw an error (or hit the catch-all, if you've specified one). + - This is unfortunately to do with a limitation of JS as a language, that makes it impossible to ergonomically specify mappings where the source value isn't a string key. If JS ever gets Map literals, this limitation can be fixed. +- Source values may only be literal values. There's no type/shape matching, like you might be used to if you've used a `match` construct in another language. + - Again, this is due to a language limitation. +- This library is completely unaware of the existence of Promises or asynchronous functions; all results are expected to be produced *synchronously*. + - Of course, it is completely valid to return a Promise as your result value, and structure your code like `Promise.resolve(matchValue(input, { ... })` to consistently get a Promise out of it. + +## License, donations, and other boilerplate + +Licensed under either the [WTFPL](http://www.wtfpl.net/txt/copying/) or [CC0](https://creativecommons.org/publicdomain/zero/1.0/), at your choice. In practice, that means it's more or less public domain, and you can do whatever you want with it. Giving credit is *not* required, but still very much appreciated! I'd love to [hear from you](mailto:admin@cryto.net) if this module was useful to you. + +Creating and maintaining open-source modules is a lot of work. A donation is also not required, but much appreciated! You can donate [here](http://cryto.net/~joepie91/donate.html). + +## Example + +A full version of the example above: + +```js +"use strict"; + +const matchValue = require("match-value"); + +let flag = "a"; // Hardcoded for example + +let mode = matchValue(flag, { + r: "readOnly", + rw: "readWrite", + a: "appendOnly", + _: () => `unknown:${someExpensiveCall(flag)}` +}); + +console.log(mode); // appendOnly +``` + +## API + +### matchValue(input, mapping) + +Converts the `input` to a result based on the `mapping`. + +- __input:__ The value to convert. +- __mapping:__ An object that describes the mapping in the format `{ possibleInput: result }`. + - A `possibleInput` of `_` defines a "catch-all" case; anything that doesn't match any of the other possible inputs, will trigger the catch-all case. + - The `result` may be either a literal value, or a function that *produces* that value. If it's a function, the function will *only* be called when there is a match, and the return value of that call will be returned from the `matchValue` call. If you want to *return* functions rather than call them, see the `.literal` method below. + +Returns a result if a match was found, or the catch-all was hit. Throws an `Error` if no match was found *and* no catch-all was specified. + +To throw a custom error type instead, define a catch-all function that throws that error, like so: + +```js +matchValue(input, { + someKey: "someResult", + someOtherKey: "someOtherResult", + _: () => { throw new CustomError("Oh no!"); } +}); +``` + +Note that if you're using an arrow function here, it *must* have curly braces around the `throw`; since a `throw` in JS is a statement, not an expression, it may not appear in a brace-less arrow function (which only accepts expressions). + +### matchValue.literal(input, mapping) + +Exactly like `matchValue` above, but if the result value is a function, it will be *returned as-is*, rather than being called; that is, the function *is* the result. + +Note that this also means that a custom error-throwing handler is not possible in this case. + +## Changelog + +### v1.0.0 (March 23, 2020) + +Initial release. diff --git a/example.js b/example.js new file mode 100644 index 0000000..3cd874a --- /dev/null +++ b/example.js @@ -0,0 +1,14 @@ +"use strict"; + +const matchValue = require("./"); + +let flag = "a"; // Hardcoded for example + +let mode = matchValue(flag, { + r: "readOnly", + rw: "readWrite", + a: "appendOnly", + _: () => `unknown:${someExpensiveCall(flag)}` +}); + +console.log(mode); // appendOnly diff --git a/index.js b/index.js new file mode 100644 index 0000000..29308ff --- /dev/null +++ b/index.js @@ -0,0 +1,30 @@ +"use strict"; + +function getValue(value, functionsAreLiterals) { + if (typeof value === "function" && !functionsAreLiterals) { + return value(); + } else { + return value; + } +} + +function doMatchValue(value, arms, functionsAreLiterals) { + if (value == null) { + return value; + } else if (arms[value] !== undefined) { + // NOTE: We intentionally only check for `undefined` here (and below), since we want to allow the mapped-to value to be an explicit `null`. + return getValue(arms[value], functionsAreLiterals); + } else if (arms._ !== undefined) { + return getValue(arms._, functionsAreLiterals); + } else { + throw new Error(`No match arm found for value '${value}'`); + } +} + +module.exports = function matchValue(value, arms) { + return doMatchValue(value, arms, true); +}; + +module.exports.literal = function matchValueLiteral(value, arms) { + return doMatchValue(value, arms, false); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..c2061ff --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "match-value", + "description": "Utility for mapping values to other values or behaviour, a bit like a match statement", + "keywords": ["match", "switch", "case", "map", "mapping"], + "version": "1.0.0", + "main": "index.js", + "repository": "http://git.cryto.net/joepie91/match-value.git", + "author": "Sven Slootweg ", + "license": "WTFPL OR CC0-1.0" +}