From ae181c2fda8719546d90acbaa0ab60d417473a2c Mon Sep 17 00:00:00 2001 From: William Chargin Date: Sun, 31 May 2020 22:33:27 -0700 Subject: [PATCH] combo: add `exactly` combinator for fixed values (#1829) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Write `C.exactly(["red", "green", "blue"])` to admit any of a fixed set of primitive values. This can already be implemented in terms of `raw` and `fmap`, but it’s more convenient to have a dedicated helper. Test Plan: Unit tests included, retaining full coverage. wchargin-branch: combo-exactly --- src/util/combo.js | 29 +++++++++++++++++++++++++++++ src/util/combo.test.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) diff --git a/src/util/combo.js b/src/util/combo.js index 6e10ea2..dad67f4 100644 --- a/src/util/combo.js +++ b/src/util/combo.js @@ -114,6 +114,35 @@ export function pure(t: T): Parser { return new Parser((_) => success(t)); } +// Create a parser that accepts any value from a fixed set. This can be +// used for enumerated values in configs: +// +// type Environment = "dev" | "prod"; +// const p: C.Parser = C.exactly(["dev", "prod"]); +// +// This function only supports value types. Performing an `any`-cast +// guarded by a deep equality check would be unsound, breaking opaque +// type boundaries: e.g., a module could `export opaque type T = {}` and +// provide two constants `ONE = {}` and `TWO = {}` (different objects), +// and then expect that any value of type `T` would be identical to +// either `ONE` or `TWO`. Using strict reference equality for array and +// object types would be sound, but would not usually be what was +// wanted, as it wouldn't match ~any actual output of `JSON.parse`. +export function exactly( + ts: $ReadOnlyArray +): Parser { + return new Parser((x) => { + for (const t of ts) { + if (x === t) { + return success(t); + } + } + const expected: string = + ts.length === 1 ? String(ts[0]) : `one of ${JSON.stringify(ts)}`; + return failure(`expected ${expected}, got ${typename(x)}`); + }); +} + // Transform the output of a parser with a pure function. For instance, // if `p: Parser` and `f = (n: number) => n % 2 === 0`, then // `fmap(p, f)` is a `Parser` that first uses `p` to parse its diff --git a/src/util/combo.test.js b/src/util/combo.test.js index a3a5a47..2b88389 100644 --- a/src/util/combo.test.js +++ b/src/util/combo.test.js @@ -106,6 +106,48 @@ describe("src/util/combo", () => { }); }); + describe("exactly", () => { + it("is type-safe", () => { + (C.exactly([1, 2, 3]): C.Parser<1 | 2 | 3>); + (C.exactly(["one", 2]): C.Parser<"one" | 2>); + (C.exactly([]): C.Parser); + // $ExpectFlowError + (C.exactly([1, 2, 3]): C.Parser<1 | 2>); + // $ExpectFlowError + (C.exactly(["one", 2]): C.Parser<1 | "two">); + // $ExpectFlowError + (C.exactly([false]): C.Parser); + }); + it("accepts any matching value", () => { + type Color = "RED" | "GREEN" | "BLUE"; + const p: C.Parser = C.exactly(["RED", "GREEN", "BLUE"]); + expect([p.parse("RED"), p.parse("GREEN"), p.parse("BLUE")]).toEqual([ + {ok: true, value: "RED"}, + {ok: true, value: "GREEN"}, + {ok: true, value: "BLUE"}, + ]); + }); + it("rejects a non-matching value among multiple alternatives", () => { + type Color = "RED" | "GREEN" | "BLUE"; + const p: C.Parser = C.exactly(["RED", "GREEN", "BLUE"]); + const thunk = () => p.parseOrThrow("YELLOW"); + expect(thunk).toThrow( + 'expected one of ["RED","GREEN","BLUE"], got string' + ); + }); + it("rejects a non-matching value from just one option", () => { + type Consent = {|+acceptedEula: true|}; + const p: C.Parser = C.object({acceptedEula: C.exactly([true])}); + const thunk = () => p.parseOrThrow({acceptedEula: false}); + expect(thunk).toThrow("expected true, got boolean"); + }); + it("rejects a non-matching value from no options", () => { + const p: C.Parser = C.exactly([]); + const thunk = () => p.parseOrThrow("wat"); + expect(thunk).toThrow("expected one of [], got string"); + }); + }); + describe("fmap", () => { type Color = "RED" | "GREEN" | "BLUE"; function stringToColor(s: string): Color {