wallet-core/src/checkable.ts

282 lines
7.0 KiB
TypeScript
Raw Normal View History

2016-02-09 21:56:06 +01:00
/*
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
2016-07-07 17:59:29 +02:00
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
2016-02-09 21:56:06 +01:00
*/
"use strict";
/**
* Decorators for type-checking JSON into
* an object.
* @module Checkable
* @author Florian Dold
*/
export namespace Checkable {
2016-09-12 20:25:56 +02:00
2016-10-11 22:58:40 +02:00
type Path = (number | string)[];
2016-09-12 20:25:56 +02:00
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;
2016-09-12 20:25:56 +02:00
}) as any as SchemaErrorConstructor;
SchemaError.prototype = new Error;
2016-02-09 21:56:06 +01:00
let chkSym = Symbol("checkable");
2016-09-12 20:25:56 +02:00
function checkNumber(target: any, prop: Prop, path: Path): any {
2016-02-09 21:56:06 +01:00
if ((typeof target) !== "number") {
throw new SchemaError(`expected number for ${path}`);
2016-02-09 21:56:06 +01:00
}
return target;
}
2016-09-12 20:25:56 +02:00
function checkString(target: any, prop: Prop, path: Path): any {
2016-02-09 21:56:06 +01:00
if (typeof target !== "string") {
throw new SchemaError(`expected string for ${path}, got ${typeof target} instead`);
2016-02-09 21:56:06 +01:00
}
return target;
}
2016-09-12 20:25:56 +02:00
function checkAnyObject(target: any, prop: Prop, path: Path): any {
2016-02-09 21:56:06 +01:00
if (typeof target !== "object") {
throw new SchemaError(`expected (any) object for ${path}, got ${typeof target} instead`);
2016-02-09 21:56:06 +01:00
}
return target;
}
2016-09-12 20:25:56 +02:00
function checkAny(target: any, prop: Prop, path: Path): any {
2016-02-09 21:56:06 +01:00
return target;
}
2016-09-12 20:25:56 +02:00
function checkList(target: any, prop: Prop, path: Path): any {
2016-02-09 21:56:06 +01:00
if (!Array.isArray(target)) {
throw new SchemaError(`array expected for ${path}, got ${typeof target} instead`);
2016-02-09 21:56:06 +01:00
}
for (let i = 0; i < target.length; i++) {
let v = target[i];
prop.elementChecker(v, prop.elementProp, path.concat([i]));
}
return target;
}
2016-09-12 20:25:56 +02:00
function checkOptional(target: any, prop: Prop, path: Path): any {
2016-02-17 17:51:25 +01:00
console.assert(prop.propertyKey);
prop.elementChecker(target,
2016-10-11 22:58:40 +02:00
prop.elementProp,
path.concat([prop.propertyKey]));
2016-02-17 17:51:25 +01:00
return target;
}
2016-09-12 20:25:56 +02:00
function checkValue(target: any, prop: Prop, path: Path): any {
2016-02-09 21:56:06 +01:00
let type = prop.type;
if (!type) {
throw Error(`assertion failed (prop is ${JSON.stringify(prop)})`);
}
let v = target;
if (!v || typeof v !== "object") {
2016-02-17 17:51:25 +01:00
throw new SchemaError(
`expected object for ${path.join(".")}, got ${typeof v} instead`);
2016-02-09 21:56:06 +01:00
}
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);
2016-02-09 21:56:06 +01:00
}
if (!remainingPropNames.delete(prop.propertyKey)) {
throw new SchemaError("assertion failed");
2016-02-09 21:56:06 +01:00
}
let propVal = v[prop.propertyKey];
obj[prop.propertyKey] = prop.checker(propVal,
2016-10-11 22:58:40 +02:00
prop,
path.concat([prop.propertyKey]));
2016-02-09 21:56:06 +01:00
}
if (remainingPropNames.size != 0) {
throw new SchemaError("superfluous properties " + JSON.stringify(Array.from(
2016-10-11 22:58:40 +02:00
remainingPropNames.values())));
2016-02-09 21:56:06 +01:00
}
return obj;
}
2016-09-12 20:25:56 +02:00
export function Class(target: any) {
target.checked = (v: any) => {
2016-02-09 21:56:06 +01:00
return checkValue(v, {
propertyKey: "(root)",
type: target,
checker: checkValue
2016-02-17 17:51:25 +01:00
}, ["(root)"]);
2016-02-09 21:56:06 +01:00
};
return target;
}
2016-11-14 00:57:29 +01:00
export function ClassWithValidator(target: any) {
target.checked = (v: any) => {
let cv = checkValue(v, {
propertyKey: "(root)",
type: target,
checker: checkValue
}, ["(root)"]);
let instance = new target();
if (typeof instance.validate !== "function") {
throw Error("invalid Checkable annotion: validate method required");
}
// May throw exception
instance.validate.call(cv);
return cv;
};
return target;
}
2016-09-12 20:25:56 +02:00
export function Value(type: any) {
2016-02-09 21:56:06 +01:00
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({
2016-10-11 22:58:40 +02:00
propertyKey: propertyKey,
checker: checkValue,
type: type
});
2016-02-09 21:56:06 +01:00
}
return deco;
}
2016-09-12 20:25:56 +02:00
export function List(type: any) {
2016-02-09 21:56:06 +01:00
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({
2016-10-11 22:58:40 +02:00
elementChecker,
elementProp,
propertyKey: propertyKey,
checker: checkList,
});
2016-02-09 21:56:06 +01:00
}
return deco;
}
2016-09-12 20:25:56 +02:00
export function Optional(type: any) {
2016-02-17 17:51:25 +01:00
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({
2016-10-11 22:58:40 +02:00
elementChecker,
elementProp,
propertyKey: propertyKey,
checker: checkOptional,
optional: true,
});
2016-02-17 17:51:25 +01:00
}
return deco;
}
2016-02-09 21:56:06 +01:00
export function Number(target: Object, propertyKey: string | symbol): void {
let chk = mkChk(target);
2016-10-11 22:58:40 +02:00
chk.props.push({ propertyKey: propertyKey, checker: checkNumber });
2016-02-09 21:56:06 +01:00
}
export function AnyObject(target: Object,
2016-10-11 22:58:40 +02:00
propertyKey: string | symbol): void {
2016-02-09 21:56:06 +01:00
let chk = mkChk(target);
chk.props.push({
2016-10-11 22:58:40 +02:00
propertyKey: propertyKey,
checker: checkAnyObject
});
2016-02-09 21:56:06 +01:00
}
export function Any(target: Object,
2016-10-11 22:58:40 +02:00
propertyKey: string | symbol): void {
2016-02-09 21:56:06 +01:00
let chk = mkChk(target);
chk.props.push({
2016-10-11 22:58:40 +02:00
propertyKey: propertyKey,
checker: checkAny,
optional: true
});
2016-02-09 21:56:06 +01:00
}
export function String(target: Object, propertyKey: string | symbol): void {
let chk = mkChk(target);
2016-10-11 22:58:40 +02:00
chk.props.push({ propertyKey: propertyKey, checker: checkString });
2016-02-09 21:56:06 +01:00
}
2016-09-12 20:25:56 +02:00
function mkChk(target: any) {
2016-02-09 21:56:06 +01:00
let chk = target[chkSym];
if (!chk) {
2016-10-11 22:58:40 +02:00
chk = { props: [] };
2016-02-09 21:56:06 +01:00
target[chkSym] = chk;
}
return chk;
}
2016-11-14 00:57:29 +01:00
}