aboutsummaryrefslogtreecommitdiff
path: root/src/checkable.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/checkable.ts')
-rw-r--r--src/checkable.ts262
1 files changed, 262 insertions, 0 deletions
diff --git a/src/checkable.ts b/src/checkable.ts
new file mode 100644
index 000000000..89d0c7150
--- /dev/null
+++ b/src/checkable.ts
@@ -0,0 +1,262 @@
+/*
+ This file is part of TALER
+ (C) 2016 GNUnet e.V.
+
+ 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.
+
+ 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
+ TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+
+"use strict";
+
+/**
+ * Decorators for type-checking JSON into
+ * an object.
+ * @module Checkable
+ * @author Florian Dold
+ */
+
+export namespace Checkable {
+
+ type Path = (number | string)[];
+
+ interface SchemaErrorConstructor {
+ new (err: string): SchemaError;
+ }
+
+ interface SchemaError {
+ name: string;
+ message: string;
+ }
+
+ interface Prop {
+ propertyKey: any;
+ checker: any;
+ type: any;
+ elementChecker?: any;
+ elementProp?: any;
+ }
+
+ export let SchemaError = (function SchemaError(message: string) {
+ this.name = 'SchemaError';
+ this.message = message;
+ this.stack = (<any>new Error()).stack;
+ }) as any as SchemaErrorConstructor;
+
+
+ SchemaError.prototype = new Error;
+
+ let chkSym = Symbol("checkable");
+
+
+ function checkNumber(target: any, prop: Prop, path: Path): any {
+ if ((typeof target) !== "number") {
+ throw new SchemaError(`expected number for ${path}`);
+ }
+ return target;
+ }
+
+
+ function checkString(target: any, prop: Prop, path: Path): any {
+ if (typeof target !== "string") {
+ throw new SchemaError(`expected string for ${path}, got ${typeof target} instead`);
+ }
+ return target;
+ }
+
+
+ function checkAnyObject(target: any, prop: Prop, path: Path): any {
+ if (typeof target !== "object") {
+ throw new SchemaError(`expected (any) object for ${path}, got ${typeof target} instead`);
+ }
+ return target;
+ }
+
+
+ function checkAny(target: any, prop: Prop, path: Path): any {
+ return target;
+ }
+
+
+ function checkList(target: any, prop: Prop, path: Path): any {
+ if (!Array.isArray(target)) {
+ throw new SchemaError(`array expected for ${path}, got ${typeof target} instead`);
+ }
+ for (let i = 0; i < target.length; i++) {
+ let v = target[i];
+ prop.elementChecker(v, prop.elementProp, path.concat([i]));
+ }
+ return target;
+ }
+
+
+ function checkOptional(target: any, prop: Prop, path: Path): any {
+ console.assert(prop.propertyKey);
+ prop.elementChecker(target,
+ prop.elementProp,
+ path.concat([prop.propertyKey]));
+ return target;
+ }
+
+
+ function checkValue(target: any, prop: Prop, path: Path): any {
+ let type = prop.type;
+ if (!type) {
+ throw Error(`assertion failed (prop is ${JSON.stringify(prop)})`);
+ }
+ let v = target;
+ if (!v || typeof v !== "object") {
+ throw new SchemaError(
+ `expected object for ${path.join(".")}, got ${typeof v} instead`);
+ }
+ let props = type.prototype[chkSym].props;
+ let remainingPropNames = new Set(Object.getOwnPropertyNames(v));
+ let obj = new type();
+ for (let prop of props) {
+ if (!remainingPropNames.has(prop.propertyKey)) {
+ if (prop.optional) {
+ continue;
+ }
+ throw new SchemaError("Property missing: " + prop.propertyKey);
+ }
+ if (!remainingPropNames.delete(prop.propertyKey)) {
+ throw new SchemaError("assertion failed");
+ }
+ let propVal = v[prop.propertyKey];
+ obj[prop.propertyKey] = prop.checker(propVal,
+ prop,
+ path.concat([prop.propertyKey]));
+ }
+
+ if (remainingPropNames.size != 0) {
+ throw new SchemaError("superfluous properties " + JSON.stringify(Array.from(
+ remainingPropNames.values())));
+ }
+ return obj;
+ }
+
+
+ export function Class(target: any) {
+ target.checked = (v: any) => {
+ return checkValue(v, {
+ propertyKey: "(root)",
+ type: target,
+ checker: checkValue
+ }, ["(root)"]);
+ };
+ return target;
+ }
+
+
+ export function Value(type: any) {
+ if (!type) {
+ throw Error("Type does not exist yet (wrong order of definitions?)");
+ }
+ function deco(target: Object, propertyKey: string | symbol): void {
+ let chk = mkChk(target);
+ chk.props.push({
+ propertyKey: propertyKey,
+ checker: checkValue,
+ type: type
+ });
+ }
+
+ return deco;
+ }
+
+
+ export function List(type: any) {
+ let stub = {};
+ type(stub, "(list-element)");
+ let elementProp = mkChk(stub).props[0];
+ let elementChecker = elementProp.checker;
+ if (!elementChecker) {
+ throw Error("assertion failed");
+ }
+ function deco(target: Object, propertyKey: string | symbol): void {
+ let chk = mkChk(target);
+ chk.props.push({
+ elementChecker,
+ elementProp,
+ propertyKey: propertyKey,
+ checker: checkList,
+ });
+ }
+
+ return deco;
+ }
+
+
+ export function Optional(type: any) {
+ let stub = {};
+ type(stub, "(optional-element)");
+ let elementProp = mkChk(stub).props[0];
+ let elementChecker = elementProp.checker;
+ if (!elementChecker) {
+ throw Error("assertion failed");
+ }
+ function deco(target: Object, propertyKey: string | symbol): void {
+ let chk = mkChk(target);
+ chk.props.push({
+ elementChecker,
+ elementProp,
+ propertyKey: propertyKey,
+ checker: checkOptional,
+ optional: true,
+ });
+ }
+
+ return deco;
+ }
+
+
+ export function Number(target: Object, propertyKey: string | symbol): void {
+ let chk = mkChk(target);
+ chk.props.push({ propertyKey: propertyKey, checker: checkNumber });
+ }
+
+
+ export function AnyObject(target: Object,
+ propertyKey: string | symbol): void {
+ let chk = mkChk(target);
+ chk.props.push({
+ propertyKey: propertyKey,
+ checker: checkAnyObject
+ });
+ }
+
+
+ export function Any(target: Object,
+ propertyKey: string | symbol): void {
+ let chk = mkChk(target);
+ chk.props.push({
+ propertyKey: propertyKey,
+ checker: checkAny,
+ optional: true
+ });
+ }
+
+
+ export function String(target: Object, propertyKey: string | symbol): void {
+ let chk = mkChk(target);
+ chk.props.push({ propertyKey: propertyKey, checker: checkString });
+ }
+
+
+ function mkChk(target: any) {
+ let chk = target[chkSym];
+ if (!chk) {
+ chk = { props: [] };
+ target[chkSym] = chk;
+ }
+ return chk;
+ }
+} \ No newline at end of file