reworking errors and fixing router
This commit is contained in:
@ -1,7 +1,13 @@
|
|||||||
import { InferSchemaType, Schema } from "@shared/utils/validator.ts";
|
import {
|
||||||
|
InferSchemaType,
|
||||||
|
Schema,
|
||||||
|
SchemaValidationError,
|
||||||
|
} from "@shared/utils/validator.ts";
|
||||||
import {
|
import {
|
||||||
RequestValidationError,
|
RequestValidationError,
|
||||||
|
requestValidationError,
|
||||||
ResponseValidationError,
|
ResponseValidationError,
|
||||||
|
responseValidationError,
|
||||||
} from "@src/lib/errors.ts";
|
} from "@src/lib/errors.ts";
|
||||||
import { ResultAsync } from "@shared/utils/resultasync.ts";
|
import { ResultAsync } from "@shared/utils/resultasync.ts";
|
||||||
|
|
||||||
@ -13,7 +19,7 @@ export type ExtractRouteParams<T extends string> = T extends string
|
|||||||
: never;
|
: never;
|
||||||
|
|
||||||
type ApiError =
|
type ApiError =
|
||||||
| RequestValidationError
|
| SchemaValidationError
|
||||||
| ResponseValidationError;
|
| ResponseValidationError;
|
||||||
|
|
||||||
export class Api<
|
export class Api<
|
||||||
@ -51,7 +57,7 @@ export class Api<
|
|||||||
return this.schema.req
|
return this.schema.req
|
||||||
.parse(reqBody)
|
.parse(reqBody)
|
||||||
.toAsync()
|
.toAsync()
|
||||||
.mapErr((e) => new RequestValidationError(e.input, e.detail))
|
.mapErr((e) => requestValidationError(e.msg))
|
||||||
.andThenAsync(async (data) => {
|
.andThenAsync(async (data) => {
|
||||||
const pathSplitted = this.pathSplitted;
|
const pathSplitted = this.pathSplitted;
|
||||||
for (const [key, value] of Object.entries(params)) {
|
for (const [key, value] of Object.entries(params)) {
|
||||||
@ -74,9 +80,7 @@ export class Api<
|
|||||||
|
|
||||||
return this.schema.res.parse(resBody).toAsync()
|
return this.schema.res.parse(resBody).toAsync()
|
||||||
.map((v) => v as InferSchemaType<ResSchema>)
|
.map((v) => v as InferSchemaType<ResSchema>)
|
||||||
.mapErr((e) =>
|
.mapErr((e) => responseValidationError(e.msg));
|
||||||
new ResponseValidationError(e.input, e.detail)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,10 @@ import {
|
|||||||
} from "@shared/utils/validator.ts";
|
} from "@shared/utils/validator.ts";
|
||||||
import { okAsync, ResultAsync } from "@shared/utils/resultasync.ts";
|
import { okAsync, ResultAsync } from "@shared/utils/resultasync.ts";
|
||||||
import log from "@shared/utils/logger.ts";
|
import log from "@shared/utils/logger.ts";
|
||||||
import { FailedToParseRequestAsJSON } from "@src/lib/errors.ts";
|
import {
|
||||||
|
FailedToParseRequestAsJSONError,
|
||||||
|
failedToParseRequestAsJSONError,
|
||||||
|
} from "@src/lib/errors.ts";
|
||||||
|
|
||||||
// https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html
|
// https://cheatsheetseries.owasp.org/cheatsheets/HTTP_Headers_Cheat_Sheet.html
|
||||||
const SECURITY_HEADERS: Headers = new Headers({
|
const SECURITY_HEADERS: Headers = new Headers({
|
||||||
@ -66,7 +69,7 @@ export class Context<
|
|||||||
|
|
||||||
public schema?: { req: ReqSchema; res: ResSchema };
|
public schema?: { req: ReqSchema; res: ResSchema };
|
||||||
|
|
||||||
public withSchema<
|
public setSchema<
|
||||||
Req extends Schema<any>,
|
Req extends Schema<any>,
|
||||||
Res extends Schema<any>,
|
Res extends Schema<any>,
|
||||||
>(
|
>(
|
||||||
@ -82,14 +85,30 @@ export class Context<
|
|||||||
return ctx as Context<S, Req, Res> & { schema: { req: Req; res: Res } };
|
return ctx as Context<S, Req, Res> & { schema: { req: Req; res: Res } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setParams(
|
||||||
|
params: Params,
|
||||||
|
): Context<S, ReqSchema, ResSchema> {
|
||||||
|
const ctx = new Context<S, ReqSchema, ResSchema>(
|
||||||
|
this.req,
|
||||||
|
this.info,
|
||||||
|
params,
|
||||||
|
);
|
||||||
|
ctx._url = this._url;
|
||||||
|
ctx._hostname = this._hostname;
|
||||||
|
ctx._port = this._port;
|
||||||
|
ctx._cookies = this._cookies;
|
||||||
|
ctx.res = this.res;
|
||||||
|
return ctx as Context<S, ReqSchema, ResSchema>;
|
||||||
|
}
|
||||||
|
|
||||||
public parseBody(): ResultAsync<
|
public parseBody(): ResultAsync<
|
||||||
InferSchemaType<ReqSchema>,
|
InferSchemaType<ReqSchema>,
|
||||||
SchemaValidationError | FailedToParseRequestAsJSON
|
SchemaValidationError | FailedToParseRequestAsJSONError
|
||||||
> {
|
> {
|
||||||
return ResultAsync
|
return ResultAsync
|
||||||
.fromPromise(
|
.fromPromise(
|
||||||
this.req.json(),
|
this.req.json(),
|
||||||
(e) => new FailedToParseRequestAsJSON(getMessageFromError(e)),
|
(e) => failedToParseRequestAsJSONError(getMessageFromError(e)),
|
||||||
)
|
)
|
||||||
.andThen((data: unknown) => {
|
.andThen((data: unknown) => {
|
||||||
if (!this.schema) {
|
if (!this.schema) {
|
||||||
@ -224,21 +243,6 @@ export class Context<
|
|||||||
delete: (name: string) => deleteCookie(this.res.headers, name),
|
delete: (name: string) => deleteCookie(this.res.headers, name),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static setParams<S extends string>(
|
|
||||||
ctx: Context<string>,
|
|
||||||
params: Params<ExtractRouteParams<S>>,
|
|
||||||
): Context<S> {
|
|
||||||
const newCtx = new Context(ctx.req, ctx.info, params) as typeof ctx;
|
|
||||||
|
|
||||||
newCtx._url = ctx._url;
|
|
||||||
newCtx._hostname = ctx._hostname;
|
|
||||||
newCtx._port = ctx._port;
|
|
||||||
newCtx._cookies = ctx._cookies;
|
|
||||||
newCtx.schema = ctx.schema;
|
|
||||||
|
|
||||||
return newCtx;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExtractPath<S extends string> = S extends
|
type ExtractPath<S extends string> = S extends
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { Database, RestBindParameters } from "@db/sqlite";
|
import { Database, RestBindParameters } from "@db/sqlite";
|
||||||
import { err, getMessageFromError, ok, Result } from "@shared/utils/result.ts";
|
import { err, getMessageFromError, ok, Result } from "@shared/utils/result.ts";
|
||||||
import { QueryExecutionError } from "@lib/errors.ts";
|
import { QueryExecutionError, queryExecutionError } from "@lib/errors.ts";
|
||||||
import { fromNullableVal, none, Option, some } from "@shared/utils/option.ts";
|
import { fromNullableVal, none, Option, some } from "@shared/utils/option.ts";
|
||||||
import log from "@shared/utils/logger.ts";
|
import log from "@shared/utils/logger.ts";
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ export class DatabaseClient {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
const message = getMessageFromError(e);
|
const message = getMessageFromError(e);
|
||||||
log.error(`Failed to execute sql! Error: ${e}`);
|
log.error(`Failed to execute sql! Error: ${e}`);
|
||||||
return err(new QueryExecutionError(message));
|
return err(queryExecutionError(message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,78 +1,74 @@
|
|||||||
import {
|
import { InferSchemaType } from "@shared/utils/validator.ts";
|
||||||
InferSchemaType,
|
import { createErrorFactory, defineError } from "@shared/utils/errors.ts";
|
||||||
Schema,
|
|
||||||
StringSchema,
|
|
||||||
z,
|
|
||||||
} from "@shared/utils/validator.ts";
|
|
||||||
|
|
||||||
export class ErrorBase extends Error {
|
export const queryExecutionErrorSchema = defineError(
|
||||||
constructor(message: string = "An unknown error has occurred") {
|
"QueryExecutionError",
|
||||||
super(message);
|
);
|
||||||
this.name = this.constructor.name;
|
export const queryExecutionError = createErrorFactory(
|
||||||
}
|
queryExecutionErrorSchema,
|
||||||
}
|
);
|
||||||
|
export type QueryExecutionError = InferSchemaType<
|
||||||
|
typeof queryExecutionErrorSchema
|
||||||
|
>;
|
||||||
|
|
||||||
export function createErrorSchema<
|
export const noAdminEntryErrorSchema = defineError("NoAdminEntryError");
|
||||||
T extends string,
|
export const noAdminEntryError = createErrorFactory(noAdminEntryErrorSchema);
|
||||||
I extends Schema<any> = StringSchema,
|
export type NoAdminEntryError = InferSchemaType<typeof noAdminEntryErrorSchema>;
|
||||||
>(
|
|
||||||
type: T,
|
|
||||||
info?: I,
|
|
||||||
) {
|
|
||||||
return z.obj({
|
|
||||||
type: z.literal(type),
|
|
||||||
info: info ?? z.string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const queryExecutionErrorSchema = createErrorSchema("QueryExecutionError");
|
export const failedToReadFileErrorSchema = defineError("FailedToReadFileError");
|
||||||
type QueryExecutionError = InferSchemaType<typeof queryExecutionErrorSchema>;
|
export const failedToReadFileError = createErrorFactory(
|
||||||
|
failedToReadFileErrorSchema,
|
||||||
const noAdminEntryErrorSchema = createErrorSchema("noAdminEntryError");
|
);
|
||||||
type NoAdminEntryError = InferSchemaType<typeof noAdminEntryErrorSchema>;
|
export type FailedToReadFileError = InferSchemaType<
|
||||||
|
|
||||||
const failedToReadFileErrorSchema = createErrorSchema("failedToReadFileError");
|
|
||||||
type failedToReadFileErrorSchema = InferSchemaType<
|
|
||||||
typeof failedToReadFileErrorSchema
|
typeof failedToReadFileErrorSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const failedToReadFileErrorSchemaSchema = createErrorSchema(
|
export const invalidSyntaxErrorSchema = defineError("InvalidSyntaxError");
|
||||||
"FailedToReadFileErrorSchema",
|
export const invalidSyntaxError = createErrorFactory(invalidSyntaxErrorSchema);
|
||||||
);
|
export type InvalidSyntaxError = InferSchemaType<
|
||||||
type FailedToReadFileErrorSchema = InferSchemaType<
|
typeof invalidSyntaxErrorSchema
|
||||||
typeof failedToReadFileErrorSchemaSchema
|
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const invalidSyntaxErrorSchema = createErrorSchema("InvalidSyntaxError");
|
export const invalidPathErrorSchema = defineError("InvalidPathError");
|
||||||
type InvalidSyntaxError = InferSchemaType<typeof invalidSyntaxErrorSchema>;
|
export const invalidPathError = createErrorFactory(invalidPathErrorSchema);
|
||||||
|
export type InvalidPathError = InferSchemaType<typeof invalidPathErrorSchema>;
|
||||||
|
|
||||||
const invalidPathErrorSchema = createErrorSchema("InvalidPathError");
|
export const adminPasswordNotSetErrorSchema = defineError(
|
||||||
type InvalidPathError = InferSchemaType<typeof invalidPathErrorSchema>;
|
|
||||||
|
|
||||||
const adminPasswordNotSetErrorSchema = createErrorSchema(
|
|
||||||
"AdminPasswordNotSetError",
|
"AdminPasswordNotSetError",
|
||||||
);
|
);
|
||||||
type AdminPasswordNotSetError = InferSchemaType<
|
export const adminPasswordNotSetError = createErrorFactory(
|
||||||
|
adminPasswordNotSetErrorSchema,
|
||||||
|
);
|
||||||
|
export type AdminPasswordNotSetError = InferSchemaType<
|
||||||
typeof adminPasswordNotSetErrorSchema
|
typeof adminPasswordNotSetErrorSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const requestValidationErrorSchema = createErrorSchema(
|
export const requestValidationErrorSchema = defineError(
|
||||||
"RequestValidationError",
|
"RequestValidationError",
|
||||||
);
|
);
|
||||||
type RequestValidationError = InferSchemaType<
|
export const requestValidationError = createErrorFactory(
|
||||||
|
requestValidationErrorSchema,
|
||||||
|
);
|
||||||
|
export type RequestValidationError = InferSchemaType<
|
||||||
typeof requestValidationErrorSchema
|
typeof requestValidationErrorSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const responseValidationErrorSchema = createErrorSchema(
|
export const responseValidationErrorSchema = defineError(
|
||||||
"ResponseValidationError",
|
"ResponseValidationError",
|
||||||
);
|
);
|
||||||
type ResponseValidationError = InferSchemaType<
|
export const responseValidationError = createErrorFactory(
|
||||||
|
responseValidationErrorSchema,
|
||||||
|
);
|
||||||
|
export type ResponseValidationError = InferSchemaType<
|
||||||
typeof responseValidationErrorSchema
|
typeof responseValidationErrorSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const failedToParseRequestAsJSONErrorSchema = createErrorSchema(
|
export const failedToParseRequestAsJSONErrorSchema = defineError(
|
||||||
"FailedToParseRequestAsJSONError",
|
"FailedToParseRequestAsJSONError",
|
||||||
);
|
);
|
||||||
type FailedToParseRequestAsJSONError = InferSchemaType<
|
export const failedToParseRequestAsJSONError = createErrorFactory(
|
||||||
|
failedToParseRequestAsJSONErrorSchema,
|
||||||
|
);
|
||||||
|
export type FailedToParseRequestAsJSONError = InferSchemaType<
|
||||||
typeof failedToParseRequestAsJSONErrorSchema
|
typeof failedToParseRequestAsJSONErrorSchema
|
||||||
>;
|
>;
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import { Api } from "@src/lib/apiValidator.ts";
|
|||||||
|
|
||||||
type RequestHandler<
|
type RequestHandler<
|
||||||
S extends string,
|
S extends string,
|
||||||
ReqSchema extends Schema<any> = never,
|
ReqSchema extends Schema<any> = Schema<unknown>,
|
||||||
ResSchema extends Schema<any> = never,
|
ResSchema extends Schema<any> = Schema<unknown>,
|
||||||
> = (c: Context<S, ReqSchema, ResSchema>) => Promise<Response> | Response;
|
> = (c: Context<S, ReqSchema, ResSchema>) => Promise<Response> | Response;
|
||||||
|
|
||||||
type RequestHandlerWithSchema<S extends string> = {
|
type RequestHandlerWithSchema<S extends string> = {
|
||||||
@ -148,6 +148,7 @@ class HttpRouter {
|
|||||||
connInfo: Deno.ServeHandlerInfo<Deno.Addr>,
|
connInfo: Deno.ServeHandlerInfo<Deno.Addr>,
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
let ctx = new Context(req, connInfo, {});
|
let ctx = new Context(req, connInfo, {});
|
||||||
|
|
||||||
let routeParams: Record<string, string> = {};
|
let routeParams: Record<string, string> = {};
|
||||||
const path = this.pathPreprocessor
|
const path = this.pathPreprocessor
|
||||||
? this.pathPreprocessor(ctx.path)
|
? this.pathPreprocessor(ctx.path)
|
||||||
@ -161,7 +162,7 @@ class HttpRouter {
|
|||||||
const route = methodHandler[req.method];
|
const route = methodHandler[req.method];
|
||||||
if (!route) return none;
|
if (!route) return none;
|
||||||
if (route.schema) {
|
if (route.schema) {
|
||||||
ctx = ctx.withSchema(route.schema);
|
ctx = ctx.setSchema(route.schema);
|
||||||
}
|
}
|
||||||
const handler = route.handler;
|
const handler = route.handler;
|
||||||
|
|
||||||
@ -172,7 +173,7 @@ class HttpRouter {
|
|||||||
const res = (await this.executeMiddlewareChain(
|
const res = (await this.executeMiddlewareChain(
|
||||||
this.middlewares,
|
this.middlewares,
|
||||||
handler,
|
handler,
|
||||||
Context.setParams(ctx, routeParams),
|
ctx = ctx.setParams(routeParams),
|
||||||
)).res;
|
)).res;
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
@ -206,10 +207,6 @@ class HttpRouter {
|
|||||||
|
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setParams(path: string, params: string[]): Params<string> {
|
|
||||||
path.split("/").filter((segmet) => segmet.startsWith(":"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExtractRouteParams<T extends string> = T extends string
|
export type ExtractRouteParams<T extends string> = T extends string
|
||||||
|
|||||||
@ -1,845 +0,0 @@
|
|||||||
import { err, ok, Result } from "@shared/utils/result.ts";
|
|
||||||
import { none, Option, some } from "@shared/utils/option.ts";
|
|
||||||
|
|
||||||
class ParseError extends Error {
|
|
||||||
type = "ParseError";
|
|
||||||
|
|
||||||
public trace: NestedArray<string> = [];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public input: any,
|
|
||||||
trace: NestedArray<string> | string,
|
|
||||||
public readonly msg: string,
|
|
||||||
) {
|
|
||||||
super(msg);
|
|
||||||
|
|
||||||
if (Array.isArray(trace)) {
|
|
||||||
this.trace = trace;
|
|
||||||
} else {
|
|
||||||
this.trace = [trace];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stackParseErr(trace: string, input: any): ParseError {
|
|
||||||
this.trace = [trace, this.trace];
|
|
||||||
this.input = input;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function pe(input: unknown, trace: NestedArray<string>, msg: string) {
|
|
||||||
return new ParseError(input, trace, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Schema<T> {
|
|
||||||
parse(input: unknown): Result<T, ParseError>;
|
|
||||||
checkIfValid(input: unknown): boolean;
|
|
||||||
nullable(): NullableSchema<Schema<T>>;
|
|
||||||
option(): OptionSchema<Schema<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<Schema<T>> {
|
|
||||||
return new NullableSchema(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
or<S extends Schema<any>[]>(...schema: S): UnionSchema<[this, ...S]> {
|
|
||||||
return new UnionSchema(this, ...schema);
|
|
||||||
}
|
|
||||||
|
|
||||||
option(): OptionSchema<Schema<T>> {
|
|
||||||
return new OptionSchema(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract parse(input: unknown): Result<T, ParseError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class PrimitiveSchema<T> 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,
|
|
||||||
): this {
|
|
||||||
const trace = `String length must be at most ${length} characters long`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input.length <= length ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public min(
|
|
||||||
length: number,
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace =
|
|
||||||
`String length must be at least ${length} characters long`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input.length >= length ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public regex(
|
|
||||||
pattern: RegExp,
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `String length must match the pattern ${String(pattern)}`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
pattern.test(input) ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public email(
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `String must be a valid email address`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
StringSchema.emailRegex.test(input)
|
|
||||||
? undefined
|
|
||||||
: pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ip(
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `String must be a valid ip address`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
StringSchema.ipRegex.test(input) ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class NumberSchema extends PrimitiveSchema<number> {
|
|
||||||
protected override initialCheck(
|
|
||||||
input: unknown,
|
|
||||||
): Result<number, ParseError> {
|
|
||||||
return this.checkPrimitive(input, "number");
|
|
||||||
}
|
|
||||||
|
|
||||||
public gt(
|
|
||||||
num: number,
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Number must be greates than ${num}`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input > num ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public gte(
|
|
||||||
num: number,
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Number must be greates than or equal to ${num}`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input >= num ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public lt(
|
|
||||||
num: number,
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Number must be less than ${num}`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input < num ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public lte(
|
|
||||||
num: number,
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Number must be less than or equal to ${num}`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input <= num ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int(
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Number must be an integer`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
Number.isInteger(input) ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public positive(
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Number must be positive`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input > 0 ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public nonnegative(
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Number must be nonnegative`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input >= 0 ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public negative(
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Number must be negative`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input < 0 ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public nonpositive(
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Number must be nonpositive`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input < 0 ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public finite(
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Number must be finite`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
Number.isFinite(input) ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public safe(
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Number must be a safe integer`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
Number.isSafeInteger(input) ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public multipleOf(
|
|
||||||
num: number,
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Number must be a multiple of ${num}`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input % num === 0 ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class BigintSchema extends PrimitiveSchema<bigint> {
|
|
||||||
protected override initialCheck(
|
|
||||||
input: unknown,
|
|
||||||
): Result<bigint, ParseError> {
|
|
||||||
return this.checkPrimitive(input, "bigint");
|
|
||||||
}
|
|
||||||
|
|
||||||
public gt(
|
|
||||||
num: number | bigint,
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Bigint must be greates than ${num}`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input > num ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public gte(
|
|
||||||
num: number | bigint,
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Bigint must be greates than or equal to ${num}`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input >= num ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public lt(
|
|
||||||
num: number | bigint,
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Bigint must be less than ${num}`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input < num ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public lte(
|
|
||||||
num: number | bigint,
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Bigint must be less than or equal to ${num}`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input <= num ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int(
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Bigint must be an integer`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
Number.isInteger(input) ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public positive(
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Bigint must be positive`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input > 0 ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public nonnegative(
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Bigint must be nonnegative`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input >= 0 ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public negative(
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Bigint must be negative`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input < 0 ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public nonpositive(
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Bigint must be nonpositive`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input < 0 ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public finite(
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Bigint must be finite`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
Number.isFinite(input) ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public safe(
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Bigint must be a safe integer`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
Number.isSafeInteger(input) ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public multipleOf(
|
|
||||||
num: bigint,
|
|
||||||
msg?: string,
|
|
||||||
): this {
|
|
||||||
const trace = `Bigint must be a multiple of ${num}`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input % num === BigInt(0) ? undefined : pe(input, trace, 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}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public min(
|
|
||||||
date: Date,
|
|
||||||
msg?: string,
|
|
||||||
) {
|
|
||||||
const trace = `Date must be after ${date.toLocaleString()}`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input >= date ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public max(
|
|
||||||
date: Date,
|
|
||||||
msg?: string,
|
|
||||||
) {
|
|
||||||
const trace = `Date must be before ${date.toLocaleString()}`;
|
|
||||||
return this.addCheck((input) =>
|
|
||||||
input <= date ? undefined : pe(input, trace, msg)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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: any): Result<any, ParseError> {
|
|
||||||
return ok(input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class UnknownSchema extends PrimitiveSchema<unknown> {
|
|
||||||
protected override initialCheck(
|
|
||||||
input: unknown,
|
|
||||||
): Result<unknown, ParseError> {
|
|
||||||
return ok(input);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
checkResult.error.stackParseErr(
|
|
||||||
`Failed to parse '${key}' attribute`,
|
|
||||||
input,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 static readonly schemasTypes: Partial<
|
|
||||||
Record<string, TypeOfString>
|
|
||||||
> = {
|
|
||||||
StringSchema: "string",
|
|
||||||
LiteralSchema: "string",
|
|
||||||
NumberSchema: "number",
|
|
||||||
BigintSchema: "bigint",
|
|
||||||
BooleanSchema: "boolean",
|
|
||||||
UndefinedSchema: "undefined",
|
|
||||||
VoidSchema: "undefined",
|
|
||||||
};
|
|
||||||
private readonly primitiveTypesMap: Map<TypeOfString, Schema<any>[]> =
|
|
||||||
new Map();
|
|
||||||
private readonly othersTypes: Schema<any>[] = [];
|
|
||||||
|
|
||||||
constructor(...schemas: S) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
for (const schema of schemas) {
|
|
||||||
const type = UnionSchema.schemasTypes[schema.constructor.name];
|
|
||||||
|
|
||||||
if (type !== undefined) {
|
|
||||||
if (!this.primitiveTypesMap.has(type)) {
|
|
||||||
this.primitiveTypesMap.set(type, []);
|
|
||||||
}
|
|
||||||
const schemasForType = this.primitiveTypesMap.get(type);
|
|
||||||
schemasForType?.push(schema);
|
|
||||||
} else {
|
|
||||||
this.othersTypes.push(schema);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override initialCheck(
|
|
||||||
input: unknown,
|
|
||||||
): Result<InferSchemaUnion<S>, ParseError> {
|
|
||||||
const schemas = this.primitiveTypesMap.get(typeof input) ||
|
|
||||||
this.othersTypes;
|
|
||||||
|
|
||||||
const errors: string[] = [];
|
|
||||||
|
|
||||||
for (const schema of schemas) {
|
|
||||||
const checkResult = schema.parse(input);
|
|
||||||
|
|
||||||
if (checkResult.isOk()) {
|
|
||||||
return ok(checkResult.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
errors.push(
|
|
||||||
`${schema.constructor.name} - ${
|
|
||||||
checkResult.error.trace.join("\n")
|
|
||||||
}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = typeof input;
|
|
||||||
return err(
|
|
||||||
pe(
|
|
||||||
input,
|
|
||||||
[
|
|
||||||
`UnionSchema (${
|
|
||||||
this.primitiveTypesMap.keys().toArray().join(" | ")
|
|
||||||
}${
|
|
||||||
this.othersTypes.length > 0
|
|
||||||
? "object"
|
|
||||||
: ""
|
|
||||||
}) - failed to parse input as any of the schemas:`,
|
|
||||||
errors.join("\n"),
|
|
||||||
].join("\n"),
|
|
||||||
"Failed to match union",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
`Array. Failed to parse element at index ${i}:\n${r.error.trace}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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<S extends Schema<any>>
|
|
||||||
extends PrimitiveSchema<Option<InferSchema<S>>> {
|
|
||||||
private schema;
|
|
||||||
|
|
||||||
constructor(private readonly valueSchema: S) {
|
|
||||||
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.string().max(4, "too long").or(v.number());
|
|
||||||
|
|
||||||
const res = r.parse(some("11234"));
|
|
||||||
|
|
||||||
console.log(res);
|
|
||||||
|
|
||||||
type InferSchema<S> = S extends Schema<infer T> ? T : never;
|
|
||||||
|
|
||||||
type NestedArray<T> = T | NestedArray<T>[];
|
|
||||||
Reference in New Issue
Block a user