Initial
This commit is contained in:
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -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?
|
||||
29
Validator.test.ts
Normal file
29
Validator.test.ts
Normal file
@@ -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));
|
||||
}
|
||||
});
|
||||
97
Validator.ts
Normal file
97
Validator.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
export type Args = {[_: string]: any};
|
||||
|
||||
export interface Validator {
|
||||
type: string;
|
||||
check: (value: any) => Promise<Args|undefined>|Args|undefined;
|
||||
message: (value: any, args?: Args) => Promise<string|undefined>|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<ValidationError[]|undefined> {
|
||||
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<ValidationError[]> {
|
||||
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<ValidationError[]> {
|
||||
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;
|
||||
}
|
||||
2
mod.ts
Normal file
2
mod.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { Args, Validator, ValidationError, validate, Validatable, ArraySymbol } from "./Validator.ts";
|
||||
export * from "./validators/string.ts";
|
||||
33
validators/string.test.ts
Normal file
33
validators/string.test.ts
Normal file
@@ -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));
|
||||
}
|
||||
});
|
||||
13
validators/string.ts
Normal file
13
validators/string.ts
Normal file
@@ -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.`
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user