add support for parsing compatible objects (#1867)

This adds Combo parsing support to the compatible module. Now, rather
than writing `fromJSON` methods which implicitly take any, we can
instead write typesafe `parseJSON` methods which will parse compatible
headers, and then choose a version-appropriate parser.

Test plan: Added unit tests; `yarn test` passes.
This commit is contained in:
Dandelion Mané 2020-06-17 20:36:38 -07:00 committed by GitHub
parent 67b74d7cfe
commit a8f353b33f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 81 additions and 1 deletions

View File

@ -1,5 +1,6 @@
// @flow
import * as C from "./combo";
export opaque type Compatible<T> = [CompatInfo, T];
type CompatInfo = {|
+type: string,
@ -43,3 +44,34 @@ export function fromCompat<T>(
}
return result;
}
const headerParser = C.object({type: C.string, version: C.string});
const wrappedParser = C.tuple([headerParser, C.raw]);
export function compatibleParser<T>(
expectedType: string,
handlers: {+[version: string]: C.Parser<T>}
): C.Parser<T> {
return new C.Parser((x) => {
const wrapResult = wrappedParser.parse(x);
if (!wrapResult.ok) {
return {ok: false, err: `unable to unwrap compatible: ${wrapResult.err}`};
}
const [{type, version}, raw] = wrapResult.value;
if (type !== expectedType) {
return {
ok: false,
err: `expected type "${expectedType}" but got "${type}"`,
};
}
if (!Object.prototype.hasOwnProperty.call(handlers, version)) {
return {ok: false, err: `no "${type}/${version}" handler`};
}
const parseResult = handlers[version].parse(raw);
if (parseResult.ok) {
return parseResult;
} else {
return {ok: false, err: `${type}/${version}: ${parseResult.err}`};
}
});
}

View File

@ -1,6 +1,7 @@
// @flow
import {toCompat, fromCompat} from "./compat";
import * as C from "./combo";
import {toCompat, fromCompat, compatibleParser} from "./compat";
import type {Compatible} from "./compat";
describe("util/compat", () => {
@ -72,6 +73,53 @@ describe("util/compat", () => {
});
});
describe("compatibleParser", () => {
function parseCompatible(x, type, handlers) {
return compatibleParser(type, handlers).parseOrThrow(x);
}
it("throws on json missing a compatible header", () => {
expect(() => parseCompatible("foo", "unused", {})).toThrow(
"unable to unwrap compatible"
);
});
it("throws on json with an invalid compatible header", () => {
expect(() =>
parseCompatible([{type: "foo", version: 3}, {}], "unused", {})
).toThrow("unable to unwrap compatible");
});
it("throws on json with the wrong type", () => {
expect(() =>
parseCompatible(
(toCompat({type: "foo", version: "1.0.0"}, {}): any),
"bar",
{}
)
).toThrow(`expected type "bar" but got "foo"`);
});
it("throws on json with no matching version handler", () => {
expect(() =>
parseCompatible(
(toCompat({type: "foo", version: "1.0.0"}, {}): any),
"foo",
{}
)
).toThrow(`no "foo/1.0.0" handler`);
});
it("uses the correct version handler", () => {
const object = {v1: 5, v2: 6};
const parseV1 = C.object({v1: C.number});
const parseV2 = C.object({v2: C.number});
const parsers = {v1: parseV1, v2: parseV2};
const compatInfo1 = {type: "foo", version: "v1"};
const compatInfo2 = {type: "foo", version: "v2"};
const compatV1 = toCompat(compatInfo1, object);
const compatV2 = toCompat(compatInfo2, object);
const parse = (x) => parseCompatible((x: any), "foo", parsers);
expect(parse(compatV1)).toEqual({v1: 5});
expect(parse(compatV2)).toEqual({v2: 6});
});
});
describe("composable versioning", () => {
class InnerV1 {
x: number;