working on validator
This commit is contained in:
@ -88,6 +88,7 @@ class StaticNode<T> implements Node<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: get rid of fixed param name
|
||||||
class DynamicNode<T> extends StaticNode<T> implements Node<T> {
|
class DynamicNode<T> extends StaticNode<T> implements Node<T> {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly paramName: string,
|
public readonly paramName: string,
|
||||||
|
|||||||
711
server/src/lib/test.ts
Normal file
711
server/src/lib/test.ts
Normal file
@ -0,0 +1,711 @@
|
|||||||
|
import { err, ok, Result } from "@shared/utils/result.ts";
|
||||||
|
import { none, Option, some } from "@shared/utils/option.ts";
|
||||||
|
|
||||||
|
class ParseError extends Error {
|
||||||
|
type = "ParseError";
|
||||||
|
constructor(
|
||||||
|
public readonly input: any,
|
||||||
|
public readonly msg: string,
|
||||||
|
) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pe(input: unknown, msg: string) {
|
||||||
|
return new ParseError(input, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Schema<T> {
|
||||||
|
parse(input: unknown): Result<T, ParseError>;
|
||||||
|
checkIfValid(input: unknown): boolean;
|
||||||
|
nullable(): NullableSchema<this>;
|
||||||
|
option(): OptionSchema<T>;
|
||||||
|
or<S extends Schema<any>[]>(...schema: S): UnionSchema<[this, ...S]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckFunction<T> = (input: T) => ParseError | void;
|
||||||
|
|
||||||
|
export abstract class BaseSchema<T> implements Schema<T> {
|
||||||
|
protected checks: CheckFunction<T>[] = [];
|
||||||
|
|
||||||
|
public addCheck(check: CheckFunction<T>): this {
|
||||||
|
this.checks.push(check);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected runChecks(input: T): Result<T, ParseError> {
|
||||||
|
for (const check of this.checks) {
|
||||||
|
const error = check(input);
|
||||||
|
if (error) {
|
||||||
|
return err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ok(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIfValid(input: unknown): boolean {
|
||||||
|
return this.parse(input).isOk();
|
||||||
|
}
|
||||||
|
|
||||||
|
nullable(): NullableSchema<this> {
|
||||||
|
return new NullableSchema(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
or<S extends Schema<any>[]>(...schema: S): UnionSchema<[this, ...S]> {
|
||||||
|
return new UnionSchema(this, ...schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
option(): OptionSchema<T> {
|
||||||
|
return new OptionSchema(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract parse(input: unknown): Result<T, ParseError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class PrimitiveSchema<
|
||||||
|
T extends
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| bigint
|
||||||
|
| undefined
|
||||||
|
| null
|
||||||
|
| object
|
||||||
|
| void
|
||||||
|
| any
|
||||||
|
| unknown
|
||||||
|
| never,
|
||||||
|
> extends BaseSchema<T> {
|
||||||
|
protected abstract initialCheck(input: unknown): Result<T, ParseError>;
|
||||||
|
|
||||||
|
protected checkPrimitive<U = T>(
|
||||||
|
input: unknown,
|
||||||
|
type:
|
||||||
|
| "string"
|
||||||
|
| "number"
|
||||||
|
| "boolean"
|
||||||
|
| "bigint"
|
||||||
|
| "undefined"
|
||||||
|
| "object"
|
||||||
|
| "symbol"
|
||||||
|
| "funciton",
|
||||||
|
): Result<U, ParseError> {
|
||||||
|
const inputType = typeof input;
|
||||||
|
|
||||||
|
if (inputType === type) {
|
||||||
|
return ok(input as U);
|
||||||
|
}
|
||||||
|
return err(
|
||||||
|
pe(input, `Expected '${type}', received '${inputType}'`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public parse(input: unknown): Result<T, ParseError> {
|
||||||
|
return this.initialCheck(input).andThen((input) => {
|
||||||
|
for (const check of this.checks) {
|
||||||
|
const e = check(input);
|
||||||
|
|
||||||
|
if (e) {
|
||||||
|
return err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(input);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StringSchema extends PrimitiveSchema<string> {
|
||||||
|
private static readonly emailRegex =
|
||||||
|
/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; // https://stackoverflow.com/questions/46155/how-can-i-validate-an-email-address-in-javascript
|
||||||
|
|
||||||
|
private static readonly ipRegex =
|
||||||
|
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; // https://stackoverflow.com/questions/4460586/javascript-regular-expression-to-check-for-ip-addresses
|
||||||
|
|
||||||
|
protected override initialCheck(
|
||||||
|
input: unknown,
|
||||||
|
): Result<string, ParseError> {
|
||||||
|
return this.checkPrimitive(input, "string");
|
||||||
|
}
|
||||||
|
|
||||||
|
public max(
|
||||||
|
length: number,
|
||||||
|
msg: string = `String length must be at most ${length} characters long`,
|
||||||
|
): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
input.length < length ? pe(input, msg) : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public min(
|
||||||
|
length: number,
|
||||||
|
msg: string =
|
||||||
|
`String length must be at least ${length} characters long`,
|
||||||
|
): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
input.length < length ? pe(input, msg) : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public regex(
|
||||||
|
pattern: RegExp,
|
||||||
|
msg: string = `String must match the patter ${String(pattern)}`,
|
||||||
|
): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
pattern.test(input) ? undefined : pe(input, msg)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public email(
|
||||||
|
msg: string = `String must match a valid email address`,
|
||||||
|
): this {
|
||||||
|
return this.regex(StringSchema.emailRegex, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ip(
|
||||||
|
msg: string = `String must match a valid ip address`,
|
||||||
|
): this {
|
||||||
|
return this.regex(StringSchema.ipRegex, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NumberSchema extends PrimitiveSchema<number> {
|
||||||
|
protected override initialCheck(
|
||||||
|
input: unknown,
|
||||||
|
): Result<number, ParseError> {
|
||||||
|
return this.checkPrimitive(input, "number");
|
||||||
|
}
|
||||||
|
|
||||||
|
gt(num: number, msg: string = `Number must be greater than ${num}`): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
input <= num ? pe(input, msg) : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
gte(
|
||||||
|
num: number,
|
||||||
|
msg: string = `Number must be greater than or equal to ${num}`,
|
||||||
|
): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
input < num ? pe(input, msg) : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
lt(num: number, msg: string = `Number must be less than ${num}`): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
input >= num ? pe(input, msg) : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
lte(
|
||||||
|
num: number,
|
||||||
|
msg: string = `Number must be less than or equal to ${num}`,
|
||||||
|
): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
input > num ? pe(input, msg) : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int(msg: string = "Number must be an integer"): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
Number.isInteger(input) ? pe(input, msg) : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
positive(msg: string = "Number must be positive"): this {
|
||||||
|
return this.gte(0, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
nonnegative(msg: string = "Number must be nonnegative"): this {
|
||||||
|
return this.gt(0, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
negative(msg: string = "Number must be negative"): this {
|
||||||
|
return this.lt(0, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
nonpositive(msg: string = "Number must be nonpositive"): this {
|
||||||
|
return this.lte(0, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
finite(msg: string = "Number must be finite"): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
Number.isFinite(input) ? undefined : pe(input, msg)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
safe(msg: string = "Number must be a safe integer"): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
Number.isSafeInteger(input) ? undefined : pe(input, msg)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
multipleOf(
|
||||||
|
num: number,
|
||||||
|
msg: string = `Number must be a multiple of ${num}`,
|
||||||
|
): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
input % num ? undefined : pe(input, msg)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BigintSchema extends PrimitiveSchema<bigint> {
|
||||||
|
protected override initialCheck(
|
||||||
|
input: unknown,
|
||||||
|
): Result<bigint, ParseError> {
|
||||||
|
return this.checkPrimitive(input, "bigint");
|
||||||
|
}
|
||||||
|
|
||||||
|
gt(
|
||||||
|
num: bigint | number,
|
||||||
|
msg: string = `Bigint must be greater than ${num}`,
|
||||||
|
): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
input <= num ? pe(input, msg) : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
gte(
|
||||||
|
num: bigint | number,
|
||||||
|
msg: string = `Bigint must be greater than or equal to ${num}`,
|
||||||
|
): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
input < num ? pe(input, msg) : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
lt(
|
||||||
|
num: bigint | number,
|
||||||
|
msg: string = `Bigint must be less than ${num}`,
|
||||||
|
): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
input >= num ? pe(input, msg) : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
lte(
|
||||||
|
num: bigint | number,
|
||||||
|
msg: string = `Bigint must be less than or equal to ${num}`,
|
||||||
|
): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
input > num ? pe(input, msg) : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int(msg: string = "Bigint must be an integer"): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
Number.isInteger(input) ? pe(input, msg) : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
positive(msg: string = "Bigint must be positive"): this {
|
||||||
|
return this.gte(0, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
nonnegative(msg: string = "Bigint must be nonnegative"): this {
|
||||||
|
return this.gt(0, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
negative(msg: string = "Bigint must be negative"): this {
|
||||||
|
return this.lt(0, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
nonpositive(msg: string = "Bigint must be nonpositive"): this {
|
||||||
|
return this.lte(0, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
finite(msg: string = "Bigint must be finite"): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
Number.isFinite(input) ? undefined : pe(input, msg)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
safe(msg: string = "Bigint must be a safe integer"): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
Number.isSafeInteger(input) ? undefined : pe(input, msg)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
multipleOf(
|
||||||
|
num: bigint,
|
||||||
|
msg: string = `Bigint must be a multiple of ${num}`,
|
||||||
|
): this {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
input % num ? undefined : pe(input, msg)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BooleanSchema extends PrimitiveSchema<boolean> {
|
||||||
|
protected override initialCheck(
|
||||||
|
input: unknown,
|
||||||
|
): Result<boolean, ParseError> {
|
||||||
|
return this.checkPrimitive(input, "boolean");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DateSchema extends PrimitiveSchema<object> {
|
||||||
|
protected override initialCheck(
|
||||||
|
input: unknown,
|
||||||
|
): Result<Date, ParseError> {
|
||||||
|
return this.checkPrimitive(input, "object").andThen((obj) => {
|
||||||
|
if (obj instanceof Date) {
|
||||||
|
return ok(obj);
|
||||||
|
}
|
||||||
|
return err(
|
||||||
|
pe(
|
||||||
|
input,
|
||||||
|
`Expected instance of Date, received ${obj.constructor.name}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
min(
|
||||||
|
date: Date,
|
||||||
|
msg: string = `Date must be after ${date.toLocaleString()}`,
|
||||||
|
) {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
input <= date ? pe(input, msg) : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
max(
|
||||||
|
date: Date,
|
||||||
|
msg: string = `Date must be before ${date.toLocaleString()}`,
|
||||||
|
) {
|
||||||
|
return this.addCheck((input) =>
|
||||||
|
input >= date ? pe(input, msg) : undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UndefinedSchema extends PrimitiveSchema<undefined> {
|
||||||
|
protected override initialCheck(
|
||||||
|
input: unknown,
|
||||||
|
): Result<undefined, ParseError> {
|
||||||
|
return this.checkPrimitive(input, "undefined");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NullSchema extends PrimitiveSchema<null> {
|
||||||
|
protected override initialCheck(
|
||||||
|
input: unknown,
|
||||||
|
): Result<null, ParseError> {
|
||||||
|
if (input === null) {
|
||||||
|
return ok(input);
|
||||||
|
}
|
||||||
|
return err(pe(input, "Expected 'null', received '${typeof input}'"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class VoidSchema extends PrimitiveSchema<void> {
|
||||||
|
protected override initialCheck(input: unknown): Result<void, ParseError> {
|
||||||
|
if (input !== undefined && input !== null) {
|
||||||
|
return err(
|
||||||
|
pe(input, `Expected 'void', received '${typeof input}'`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AnySchema extends PrimitiveSchema<any> {
|
||||||
|
protected override initialCheck(input: unknown): Result<any, ParseError> {
|
||||||
|
return ok(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UnknownSchema extends PrimitiveSchema<unknown> {
|
||||||
|
protected override initialCheck(
|
||||||
|
input: unknown,
|
||||||
|
): Result<unknown, ParseError> {
|
||||||
|
return ok(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type InferSchema<S> = S extends Schema<infer T> ? T : never;
|
||||||
|
|
||||||
|
class ObjectSchema<O extends Record<string, Schema<any>>>
|
||||||
|
extends PrimitiveSchema<{ [K in keyof O]: InferSchema<O[K]> }> {
|
||||||
|
private strict: boolean = false;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly schema: O,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override initialCheck(
|
||||||
|
input: unknown,
|
||||||
|
): Result<{ [K in keyof O]: InferSchema<O[K]> }, ParseError> {
|
||||||
|
return this.checkPrimitive<object>(input, "object").andThen(
|
||||||
|
(objPrimitive) => {
|
||||||
|
let obj = objPrimitive as Record<string, any>;
|
||||||
|
let parsedObj: Record<string, any> = {};
|
||||||
|
|
||||||
|
for (const [key, schema] of Object.entries(this.schema)) {
|
||||||
|
const value = obj[key];
|
||||||
|
|
||||||
|
const checkResult = schema.parse(value);
|
||||||
|
|
||||||
|
if (checkResult.isErr()) {
|
||||||
|
return err(
|
||||||
|
pe(
|
||||||
|
input,
|
||||||
|
`Failed to parse '${key}' attribute: ${checkResult.error.msg}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedObj[key] = checkResult.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(parsedObj as { [K in keyof O]: InferSchema<O[K]> });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NullableSchema<S extends Schema<any>>
|
||||||
|
extends PrimitiveSchema<InferSchema<S> | void> {
|
||||||
|
private static readonly voidSchema = new VoidSchema();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly schema: S,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override initialCheck(
|
||||||
|
input: unknown,
|
||||||
|
): Result<void | InferSchema<S>, ParseError> {
|
||||||
|
if (NullableSchema.voidSchema.checkIfValid(input)) {
|
||||||
|
return ok();
|
||||||
|
}
|
||||||
|
return this.schema.parse(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LiteralSchema<L extends string> extends PrimitiveSchema<L> {
|
||||||
|
constructor(
|
||||||
|
private readonly literal: L,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override initialCheck(input: unknown): Result<L, ParseError> {
|
||||||
|
if (input === this.literal) {
|
||||||
|
return ok(this.literal);
|
||||||
|
}
|
||||||
|
return err(pe(input, `Input must match literal '${this.literal}'`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type InferSchemaUnion<S extends Schema<any>[]> = S[number] extends
|
||||||
|
Schema<infer U> ? U : never;
|
||||||
|
|
||||||
|
class UnionSchema<S extends Schema<any>[]>
|
||||||
|
extends PrimitiveSchema<InferSchemaUnion<S>> {
|
||||||
|
private readonly schemas: S;
|
||||||
|
|
||||||
|
constructor(...schemas: S) {
|
||||||
|
super();
|
||||||
|
this.schemas = schemas;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override initialCheck(
|
||||||
|
input: unknown,
|
||||||
|
): Result<InferSchemaUnion<S>, ParseError> {
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
for (const schema of this.schemas) {
|
||||||
|
const checkResult = schema.parse(input);
|
||||||
|
|
||||||
|
if (checkResult.isOk()) {
|
||||||
|
return ok(checkResult.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
errors.push(
|
||||||
|
`${schema.constructor.name} - ${checkResult.error.msg}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return err(
|
||||||
|
pe(
|
||||||
|
input,
|
||||||
|
[
|
||||||
|
"No matching schema found for a union:",
|
||||||
|
errors.join("\n"),
|
||||||
|
].join("\n"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ArraySchema<S extends Schema<any>>
|
||||||
|
extends PrimitiveSchema<InferSchema<S>[]> {
|
||||||
|
constructor(
|
||||||
|
private readonly schema: S,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override initialCheck(
|
||||||
|
input: unknown[],
|
||||||
|
): Result<InferSchema<S>[], ParseError> {
|
||||||
|
const parsed = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
|
const r = this.schema.parse(input[i]);
|
||||||
|
|
||||||
|
if (r.isErr()) {
|
||||||
|
return err(
|
||||||
|
pe(
|
||||||
|
input,
|
||||||
|
`Element at index ${i} does not conform to schema:\n${r.error.msg}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed.push(r.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ResultSchema<T, E> extends PrimitiveSchema<Result<T, E>> {
|
||||||
|
private schema;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly valueSchema: Schema<T>,
|
||||||
|
private readonly errorSchema: Schema<E>,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.schema = new UnionSchema(
|
||||||
|
new ObjectSchema({
|
||||||
|
tag: new LiteralSchema("ok"),
|
||||||
|
value: valueSchema,
|
||||||
|
}),
|
||||||
|
new ObjectSchema({
|
||||||
|
tag: new LiteralSchema("err"),
|
||||||
|
error: errorSchema,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override initialCheck(
|
||||||
|
input: unknown,
|
||||||
|
): Result<Result<T, E>, ParseError> {
|
||||||
|
return this.schema.parse(input).map((result) => {
|
||||||
|
switch (result.tag) {
|
||||||
|
case "ok":
|
||||||
|
return ok(result.value);
|
||||||
|
case "err":
|
||||||
|
return err(result.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class OptionSchema<T> extends PrimitiveSchema<Option<T>> {
|
||||||
|
private schema;
|
||||||
|
|
||||||
|
constructor(private readonly valueSchema: Schema<T>) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.schema = new UnionSchema(
|
||||||
|
new ObjectSchema({
|
||||||
|
tag: new LiteralSchema("some"),
|
||||||
|
value: valueSchema,
|
||||||
|
}),
|
||||||
|
new ObjectSchema({
|
||||||
|
tag: new LiteralSchema("none"),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override initialCheck(
|
||||||
|
input: unknown,
|
||||||
|
): Result<Option<T>, ParseError> {
|
||||||
|
return this.schema.parse(input).map((option) => {
|
||||||
|
switch (option.tag) {
|
||||||
|
case "some":
|
||||||
|
return some(option.value);
|
||||||
|
case "none":
|
||||||
|
return none;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Validator {
|
||||||
|
string(): StringSchema {
|
||||||
|
return new StringSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
literal<L extends string>(literal: L): LiteralSchema<L> {
|
||||||
|
return new LiteralSchema(literal);
|
||||||
|
}
|
||||||
|
|
||||||
|
number(): NumberSchema {
|
||||||
|
return new NumberSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
bigint(): BigintSchema {
|
||||||
|
return new BigintSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean(): BooleanSchema {
|
||||||
|
return new BooleanSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
date(): DateSchema {
|
||||||
|
return new DateSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
undefined(): UndefinedSchema {
|
||||||
|
return new UndefinedSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
null(): NullSchema {
|
||||||
|
return new NullSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
void(): VoidSchema {
|
||||||
|
return new VoidSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
any(): AnySchema {
|
||||||
|
return new AnySchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
unknown(): UnknownSchema {
|
||||||
|
return new UnknownSchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
union<S extends Schema<any>[]>(...schemas: S): UnionSchema<S> {
|
||||||
|
return new UnionSchema(...schemas);
|
||||||
|
}
|
||||||
|
|
||||||
|
array<S extends Schema<any>>(elementSchema: S): ArraySchema<S> {
|
||||||
|
return new ArraySchema(elementSchema);
|
||||||
|
}
|
||||||
|
|
||||||
|
result<T, E>(
|
||||||
|
valueSchema: Schema<T>,
|
||||||
|
errorSchema: Schema<E>,
|
||||||
|
): ResultSchema<T, E> {
|
||||||
|
return new ResultSchema(valueSchema, errorSchema);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const v = new Validator();
|
||||||
|
|
||||||
|
const r = v.array(v.union(v.string(), v.number().gt(5)));
|
||||||
|
|
||||||
|
console.log(r.parse(["5", true]));
|
||||||
344
server/src/lib/validator.ts
Normal file
344
server/src/lib/validator.ts
Normal file
@ -0,0 +1,344 @@
|
|||||||
|
import { err, ok, Result } from "@shared/utils/result.ts";
|
||||||
|
import console from "node:console";
|
||||||
|
|
||||||
|
class ParseError extends Error {
|
||||||
|
code = "ParseError";
|
||||||
|
constructor(
|
||||||
|
public readonly input: any,
|
||||||
|
public readonly msg: string,
|
||||||
|
) {
|
||||||
|
super(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pe(input: any, msg: string): ParseError {
|
||||||
|
return new ParseError(input, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
type CheckFunction<T> = (
|
||||||
|
input: T,
|
||||||
|
msg?: string,
|
||||||
|
) => ParseError | void;
|
||||||
|
|
||||||
|
export abstract class BaseSchema<T> {
|
||||||
|
private checks: CheckFunction<T>[] = [];
|
||||||
|
abstract initialCheck(input: unknown): Result<T, ParseError>;
|
||||||
|
public addCheck(check: CheckFunction<T>) {
|
||||||
|
this.checks.push(check);
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(input: any): Result<T, ParseError> {
|
||||||
|
return this.initialCheck(input).andThen((input) => {
|
||||||
|
for (const check of this.checks) {
|
||||||
|
const e = check(input);
|
||||||
|
if (e) {
|
||||||
|
return err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ok(input);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StringSchema extends BaseSchema<string> {
|
||||||
|
private static readonly emailRegex =
|
||||||
|
/^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i; // https://stackoverflow.com/questions/46155/how-can-i-validate-an-email-address-in-javascript
|
||||||
|
|
||||||
|
private static readonly ipRegex =
|
||||||
|
/^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; // https://stackoverflow.com/questions/4460586/javascript-regular-expression-to-check-for-ip-addresses
|
||||||
|
|
||||||
|
private initialCheck(input: unknown): Result<string, ParseError> {
|
||||||
|
if (typeof input !== "string") {
|
||||||
|
return err(pe(input, `Expected string, received ${typeof input}`));
|
||||||
|
}
|
||||||
|
return ok(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
max(
|
||||||
|
length: number,
|
||||||
|
msg: string = `String length must be at most ${length} characters long`,
|
||||||
|
): this {
|
||||||
|
this.addCheck((input) => {
|
||||||
|
if (input.length > length) {
|
||||||
|
return pe(
|
||||||
|
input,
|
||||||
|
msg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
min(
|
||||||
|
length: number,
|
||||||
|
msg: string =
|
||||||
|
`String length must be at least ${length} characters long`,
|
||||||
|
): this {
|
||||||
|
this.addCheck((input) => {
|
||||||
|
if (input.length < length) {
|
||||||
|
return pe(
|
||||||
|
input,
|
||||||
|
msg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
regex(
|
||||||
|
pattern: RegExp,
|
||||||
|
msg: string = `String must match the pattern ${String(pattern)}`,
|
||||||
|
): this {
|
||||||
|
this.addCheck((input) => {
|
||||||
|
if (!pattern.test(input)) {
|
||||||
|
return pe(
|
||||||
|
input,
|
||||||
|
msg,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
email(msg: string = `String must be a valid email address`): this {
|
||||||
|
return this.regex(StringSchema.emailRegex, msg);
|
||||||
|
}
|
||||||
|
ip(msg: string = "String must be a valid ip address"): this {
|
||||||
|
return this.regex(StringSchema.ipRegex, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NumberSchema extends BaseSchema<number> {
|
||||||
|
private initialCheck(input: unknown): Result<number, ParseError> {
|
||||||
|
if (typeof input !== "number") {
|
||||||
|
return err(pe(input, `Expected number, recieved ${typeof input}`));
|
||||||
|
}
|
||||||
|
return ok(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
gt(num: number, msg: string = `Number must be greater than ${num}`): this {
|
||||||
|
this.addCheck((input) => {
|
||||||
|
if (input <= num) return pe(input, msg);
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
gte(
|
||||||
|
num: number,
|
||||||
|
msg: string = `Number must be greater than or equal to ${num}`,
|
||||||
|
): this {
|
||||||
|
this.addCheck((input) => {
|
||||||
|
if (input < num) return pe(input, msg);
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
lt(num: number, msg: string = `Number must be less than ${num}`): this {
|
||||||
|
this.addCheck((input) => {
|
||||||
|
if (input >= num) return pe(input, msg);
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
lte(
|
||||||
|
num: number,
|
||||||
|
msg: string = `Number must be less than or equal to ${num}`,
|
||||||
|
): this {
|
||||||
|
this.addCheck((input) => {
|
||||||
|
if (input > num) return pe(input, msg);
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
int(msg: string = "Number must be an integer"): this {
|
||||||
|
this.addCheck((input) => {
|
||||||
|
if (!Number.isInteger(input)) return pe(input, msg);
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
positive(msg: string = "Number must be positive"): this {
|
||||||
|
return this.gte(0, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
nonnegative(msg: string = "Number must be nonnegative"): this {
|
||||||
|
return this.gt(0, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
negative(msg: string = "Number must be negative"): this {
|
||||||
|
return this.lt(0, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
nonpositive(msg: string = "Number must be nonpositive"): this {
|
||||||
|
return this.lte(0, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BooleanSchema extends BaseSchema<boolean> {
|
||||||
|
private initialCheck(input: unknown): Result<boolean, ParseError> {
|
||||||
|
if (typeof input !== "boolean") {
|
||||||
|
return err(pe(input, `Expected boolean, received ${typeof input}`));
|
||||||
|
}
|
||||||
|
return ok(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DateSchema extends BaseSchema<Date> {
|
||||||
|
override initialCheck(input: unknown): Result<Date, ParseError> {
|
||||||
|
if (typeof input === "object" && input !== null) {
|
||||||
|
if (input instanceof Date) {
|
||||||
|
return ok(input);
|
||||||
|
}
|
||||||
|
// TODO: add check for an non Date instance. Right now lsp freaks out for some reason
|
||||||
|
}
|
||||||
|
|
||||||
|
return err(pe(input, `Expected object, received ${typeof input}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
min(
|
||||||
|
date: Date,
|
||||||
|
msg: string = `Date must come after ${date.toLocaleString()}`,
|
||||||
|
) {
|
||||||
|
this.addCheck((input) => {
|
||||||
|
if (input < date) {
|
||||||
|
return pe(input, msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
max(
|
||||||
|
date: Date,
|
||||||
|
msg: string = `Date must come before ${date.toLocaleString()}`,
|
||||||
|
) {
|
||||||
|
this.addCheck((input) => {
|
||||||
|
if (input > date) {
|
||||||
|
return pe(input, msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const validatorsObjects = [
|
||||||
|
StringSchema.name,
|
||||||
|
NumberSchema.name,
|
||||||
|
BooleanSchema.name,
|
||||||
|
DateSchema.name,
|
||||||
|
];
|
||||||
|
|
||||||
|
class UnionSchema<U extends Schema[]> {
|
||||||
|
private readonly schemas: Schema[];
|
||||||
|
|
||||||
|
constructor(...schemas: Schema[]) {
|
||||||
|
this.schemas = schemas;
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(input: unknown): Result<InferTypeFromSchema<U>, ParseError> {
|
||||||
|
for (const schema of this.schemas) {
|
||||||
|
const result = schema.parse(input);
|
||||||
|
|
||||||
|
if (result.isOk()) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err(
|
||||||
|
pe(
|
||||||
|
input,
|
||||||
|
`Failed to parse as one of the types: ${
|
||||||
|
this.schemas.map((s) => s.constructor.name).join(", ")
|
||||||
|
}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Schema =
|
||||||
|
| NumberSchema
|
||||||
|
| StringSchema
|
||||||
|
| BooleanSchema
|
||||||
|
| DateSchema
|
||||||
|
| ObjectSchema<Record<string, Schema>>
|
||||||
|
| UnionSchema<Schema[]>;
|
||||||
|
|
||||||
|
export class ObjectSchema<
|
||||||
|
S extends Record<string, Schema>,
|
||||||
|
> {
|
||||||
|
// TODO: rewrite this to be a static method returning result
|
||||||
|
constructor(
|
||||||
|
private readonly schema: S,
|
||||||
|
) {
|
||||||
|
for (const [_, value] of Object.entries(schema)) {
|
||||||
|
if (validatorsObjects.indexOf(schema.constructor.name) === -1) {
|
||||||
|
throw "Not a valid ObjectSchema >:(";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Yay! It is valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(
|
||||||
|
input: unknown,
|
||||||
|
): Result<{ [K in keyof S]: InferTypeFromSchema<S[K]> }, ParseError> {
|
||||||
|
if (typeof input !== "object") {
|
||||||
|
return err(pe(input, `Expected object, received ${typeof input}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input === null) {
|
||||||
|
return err(pe(input, `Expected object, received null`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const obj = input as Record<string, unknown>;
|
||||||
|
const resultObj: Partial<InferTypeFromSchema<S>> = {};
|
||||||
|
|
||||||
|
for (const [key, schema] of Object.entries(this.schema)) {
|
||||||
|
const inputValue = obj[key];
|
||||||
|
|
||||||
|
if (!inputValue) {
|
||||||
|
return err(pe(
|
||||||
|
input,
|
||||||
|
`Object ${input} missing the '${key}' attribute`,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = schema.parse(inputValue);
|
||||||
|
|
||||||
|
if (result.isErr()) {
|
||||||
|
return err(result.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
resultObj[key as keyof S] = result.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(resultObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type InferTypeFromSchema<S extends Schema | Record<string, Schema> | Schema[]> =
|
||||||
|
S extends Record<string, Schema>
|
||||||
|
? { [K in keyof S]: InferTypeFromSchema<S[K]> }
|
||||||
|
: S extends StringSchema ? string
|
||||||
|
: S extends NumberSchema ? number
|
||||||
|
: S extends DateSchema ? Date
|
||||||
|
: S extends BooleanSchema ? boolean
|
||||||
|
: S extends ObjectSchema<infer O> ? InferTypeFromSchema<O>
|
||||||
|
: S extends UnionSchema<infer U> ? InferTypeFromSchema<U[number]>
|
||||||
|
: never;
|
||||||
|
|
||||||
|
export const s = {
|
||||||
|
string: () => new StringSchema(),
|
||||||
|
number: () => new NumberSchema(),
|
||||||
|
date: () => new DateSchema(),
|
||||||
|
};
|
||||||
|
|
||||||
|
function createObjectSchema<S extends Record<string, Schema>>(
|
||||||
|
schema: S,
|
||||||
|
): ObjectSchema<S> {
|
||||||
|
return new ObjectSchema(schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
const union = new UnionSchema(s.string(), s.number());
|
||||||
|
|
||||||
|
console.log(union.parse({}));
|
||||||
Reference in New Issue
Block a user