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:
William Chargin 2020-05-31 22:17:54 -07:00 committed by GitHub
parent 64169da128
commit 205f6e064c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 76 additions and 0 deletions

View File

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

View File

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