aboutsummaryrefslogtreecommitdiff
path: root/src/util
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2019-12-14 17:23:31 +0100
committerFlorian Dold <florian.dold@gmail.com>2019-12-14 17:23:31 +0100
commit749b96284ae0a7e6d03034806deab998a36b7cf6 (patch)
treee96b41a1cc4e6b963b0f3890ba6d97fc54137851 /src/util
parente018e073a4666c9521c0a802caa704d5ae5089b7 (diff)
codecs WIP
Diffstat (limited to 'src/util')
-rw-r--r--src/util/codec-test.ts36
-rw-r--r--src/util/codec.ts181
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, {}>();
+}