diff options
author | Florian Dold <florian.dold@gmail.com> | 2019-12-14 17:23:31 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2019-12-14 17:23:31 +0100 |
commit | 749b96284ae0a7e6d03034806deab998a36b7cf6 (patch) | |
tree | e96b41a1cc4e6b963b0f3890ba6d97fc54137851 /src/util | |
parent | e018e073a4666c9521c0a802caa704d5ae5089b7 (diff) |
codecs WIP
Diffstat (limited to 'src/util')
-rw-r--r-- | src/util/codec-test.ts | 36 | ||||
-rw-r--r-- | src/util/codec.ts | 181 |
2 files changed, 217 insertions, 0 deletions
diff --git a/src/util/codec-test.ts b/src/util/codec-test.ts new file mode 100644 index 000000000..d7edd545f --- /dev/null +++ b/src/util/codec-test.ts @@ -0,0 +1,36 @@ +/* + This file is part of GNU Taler + (C) 2018-2019 GNUnet e.V. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Type-safe codecs for converting from/to JSON. + */ + +import test from "ava"; +import { stringCodec, objectCodec } from "./codec"; + +interface MyObj { + foo: string; +} + +test("basic codec", (t) => { + const myObjCodec = objectCodec<MyObj>().property("foo", stringCodec).build("MyObj"); + const res = myObjCodec.decode({ foo: "hello" }); + t.assert(res.foo === "hello"); + + t.throws(() => { + const res2 = myObjCodec.decode({ foo: 123 }); + }); +}); diff --git a/src/util/codec.ts b/src/util/codec.ts new file mode 100644 index 000000000..690486b7d --- /dev/null +++ b/src/util/codec.ts @@ -0,0 +1,181 @@ +/* + This file is part of GNU Taler + (C) 2018-2019 GNUnet e.V. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Type-safe codecs for converting from/to JSON. + */ + +/** + * Error thrown when decoding fails. + */ +export class DecodingError extends Error { + constructor(message: string) { + super(message); + Object.setPrototypeOf(this, DecodingError.prototype); + this.name = "DecodingError"; + } +} + +/** + * Context information to show nicer error messages when decoding fails. + */ +interface Context { + readonly path?: string[]; +} + +function renderContext(c?: Context): string { + const p = c?.path; + if (p) { + return p.join("."); + } else { + return "(unknown)"; + } +} + +function joinContext(c: Context | undefined, part: string): Context { + const path = c?.path ?? []; + return { + path: path.concat([part]), + }; +} + +/** + * A codec converts untyped JSON to a typed object. + */ +export interface Codec<V> { + /** + * Decode untyped JSON to an object of type [[V]]. + */ + readonly decode: (x: any, c?: Context) => V; +} + +type SingletonRecord<K extends keyof any, V> = { [Y in K]: V }; + +interface Prop { + name: string; + codec: Codec<any>; +} + +class ObjectCodecBuilder<T, TC> { + private propList: Prop[] = []; + + /** + * Define a property for the object. + */ + property<K extends keyof T & string, V>( + x: K, + codec: Codec<V>, + ): ObjectCodecBuilder<T, TC & SingletonRecord<K, V>> { + this.propList.push({ name: x, codec: codec }); + 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<TC> { + const propList = this.propList; + return { + decode(x: any, c?: Context): TC { + if (!c) { + c = { + path: [`(${objectDisplayName})`], + }; + } + const obj: any = {}; + for (const prop of propList) { + const propRawVal = x[prop.name]; + const propVal = prop.codec.decode( + propRawVal, + joinContext(c, prop.name), + ); + obj[prop.name] = propVal; + } + return obj as TC; + }, + }; + } +} + +/** + * Return a codec for a value that must be a string. + */ +export const stringCodec: Codec<string> = { + decode(x: any, c?: Context): string { + if (typeof x === "string") { + return x; + } + throw new DecodingError(`expected string at ${renderContext(c)}`); + }, +}; + +/** + * Return a codec for a value that must be a number. + */ +export const numberCodec: Codec<number> = { + decode(x: any, c?: Context): number { + if (typeof x === "number") { + return x; + } + throw new DecodingError(`expected number at ${renderContext(c)}`); + }, +}; + +/** + * Return a codec for a list, containing values described by the inner codec. + */ +export function listCodec<T>(innerCodec: Codec<T>): Codec<T[]> { + return { + decode(x: any, c?: Context): T[] { + const arr: T[] = []; + if (!Array.isArray(x)) { + throw new DecodingError(`expected array at ${renderContext(c)}`); + } + for (const i in x) { + arr.push(innerCodec.decode(x[i], joinContext(c, `[${i}]`))); + } + return arr; + }, + }; +} + +/** + * Return a codec for a mapping from a string to values described by the inner codec. + */ +export function mapCodec<T>(innerCodec: Codec<T>): Codec<{ [x: string]: T }> { + return { + decode(x: any, c?: Context): { [x: string]: T } { + const map: { [x: string]: T } = {}; + if (typeof x !== "object") { + throw new DecodingError(`expected object at ${renderContext(c)}`); + } + for (const i in x) { + map[i] = innerCodec.decode(x[i], joinContext(c, `[${i}]`)); + } + return map; + }, + }; +} + +/** + * Return a builder for a codec that decodes an object with properties. + */ +export function objectCodec<T>(): ObjectCodecBuilder<T, {}> { + return new ObjectCodecBuilder<T, {}>(); +} |