From 60d154c36bbd6773bbed44da82b17f211604c4b4 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Sat, 14 Dec 2019 17:55:31 +0100 Subject: union codecs, error messages --- src/util/codec-test.ts | 47 +++++++++++++++++++++++++++++--- src/util/codec.ts | 72 +++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 113 insertions(+), 6 deletions(-) (limited to 'src/util') diff --git a/src/util/codec-test.ts b/src/util/codec-test.ts index d7edd545f..0d1ab5603 100644 --- a/src/util/codec-test.ts +++ b/src/util/codec-test.ts @@ -19,14 +19,34 @@ */ import test from "ava"; -import { stringCodec, objectCodec } from "./codec"; +import { + stringCodec, + objectCodec, + unionCodec, + Codec, + stringConstCodec, +} from "./codec"; interface MyObj { foo: string; } -test("basic codec", (t) => { - const myObjCodec = objectCodec().property("foo", stringCodec).build("MyObj"); +interface AltOne { + type: "one"; + foo: string; +} + +interface AltTwo { + type: "two"; + bar: string; +} + +type MyUnion = AltOne | AltTwo; + +test("basic codec", t => { + const myObjCodec = objectCodec() + .property("foo", stringCodec) + .build("MyObj"); const res = myObjCodec.decode({ foo: "hello" }); t.assert(res.foo === "hello"); @@ -34,3 +54,24 @@ test("basic codec", (t) => { const res2 = myObjCodec.decode({ foo: 123 }); }); }); + +test("union", t => { + const altOneCodec: Codec = objectCodec() + .property("type", stringConstCodec("one")) + .property("foo", stringCodec) + .build("AltOne"); + const altTwoCodec: Codec = objectCodec() + .property("type", stringConstCodec("two")) + .property("bar", stringCodec) + .build("AltTwo"); + const myUnionCodec: Codec = unionCodec("type") + .alternative("one", altOneCodec) + .alternative("two", altTwoCodec) + .build("MyUnion"); + + const res = myUnionCodec.decode({ type: "one", foo: "bla" }); + t.is(res.type, "one"); + if (res.type == "one") { + t.is(res.foo, "bla"); + } +}); diff --git a/src/util/codec.ts b/src/util/codec.ts index 690486b7d..78516183c 100644 --- a/src/util/codec.ts +++ b/src/util/codec.ts @@ -69,6 +69,11 @@ interface Prop { codec: Codec; } +interface Alternative { + tagValue: any; + codec: Codec; +} + class ObjectCodecBuilder { private propList: Prop[] = []; @@ -89,10 +94,10 @@ class ObjectCodecBuilder { * @param objectDisplayName name of the object that this codec operates on, * used in error messages. */ - build(objectDisplayName: string): Codec { + build(objectDisplayName: string): Codec { const propList = this.propList; return { - decode(x: any, c?: Context): TC { + decode(x: any, c?: Context): R { if (!c) { c = { path: [`(${objectDisplayName})`], @@ -107,12 +112,53 @@ class ObjectCodecBuilder { ); obj[prop.name] = propVal; } - return obj as TC; + return obj as R; }, }; } } +class UnionCodecBuilder { + private alternatives = new Map(); + + constructor(private discriminator: D) {} + + /** + * Define a property for the object. + */ + alternative( + tagValue: T[D], + codec: Codec, + ): UnionCodecBuilder { + this.alternatives.set(tagValue, { codec, tagValue }); + return this as any; + } + + /** + * Return the built codec. + * + * @param objectDisplayName name of the object that this codec operates on, + * used in error messages. + */ + build(objectDisplayName: string): Codec { + const alternatives = this.alternatives; + const discriminator = this.discriminator; + return { + decode(x: any, c?: Context): R { + const d = x[discriminator]; + if (d === undefined) { + throw new DecodingError(`expected tag for ${objectDisplayName} at ${renderContext(c)}.${discriminator}`); + } + const alt = alternatives.get(d); + if (!alt) { + throw new DecodingError(`unknown tag for ${objectDisplayName} ${d} at ${renderContext(c)}.${discriminator}`); + } + return alt.codec.decode(x); + } + }; + } +} + /** * Return a codec for a value that must be a string. */ @@ -125,6 +171,20 @@ export const stringCodec: Codec = { }, }; +/** + * Return a codec for a value that must be a string. + */ +export function stringConstCodec(s: V): Codec { + return { + decode(x: any, c?: Context): V { + if (x === s) { + return x; + } + throw new DecodingError(`expected string constant "${s}" at ${renderContext(c)}`); + } + } +}; + /** * Return a codec for a value that must be a number. */ @@ -179,3 +239,9 @@ export function mapCodec(innerCodec: Codec): Codec<{ [x: string]: T }> { export function objectCodec(): ObjectCodecBuilder { return new ObjectCodecBuilder(); } + +export function unionCodec( + discriminator: D, +): UnionCodecBuilder { + return new UnionCodecBuilder(discriminator); +} -- cgit v1.2.3