import type { SchemaReference } from "./schema_reference.ts";
type ValidationFN = <T extends Schema.BasicType>(value: unknown, field: Schema.AdvancedType<T>) => string[]
type ValidatorCallback = (input: boolean, error: string) => void
type ValidatorChainFN = <T extends Schema.BasicType>(validation_fn: ValidationFN, value: unknown, field: Schema.AdvancedType<T>) => void
type ValidatorCallbackComplex = ValidatorCallback & { chain: ValidatorChainFN, addErrors: (errors: string[]) => void }
const Validator = (
validationCallback: (
validate: ValidatorCallbackComplex
) => void
) => {
const errors: string[] = [];
validationCallback(
Object.apply({
chain: ((validation_fn, value, field) => {
errors.push(...validation_fn(value, field));
}) as ValidatorChainFN,
addErrors: (errors: string[]) => {
errors.push(...errors);
}
},
[
(input, error) => {
if(!input) errors.push(error);
},
] as [ValidatorCallback]
)
)
return errors;
}
type PluginValidatorFn<T extends Schema.BasicType> =
(value: unknown, field: Schema.AdvancedType<T> & { required: boolean, type: T }) => string[];
type PluginValidators =
{ [K in Schema.BasicType]: PluginValidatorFn<K> } & {
all: <T extends Schema.BasicType>(value: unknown, field: Schema.AdvancedType<T>) => string[];
};
class PluginSchema<T extends Schema.Object> {
readonly schema: Schema.Normalized<T>
constructor(schema: Schema.Normalized<T>) {
this.schema = Object.freeze(schema);
}
IsValid(input: Record<string, unknown>): input is T {
return this.Validate(input).success;
}
Validate(input: Record<string, unknown>) {
return PluginSchema.Validate(this, input);
}
private static ValidationMap: PluginValidators = {
all: (value, field) => {
return Validator(validate => {
if(field.required) validate(value !== undefined, "this field is required");
})
},
boolean(value, _field) {
return Validator(validate => {
validate(typeof value === 'boolean', "value must be a boolean");
})
},
date(_value, _field) {
return [];
},
email(_value) { return [] },
number(_value) { return [] },
password(value, field) {
return Validator(validate => {
if(typeof value === 'string') {
if(field.length?.min) validate(value.length >= field.length.min, 'password requires a lowercase character');
if(field.requirements?.lowercase) validate(!!value.match(/[a-z]/), 'password requires a lowercase character');
if(field.requirements?.uppercase) validate(!!value.match(/[A-Z]/), 'password requires an uppercase character');
if(field.requirements?.number) validate(!!value.match(/[0-9]/), 'password requires a number');
if(field.requirements?.special_character) validate(!!value.match(/[^\w\s]/), 'password requires a special character');
if(field.custom_validation) validate.addErrors(field.custom_validation(value));
}
})
},
string(_value) { return [] },
msisdn() { return [] },
}
static Validate<T extends Schema.Object>(plugin_schema: PluginSchema<T>, input: Record<string, unknown>): Schema.Validation<T> {
const errors: Record<string, string[]>[] = [];
const entries = Object.entries(plugin_schema.schema) as [string, Schema.Field.Normalized][];
for(const [key, field] of entries) {
const field_errors: string[] = [];
field_errors.push(...PluginSchema.ValidationMap.all(input[key], field));
field_errors.push(...(PluginSchema.ValidationMap[field.type] as PluginValidatorFn<Schema.BasicType>)(input[key], field));
if(field_errors.length > 0) errors.push({ [key]: field_errors });
}
return {
success: errors.length === 0,
errors: errors.length === 0 ? null : Object.assign({}, ...errors)
};
}
static Normalize<T extends Schema.Object>(schema: T): Schema.Normalized<T> {
return Object.assign({}, ...Object.entries(schema).map(([key, field]) => ({[key]: this.NormalizeField(field)})));
}
static NormalizeField<T extends Schema.Field>(field: T) {
if (typeof field === "string") {
return { type: (field as Schema.BasicType), required: true } as Schema.Field.Normalize<T>;
}
return {
...(field as Schema.AdvancedType),
required: field.required ?? true,
} as Schema.Field.Normalize<T>;
}
static Create = <S extends Schema.Object>(schema: S) => new PluginSchema(PluginSchema.Normalize(schema));
}
type SchemaModule = typeof PluginSchema & (<S extends Schema.Object>(schema: S) => PluginSchema<S>)
export const Schema = Object.apply(
PluginSchema,
[PluginSchema.Create]
) as SchemaModule;
export declare namespace Schema {
type Class<T extends Schema.Object> = typeof PluginSchema & { new: (...params: unknown[]) => PluginSchema<T> };
type Base<T extends BasicType> = {
type: T;
required?: boolean;
nullable?: boolean;
default?: Maps.TypeInfer[T] | (() => Maps.TypeInfer[T]);
placeholder?: string;
description?: string;
}
namespace Reference {
type Config = SchemaReference.Config
}
namespace Maps {
type AdvancedType = {
"string": {
length?: number | { min?: number, max?: number }
}
// deno-lint-ignore ban-types
"boolean": {}
"number": {
min?: number
max?: number
integer?: number
}
"date": {
range?: boolean
min?: Date | (() => Date)
max?: Date | (() => Date)
}
"password": {
length?: { min?: number, max?: number }
requirements?: {
uppercase?: boolean
lowercase?: boolean
number?: boolean
special_character?: boolean
}
/** Custom validation steps
*
* Should return an array of error messages. If the array is empty, validation will succeeded.
*
* **Do not use custom validation for:**
* - Missing values marked as required
* - Min/Max length
* - Character requirements
*
* *Standard validation will still run*.
*/
custom_validation?: (input: string) => string[]
}
// deno-lint-ignore ban-types
"email": {}
// deno-lint-ignore ban-types
"msisdn": {}
}
type TypeInfer = {
string: string;
boolean: boolean;
number: number;
date: Date;
email: `${string}@${string}`;
password: string;
msisdn: string;
};
}
namespace Field {
type InferType<F> =
F extends { type: infer T }
? T extends keyof Maps.TypeInfer
? Maps.TypeInfer[T]
: never
: F extends keyof Maps.TypeInfer
? Maps.TypeInfer[F]
: never;
type IsOptional<F> =
F extends { required: false } ? true : false;
type IsNullable<F> =
F extends { nullable: true } ? true : false;
type Normalize<F extends Field> =
F extends Schema.BasicType
? Schema.AdvancedType<F> & { required: true }
: F extends Schema.AdvancedType
? F & { required: boolean }
: never;
type Normalized =
{ [K in Schema.BasicType]:
Schema.AdvancedType<K> & { required: boolean; type: K }
}[Schema.BasicType];
}
// deno-lint-ignore no-explicit-any
type InferClass<PS extends PluginSchema<any>, S = PS['schema']> = {
[K in keyof S as Field.IsOptional<S[K]> extends true ? never : K]:
Field.IsNullable<S[K]> extends true
? Field.InferType<S[K]> | null
: Field.InferType<S[K]>
} & {
[K in keyof S as Field.IsOptional<S[K]> extends true ? K : never]?:
Field.IsNullable<S[K]> extends true
? Field.InferType<S[K]> | null
: Field.InferType<S[K]>
};
type Infer<S extends Schema.Object> = {
[K in keyof S as Field.IsOptional<S[K]> extends true ? never : K]:
Field.IsNullable<S[K]> extends true
? Field.InferType<S[K]> | null
: Field.InferType<S[K]>
} & {
[K in keyof S as Field.IsOptional<S[K]> extends true ? K : never]?:
Field.IsNullable<S[K]> extends true
? Field.InferType<S[K]> | null
: Field.InferType<S[K]>
};
type Normalized<S extends Object> = {
[F in keyof S]:
S[F] extends Field
? Field.Normalize<S[F]>
: never;
}
type BasicType = keyof Maps.AdvancedType;
type AdvancedType<T extends BasicType = BasicType> = Base<T> & Maps.AdvancedType[T];
type ReferenceType<T extends SchemaReference.Types> = SchemaReference.Base<T>;
type AdvancedField<K extends Schema.BasicType = Schema.BasicType> =
Schema.AdvancedType<K> & { type: K; $ref?: never };
type ReferenceField<R extends SchemaReference.Types = SchemaReference.Types> =
SchemaReference.Base<R> & { $ref: R; type?: never };
type Field = Schema.BasicType | AdvancedField;
type Object = { [key: string]: Field }
type Validation<T> = Validation.Response<T>
namespace Validation {
type Response<T> = Success<T> | Fail<T>
interface Base<T> {
success: boolean
errors: undefined | {[P in keyof T]: string[]}
}
type Success<T> = Base<T> & {
success: true
errors: undefined
}
type Fail<T> = Base<T> & {
success: false
errors: {[P in keyof T]: string[]}
}
}
}