combo: add `dict` combinator (#1826)

Summary:
This is for homogeneous object types with unbounded key sets: roughly,
`dict` is to `object` as `array` is to `tuple`. Its implementation
requires no terrifying type magic whatsoever.

Test Plan:
Unit tests included, retaining full coverage.

wchargin-branch: combo-dict
This commit is contained in:
William Chargin 2020-05-31 22:21:10 -07:00 committed by GitHub
parent 205f6e064c
commit 500140d292
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 60 additions and 0 deletions

View File

@ -314,3 +314,31 @@ export function tuple<T: Iterable<Parser<mixed>>>(
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<V>(valueParser: Parser<V>): 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);
});
}

View File

@ -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');
});
});
});