diff --git a/src/util/combo.js b/src/util/combo.js index 1b5349d..dadc479 100644 --- a/src/util/combo.js +++ b/src/util/combo.js @@ -314,3 +314,31 @@ export function tuple>>( return success(result); }); } + +// Create a parser for objects with arbitrary string keys and +// homogeneous values. For instance, a set of package versions: +// +// {"better-sqlite3": "^7.0.0", "react": "^16.13.0"} +// +// might be parsed by the following parser: +// +// C.dict(C.fmap(C.string, (s) => SemVer.parse(s))) +// +// Objects may have any number of entries, including zero. +export function dict(valueParser: Parser): Parser<{|[string]: V|}> { + return new Parser((x) => { + if (typeof x !== "object" || Array.isArray(x) || x == null) { + return failure("expected object, got " + typename(x)); + } + const result: {|[string]: V|} = ({}: any); + for (const key of Object.keys(x)) { + const raw = x[key]; + const parsed = valueParser.parse(raw); + if (!parsed.ok) { + return failure(`key ${JSON.stringify(key)}: ${parsed.err}`); + } + result[key] = parsed.value; + } + return success(result); + }); +} diff --git a/src/util/combo.test.js b/src/util/combo.test.js index b9895bc..8309ef9 100644 --- a/src/util/combo.test.js +++ b/src/util/combo.test.js @@ -439,4 +439,36 @@ describe("src/util/combo", () => { }); }); }); + + describe("dict", () => { + const makeParser = (): C.Parser<{|[string]: number|}> => C.dict(C.number); + it("rejects null", () => { + const p = makeParser(); + const thunk = () => p.parseOrThrow(null); + expect(thunk).toThrow("expected object, got null"); + }); + it("rejects arrays", () => { + const p = makeParser(); + const thunk = () => p.parseOrThrow([1, 2, 3]); + expect(thunk).toThrow("expected object, got array"); + }); + it("accepts an empty object", () => { + const p = makeParser(); + expect(p.parseOrThrow({})).toEqual({}); + }); + it("accepts an object with one entries", () => { + const p = makeParser(); + expect(p.parseOrThrow({one: 1})).toEqual({one: 1}); + }); + it("accepts an object with multiple entries", () => { + const p = makeParser(); + const input = {one: 1, two: 2, three: 3}; + expect(p.parseOrThrow(input)).toEqual({one: 1, two: 2, three: 3}); + }); + it("rejects an object with bad values", () => { + const p = makeParser(); + const thunk = () => p.parseOrThrow({one: "two?"}); + expect(thunk).toThrow('key "one": expected number, got string'); + }); + }); });