combo: add `tuple` combinator (#1825)
Summary: This new combinator parses heterogeneous tuples: arrays of fixed length, which may have different, fixed types at each index. For example: ```javascript type CompactDistribution = [NodeAddressT, number][]; function compactDistributionParser(): C.Parser<CompactDistribution> { return C.array( C.tuple([ C.fmap(C.array(C.string), NodeAddress.fromParts), C.number, ]) ); } ``` Or: ```javascript function compatParser<T>(underlying: C.Parser<T>): C.Parser<Compatible<T>> { return C.tuple([ C.object({type: C.string, version: C.string}), underlying, ]); } ``` Test Plan: Unit tests included, retaining full coverage. wchargin-branch: combo-tuple
This commit is contained in:
parent
64169da128
commit
205f6e064c
|
@ -41,6 +41,7 @@ export class Parser<+T> {
|
|||
// Helper type to extract the underlying type of a parser: for instance,
|
||||
// `ParserOutput<Parser<string>>` is just `string`.
|
||||
export type ParserOutput<P: Parser<mixed>> = $PropertyType<P, "_phantomT">;
|
||||
type ExtractParserOutput = <P: Parser<mixed>>(P) => ParserOutput<P>;
|
||||
|
||||
// Helper to make a successful parse result. For readability.
|
||||
function success<T>(t: T): ParseResult<T> {
|
||||
|
@ -281,3 +282,35 @@ export const object: PObject = (function object(
|
|||
export const shape: PObjectShape = function shape(fields) {
|
||||
return object({}, fields);
|
||||
};
|
||||
|
||||
// Create a parser for a tuple: a fixed-length array with possibly
|
||||
// heterogeneous element types. For instance,
|
||||
//
|
||||
// C.tuple([C.string, C.number, C.boolean])
|
||||
//
|
||||
// is a parser that accepts length-3 arrays whose first element is a
|
||||
// string, second element is a number, and third element is a boolean.
|
||||
export function tuple<T: Iterable<Parser<mixed>>>(
|
||||
parsers: T
|
||||
): Parser<$TupleMap<T, ExtractParserOutput>> {
|
||||
const ps = Array.from(parsers);
|
||||
return new Parser((x) => {
|
||||
if (!Array.isArray(x)) {
|
||||
return failure("expected array, got " + typename(x));
|
||||
}
|
||||
if (x.length !== ps.length) {
|
||||
return failure(`expected array of length ${ps.length}, got ${x.length}`);
|
||||
}
|
||||
const result = Array(ps.length);
|
||||
for (let i = 0; i < result.length; i++) {
|
||||
const raw = x[i];
|
||||
const parser = ps[i];
|
||||
const parsed = parser.parse(raw);
|
||||
if (!parsed.ok) {
|
||||
return failure(`index ${i}: ${parsed.err}`);
|
||||
}
|
||||
result[i] = parsed.value;
|
||||
}
|
||||
return success(result);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -396,4 +396,47 @@ describe("src/util/combo", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("tuple", () => {
|
||||
describe("for an empty tuple type", () => {
|
||||
const makeParser = (): C.Parser<[]> => C.tuple([]);
|
||||
it("accepts an empty array", () => {
|
||||
const p: C.Parser<[]> = makeParser();
|
||||
expect(p.parseOrThrow([])).toEqual([]);
|
||||
});
|
||||
it("rejects a non-empty array", () => {
|
||||
const p: C.Parser<[]> = makeParser();
|
||||
const thunk = () => p.parseOrThrow([1, 2, 3]);
|
||||
expect(thunk).toThrow("expected array of length 0, got 3");
|
||||
});
|
||||
});
|
||||
describe("for a heterogeneous tuple type", () => {
|
||||
it("is typesafe", () => {
|
||||
(C.tuple([C.string, C.number]): C.Parser<[string, number]>);
|
||||
// $ExpectFlowError
|
||||
(C.tuple([C.string, C.number]): C.Parser<[string, string]>);
|
||||
});
|
||||
const makeParser = (): C.Parser<[string, number]> =>
|
||||
C.tuple([C.fmap(C.string, (s) => s + "!"), C.number]);
|
||||
it("rejects a non-array", () => {
|
||||
const p: C.Parser<[string, number]> = makeParser();
|
||||
const thunk = () => p.parseOrThrow({hmm: "hum"});
|
||||
expect(thunk).toThrow("expected array, got object");
|
||||
});
|
||||
it("rejects an empty array", () => {
|
||||
const p: C.Parser<[string, number]> = makeParser();
|
||||
const thunk = () => p.parseOrThrow([]);
|
||||
expect(thunk).toThrow("expected array of length 2, got 0");
|
||||
});
|
||||
it("rejects an array of proper length but bad values", () => {
|
||||
const p: C.Parser<[string, number]> = makeParser();
|
||||
const thunk = () => p.parseOrThrow(["one", "two"]);
|
||||
expect(thunk).toThrow("index 1: expected number, got string");
|
||||
});
|
||||
it("accepts a properly typed input", () => {
|
||||
const p: C.Parser<[string, number]> = makeParser();
|
||||
expect(p.parseOrThrow(["one", 23])).toEqual(["one!", 23]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue