Compare commits

..

7 Commits

Author SHA1 Message Date
Sebastian Seedorf
adf8c400bc deno fmt 2020-05-28 01:45:30 +02:00
Sebastian Seedorf
ff891cd303 Added readme 2020-05-28 01:44:42 +02:00
Sebastian Seedorf
1ddb84234e Added conversion 2020-05-28 00:52:52 +02:00
Sebastian Seedorf
741b96ee50 Fixed messages 2020-05-27 21:52:15 +02:00
Sebastian Seedorf
69a350c4da Added regex 2020-05-27 21:44:00 +02:00
Sebastian Seedorf
24810302b2 Added number range 2020-05-27 19:16:48 +02:00
Sebastian Seedorf
5fc32d7414 Better tests 2020-05-27 13:02:19 +02:00
13 changed files with 515 additions and 49 deletions

88
README.md Normal file
View File

@@ -0,0 +1,88 @@
# valideno
Validate your values and schemas with **valideno**. Convert values to your desired type if needed. All you need is a single function.
`validate(data, validators, options = {})`;
## Examples
```ts
import {
ValidationError, validate, ArraySymbol,
isString, isEmail, fulfillsRegex,
isNumber, isInRange,
} from "./mod.ts";
const errorsA: ValidationError[] = await validate(3.14, isNumber());
console.log(errorsA);
// []
const errorsB: ValidationError[] = await validate(3.14, isString());
console.log(errorsB);
// [ { type: "isString", args: {}, message: "This value has to be a string." } ]
const errorsC: ValidationError[] = await validate(
"john.doe@example.com",
[isString(), isEmail()],
);
console.log(errorsC);
// []
const entity = {
name: "John Doe",
age: 25,
emails: [
{ type: "work", address: "john.doe@example.com" },
{ type: "home", address: "fancy_joe69@rainbow.com" },
],
};
const scheme = {
name: isString(),
age: isInRange({ greaterThanOrEqualTo: 18 }),
emails: {
[ArraySymbol]: {
type: fulfillsRegex({ regex: /^[a-z]+$/ }),
address: isEmail(),
},
},
};
const errorsD: ValidationError[] = await validate(entity, scheme);
console.log(errorsD);
// []
```
## Conversion
To allow automatic conversion to the desired data type add the `doConversion: true` option. To retreive the converted values, you need to pass a reference to an object to `converted`. If the conversion is not possible (e.g. when passing a `string` to `isBoolean()`) no conversion will take place and the value stays the same. In this case errors are thrown, because the non-converted `string` is not a Boolean.
The converted data is not in the reference passed to `converted` in the property `output`. This also works for schemes.
```ts
import {
ValidationError, validate,
isNumber
} from "./mod.ts";
const converted: any = {};
const errors: ValidationError[] = await validate("420", isNumber(), { doConversion: true, converted });
console.log(errors);
// []
console.log(converted);
// { output: 420 }
```
To disable validation and do "best effort" conversion only, disable validation with `doValidation: false`.
```ts
import {
ValidationError, validate,
isBoolean
} from "./mod.ts";
const converted: any = {};
const errors: ValidationError[] = await validate("foobar", isBoolean(), { doConversion: true, converted });
console.log(errors);
// [ { type: "isBoolean", args: {}, message: "This value has to be a boolean." } ]
console.log(converted);
// { output: "foobar" }
```

View File

@@ -52,7 +52,10 @@ Deno.test("validate schema (match)", async () => {
], ],
]; ];
for (const [value, constraints] of values) { for (const [value, constraints] of values) {
assertEquals([], await validate(value, constraints)); const valueBefore = JSON.stringify(value);
assertEquals(await validate(value, constraints), [], String(value));
const valueAfter = JSON.stringify(value);
assertEquals(valueAfter, valueBefore);
} }
}); });
@@ -92,6 +95,73 @@ Deno.test("validate schema (no match)", async () => {
], ],
]; ];
for (const [value, constraints] of values) { for (const [value, constraints] of values) {
assertNotEquals([], await validate(value, constraints)); 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);
} }
}); });

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,
@@ -14,7 +15,7 @@ export interface ValidationError {
type: string; type: string;
param?: string[]; param?: string[];
message?: string | null; message?: string | null;
args?: Args; args: Args;
} }
export type Validatable = Schema | Validator | Validator[]; export type Validatable = Schema | Validator | Validator[];
@@ -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;
} }

