commit cd6ed23d47d19365abf8414faffca02616175bf7 Author: Sebastian Seedorf Date: Tue May 26 00:11:46 2020 +0200 Initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9f820d --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +*.orig +*.pyc +*.swp + +/.idea +/.vscode +gclient_config.py_entries +/gh-pages +/target + +# Files that help ensure VSCode can work but we don't want checked into the +# repo +/node_modules +/tsconfig.json +package.json +package-lock.json + +# yarn creates this in error. +tools/node_modules/ + +# MacOS generated files +.DS_Store +.DS_Store? \ No newline at end of file diff --git a/Validator.test.ts b/Validator.test.ts new file mode 100644 index 0000000..ddd07ae --- /dev/null +++ b/Validator.test.ts @@ -0,0 +1,29 @@ +import { assertEquals, assertNotEquals } from "https://deno.land/std@0.53.0/testing/asserts.ts"; +import { + validate, Validatable, ArraySymbol, + isString +} from "./mod.ts"; + +Deno.test("validate schema (match)", async () => { + const values: [any, Validatable][] = [ + ["string", isString], + ["string", [isString]], + [["arr", "ay"], {[ArraySymbol]: isString}], + [{foo: "bar", lorem: "ipsum"}, {foo: isString, lorem: [isString]}], + ]; + for (const [value, constraints] of values) { + assertEquals([], await validate(value, constraints)); + } +}); + +Deno.test("validate schema (no match)", async () => { + const values: [any, Validatable][] = [ + [6, isString], + [false, [isString]], + [["arr", ["ay"]], {[ArraySymbol]: isString}], + [{foo: {}, lorem: "ipsum"}, {foo: isString, lorem: [isString]}], + ]; + for (const [value, constraints] of values) { + assertNotEquals([], await validate(value, constraints)); + } +}); \ No newline at end of file diff --git a/Validator.ts b/Validator.ts new file mode 100644 index 0000000..f8960b9 --- /dev/null +++ b/Validator.ts @@ -0,0 +1,97 @@ +export type Args = {[_: string]: any}; + +export interface Validator { + type: string; + check: (value: any) => Promise|Args|undefined; + message: (value: any, args?: Args) => Promise|string|undefined; +} + +export interface ValidationError { + type: string; + param?: string[]; + message?: string|null; + args?: Args; +} + +export type Validatable = Schema|Validator|Validator[]; + +export const ArraySymbol: unique symbol = Symbol("ArraySymbol"); +export type Schema = {[key: string]: Validatable}|{[ArraySymbol]: Validatable}; + +export async function validate(value: any, validators: Validatable): Promise { + if (instanceofValidatorArray(validators) || instanceofValidator(validators)) { + return validateValue(value, validators); + } else { + return validateSchema(value, validators); + } +} + +function instanceofValidator(value: any): value is Validator { + return value && + value.hasOwnProperty("type") && typeof value.type === "string" && + value.hasOwnProperty("check") && typeof value.check === "function" && + value.hasOwnProperty("message") && typeof value.message === "function"; +} + +function instanceofValidatorArray(value: any): value is Validator[] { + return Array.isArray(value) && value.every(x => instanceofValidator(x)); +} + +async function validateValue(value: any, validators: Validator|Validator[]): Promise { + if (!Array.isArray(validators)) { + validators = [validators]; + } + const result: ValidationError[] = []; + for (const validator of validators) { + const args = await validator.check(value); + if (args !== undefined) { + const message = await validator.message(value, args); + result.push({ + type: validator.type, + args, + message + }); + } + } + return result; +} + +async function validateSchema(value: any, validators: Schema): Promise { + if (validators.hasOwnProperty(ArraySymbol)) { + const v = validators as {[ArraySymbol]: Validatable}; + if (Array.isArray(value)) { + const arr = await Promise.all(value.map(val => validate(val, v[ArraySymbol]))); + const errors = arr.flatMap((val, idx) => (val ?? []).map(error => ({ + ...error, + param: [`[${idx}]`, ...(error.param || [])] + }))); + return errors; + } else { + return [{ + type: "array", + param: ["[]"], + message: "Array expected!", + args: {} + }]; + } + } + const v = validators as {[key: string]: Validatable}; + const valErrors: ValidationError[] = []; + for (const prop in validators) { + if (!validators.hasOwnProperty(prop)) { + continue; + } + if (value.hasOwnProperty(prop)) { + const errors = await validate(value[prop], v[prop]); + valErrors.push(...(errors ?? [])); + } else { + valErrors.push({ + type: "property", + param: [prop], + message: `Property '${prop}' expected but not found!`, + args: {property: prop} + }); + } + } + return valErrors; +} \ No newline at end of file diff --git a/deps.ts b/deps.ts new file mode 100644 index 0000000..e69de29 diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..c48a64d --- /dev/null +++ b/mod.ts @@ -0,0 +1,2 @@ +export { Args, Validator, ValidationError, validate, Validatable, ArraySymbol } from "./Validator.ts"; +export * from "./validators/string.ts"; \ No newline at end of file diff --git a/validators/string.test.ts b/validators/string.test.ts new file mode 100644 index 0000000..fed2f00 --- /dev/null +++ b/validators/string.test.ts @@ -0,0 +1,33 @@ +import { isString } from "./string.ts"; +import { validate } from "../mod.ts"; +import { assertEquals, assertNotEquals } from "https://deno.land/std@0.53.0/testing/asserts.ts"; + +Deno.test("isString (match)", async () => { + const values = [ + "", + "foo", + new String(), + new String("bar") + ]; + for (const value of values) { + assertEquals([], await validate(value, isString)); + } +}); + +Deno.test("isString (no match)", async () => { + const values = [ + undefined, + null, + 0, + 1, + true, + false, + () => {}, + function named() {}, + new Object(), + Symbol() + ]; + for (const value of values) { + assertNotEquals([], await validate(value, isString)); + } +}); \ No newline at end of file diff --git a/validators/string.ts b/validators/string.ts new file mode 100644 index 0000000..034b986 --- /dev/null +++ b/validators/string.ts @@ -0,0 +1,13 @@ +import { Validator, Args } from "../mod.ts"; + +export const isString: Validator = { + type: "isString", + check: (value: any) => { + if (typeof value !== 'string' && !(value instanceof String)) { + return {}; + } + }, + message: (value: any, args?: Args) => { + return `The value '${value && value.toString()}' has to be a string.` + } +} \ No newline at end of file