Added conversion
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
80
Validator.ts
80
Validator.ts
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
Reference in New Issue
Block a user