14
mod.ts
View File

@@ -6,8 +6,18 @@ export {
Validatable, Validatable,
ArraySymbol, ArraySymbol,
} from "./Validator.ts"; } from "./Validator.ts";
export { isString, isUrl, isEmail } from "./validators/string.ts"; export {
export { isNumber, isInteger } from "./validators/number.ts"; isString,
isUrl,
isEmail,
fulfillsRegex,
} from "./validators/string.ts";
export {
isNumber,
isInteger,
isInRange,
hasParity,
} from "./validators/number.ts";
export { isArray } from "./validators/array.ts"; export { isArray } from "./validators/array.ts";
export { or } from "./validators/logic.ts"; export { or } from "./validators/logic.ts";
export { isRequired, isDefined, isNotEmpty } from "./validators/empty.ts"; export { isRequired, isDefined, isNotEmpty } from "./validators/empty.ts";

View File

@@ -13,7 +13,7 @@ Deno.test("isArray (match)", async () => {
["foo"], ["foo"],
]; ];
for (const value of values) { for (const value of values) {
assertEquals(await validate(value, isArray()), []); assertEquals(await validate(value, isArray()), [], String(value));
} }
}); });
@@ -33,6 +33,6 @@ Deno.test("isArray (no match)", async () => {
Symbol(), Symbol(),
]; ];
for (const value of values) { for (const value of values) {
assertNotEquals(await validate(value, isArray()), []); assertNotEquals(await validate(value, isArray()), [], String(value));
} }
}); });

View File

@@ -13,7 +13,7 @@ Deno.test("isBoolean (match)", async () => {
false, false,
]; ];
for (const value of values) { for (const value of values) {
assertEquals(await validate(value, isBoolean()), []); assertEquals(await validate(value, isBoolean()), [], String(value));
} }
}); });
@@ -31,6 +31,6 @@ Deno.test("isBoolean (no match)", async () => {
Symbol(), Symbol(),
]; ];
for (const value of values) { for (const value of values) {
assertNotEquals(await validate(value, isBoolean()), []); assertNotEquals(await validate(value, isBoolean()), [], String(value));
} }
}); });

View File

@@ -80,7 +80,7 @@ Deno.test("isNotEmpty (match)", async () => {
Symbol("foo"), Symbol("foo"),
]; ];
for (const value of values) { for (const value of values) {
assertEquals(await validate(value, isNotEmpty()), []); assertEquals(await validate(value, isNotEmpty()), [], String(value));
} }
}); });
@@ -94,6 +94,6 @@ Deno.test("isNotEmpty (no match)", async () => {
[], [],
]; ];
for (const value of values) { for (const value of values) {
assertNotEquals(await validate(value, isNotEmpty()), []); assertNotEquals(await validate(value, isNotEmpty()), [], String(value));
} }
}); });

View File

@@ -11,7 +11,7 @@ Deno.test("or (match)", async () => {
[1, [isString(), isInteger()]], [1, [isString(), isInteger()]],
]; ];
for (const [value, validators] of values) { for (const [value, validators] of values) {
assertEquals(await validate(value, or(...validators)), []); assertEquals(await validate(value, or(...validators)), [], String(value));
} }
}); });
@@ -22,6 +22,10 @@ Deno.test("or (no match)", async () => {
[1, [isString()]], [1, [isString()]],
]; ];
for (const [value, validators] of values) { for (const [value, validators] of values) {
assertNotEquals(await validate(value, or(...validators)), []); assertNotEquals(
await validate(value, or(...validators)),
[],
String(value),
);
} }
}); });

