summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--gulpfile.js1
-rw-r--r--src/util/codec-test.ts36
-rw-r--r--src/util/codec.ts181
-rw-r--r--tsconfig.json3
4 files changed, 221 insertions, 0 deletions
diff --git a/gulpfile.js b/gulpfile.js
index dbdb33cc0..15f6ff10f 100644
--- a/gulpfile.js
+++ b/gulpfile.js
@@ -99,6 +99,7 @@ const tsBaseArgs = {
strictPropertyInitialization: false,
outDir: "dist/node",
noImplicitAny: true,
+ noImplicitThis: true,
allowJs: true,
checkJs: true,
incremental: true,
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, {}>();
+}
diff --git a/tsconfig.json b/tsconfig.json
index 8d696591c..cb2985aeb 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -17,6 +17,7 @@
"strictPropertyInitialization": false,
"outDir": "dist/node",
"noImplicitAny": true,
+ "noImplicitThis": true,
"allowJs": true,
"checkJs": true,
"incremental": true,
@@ -69,6 +70,8 @@
"src/util/assertUnreachable.ts",
"src/util/asyncMemo.ts",
"src/util/checkable.ts",
+ "src/util/codec-test.ts",
+ "src/util/codec.ts",
"src/util/helpers-test.ts",
"src/util/helpers.ts",
"src/util/http.ts",