From 19013984f3834a837b2a8e9946a96465c3f4af76 Mon Sep 17 00:00:00 2001 From: ton1c Date: Mon, 3 Feb 2025 15:59:15 +0300 Subject: [PATCH] finished validator --- server/src/lib/validator.ts | 813 +++++++++++++++++++++++++++--------- 1 file changed, 626 insertions(+), 187 deletions(-) diff --git a/server/src/lib/validator.ts b/server/src/lib/validator.ts index 758c29d..844c245 100644 --- a/server/src/lib/validator.ts +++ b/server/src/lib/validator.ts @@ -1,89 +1,152 @@ import { err, ok, Result } from "@shared/utils/result.ts"; + // ── Error Types ───────────────────────────────────────────────────── -type ParseErrorDetail = +type ValidationErrorDetail = | { - type: "typeError"; + kind: "typeMismatch"; expected: string; received: string; msg?: string; } | { - type: "objectStrictError" | "objectKeyError"; - key: string; - detail?: ParseErrorDetail; + kind: "propertyValidation"; + property: string; + detail: ValidationErrorDetail; msg?: string; } | { - type: "unionTypeError" | "unionMatchError"; - details?: ParseErrorDetail[]; + kind: "missingProperties" | "unexpectedProperties"; + keys: string[]; msg?: string; } | { - type: "arrayError"; + kind: "unionValidation"; + details: ValidationErrorDetail[]; + msg?: string; + } + | { + kind: "arrayElement"; index: number; - detail: ParseErrorDetail; + detail: ValidationErrorDetail; msg?: string; } - | { type: "validationError"; msg: string }; + | { kind: "general"; mark?: string; msg: string }; -class ParseError extends Error { - public readonly type = "ParseError"; +class SchemaValidationError extends Error { + public readonly type = "SchemaValidationError"; constructor( public readonly input: unknown, - public readonly detail: ParseErrorDetail, - msg?: string, + public readonly detail: ValidationErrorDetail, ) { - super(msg ?? `Validation failed: ${JSON.stringify(detail)}`); + super(detail.msg || "Schema validation error"); } - public format(): Record { + public format(): Record { return { input: this.input, - failedAt: ParseError.formatDetail(this.detail), + error: SchemaValidationError.formatDetail(this.detail), }; } - private static formatDetail(detail: ParseErrorDetail): any { - switch (detail.type) { - case "typeError": - return detail.msg ?? - `Expected ${detail.expected}, received ${detail.received}`; - case "objectKeyError": - return { - [detail.key]: this.formatDetail(detail.detail!) || - detail.msg, - }; - case "objectStrictError": - return { - [detail.key]: detail.msg, - }; - case "arrayError": - return { - [`index_${detail.index}`]: this.formatDetail( - detail.detail!, - ) || detail.msg, - }; - case "unionMatchError": - return detail.details?.map((err): any => - this.formatDetail(err) - ); - case "unionTypeError": - return detail.msg; - case "validationError": - return detail.msg; + 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 createParseError(input: any, error: ParseErrorDetail, msg?: string) { - return new ParseError(input, error, msg); +function createValidationError( + input: unknown, + error: ValidationErrorDetail, +) { + return new SchemaValidationError(input, error); } -type TypeofEnum = +type PrimitiveTypeName = | "string" | "number" | "bigint" @@ -95,37 +158,38 @@ type TypeofEnum = // ── Core Schema Types ─────────────────────────────────────────────── export interface Schema { - parse(input: unknown): Result; + parse(input: unknown): Result; } -type ValidationCheck = (value: T) => ParseError | void; +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.validateType(input).andThen((value) => - this.applyChecks(value) + public parse(input: unknown): Result { + return this.validateInput(input).andThen((value) => + this.applyValidationChecks(value) ); } - protected abstract validateType(input: unknown): Result; + protected abstract validateInput( + input: unknown, + ): Result; protected static validatePrimitive( input: unknown, - expectedType: TypeofEnum, + expectedType: PrimitiveTypeName, msg?: string, - ): Result { + ): Result { const receivedType = typeof input; return receivedType === expectedType ? ok(input as U) : err( - createParseError(input, { - type: "typeError", + createValidationError(input, { + kind: "typeMismatch", expected: expectedType, received: receivedType, - msg: msg || - `Expected ${expectedType} but received ${receivedType}`, + msg: msg, }), ); } @@ -135,7 +199,9 @@ export abstract class BaseSchema implements Schema { return this; } - protected applyChecks(value: T): Result { + protected applyValidationChecks( + value: T, + ): Result { for (const check of this.checks) { const error = check(value); if (error) { @@ -145,40 +211,77 @@ export abstract class BaseSchema implements Schema { return ok(value); } - private static flattenUnion[]>( - ...schemas: S - ): Schema[] { - return schemas.map((s) => - s instanceof UnionSchema ? this.flattenUnion(s) : s - ).flat(); - } - - public or[]>( - ...schemas: S - ): UnionSchema<[this, ...S]> { - return new UnionSchema([this, ...schemas]); + protected static isNullishSchema(schema: Schema): boolean { + if (schema.parse(null).isOk() || schema.parse(undefined).isOk()) { + return true; + } + return false; } } class StringSchema extends BaseSchema { - readonly type = "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 - protected override validateType( + 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"); + ): Result { + return BaseSchema.validatePrimitive( + input, + "string", + this.msg, + ); } - public max(len: number): this { + public max(len: number, msg?: string): this { return this.addCheck((value) => { if (value.length > len) { - return createParseError(value, { - type: "validationError", - msg: `String must be at most ${len} characters long`, + 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 { @@ -189,17 +292,23 @@ class LiteralSchema extends BaseSchema { super(msg); } - protected override validateType(input: unknown): Result { - return BaseSchema.validatePrimitive(input, "string", this.msg) + protected override validateInput( + input: unknown, + ): Result { + return BaseSchema.validatePrimitive( + input, + "string", + this.msg, + ) .andThen((str) => str === this.literal ? ok(str as L) : err( - createParseError( + createValidationError( input, { - type: "typeError", + kind: "typeMismatch", expected: this.literal, received: str, - msg: `Expected '${this.literal}' but received '${str}'`, + msg: this.msg, }, ), ) @@ -208,25 +317,195 @@ class LiteralSchema extends BaseSchema { } class NumberSchema extends BaseSchema { - protected override validateType( + protected override validateInput( input: unknown, - ): Result { - return BaseSchema.validatePrimitive(input, "number", this.msg); + ): 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 validateType( + protected override validateInput( input: unknown, - ): Result { - return BaseSchema.validatePrimitive(input, "bigint", this.msg); + ): Result { + return BaseSchema.validatePrimitive( + input, + "bigint", + this.msg, + ); } } class BooleanSchema extends BaseSchema { - protected override validateType( + protected override validateInput( input: unknown, - ): Result { + ): Result { return BaseSchema.validatePrimitive( input, "boolean", @@ -236,19 +515,25 @@ class BooleanSchema extends BaseSchema { } class DateSchema extends BaseSchema { - protected override validateType(input: unknown): Result { - return BaseSchema.validatePrimitive(input, "object", this.msg) + 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(createParseError( + return err(createValidationError( input, { - type: "typeError", + kind: "typeMismatch", expected: "Date instance", received, - msg: `Expected a Date instance but received ${received}`, + msg: this.msg, }, )); }); @@ -256,17 +541,21 @@ class DateSchema extends BaseSchema { } class SymbolSchema extends BaseSchema { - protected override validateType( + protected override validateInput( input: unknown, - ): Result { - return BaseSchema.validatePrimitive(input, "symbol", this.msg); + ): Result { + return BaseSchema.validatePrimitive( + input, + "symbol", + this.msg, + ); } } class UndefinedSchema extends BaseSchema { - protected override validateType( + protected override validateInput( input: unknown, - ): Result { + ): Result { return BaseSchema.validatePrimitive( input, "undefined", @@ -276,7 +565,9 @@ class UndefinedSchema extends BaseSchema { } class NullSchema extends BaseSchema { - protected override validateType(input: unknown): Result { + protected override validateInput( + input: unknown, + ): Result { if (input === null) return ok(input); const received = typeof input === "object" @@ -284,10 +575,10 @@ class NullSchema extends BaseSchema { : typeof input; return err( - createParseError( + createValidationError( input, { - type: "typeError", + kind: "typeMismatch", expected: "null", received, msg: this.msg, @@ -298,17 +589,19 @@ class NullSchema extends BaseSchema { } class VoidSchema extends BaseSchema { - protected override validateType(input: unknown): Result { + 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(createParseError( + return err(createValidationError( input, { - type: "typeError", + kind: "typeMismatch", expected: "void (undefined/null)", received, msg: this.msg, @@ -318,29 +611,33 @@ class VoidSchema extends BaseSchema { } class AnySchema extends BaseSchema { - protected override validateType(input: unknown): Result { + protected override validateInput( + input: unknown, + ): Result { return ok(input); } } class UnknownSchema extends BaseSchema { - protected override validateType( + protected override validateInput( input: unknown, - ): Result { + ): Result { return ok(input); } } class NeverSchema extends BaseSchema { - protected override validateType(input: unknown): Result { + protected override validateInput( + input: unknown, + ): Result { return err( - createParseError( + createValidationError( input, { - type: "typeError", + kind: "typeMismatch", expected: "never", received: typeof input, - msg: "No values are allowed for this schema (never)", + msg: this.msg, }, ), ); @@ -352,41 +649,75 @@ type InferSchemaType = S extends Schema ? T : never; class ObjectSchema>> extends BaseSchema<{ [K in keyof S]: InferSchemaType }> { private strictMode: boolean = false; + private objectMsg?; - constructor(private readonly shape: S, msg?: string) { - super(msg); + 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 validateType( + protected override validateInput( input: unknown, - ): Result<{ [K in keyof S]: InferSchemaType }, ParseError> { - return BaseSchema.validatePrimitive(input, "object", this.msg) + ): Result< + { [K in keyof S]: InferSchemaType }, + SchemaValidationError + > { + return BaseSchema.validatePrimitive( + input, + "object", + this.msg || this.objectMsg?.mismatch, + ) .andThen((obj) => { if (obj === null) { return err( - createParseError(input, { - type: "typeError", + createValidationError(input, { + kind: "typeMismatch", expected: "Non-null object", received: "null", - msg: "Expected a non-null object", + 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( - createParseError( + createValidationError( input, { - type: "objectStrictError", - key, - msg: `Encountered a key (${key}) that is not in a strict object schema`, + kind: "unexpectedProperties", + keys, + msg: this.msg || + this.objectMsg + ?.unexpectedProperty, }, ), ); @@ -394,25 +725,43 @@ class ObjectSchema>> continue; } - const result = schema.parse( - (obj as any)[key], - ); + const result = schema.parse((obj as any)[key]); if (result.isErr()) { return err( - createParseError( + createValidationError( input, { - type: "objectKeyError", - key, + kind: "propertyValidation", + property: key, detail: result.error.detail, - msg: this.msg, + 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 }, ); @@ -425,13 +774,31 @@ class ObjectSchema>> } } -type InferUnionSchemaType[]> = S[number] extends +type InferUnionSchemaType[]> = U[number] extends Schema ? T : never; class UnionSchema[]> - extends BaseSchema> { - constructor(public readonly schemas: U, msg?: string) { - super(msg); + 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 { @@ -454,38 +821,40 @@ class UnionSchema[]> } } - protected validateType( + protected validateInput( input: unknown, - ): Result, ParseError> { - const errors: ParseErrorDetail[] = []; - - let typeDoesNotMatch = true; + ): 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); + return ok(result.value as InferUnionSchemaType); } - typeDoesNotMatch = result.error.detail?.type === "typeError" && - typeDoesNotMatch; + typeMismatch = result.error.detail?.kind === "typeMismatch" && + typeMismatch; errors.push(result.error.detail); } - if (typeDoesNotMatch) { - return err(createParseError(input, { - type: "unionTypeError", - msg: `Input (${typeof input}) did not match any of the union's types (${ - this.schemas.map((s) => - UnionSchema.getTypeFromSchemaName(s.constructor.name) - ).join(" | ") - })`, + 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( - createParseError(input, { - type: "unionMatchError", - msg: "Input did not match any union member", + createValidationError(input, { + kind: "unionValidation", + msg: this.msg || this.unionMsg?.unionValidation || + "Input did not match any union member", details: errors, }), ); @@ -494,33 +863,50 @@ class UnionSchema[]> class ArraySchema> extends BaseSchema[]> { + private arrayMsg?; + constructor( private readonly schema: S, - msg?: string, + msg?: { + mismatch?: string; + element?: string; + } | string, ) { - super(msg); + 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 validateType( + protected override validateInput( input: unknown, - ): Result[], ParseError> { + ): Result[], SchemaValidationError> { if (!Array.isArray(input)) { - return err(createParseError(input, { - type: "validationError", - msg: `Expected an array`, + 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 r = this.schema.parse(input[i]); + const result = this.schema.parse(input[i]); - if (r.isErr()) { + if (result.isErr()) { return err( - createParseError(input, { - type: "arrayError", + createValidationError(input, { + kind: "arrayElement", index: i, - detail: r.error.detail, - msg: `Failed to parse an element at index ${i}`, + detail: result.error.detail, + msg: this.msg || this.arrayMsg?.element, }), ); } @@ -530,6 +916,63 @@ class ArraySchema> } } +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) => @@ -552,20 +995,16 @@ const z = { }; const schema = z.obj({ - test: z.union([ - z.string().max(2), - z.number(), - z.bigint(), - ]), - test1: z.literal("ok"), + 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 union = z.string().or(z.number().or(z.boolean())); +const result = schema.parse({ + login: "testLogin ", + password: "veryStrongPassword", +}); -const r = schema.parse({ test: "123", test1: "ok" }); - -if (r.isErr()) { - console.log(r.error.format()); -} else { - console.log(r.value); -} +console.log(result.unwrapErr().unwrap().format());