View File

@@ -1,4 +1,4 @@
import { isNumber, isInteger } from "./number.ts"; import { isNumber, isInteger, isInRange, hasParity } from "./number.ts";
import { validate } from "../mod.ts"; import { validate } from "../mod.ts";
import { import {
assertEquals, assertEquals,
@@ -49,9 +49,13 @@ Deno.test("isInteger (match)", async () => {
-1, -1,
]; ];
for (const value of values) { for (const value of values) {
assertEquals(await validate(value, isInteger()), []); assertEquals(await validate(value, isInteger()), [], String(value));
} }
assertEquals(await validate(NaN, isInteger({ allowNaN: true })), []); assertEquals(
await validate(NaN, isInteger({ allowNaN: true })),
[],
String(NaN),
);
}); });
Deno.test("isInteger (no match)", async () => { Deno.test("isInteger (no match)", async () => {
@@ -67,7 +71,102 @@ Deno.test("isInteger (no match)", async () => {
0.1, 0.1,
]; ];
for (const value of values) { for (const value of values) {
assertNotEquals(await validate(value, isInteger()), []); assertNotEquals(await validate(value, isInteger()), [], String(value));
}
assertNotEquals(
await validate(NaN, isInteger({ allowNaN: false })),
[],
String(NaN),
);
});
Deno.test("isInRange (match)", async () => {
const values: [number[], object][] = [
[[10.5, 11], { greaterThan: 10 }],
[[10, 10.5, 11], { greaterThanOrEqualTo: 10 }],
[[10], { equalTo: 10 }],
[[9.5, 9], { lessThan: 10 }],
[[10, 9.5, 9], { lessThanOrEqualTo: 10 }],
[[11, 11.5], { greaterThan: 10, lessThan: 12 }],
[[10, 11, 11.5], { greaterThanOrEqualTo: 10, lessThan: 12 }],
[[NaN], { allowNaN: true }],
];
for (const [vals, options] of values) {
for (const value of vals) {
assertEquals(
await validate(value, isInRange(options)),
[],
`${String(value)} - ${JSON.stringify(options)}`,
);
}
}
});
Deno.test("isInRange (no match)", async () => {
const values: [number[], any][] = [
[[8.5, 9, 10], { greaterThan: 10 }],
[[8.5, 9], { greaterThanOrEqualTo: 10 }],
[[9, 9.5, 11], { equalTo: 10 }],
[[10, 10.5, 11], { lessThan: 10 }],
[[10.5, 11], { lessThanOrEqualTo: 10 }],
[[9.5, 10, 12, 13], { greaterThan: 10, lessThan: 12 }],
[[9.5, 12, 13], { greaterThanOrEqualTo: 10, lessThan: 12 }],
[[NaN], { allowNaN: false }],
[[NaN], {}],
];
for (const [vals, options] of values) {
for (const value of vals) {
assertNotEquals(
await validate(value, isInRange(options)),
[],
`${String(value)} - ${JSON.stringify(options)}`,
);
}
}
});
Deno.test("hasParity (match)", async () => {
const values: [number[], object][] = [
[[-2, -1, 0, 1, 2, 3, 4, 5, 6], {}],
[[-2, 0, 2, 4, 6], { odd: false }],
[[-1, 1, 3, 5], { even: false }],
[[0, 3, 6], { divisibleBy: 3 }],
[[], { odd: false, even: false }],
[[0, 6], { odd: false, divisibleBy: 3 }],
[[3], { even: false, divisibleBy: 3 }],
[[NaN], { allowNaN: true }],
];
for (const [vals, options] of values) {
for (const value of vals) {
assertEquals(
await validate(value, hasParity(options)),
[],
`${String(value)} - ${JSON.stringify(options)}`,
);
}
}
});
Deno.test("hasParity (no match)", async () => {
const values: [number[], any][] = [
[[], {}],
[[-1, 1, 3, 5], { odd: false }],
[[-2, 0, 2, 4, 6], { even: false }],
[[-2, -1, 1, 2, 4, 5], { divisibleBy: 3 }],
[[-2, -1, 0, 1, 2, 3, 4, 5, 6], { odd: false, even: false }],
[[-2, -1, 1, 2, 3, 4, 5], { odd: false, divisibleBy: 3 }],
[[-2, -1, 0, 1, 2, 4, 5, 6], { even: false, divisibleBy: 3 }],
[[NaN], { allowNaN: false }],
[[NaN], {}],
[[0.5], {}],
];
for (const [vals, options] of values) {
for (const value of vals) {
assertNotEquals(
await validate(value, hasParity(options)),
[],
`${String(value)} - ${JSON.stringify(options)}`,
);
}
} }
assertNotEquals(await validate(NaN, isInteger({ allowNaN: false })), []);
}); });

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;
@@ -35,3 +42,87 @@ export function isInteger(
}, },
}; };
} }
export function isInRange(
{
allowNaN = false,
greaterThan = undefined,
greaterThanOrEqualTo = undefined,
equalTo = undefined,
lessThan = undefined,
lessThanOrEqualTo = undefined,
}: {
allowNaN?: boolean;
greaterThan?: number;
greaterThanOrEqualTo?: number;
equalTo?: number;
lessThan?: number;
lessThanOrEqualTo?: number;
} = {},
): Validator {
return {
type: "isInRange",
extends: [isNumber({ allowNaN })],
check: (value: any) => {
if (value === null || value === undefined) return;
if (allowNaN && Number.isNaN(value)) return;
if (greaterThan !== undefined && value <= greaterThan) {
return { range: "greaterThan" };
}
if (greaterThanOrEqualTo !== undefined && value < greaterThanOrEqualTo) {
return { range: "greaterThanOrEqualTo" };
}
if (equalTo !== undefined && value !== equalTo) {
return { range: "equalTo" };
}
if (lessThan !== undefined && value >= lessThan) {
return { range: "lessThan" };
}
if (lessThanOrEqualTo !== undefined && value > lessThanOrEqualTo) {
return { range: "lessThanOrEqualTo" };
}
},
message: (value: any, args?: Args) => {
return `This value is not in the given range.`;
},
};
}
export function hasParity(
{
allowNaN = false,
odd = true,
even = true,
divisibleBy = undefined,
}: {
allowNaN?: boolean;
odd?: boolean;
even?: boolean;
divisibleBy?: number;
} = {},
): Validator {
return {
type: "hasParity",
extends: [isInteger({ allowNaN })],
check: (value: any) => {
if (value === null || value === undefined) return;
if (allowNaN && Number.isNaN(value)) return;
if (!odd && Math.abs(value % 2) == 1) {
return { partity: "odd" };
}
if (!even && value % 2 == 0) {
return { partity: "even" };
}
if (divisibleBy !== undefined && (value % divisibleBy !== 0)) {
return { divisibleBy };
}
},
message: (value: any, args?: Args) => {
if (args?.hasOwnProperty("divisibleBy")) {
return `This value is not divisible by ${divisibleBy}.`;
} else {
return `This value has a wrong partiy.`;
}
},
};
}

