diff --git a/server/src/lib/validator.ts b/server/src/lib/validator.ts deleted file mode 100644 index 844c245..0000000 --- a/server/src/lib/validator.ts +++ /dev/null @@ -1,1010 +0,0 @@ -import { err, ok, Result } from "@shared/utils/result.ts"; - -// ── Error Types ───────────────────────────────────────────────────── -type ValidationErrorDetail = - | { - kind: "typeMismatch"; - expected: string; - received: string; - msg?: string; - } - | { - kind: "propertyValidation"; - property: string; - detail: ValidationErrorDetail; - msg?: string; - } - | { - kind: "missingProperties" | "unexpectedProperties"; - keys: string[]; - msg?: string; - } - | { - kind: "unionValidation"; - details: ValidationErrorDetail[]; - msg?: string; - } - | { - kind: "arrayElement"; - index: number; - detail: ValidationErrorDetail; - msg?: string; - } - | { kind: "general"; mark?: string; msg: string }; - -class SchemaValidationError extends Error { - public readonly type = "SchemaValidationError"; - - constructor( - public readonly input: unknown, - public readonly detail: ValidationErrorDetail, - ) { - super(detail.msg || "Schema validation error"); - } - - public format(): Record { - return { - input: this.input, - error: SchemaValidationError.formatDetail(this.detail), - }; - } - - get msg(): string { - return SchemaValidationError.getBestMsg(this.detail); - } - - private static getBestMsg(detail: ValidationErrorDetail): string { - switch (detail.kind) { - case "typeMismatch": - case "unexpectedProperties": - case "missingProperties": - case "general": - case "unionValidation": - return SchemaValidationError.formMsg(detail); - case "propertyValidation": - case "arrayElement": - return detail.msg || - SchemaValidationError.getBestMsg(detail.detail); - default: - return "Unknown error"; - } - } - - private static formatDetail(detail: ValidationErrorDetail): any { - switch (detail.kind) { - case "general": - case "typeMismatch": - return SchemaValidationError.formMsg(detail); - case "propertyValidation": - return { - [detail.property]: detail.msg || - this.formatDetail(detail.detail!), - }; - case "unexpectedProperties": - case "missingProperties": { - const resObj: Record = {}; - const msg = detail.msg || - (detail.kind === "unexpectedProperties" - ? "Property is not allowed in a strict schema object" - : "Property is required, but missing"); - - for (const key of detail.keys) { - resObj[key] = msg; - } - - return resObj; - } - case "arrayElement": { - const obj: Record = {}; - if (detail.msg) { - obj["msg"] = detail.msg; - } - obj[`index_${detail.index}`] = this.formatDetail(detail.detail); - return obj; - } - case "unionValidation": { - const arr: unknown[] = detail.details?.map( - (err): unknown => this.formatDetail(err), - ); - if (detail.msg) { - arr.unshift("Msg: " + detail.msg); - } - return arr; - } - default: - return "Unknown error type"; - } - } - - private static formMsg(detail: ValidationErrorDetail): string { - if (detail.msg || detail.kind === "general") { - return detail.msg || "Unknown error"; - } - switch (detail.kind) { - case "typeMismatch": - return `Expected ${detail.expected}, but received ${detail.received}`; - case "unexpectedProperties": - return `Properties are not allowed in a strict object schema: ${ - detail.keys.join(", ") - }`; - case "missingProperties": - return `Missing required properties: ${detail.keys.join(", ")}`; - case "unionValidation": - return `Input did not match any of the union member`; - case "propertyValidation": - case "arrayElement": - default: - return "Unknown error"; - } - } -} - -function createValidationError( - input: unknown, - error: ValidationErrorDetail, -) { - return new SchemaValidationError(input, error); -} - -type PrimitiveTypeName = - | "string" - | "number" - | "bigint" - | "boolean" - | "symbol" - | "undefined" - | "object" - | "function"; - -// ── Core Schema Types ─────────────────────────────────────────────── -export interface Schema { - parse(input: unknown): Result; -} - -type ValidationCheck = (value: T) => SchemaValidationError | void; - -export abstract class BaseSchema implements Schema { - protected checks: ValidationCheck[] = []; - - constructor(public readonly msg?: string) {} - - public parse(input: unknown): Result { - return this.validateInput(input).andThen((value) => - this.applyValidationChecks(value) - ); - } - - protected abstract validateInput( - input: unknown, - ): Result; - - protected static validatePrimitive( - input: unknown, - expectedType: PrimitiveTypeName, - msg?: string, - ): Result { - const receivedType = typeof input; - return receivedType === expectedType ? ok(input as U) : err( - createValidationError(input, { - kind: "typeMismatch", - expected: expectedType, - received: receivedType, - msg: msg, - }), - ); - } - - public addCheck(check: ValidationCheck): this { - this.checks.push(check); - return this; - } - - protected applyValidationChecks( - value: T, - ): Result { - for (const check of this.checks) { - const error = check(value); - if (error) { - return err(error); - } - } - return ok(value); - } - - protected static isNullishSchema(schema: Schema): boolean { - if (schema.parse(null).isOk() || schema.parse(undefined).isOk()) { - return true; - } - return false; - } -} - -class StringSchema extends BaseSchema { - 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 validateInput( - input: unknown, - ): Result { - return BaseSchema.validatePrimitive( - input, - "string", - this.msg, - ); - } - - public max(len: number, msg?: string): this { - return this.addCheck((value) => { - if (value.length > len) { - return createValidationError(value, { - kind: "general", - msg: msg || `String must be at most ${len} characters long`, - }); - } - }); - } - - public min(len: number, msg?: string): this { - return this.addCheck((value) => { - if (value.length < len) { - return createValidationError(value, { - kind: "general", - msg: msg || `String must be at most ${len} characters long`, - }); - } - }); - } - - public regex(pattern: RegExp, msg?: string): this { - return this.addCheck((value) => { - if (!pattern.test(value)) { - return createValidationError(value, { - kind: "general", - msg: msg || `String must match pattern ${String(pattern)}`, - }); - } - }); - } - - public email(msg?: string): this { - return this.regex( - StringSchema.emailRegex, - msg || "String must be a valid email address", - ); - } - - public ip(msg?: string): this { - return this.regex( - StringSchema.ipRegex, - msg || "String must be a valid ip address", - ); - } -} - -class LiteralSchema extends BaseSchema { - constructor( - public readonly literal: L, - msg?: string, - ) { - super(msg); - } - - protected override validateInput( - input: unknown, - ): Result { - return BaseSchema.validatePrimitive( - input, - "string", - this.msg, - ) - .andThen((str) => - str === this.literal ? ok(str as L) : err( - createValidationError( - input, - { - kind: "typeMismatch", - expected: this.literal, - received: str, - msg: this.msg, - }, - ), - ) - ); - } -} - -class NumberSchema extends BaseSchema { - protected override validateInput( - input: unknown, - ): Result { - return BaseSchema.validatePrimitive( - input, - "number", - this.msg, - ); - } - - public gt( - num: number, - msg?: string, - ): this { - return this.addCheck((value) => { - if (value <= num) { - return createValidationError( - value, - { - kind: "general", - msg: msg || `Number must be greater than ${num}`, - }, - ); - } - }); - } - - public gte( - num: number, - msg?: string, - ): this { - return this.addCheck((value) => { - if (value < num) { - return createValidationError( - value, - { - kind: "general", - msg: msg || - `Number must be greater than or equal to ${num}`, - }, - ); - } - }); - } - - public lt( - num: number, - msg?: string, - ): this { - return this.addCheck((value) => { - if (value >= num) { - return createValidationError( - value, - { - kind: "general", - msg: msg || `Number must be less than ${num}`, - }, - ); - } - }); - } - - public lte( - num: number, - msg?: string, - ): this { - return this.addCheck((value) => { - if (value > num) { - return createValidationError( - value, - { - kind: "general", - msg: msg || - `Number must be less than or equal to ${num}`, - }, - ); - } - }); - } - - public int( - msg?: string, - ): this { - return this.addCheck((value) => { - if (!Number.isInteger(value)) { - return createValidationError( - value, - { - kind: "general", - msg: msg || - `Number must be an integer`, - }, - ); - } - }); - } - - public positive( - msg?: string, - ): this { - return this.gt(0, msg || "Number must be positive"); - } - - public nonnegative( - msg?: string, - ) { - return this.gte(0, msg || "Number must be nonnegative"); - } - - public negative( - msg?: string, - ): this { - return this.lt(0, msg || "Number must be negative"); - } - - public nonpositive( - msg?: string, - ) { - return this.lte(0, msg || "Number must be nonpositive"); - } - - public finite( - msg?: string, - ): this { - return this.addCheck((value) => { - if (!Number.isFinite(value)) { - return createValidationError( - value, - { - kind: "general", - msg: msg || - `Number must be an integer`, - }, - ); - } - }); - } - - public safe( - msg?: string, - ): this { - return this.addCheck((value) => { - if (!Number.isSafeInteger(value)) { - return createValidationError( - value, - { - kind: "general", - msg: msg || - `Number must be an integer`, - }, - ); - } - }); - } - - public multipleOf( - num: number, - msg?: string, - ): this { - return this.addCheck((value) => { - if (value % num !== 0) { - return createValidationError( - value, - { - kind: "general", - msg: msg || `Number must be a multiple of ${num}`, - }, - ); - } - }); - } -} - -class BigintSchema extends BaseSchema { - protected override validateInput( - input: unknown, - ): Result { - return BaseSchema.validatePrimitive( - input, - "bigint", - this.msg, - ); - } -} - -class BooleanSchema extends BaseSchema { - protected override validateInput( - input: unknown, - ): Result { - return BaseSchema.validatePrimitive( - input, - "boolean", - this.msg, - ); - } -} - -class DateSchema extends BaseSchema { - protected override validateInput( - input: unknown, - ): Result { - return BaseSchema.validatePrimitive( - input, - "object", - this.msg, - ) - .andThen((o) => { - if (o instanceof Date) return ok(o as Date); - - const received = o?.constructor?.name ?? "unknown"; - return err(createValidationError( - input, - { - kind: "typeMismatch", - expected: "Date instance", - received, - msg: this.msg, - }, - )); - }); - } -} - -class SymbolSchema extends BaseSchema { - protected override validateInput( - input: unknown, - ): Result { - return BaseSchema.validatePrimitive( - input, - "symbol", - this.msg, - ); - } -} - -class UndefinedSchema extends BaseSchema { - protected override validateInput( - input: unknown, - ): Result { - return BaseSchema.validatePrimitive( - input, - "undefined", - this.msg, - ); - } -} - -class NullSchema extends BaseSchema { - protected override validateInput( - input: unknown, - ): Result { - if (input === null) return ok(input); - - const received = typeof input === "object" - ? input?.constructor?.name ?? "unknown" - : typeof input; - - return err( - createValidationError( - input, - { - kind: "typeMismatch", - expected: "null", - received, - msg: this.msg, - }, - ), - ); - } -} - -class VoidSchema extends BaseSchema { - protected override validateInput( - input: unknown, - ): Result { - if (input === undefined || input === null) return ok(); - - const received = typeof input === "object" - ? input?.constructor?.name ?? "unknown" - : typeof input; - - return err(createValidationError( - input, - { - kind: "typeMismatch", - expected: "void (undefined/null)", - received, - msg: this.msg, - }, - )); - } -} - -class AnySchema extends BaseSchema { - protected override validateInput( - input: unknown, - ): Result { - return ok(input); - } -} - -class UnknownSchema extends BaseSchema { - protected override validateInput( - input: unknown, - ): Result { - return ok(input); - } -} - -class NeverSchema extends BaseSchema { - protected override validateInput( - input: unknown, - ): Result { - return err( - createValidationError( - input, - { - kind: "typeMismatch", - expected: "never", - received: typeof input, - msg: this.msg, - }, - ), - ); - } -} - -type InferSchemaType = S extends Schema ? T : never; - -class ObjectSchema>> - extends BaseSchema<{ [K in keyof S]: InferSchemaType }> { - private strictMode: boolean = false; - private objectMsg?; - - constructor( - public readonly shape: S, - msg?: { - mismatch?: string; - nullObject?: string; - unexpectedProperty?: string; - propertyValidation?: string; - missingProperty?: string; - } | string, - ) { - let mismatchMsg: string | undefined; - let objectMsg; - - if (typeof msg === "string") { - mismatchMsg = msg; - } else if (typeof msg === "object") { - objectMsg = msg; - } - - super(mismatchMsg); - - this.objectMsg = objectMsg; - } - - protected override validateInput( - input: unknown, - ): Result< - { [K in keyof S]: InferSchemaType }, - SchemaValidationError - > { - return BaseSchema.validatePrimitive( - input, - "object", - this.msg || this.objectMsg?.mismatch, - ) - .andThen((obj) => { - if (obj === null) { - return err( - createValidationError(input, { - kind: "typeMismatch", - expected: "Non-null object", - received: "null", - msg: this.msg || this.objectMsg?.nullObject, - }), - ); - } - - let resultObj: Record = {}; - const expectedKeys = new Set(Object.keys(this.shape)); - - for (const key of Object.keys(obj)) { - const schema = this.shape[key]; - if (schema === undefined) { - if (this.strictMode) { - const keys = new Set(Object.keys(obj)).difference( - new Set(Object.keys(this.shape)), - ).keys().toArray(); - - return err( - createValidationError( - input, - { - kind: "unexpectedProperties", - keys, - msg: this.msg || - this.objectMsg - ?.unexpectedProperty, - }, - ), - ); - } - continue; - } - - const result = schema.parse((obj as any)[key]); - if (result.isErr()) { - return err( - createValidationError( - input, - { - kind: "propertyValidation", - property: key, - detail: result.error.detail, - msg: this.msg || - this.objectMsg?.propertyValidation, - }, - ), - ); - } - expectedKeys.delete(key); - resultObj[key] = result.value; - } - - const missingProperties = expectedKeys.keys().filter( - (key) => !BaseSchema.isNullishSchema(this.shape[key]), - ).toArray(); - - if (missingProperties.length > 0) { - return err( - createValidationError( - input, - { - kind: "missingProperties", - keys: missingProperties, - msg: this.msg || - this.objectMsg?.missingProperty, - }, - ), - ); - } - - return ok( - resultObj as { [K in keyof S]: InferSchemaType }, - ); - }); - } - - strict(): this { - this.strictMode = true; - return this; - } -} - -type InferUnionSchemaType[]> = U[number] extends - Schema ? T : never; - -class UnionSchema[]> - extends BaseSchema> { - private unionMsg?; - - constructor( - public readonly schemas: U, - msg?: { - mismatch?: string; - unionValidation?: string; - } | string, - ) { - let mismatchMsg: string | undefined; - let unionMsg; - - if (typeof msg === "string") { - mismatchMsg = msg; - } else if (typeof msg === "object") { - unionMsg = msg; - } - - super(mismatchMsg); - this.unionMsg = unionMsg; - } - - private static getTypeFromSchemaName(name: string): string { - switch (name) { - case "StringSchema": - case "LiteralSchema": - return "string"; - case "NumberSchema": - return "number"; - case "BigintSchema": - return "bigint"; - case "BooleanSchema": - return "boolean"; - case "UndefinedSchema": - return "undefined"; - case "SymbolSchema": - return "symbol"; - default: - return "object"; - } - } - - protected validateInput( - input: unknown, - ): Result, SchemaValidationError> { - const errors: ValidationErrorDetail[] = []; - let typeMismatch = true; - - for (const schema of this.schemas) { - const result = schema.parse(input); - if (result.isOk()) { - return ok(result.value as InferUnionSchemaType); - } - typeMismatch = result.error.detail?.kind === "typeMismatch" && - typeMismatch; - errors.push(result.error.detail); - } - - if (typeMismatch) { - return err(createValidationError(input, { - kind: "typeMismatch", - expected: this.schemas.map((s) => - UnionSchema.getTypeFromSchemaName( - s.constructor.name, - ) - ).join(" | "), - received: typeof input, - msg: this.msg || this.unionMsg?.mismatch, - })); - } - - return err( - createValidationError(input, { - kind: "unionValidation", - msg: this.msg || this.unionMsg?.unionValidation || - "Input did not match any union member", - details: errors, - }), - ); - } -} - -class ArraySchema> - extends BaseSchema[]> { - private arrayMsg?; - - constructor( - private readonly schema: S, - msg?: { - mismatch?: string; - element?: string; - } | string, - ) { - let mismatchMsg: string | undefined; - let arrayMsg; - - if (typeof msg === "string") { - mismatchMsg = msg; - } else if (typeof msg === "object") { - arrayMsg = msg; - } - - super(mismatchMsg); - this.arrayMsg = arrayMsg; - } - - protected override validateInput( - input: unknown, - ): Result[], SchemaValidationError> { - if (!Array.isArray(input)) { - return err(createValidationError(input, { - kind: "typeMismatch", - expected: "Array", - received: "Non-array", - msg: this.msg || this.arrayMsg?.mismatch, - })); - } - - for (let i = 0; i < input.length; i++) { - const result = this.schema.parse(input[i]); - - if (result.isErr()) { - return err( - createValidationError(input, { - kind: "arrayElement", - index: i, - detail: result.error.detail, - msg: this.msg || this.arrayMsg?.element, - }), - ); - } - } - - return ok(input as InferSchemaType[]); - } -} - -class OptionalSchema> - extends BaseSchema | undefined> { - constructor( - public readonly schema: S, - msg?: string, - ) { - super(msg); - } - - protected override validateInput( - input: unknown, - ): Result | undefined, SchemaValidationError> { - if (input === undefined) { - return ok(input); - } - return this.schema.parse(input); - } -} - -class NullableSchema> - extends BaseSchema | null> { - constructor( - public readonly schema: S, - msg?: string, - ) { - super(msg); - } - - protected override validateInput( - input: unknown, - ): Result | null, SchemaValidationError> { - if (input === null) { - return ok(input); - } - return this.schema.parse(input); - } -} - -class NullishSchema> - extends BaseSchema | undefined | null> { - constructor( - public readonly schema: S, - msg?: string, - ) { - super(msg); - } - - protected override validateInput( - input: unknown, - ): Result | undefined | null, SchemaValidationError> { - if (input === undefined || input === null) { - return ok(input); - } - return this.schema.parse(input); - } -} - -const z = { - string: (msg?: string) => new StringSchema(msg), - literal: (lit: L, msg?: string) => - new LiteralSchema(lit, msg), - number: (msg?: string) => new NumberSchema(msg), - bigint: (msg?: string) => new BigintSchema(msg), - boolean: (msg?: string) => new BooleanSchema(msg), - date: (msg?: string) => new DateSchema(msg), - symbol: (msg?: string) => new StringSchema(msg), - undefined: (msg?: string) => new UndefinedSchema(msg), - null: (msg?: string) => new NullSchema(msg), - void: (msg?: string) => new VoidSchema(msg), - any: (msg?: string) => new AnySchema(msg), - unknown: (msg?: string) => new UnknownSchema(msg), - never: (msg?: string) => new NeverSchema(msg), - obj: >>(schema: S, msg?: string) => - new ObjectSchema(schema, msg), - union: []>(schemas: U, msg?: string) => - new UnionSchema(schemas, msg), -}; - -const schema = z.obj({ - login: z.string().regex( - /^[A-Za-z0-9]+$/, - "Only lower/upper case latin characters and numbers are allowed", - ), - password: z.string().max(255), -}).strict(); - -const result = schema.parse({ - login: "testLogin ", - password: "veryStrongPassword", -}); - -console.log(result.unwrapErr().unwrap().format()); diff --git a/shared/utils/api.ts b/shared/utils/api.ts index 2c15141..e4a1069 100644 --- a/shared/utils/api.ts +++ b/shared/utils/api.ts @@ -1,11 +1,23 @@ import { Result } from "@shared/utils/result.ts"; +import { Schema } from "@shared/utils/validator.ts"; -class ClientApi { - constructor(path: string, method: string) {} - validate(res: Response): ResultAsync { - const body = await res.json(); +class ApiRoute< + Path extends string, + ReqSchema extends Schema, + ResSchema extends Schema, +> { + constructor( + public readonly path: Path, + public readonly reqSchema: ReqSchema, + public readonly resSchema: ResSchema, + ) { } } -class ServerApi { -} +export type ExtractRouteParams = T extends string + ? T extends `${infer _Start}:${infer Param}/${infer Rest}` + ? Param | ExtractRouteParams + : T extends `${infer _Start}:${infer Param}` ? Param + : T extends `${infer _Start}*` ? "restOfThePath" + : never + : never; diff --git a/shared/utils/result.ts b/shared/utils/result.ts index b3b133e..1b92f63 100644 --- a/shared/utils/result.ts +++ b/shared/utils/result.ts @@ -124,7 +124,7 @@ export class Ok implements IResult { } mapErr(fn: (err: E) => U): Result { - return new Ok(this.value); + return ok(this.value); } mapErrAsync(fn: (err: E) => Promise): ResultAsync { @@ -236,8 +236,7 @@ export class Err implements IResult { return errAsync(this.error); } mapErr(fn: (err: E) => U): Result { - const mappedError = fn(this.error); - return new Err(mappedError); + return new Err(fn(this.error)); } mapErrAsync(fn: (err: E) => Promise): ResultAsync { return ResultAsync.fromPromise( diff --git a/shared/utils/validator.ts b/shared/utils/validator.ts index 569b5f2..f71d154 100644 --- a/shared/utils/validator.ts +++ b/shared/utils/validator.ts @@ -1,6 +1,6 @@ import { err, Result } from "@shared/utils/result.ts"; import { ok } from "@shared/utils/index.ts"; - +import { None, none, Option, some } from "@shared/utils/option.ts"; // ── Error Types ───────────────────────────────────────────────────── type ValidationErrorDetail = | { @@ -973,6 +973,180 @@ class NullishSchema> } } +class ResultSchema, E extends Schema> + extends BaseSchema, InferSchemaType>> { + constructor( + private readonly okSchema: T, + private readonly errSchema: E, + ) { + super(); + } + + protected override validateInput( + input: unknown, + ): Result< + Result, InferSchemaType>, + SchemaValidationError + > { + return BaseSchema.validatePrimitive(input, "object").andThen( + ( + obj, + ): Result< + Result, InferSchemaType>, + SchemaValidationError + > => { + if ("tag" in obj) { + switch (obj.tag) { + case "ok": { + if ("value" in obj) { + return this.okSchema.parse( + obj.value, + ).match( + (v) => ok(ok(v as InferSchemaType)), + (e) => + err(createValidationError(input, { + kind: "propertyValidation", + property: "value", + detail: e.detail, + })), + ); + } else if ( + BaseSchema.isNullishSchema(this.okSchema) + ) { + return ok( + ok() as Result< + InferSchemaType, + InferSchemaType + >, + ); + } + return err(createValidationError(input, { + kind: "missingProperties", + keys: ["value"], + msg: "If tag is set to 'ok', than result must contain a 'value' property", + })); + } + case "err": { + if ( + "error" in obj + ) { + return this.errSchema.parse( + obj.error, + ).match( + (e) => ok(err(e as InferSchemaType)), + (e) => + err(createValidationError(input, { + kind: "propertyValidation", + property: "error", + detail: e.detail, + })), + ); + } else if ( + BaseSchema.isNullishSchema(this.errSchema) + ) { + return ok( + err() as Result< + InferSchemaType, + InferSchemaType + >, + ); + } + return err(createValidationError(input, { + kind: "missingProperties", + keys: ["error"], + msg: "If tag is set to 'err', than result must contain a 'error' property", + })); + } + default: + return err(createValidationError(input, { + kind: "propertyValidation", + property: "tag", + detail: { + kind: "typeMismatch", + expected: "'ok' or 'err'", + received: `'${obj.tag}'`, + }, + })); + } + } else { + return err(createValidationError(input, { + kind: "missingProperties", + keys: ["tag"], + msg: "Result must contain a tag property", + })); + } + }, + ); + } +} + +class OptionSchema> + extends BaseSchema>> { + constructor( + private readonly schema: T, + ) { + super(); + } + + protected override validateInput( + input: unknown, + ): Result>, SchemaValidationError> { + return BaseSchema.validatePrimitive(input, "object").andThen( + ( + obj, + ): Result>, SchemaValidationError> => { + if ("tag" in obj) { + switch (obj.tag) { + case "some": { + if ("value" in obj) { + return this.schema.parse( + obj.value, + ).match( + (v) => ok(some(v as InferSchemaType)), + (e) => + err(createValidationError(input, { + kind: "propertyValidation", + property: "value", + detail: e.detail, + })), + ); + } else if ( + BaseSchema.isNullishSchema(this.schema) + ) { + return ok(some() as Option>); + } + return err(createValidationError(input, { + kind: "missingProperties", + keys: ["value"], + msg: "If tag is set to 'some', than option must contain a 'value' property", + })); + } + case "none": { + return ok(none); + } + default: + return err(createValidationError(input, { + kind: "propertyValidation", + property: "tag", + detail: { + kind: "typeMismatch", + expected: "'some' or 'none'", + received: `'${obj.tag}'`, + }, + })); + } + } else { + return err(createValidationError(input, { + kind: "missingProperties", + keys: ["tag"], + msg: "Option must contain a tag property", + })); + } + }, + ); + } +} + /* ── Helper Object for Schema Creation (z) ───────────────────────────────────── */ export const z = { @@ -1023,13 +1197,11 @@ export const z = { schema: S, msg?: string, ) => new NullishSchema(schema, msg), + result: , E extends Schema>( + okSchema: T, + errSchema: E, + ) => new ResultSchema(okSchema, errSchema), + option: >( + schema: T, + ) => new OptionSchema(schema), }; - -const schema = z.obj({ - string: z.string(), - number: z.number(), -}); - -type schemaType = typeof schema; - -type test = InferSchemaType;