Skip to content

Commit

Permalink
Initial
Browse files Browse the repository at this point in the history
  • Loading branch information
Caesar2011 committed May 25, 2020
0 parents commit cd6ed23
Show file tree
Hide file tree
Showing 7 changed files with 197 additions and 0 deletions.
23 changes: 23 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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 changes: 29 additions & 0 deletions Validator.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { assertEquals, assertNotEquals } from "https://deno.land/[email protected]/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 changes: 97 additions & 0 deletions Validator.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Empty file added deps.ts
Empty file.
2 changes: 2 additions & 0 deletions mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { Args, Validator, ValidationError, validate, Validatable, ArraySymbol } from "./Validator.ts";
export * from "./validators/string.ts";
33 changes: 33 additions & 0 deletions validators/string.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { isString } from "./string.ts";
import { validate } from "../mod.ts";
import { assertEquals, assertNotEquals } from "https://deno.land/[email protected]/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 changes: 13 additions & 0 deletions validators/string.ts
Original file line number Diff line number Diff line change
@@ -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.`
}
}

0 comments on commit cd6ed23

Please sign in to comment.