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";
|
|
|
|
|
|
|
|
/**
|
2017-05-24 15:46:49 +02:00
|
|
|
* Decorators for validating JSON objects and converting them to a typed
|
|
|
|
* object.
|
|
|
|
*
|
|
|
|
* The decorators are put onto classes, and the validation is done
|
|
|
|
* via a static method that is filled in by the annotation.
|
|
|
|
*
|
|
|
|
* Example:
|
|
|
|
* ```
|
|
|
|
* @Checkable.Class
|
|
|
|
* class Person {
|
|
|
|
* @Checkable.String
|
|
|
|
* name: string;
|
|
|
|
* @Checkable.Number
|
|
|
|
* age: number;
|
2017-05-24 16:14:23 +02:00
|
|
|
*
|
|
|
|
* // Method will be implemented automatically
|
|
|
|
* static checked(obj: any): Person;
|
2017-05-24 15:46:49 +02:00
|
|
|
* }
|
|
|
|
* ```
|
2016-02-09 21:56:06 +01:00
|
|
|
*/
|
|
|
|
export namespace Checkable {
|
2016-09-12 20:25:56 +02:00
|
|
|
|
2017-05-28 01:10:54 +02:00
|
|
|
type Path = Array<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;
|
2017-04-27 03:09:29 +02:00
|
|
|
type?: any;
|
2016-09-12 20:25:56 +02:00
|
|
|
elementChecker?: any;
|
|
|
|
elementProp?: any;
|
2017-04-27 03:09:29 +02:00
|
|
|
keyProp?: any;
|
|
|
|
valueProp?: any;
|
|
|
|
optional?: boolean;
|
|
|
|
extraAllowed?: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface CheckableInfo {
|
|
|
|
props: Prop[];
|
2016-09-12 20:25:56 +02:00
|
|
|
}
|
|
|
|
|
2017-10-15 19:28:35 +02:00
|
|
|
// tslint:disable-next-line:no-shadowed-variable
|
2017-05-28 01:10:54 +02:00
|
|
|
export const SchemaError = (function SchemaError(this: any, message: string) {
|
|
|
|
const that: any = this as any;
|
|
|
|
that.name = "SchemaError";
|
2017-05-27 16:31:11 +02:00
|
|
|
that.message = message;
|
2017-05-28 01:10:54 +02:00
|
|
|
that.stack = (new Error() as any).stack;
|
2016-09-12 20:25:56 +02:00
|
|
|
}) as any as SchemaErrorConstructor;
|
|
|
|
|
2016-02-17 15:56:48 +01:00
|
|
|
|
2017-05-28 01:10:54 +02:00
|
|
|
SchemaError.prototype = new Error();
|
2016-02-17 15:56:48 +01:00
|
|
|
|
2017-04-27 03:09:29 +02:00
|
|
|
/**
|
|
|
|
* Classes that are checkable are annotated with this
|
|
|
|
* checkable info symbol, which contains the information necessary
|
|
|
|
* to check if they're valid.
|
|
|
|
*/
|
2017-05-28 01:10:54 +02:00
|
|
|
const checkableInfoSym = Symbol("checkableInfo");
|
2017-04-27 03:09:29 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the current property list for a checkable type.
|
|
|
|
*/
|
|
|
|
function getCheckableInfo(target: any): CheckableInfo {
|
|
|
|
let chk = target[checkableInfoSym] as CheckableInfo|undefined;
|
|
|
|
if (!chk) {
|
|
|
|
chk = { props: [] };
|
|
|
|
target[checkableInfoSym] = chk;
|
|
|
|
}
|
|
|
|
return chk;
|
|
|
|
}
|
2016-02-09 21:56:06 +01:00
|
|
|
|
|
|
|
|
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") {
|
2016-02-17 15:56:48 +01:00
|
|
|
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") {
|
2016-02-17 15:56:48 +01:00
|
|
|
throw new SchemaError(`expected string for ${path}, got ${typeof target} instead`);
|
2016-02-09 21:56:06 +01:00
|
|
|
}
|
|
|
|
return target;
|
|
|
|
}
|
|
|
|
|
2016-11-16 01:59:39 +01:00
|
|
|
function checkBoolean(target: any, prop: Prop, path: Path): any {
|
|
|
|
if (typeof target !== "boolean") {
|
|
|
|
throw new SchemaError(`expected boolean for ${path}, got ${typeof target} instead`);
|
|
|
|
}
|
|
|
|
return target;
|
|
|
|
}
|
|
|
|
|
2016-02-09 21:56:06 +01:00
|
|
|
|
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") {
|
2016-02-17 15:56:48 +01:00
|
|
|
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)) {
|
2016-02-17 15:56:48 +01:00
|
|
|
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++) {
|
2017-05-28 01:10:54 +02:00
|
|
|
const v = target[i];
|
2016-02-09 21:56:06 +01:00
|
|
|
prop.elementChecker(v, prop.elementProp, path.concat([i]));
|
|
|
|
}
|
|
|
|
return target;
|
|
|
|
}
|
|
|
|
|
2017-04-27 03:09:29 +02:00
|
|
|
function checkMap(target: any, prop: Prop, path: Path): any {
|
|
|
|
if (typeof target !== "object") {
|
|
|
|
throw new SchemaError(`expected object for ${path}, got ${typeof target} instead`);
|
|
|
|
}
|
2017-05-28 01:10:54 +02:00
|
|
|
for (const key in target) {
|
2017-04-27 03:09:29 +02:00
|
|
|
prop.keyProp.checker(key, prop.keyProp, path.concat([key]));
|
2017-05-28 01:10:54 +02:00
|
|
|
const value = target[key];
|
2017-04-27 03:09:29 +02:00
|
|
|
prop.valueProp.checker(value, prop.valueProp, path.concat([key]));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-02-09 21:56:06 +01:00
|
|
|
|
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 {
|
2017-05-28 01:10:54 +02:00
|
|
|
const type = prop.type;
|
2017-10-15 19:28:35 +02:00
|
|
|
const typeName = type.name || "??";
|
2016-02-09 21:56:06 +01:00
|
|
|
if (!type) {
|
|
|
|
throw Error(`assertion failed (prop is ${JSON.stringify(prop)})`);
|
|
|
|
}
|
2017-05-28 01:10:54 +02:00
|
|
|
const v = target;
|
2016-02-09 21:56:06 +01:00
|
|
|
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
|
|
|
}
|
2017-05-28 01:10:54 +02:00
|
|
|
const props = type.prototype[checkableInfoSym].props;
|
|
|
|
const remainingPropNames = new Set(Object.getOwnPropertyNames(v));
|
|
|
|
const obj = new type();
|
|
|
|
for (const innerProp of props) {
|
|
|
|
if (!remainingPropNames.has(innerProp.propertyKey)) {
|
|
|
|
if (innerProp.optional) {
|
2016-02-17 15:56:48 +01:00
|
|
|
continue;
|
|
|
|
}
|
2017-10-15 19:28:35 +02:00
|
|
|
throw new SchemaError(`Property ${innerProp.propertyKey} missing on ${path} of ${typeName}`);
|
2016-02-09 21:56:06 +01:00
|
|
|
}
|
2017-05-28 01:10:54 +02:00
|
|
|
if (!remainingPropNames.delete(innerProp.propertyKey)) {
|
2016-02-17 15:56:48 +01:00
|
|
|
throw new SchemaError("assertion failed");
|
2016-02-09 21:56:06 +01:00
|
|
|
}
|
2017-05-28 01:10:54 +02:00
|
|
|
const propVal = v[innerProp.propertyKey];
|
|
|
|
obj[innerProp.propertyKey] = innerProp.checker(propVal,
|
|
|
|
innerProp,
|
|
|
|
path.concat([innerProp.propertyKey]));
|
2016-02-09 21:56:06 +01:00
|
|
|
}
|
|
|
|
|
2017-05-28 01:10:54 +02:00
|
|
|
if (!prop.extraAllowed && remainingPropNames.size !== 0) {
|
2017-10-15 19:28:35 +02:00
|
|
|
const err = `superfluous properties ${JSON.stringify(Array.from(remainingPropNames.values()))} of ${typeName}`;
|
|
|
|
throw new SchemaError(err);
|
2016-02-09 21:56:06 +01:00
|
|
|
}
|
|
|
|
return obj;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-05-24 15:46:49 +02:00
|
|
|
/**
|
|
|
|
* Class with checkable annotations on fields.
|
|
|
|
* This annotation adds the implementation of the `checked`
|
|
|
|
* static method.
|
|
|
|
*/
|
2017-05-27 15:05:41 +02:00
|
|
|
export function Class(opts: {extra?: boolean, validate?: boolean} = {}) {
|
|
|
|
return (target: any) => {
|
|
|
|
target.checked = (v: any) => {
|
2017-05-28 01:10:54 +02:00
|
|
|
const cv = checkValue(v, {
|
|
|
|
checker: checkValue,
|
|
|
|
extraAllowed: !!opts.extra,
|
2017-05-27 15:05:41 +02:00
|
|
|
propertyKey: "(root)",
|
|
|
|
type: target,
|
|
|
|
}, ["(root)"]);
|
|
|
|
if (opts.validate) {
|
2017-10-15 18:55:34 +02:00
|
|
|
if (typeof target.validate !== "function") {
|
|
|
|
console.error("target", target);
|
2017-05-27 15:05:41 +02:00
|
|
|
throw Error("invalid Checkable annotion: validate method required");
|
|
|
|
}
|
|
|
|
// May throw exception
|
2017-08-14 04:16:12 +02:00
|
|
|
target.validate(cv);
|
2017-05-27 15:05:41 +02:00
|
|
|
}
|
|
|
|
return cv;
|
|
|
|
};
|
|
|
|
return target;
|
2017-05-28 01:10:54 +02:00
|
|
|
};
|
2016-11-14 00:57:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-05-24 15:46:49 +02:00
|
|
|
/**
|
|
|
|
* Target property must be a Checkable object of the given type.
|
|
|
|
*/
|
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?)");
|
|
|
|
}
|
2017-05-28 01:10:54 +02:00
|
|
|
function deco(target: object, propertyKey: string | symbol): void {
|
|
|
|
const chk = getCheckableInfo(target);
|
2016-02-09 21:56:06 +01:00
|
|
|
chk.props.push({
|
2016-10-11 22:58:40 +02:00
|
|
|
checker: checkValue,
|
2017-05-28 01:10:54 +02:00
|
|
|
propertyKey,
|
|
|
|
type,
|
2016-10-11 22:58:40 +02:00
|
|
|
});
|
2016-02-09 21:56:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return deco;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-05-24 15:46:49 +02:00
|
|
|
/**
|
|
|
|
* List of values that match the given annotation. For example, `@Checkable.List(Checkable.String)` is
|
|
|
|
* an annotation for a list of strings.
|
|
|
|
*/
|
2016-09-12 20:25:56 +02:00
|
|
|
export function List(type: any) {
|
2017-05-28 01:10:54 +02:00
|
|
|
const stub = {};
|
2016-02-09 21:56:06 +01:00
|
|
|
type(stub, "(list-element)");
|
2017-05-28 01:10:54 +02:00
|
|
|
const elementProp = getCheckableInfo(stub).props[0];
|
|
|
|
const elementChecker = elementProp.checker;
|
2016-02-09 21:56:06 +01:00
|
|
|
if (!elementChecker) {
|
|
|
|
throw Error("assertion failed");
|
|
|
|
}
|
2017-05-28 01:10:54 +02:00
|
|
|
function deco(target: object, propertyKey: string | symbol): void {
|
|
|
|
const chk = getCheckableInfo(target);
|
2016-02-09 21:56:06 +01:00
|
|
|
chk.props.push({
|
2017-05-28 01:10:54 +02:00
|
|
|
checker: checkList,
|
2016-10-11 22:58:40 +02:00
|
|
|
elementChecker,
|
|
|
|
elementProp,
|
2017-05-28 01:10:54 +02:00
|
|
|
propertyKey,
|
2016-10-11 22:58:40 +02:00
|
|
|
});
|
2016-02-09 21:56:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return deco;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-05-24 15:46:49 +02:00
|
|
|
/**
|
|
|
|
* Map from the key type to value type. Takes two annotations,
|
|
|
|
* one for the key type and one for the value type.
|
|
|
|
*/
|
2017-04-27 03:09:29 +02:00
|
|
|
export function Map(keyType: any, valueType: any) {
|
2017-05-28 01:10:54 +02:00
|
|
|
const keyStub = {};
|
2017-04-27 03:09:29 +02:00
|
|
|
keyType(keyStub, "(map-key)");
|
2017-05-28 01:10:54 +02:00
|
|
|
const keyProp = getCheckableInfo(keyStub).props[0];
|
2017-04-27 03:09:29 +02:00
|
|
|
if (!keyProp) {
|
|
|
|
throw Error("assertion failed");
|
|
|
|
}
|
2017-05-28 01:10:54 +02:00
|
|
|
const valueStub = {};
|
2017-04-27 03:09:29 +02:00
|
|
|
valueType(valueStub, "(map-value)");
|
2017-05-28 01:10:54 +02:00
|
|
|
const valueProp = getCheckableInfo(valueStub).props[0];
|
2017-04-27 03:09:29 +02:00
|
|
|
if (!valueProp) {
|
|
|
|
throw Error("assertion failed");
|
|
|
|
}
|
2017-05-28 01:10:54 +02:00
|
|
|
function deco(target: object, propertyKey: string | symbol): void {
|
|
|
|
const chk = getCheckableInfo(target);
|
2017-04-27 03:09:29 +02:00
|
|
|
chk.props.push({
|
2017-05-28 01:10:54 +02:00
|
|
|
checker: checkMap,
|
2017-04-27 03:09:29 +02:00
|
|
|
keyProp,
|
2017-05-28 01:10:54 +02:00
|
|
|
propertyKey,
|
2017-04-27 03:09:29 +02:00
|
|
|
valueProp,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return deco;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-05-24 15:46:49 +02:00
|
|
|
/**
|
|
|
|
* Makes another annotation optional, for example `@Checkable.Optional(Checkable.Number)`.
|
|
|
|
*/
|
2016-09-12 20:25:56 +02:00
|
|
|
export function Optional(type: any) {
|
2017-05-28 01:10:54 +02:00
|
|
|
const stub = {};
|
2016-02-17 17:51:25 +01:00
|
|
|
type(stub, "(optional-element)");
|
2017-05-28 01:10:54 +02:00
|
|
|
const elementProp = getCheckableInfo(stub).props[0];
|
|
|
|
const elementChecker = elementProp.checker;
|
2016-02-17 17:51:25 +01:00
|
|
|
if (!elementChecker) {
|
|
|
|
throw Error("assertion failed");
|
|
|
|
}
|
2017-05-28 01:10:54 +02:00
|
|
|
function deco(target: object, propertyKey: string | symbol): void {
|
|
|
|
const chk = getCheckableInfo(target);
|
2016-02-17 17:51:25 +01:00
|
|
|
chk.props.push({
|
2017-05-28 01:10:54 +02:00
|
|
|
checker: checkOptional,
|
2016-10-11 22:58:40 +02:00
|
|
|
elementChecker,
|
|
|
|
elementProp,
|
|
|
|
optional: true,
|
2017-05-28 01:10:54 +02:00
|
|
|
propertyKey,
|
2016-10-11 22:58:40 +02:00
|
|
|
});
|
2016-02-17 17:51:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return deco;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-05-24 15:46:49 +02:00
|
|
|
/**
|
|
|
|
* Target property must be a number.
|
|
|
|
*/
|
2017-05-28 01:10:54 +02:00
|
|
|
export function Number(target: object, propertyKey: string | symbol): void {
|
|
|
|
const chk = getCheckableInfo(target);
|
|
|
|
chk.props.push({checker: checkNumber, propertyKey});
|
2016-02-09 21:56:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-05-24 15:46:49 +02:00
|
|
|
/**
|
|
|
|
* Target property must be an arbitary object.
|
|
|
|
*/
|
2017-05-28 01:10:54 +02:00
|
|
|
export function AnyObject(target: object, propertyKey: string | symbol): void {
|
|
|
|
const chk = getCheckableInfo(target);
|
2016-02-17 15:56:48 +01:00
|
|
|
chk.props.push({
|
2017-05-28 01:10:54 +02:00
|
|
|
checker: checkAnyObject,
|
|
|
|
propertyKey,
|
2016-10-11 22:58:40 +02:00
|
|
|
});
|
2016-02-09 21:56:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-05-24 15:46:49 +02:00
|
|
|
/**
|
|
|
|
* Target property can be anything.
|
|
|
|
*
|
|
|
|
* Not useful by itself, but in combination with higher-order annotations
|
|
|
|
* such as List or Map.
|
|
|
|
*/
|
2017-05-28 01:10:54 +02:00
|
|
|
export function Any(target: object, propertyKey: string | symbol): void {
|
|
|
|
const chk = getCheckableInfo(target);
|
2016-02-17 15:56:48 +01:00
|
|
|
chk.props.push({
|
2016-10-11 22:58:40 +02:00
|
|
|
checker: checkAny,
|
2017-05-28 01:10:54 +02:00
|
|
|
optional: true,
|
|
|
|
propertyKey,
|
2016-10-11 22:58:40 +02:00
|
|
|
});
|
2016-02-09 21:56:06 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-05-24 15:46:49 +02:00
|
|
|
/**
|
|
|
|
* Target property must be a string.
|
|
|
|
*/
|
2017-05-28 01:10:54 +02:00
|
|
|
export function String(target: object, propertyKey: string | symbol): void {
|
|
|
|
const chk = getCheckableInfo(target);
|
|
|
|
chk.props.push({ checker: checkString, propertyKey });
|
2016-02-09 21:56:06 +01:00
|
|
|
}
|
|
|
|
|
2017-05-24 15:46:49 +02:00
|
|
|
/**
|
|
|
|
* Target property must be a boolean value.
|
|
|
|
*/
|
2017-05-28 01:10:54 +02:00
|
|
|
export function Boolean(target: object, propertyKey: string | symbol): void {
|
|
|
|
const chk = getCheckableInfo(target);
|
|
|
|
chk.props.push({ checker: checkBoolean, propertyKey });
|
2016-11-16 01:59:39 +01:00
|
|
|
}
|
2016-11-14 00:57:29 +01:00
|
|
|
}
|