combo: add `exactly` combinator for fixed values (#1829)
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
This commit is contained in:
parent
ee0beaf1f1
commit
ae181c2fda
|
@ -114,6 +114,35 @@ export function pure<T>(t: T): Parser<T> {
|
|||
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<Environment> = 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<T: string | number | boolean | null>(
|
||||
ts: $ReadOnlyArray<T>
|
||||
): Parser<T> {
|
||||
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<number>` and `f = (n: number) => n % 2 === 0`, then
|
||||
// `fmap(p, f)` is a `Parser<boolean>` that first uses `p` to parse its
|
||||
|
|
|
@ -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<empty>);
|
||||
// $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<empty>);
|
||||
});
|
||||
it("accepts any matching value", () => {
|
||||
type Color = "RED" | "GREEN" | "BLUE";
|
||||
const p: C.Parser<Color> = 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<Color> = 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<Consent> = 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<empty> = 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 {
|
||||
|
|
Loading…
Reference in New Issue