Added conversion

This commit is contained in:
Sebastian Seedorf
2020-05-28 00:52:52 +02:00
parent 741b96ee50
commit 1ddb84234e
5 changed files with 161 additions and 21 deletions

View File

@@ -52,7 +52,10 @@ Deno.test("validate schema (match)", async () => {
], ],
]; ];
for (const [value, constraints] of values) { for (const [value, constraints] of values) {
const valueBefore = JSON.stringify(value);
assertEquals(await validate(value, constraints), [], String(value)); assertEquals(await validate(value, constraints), [], String(value));
const valueAfter = JSON.stringify(value);
assertEquals(valueAfter, valueBefore);
} }
}); });
@@ -92,6 +95,71 @@ Deno.test("validate schema (no match)", async () => {
], ],
]; ];
for (const [value, constraints] of values) { for (const [value, constraints] of values) {
const valueBefore = JSON.stringify(value);
assertNotEquals(await validate(value, constraints), [], String(value)); assertNotEquals(await validate(value, constraints), [], String(value));
const valueAfter = JSON.stringify(value);
assertEquals(valueAfter, valueBefore);
}
});
Deno.test("validate doConversion (match)", async () => {
const values: [any, Validatable, any][] = [
[
1,
isNumber(),
1
],
[
"2",
isNumber(),
2
],
[
"03",
isNumber(),
3
],
[
{ foo: 3, bar: { baz: "4", other: "val" } },
{ foo: isNumber(), bar: { baz: isNumber() } },
{ foo: 3, bar: { baz: 4 } }
],
];
for (const [value, constraints, conv] of values) {
const valueBefore = JSON.stringify(value);
const converted: any = {};
assertEquals(await validate(value, constraints, {doConversion: true, converted}), [], String(value));
const valueAfter = JSON.stringify(value);
assertEquals(valueAfter, valueBefore);
assertEquals(converted.hasOwnProperty("output"), true);
assertEquals(converted?.output, conv);
}
});
Deno.test("validate doConversion (no match)", async () => {
const values: [any, Validatable][] = [
[
"1x",
isNumber(),
],
[
Symbol(),
isNumber(),
],
];
for (const [value, constraints] of values) {
const valueBefore = JSON.stringify(value);
const converted: any = {};
assertNotEquals(await validate(value, constraints, {doConversion: true, converted}), [], String(value));
const valueAfter = JSON.stringify(value);
assertEquals(valueAfter, valueBefore);
assertEquals(converted.hasOwnProperty("output"), true);
assertEquals(converted?.output, value);
} }
}); });

View File