View File

@@ -1,4 +1,4 @@
import { isString, isUrl, isEmail } from "./string.ts"; import { isString, isUrl, isEmail, fulfillsRegex } from "./string.ts";
import { validate } from "../mod.ts"; import { validate } from "../mod.ts";
import { import {
assertEquals, assertEquals,
@@ -15,7 +15,7 @@ Deno.test("isString (match)", async () => {
new String("bar"), new String("bar"),
]; ];
for (const value of values) { for (const value of values) {
assertEquals(await validate(value, isString()), []); assertEquals(await validate(value, isString()), [], String(value));
} }
}); });
@@ -31,12 +31,14 @@ Deno.test("isString (no match)", async () => {
Symbol(), Symbol(),
]; ];
for (const value of values) { for (const value of values) {
assertNotEquals(await validate(value, isString()), []); assertNotEquals(await validate(value, isString()), [], String(value));
} }
}); });
Deno.test("isUrl (match)", async () => { Deno.test("isUrl (match)", async () => {
const values = [ const values = [
undefined,
null,
"http://google.com", "http://google.com",
"http://10.1.1.1", "http://10.1.1.1",
"http://10.1.1.254", "http://10.1.1.254",
@@ -47,7 +49,7 @@ Deno.test("isUrl (match)", async () => {
assertEquals( assertEquals(
await validate(value, isUrl({ allowLocal: true, allowDataUrl: true })), await validate(value, isUrl({ allowLocal: true, allowDataUrl: true })),
[], [],
"" + value, String(value),
); );
} }
}); });
@@ -66,12 +68,15 @@ Deno.test("isUrl (no match)", async () => {
assertNotEquals( assertNotEquals(
await validate(value, isUrl({ allowLocal: true })), await validate(value, isUrl({ allowLocal: true })),
[], [],
"" + value, String(value),
); );
} }
}); });
Deno.test("isEmail (match)", async () => { Deno.test("isEmail (match)", async () => {
const values = [ const values = [
undefined,
null,
"email@example.com", "email@example.com",
"firstname.lastname@example.com", "firstname.lastname@example.com",
"email@subdomain.example.com", "email@subdomain.example.com",
@@ -88,11 +93,7 @@ Deno.test("isEmail (match)", async () => {
"firstname-lastname@example.com", "firstname-lastname@example.com",
]; ];
for (const value of values) { for (const value of values) {
assertEquals( assertEquals(await validate(value, isEmail()), [], String(value));
await validate(value, isEmail()),
[],
"" + value,
);
} }
}); });
@@ -117,10 +118,40 @@ Deno.test("isEmail (no match)", async () => {
"Abc..123@example.com", "Abc..123@example.com",
]; ];
for (const value of values) { for (const value of values) {
assertNotEquals( assertNotEquals(await validate(value, isEmail()), [], String(value));
await validate(value, isEmail()), }
});
Deno.test("fulfillsRegex (match)", async () => {
const values: [any, RegExp][] = [
["abc", /[a-z]+/],
[null, /[a-z]+/],
[undefined, /[a-z]+/],
["123abc", /[a-z]+/],
["abc", /^[a-z]+$/],
["^ab$", /^\^(ab)|(cd)\$$/],
];
for (const [value, regex] of values) {
assertEquals(
await validate(value, fulfillsRegex({ regex })),
[], [],
"" + value, `${String(value)} - ${regex}`,
);
}
});
Deno.test("fulfillsRegex (no match)", async () => {
const values: [any, RegExp][] = [
[Symbol(), /[a-z]+/],
["", /[a-z]+/],
["abc123", /^[a-z]+$/],
["^abcd$", /^\^(ab|cd)\$$/],
];
for (const [value, regex] of values) {
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 =
@@ -140,3 +149,19 @@ export function isEmail(): Validator {
}, },
}; };
} }
export function fulfillsRegex({ regex }: { regex: RegExp }): Validator {
return {
type: "fulfillsRegex",
extends: [isString()],
check: (value: any) => {
if (value === null || value === undefined) return;
if (!value.match(regex)) {
return {};
}
},
message: (value: any, args?: Args) => {
return `This value has to fulfill the regex ${regex}.`;
},
};
}

View File

@@ -12,7 +12,7 @@ Deno.test("isSymbol (match)", async () => {
Symbol(), Symbol(),
]; ];
for (const value of values) { for (const value of values) {
assertEquals(await validate(value, isSymbol()), []); assertEquals(await validate(value, isSymbol()), [], String(value));
} }
}); });
@@ -31,6 +31,6 @@ Deno.test("isSymbol (no match)", async () => {
new Object(), new Object(),
]; ];
for (const value of values) { for (const value of values) {
assertNotEquals(await validate(value, isSymbol()), []); assertNotEquals(await validate(value, isSymbol()), [], String(value));
} }
}); });