From 1ddb84234e6dfc3c36234feea4127fadb3e22530 Mon Sep 17 00:00:00 2001 From: Sebastian Seedorf Date: Thu, 28 May 2020 00:52:52 +0200 Subject: [PATCH] Added conversion --- Validator.test.ts | 68 +++++++++++++++++++++++++++++++++ Validator.ts | 80 +++++++++++++++++++++++++++++++-------- validators/number.ts | 7 ++++ validators/string.test.ts | 16 ++++++-- validators/string.ts | 11 +++++- 5 files changed, 161 insertions(+), 21 deletions(-) diff --git a/Validator.test.ts b/Validator.test.ts index ed0148c..1208bee 100644 --- a/Validator.test.ts +++ b/Validator.test.ts @@ -52,7 +52,10 @@ Deno.test("validate schema (match)", async () => { ], ]; for (const [value, constraints] of values) { + const valueBefore = JSON.stringify(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) { + const valueBefore = JSON.stringify(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); + } +}); \ No newline at end of file diff --git a/Validator.ts b/Validator.ts index dbe0100..109ad97 100644 --- a/Validator.ts +++ b/Validator.ts @@ -3,6 +3,7 @@ export type Args = { [_: string]: any }; export interface Validator { type: string; extends?: Validator[]; + convert?: (value: any) => Promise | any; check: (value: any) => Promise | Args | undefined; message: ( value: any, @@ -27,42 +28,60 @@ export type Schema = { [key: string]: Validatable } | { export async function validate( value: any, validators: Validatable, + { doConversion = false, converted = undefined, doValidation = true }: { + doConversion?: boolean; + converted?: any; + doValidation?: boolean; + } = {}, ): Promise { - if (instanceofValidatorArray(validators) || instanceofValidator(validators)) { - return validateValue(value, validators); + const options = { doConversion, converted, doValidation }; + if (Array.isArray(validators) || instanceofValidator(validators)) { + return validateValue(value, validators, options); } 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 && 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( +export async function validateValue( value: any, validators: Validator | Validator[], + { doConversion = false, converted = undefined, doValidation = true }: { + doConversion?: boolean; + converted?: any; + doValidation?: boolean; + } = {}, ): Promise { + const options = { doConversion, converted, doValidation }; if (!Array.isArray(validators)) { validators = [validators]; } const result: ValidationError[] = []; for (const validator of validators) { + // 1. convert + if (doConversion && validator.convert) { + value = await validator.convert(value); + } + + // 2. extends if (validator.extends) { - const prerequisites = await validate(value, validator.extends); + const prerequisites = await validate(value, validator.extends, options); if (prerequisites.length > 0) { result.push(...prerequisites); continue; } } - const args = await validator.check(value); + + // 3. check + const args = doValidation ? await validator.check(value) : undefined; + + // 4. message if (args !== undefined) { const message = await validator.message(value, args); result.push({ @@ -72,19 +91,39 @@ async function validateValue( }); } } + if (converted) { + converted.output = value; + } return result; } -async function validateSchema( +export async function validateSchema( value: any, validators: Schema, + { doConversion = false, converted = undefined, doValidation = true }: { + doConversion?: boolean; + converted?: any; + doValidation?: boolean; + } = {}, ): Promise { + const conv: any = {}; + const options = { doConversion, converted: conv, doValidation }; + + // Validate array 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])), - ); + if (converted) { + 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) => (val ?? []).map((error) => ({ ...error, @@ -101,14 +140,23 @@ async function validateSchema( }]; } } + + // Validate object const v = validators as { [key: string]: Validatable }; const valErrors: ValidationError[] = []; + + if (converted) { + converted.output = {}; + } for (const prop in validators) { if (!validators.hasOwnProperty(prop)) { continue; } - const errors = await validate(value && value[prop], v[prop]); + const errors = await validate(value && value[prop], v[prop], options); valErrors.push(...(errors ?? [])); + if (converted) { + converted.output[prop] = conv.output; + } } return valErrors; } diff --git a/validators/number.ts b/validators/number.ts index e32ffe5..3764d1b 100644 --- a/validators/number.ts +++ b/validators/number.ts @@ -5,6 +5,13 @@ export function isNumber( ): Validator { return { 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) => { if (value === null || value === undefined) return; if (allowNaN && Number.isNaN(value)) return; diff --git a/validators/string.test.ts b/validators/string.test.ts index 4a2e14d..2584087 100644 --- a/validators/string.test.ts +++ b/validators/string.test.ts @@ -129,10 +129,14 @@ Deno.test("fulfillsRegex (match)", async () => { [undefined, /[a-z]+/], ["123abc", /[a-z]+/], ["abc", /^[a-z]+$/], - ["^ab$", /^\^(ab)|(cd)\$$/] + ["^ab$", /^\^(ab)|(cd)\$$/], ]; 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]+/], ["", /[a-z]+/], ["abc123", /^[a-z]+$/], - ["^abcd$", /^\^(ab|cd)\$$/] + ["^abcd$", /^\^(ab|cd)\$$/], ]; for (const [value, regex] of values) { - assertNotEquals(await validate(value, fulfillsRegex({ regex })), [], `${String(value)} - ${regex}`); + assertNotEquals( + await validate(value, fulfillsRegex({ regex })), + [], + `${String(value)} - ${regex}`, + ); } }); diff --git a/validators/string.ts b/validators/string.ts index 986cc50..4357ac4 100644 --- a/validators/string.ts +++ b/validators/string.ts @@ -126,6 +126,15 @@ export function isEmail(): Validator { return { type: "isEmail", 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) => { if (value === null || value === undefined) return; const regex = @@ -141,7 +150,7 @@ export function isEmail(): Validator { }; } -export function fulfillsRegex({regex}: {regex: RegExp}): Validator { +export function fulfillsRegex({ regex }: { regex: RegExp }): Validator { return { type: "fulfillsRegex", extends: [isString()],