@@ -3,6 +3,7 @@ export type Args = { [_: string]: any };
export interface Validator { export interface Validator {
type: string; type: string;
extends?: Validator[]; extends?: Validator[];
convert?: (value: any) => Promise<any> | any;
check: (value: any) => Promise<Args | undefined> | Args | undefined; check: (value: any) => Promise<Args | undefined> | Args | undefined;
message: ( message: (
value: any, value: any,
@@ -27,42 +28,60 @@ export type Schema = { [key: string]: Validatable } | {
export async function validate( export async function validate(
value: any, value: any,
validators: Validatable, validators: Validatable,
{ doConversion = false, converted = undefined, doValidation = true }: {
doConversion?: boolean;
converted?: any;
doValidation?: boolean;
} = {},
): Promise<ValidationError[]> { ): Promise<ValidationError[]> {
if (instanceofValidatorArray(validators) || instanceofValidator(validators)) { const options = { doConversion, converted, doValidation };
return validateValue(value, validators); if (Array.isArray(validators) || instanceofValidator(validators)) {
return validateValue(value, validators, options);
} else { } else {
return validateSchema(value, validators); return validateSchema(value, validators, options);
} }
} }
function instanceofValidator(value: any): value is Validator { export function instanceofValidator(value: any): value is Validator {
return value && return value &&
value.hasOwnProperty("type") && typeof value.type === "string" && value.hasOwnProperty("type") && typeof value.type === "string" &&
value.hasOwnProperty("check") && typeof value.check === "function" && value.hasOwnProperty("check") && typeof value.check === "function" &&
value.hasOwnProperty("message") && typeof value.message === "function"; value.hasOwnProperty("message") && typeof value.message === "function";
} }
function instanceofValidatorArray(value: any): value is Validator[] { export async function validateValue(
return Array.isArray(value) && value.every((x) => instanceofValidator(x));
}
async function validateValue(
value: any, value: any,
validators: Validator | Validator[], validators: Validator | Validator[],
{ doConversion = false, converted = undefined, doValidation = true }: {
doConversion?: boolean;
converted?: any;
doValidation?: boolean;
} = {},
): Promise<ValidationError[]> { ): Promise<ValidationError[]> {
const options = { doConversion, converted, doValidation };
if (!Array.isArray(validators)) { if (!Array.isArray(validators)) {
validators = [validators]; validators = [validators];
} }
const result: ValidationError[] = []; const result: ValidationError[] = [];
for (const validator of validators) { for (const validator of validators) {
// 1. convert
if (doConversion && validator.convert) {
value = await validator.convert(value);
}
// 2. extends
if (validator.extends) { if (validator.extends) {
const prerequisites = await validate(value, validator.extends); const prerequisites = await validate(value, validator.extends, options);
if (prerequisites.length > 0) { if (prerequisites.length > 0) {
result.push(...prerequisites); result.push(...prerequisites);
continue; continue;
} }
} }
const args = await validator.check(value);
// 3. check
const args = doValidation ? await validator.check(value) : undefined;
// 4. message
if (args !== undefined) { if (args !== undefined) {
const message = await validator.message(value, args); const message = await validator.message(value, args);
result.push({ result.push({
@@ -72,19 +91,39 @@ async function validateValue(
}); });
} }
} }
if (converted) {
converted.output = value;
}
return result; return result;
} }
async function validateSchema( export async function validateSchema(
value: any, value: any,
validators: Schema, validators: Schema,
{ doConversion = false, converted = undefined, doValidation = true }: {
doConversion?: boolean;
converted?: any;
doValidation?: boolean;
} = {},
): Promise<ValidationError[]> { ): Promise<ValidationError[]> {
const conv: any = {};
const options = { doConversion, converted: conv, doValidation };
// Validate array
if (validators.hasOwnProperty(ArraySymbol)) { if (validators.hasOwnProperty(ArraySymbol)) {
const v = validators as { [ArraySymbol]: Validatable }; const v = validators as { [ArraySymbol]: Validatable };
if (Array.isArray(value)) { if (Array.isArray(value)) {
const arr = await Promise.all( if (converted) {
value.map((val) => validate(val, v[ArraySymbol])), converted.output = [];
); }
const arr: ValidationError[][] = [];
for (const val of value) {
const errors = await validate(val, v[ArraySymbol], options);
arr.push(errors);
if (converted) {
converted.output.push(conv.output);
}
}
const errors = arr.flatMap((val, idx) => const errors = arr.flatMap((val, idx) =>
(val ?? []).map((error) => ({ (val ?? []).map((error) => ({
...error, ...error,
@@ -101,14 +140,23 @@ async function validateSchema(
}]; }];
} }
} }
// Validate object
const v = validators as { [key: string]: Validatable }; const v = validators as { [key: string]: Validatable };
const valErrors: ValidationError[] = []; const valErrors: ValidationError[] = [];
if (converted) {
converted.output = {};
}
for (const prop in validators) { for (const prop in validators) {
if (!validators.hasOwnProperty(prop)) { if (!validators.hasOwnProperty(prop)) {
continue; continue;
} }
const errors = await validate(value && value[prop], v[prop]); const errors = await validate(value && value[prop], v[prop], options);
valErrors.push(...(errors ?? [])); valErrors.push(...(errors ?? []));
if (converted) {
converted.output[prop] = conv.output;
}
} }
return valErrors; return valErrors;
} }

View File

@@ -5,6 +5,13 @@ export function isNumber(
): Validator { ): Validator {
return { return {
type: "isNumber", type: "isNumber",
convert: (value: any) => {
if (typeof value === "string" || value instanceof String) {
const num = Number(value);
return Number.isNaN(num) ? value : num;
}
return value;
},
check: (value: any) => { check: (value: any) => {
if (value === null || value === undefined) return; if (value === null || value === undefined) return;
if (allowNaN && Number.isNaN(value)) return; if (allowNaN && Number.isNaN(value)) return;

View File

@@ -129,10 +129,14 @@ Deno.test("fulfillsRegex (match)", async () => {
[undefined, /[a-z]+/], [undefined, /[a-z]+/],
["123abc", /[a-z]+/], ["123abc", /[a-z]+/],
["abc", /^[a-z]+$/], ["abc", /^[a-z]+$/],
["^ab$", /^\^(ab)|(cd)\$$/] ["^ab$", /^\^(ab)|(cd)\$$/],
]; ];
for (const [value, regex] of values) { for (const [value, regex] of values) {
assertEquals(await validate(value, fulfillsRegex({ regex })), [], `${String(value)} - ${regex}`); assertEquals(
await validate(value, fulfillsRegex({ regex })),
[],
`${String(value)} - ${regex}`,
);
} }
}); });
@@ -141,9 +145,13 @@ Deno.test("fulfillsRegex (no match)", async () => {
[Symbol(), /[a-z]+/], [Symbol(), /[a-z]+/],
["", /[a-z]+/], ["", /[a-z]+/],
["abc123", /^[a-z]+$/], ["abc123", /^[a-z]+$/],
["^abcd$", /^\^(ab|cd)\$$/] ["^abcd$", /^\^(ab|cd)\$$/],
]; ];
for (const [value, regex] of values) { for (const [value, regex] of values) {
assertNotEquals(await validate(value, fulfillsRegex({ regex })), [], `${String(value)} - ${regex}`); assertNotEquals(
await validate(value, fulfillsRegex({ regex })),
[],
`${String(value)} - ${regex}`,
);
} }
}); });

View File

@@ -126,6 +126,15 @@ export function isEmail(): Validator {
return { return {
type: "isEmail", type: "isEmail",
extends: [isString()], extends: [isString()],
convert: (value: any) => {
if (value === true) {
return "true";
} else if (value === false) {
return "false";
} else if (typeof value === "number") {
return value.toString();
}
},
check: (value: any) => { check: (value: any) => {
if (value === null || value === undefined) return; if (value === null || value === undefined) return;
const regex = const regex =