import type { Schema, Infer } from '../base.ts'; import { createIssue, failure, success, createParseMethods, SCHEMA_KIND } from '../base.ts'; import { optional, OptionalSchema } from '../utils/optional.ts'; import { nullable } from '../utils/nullable.ts'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type ObjectShape = Record>; // Helper to check if a schema is optional // eslint-disable-next-line @typescript-eslint/no-explicit-any type IsOptional = T extends OptionalSchema ? true : false; // Split required and optional keys type RequiredKeys = { [K in keyof T]: IsOptional extends true ? never : K; }[keyof T]; type OptionalKeys = { [K in keyof T]: IsOptional extends true ? K : never; }[keyof T]; // Infer object shape with proper optional handling type InferObjectShape = { [K in RequiredKeys]: Infer; } & { [K in OptionalKeys]?: Infer; }; // Helper type to make all properties optional schemas type PartialShape = { [K in keyof T]: OptionalSchema; }; // Helper type for picking keys from an object shape type PickShape = Pick; // Helper type for omitting keys from an object shape type OmitShape = Omit; // Helper type for extending an object shape type ExtendShape = Omit & U; /** * Schema for validating objects with typed properties. * Validates each property according to its schema and collects all validation errors. * * @template T - The object shape definition * * @example * ```typescript * const userSchema = s.object({ * name: s.string(), * age: s.number(), * email: s.string() * }); * * const user = userSchema.parse({ * name: 'John', * age: 30, * email: 'john@example.com' * }); * ``` */ export class ObjectSchema implements Schema, InferObjectShape> { readonly [SCHEMA_KIND] = 'ObjectSchema'; description?: string; private parseMethods = createParseMethods>(); constructor(private shape: T) {} readonly '~standard' = { version: 1 as const, vendor: 'agentuity', validate: (value: unknown) => { if (value === null) { return failure([createIssue('Expected object, got null')]); } if (Array.isArray(value)) { return failure([createIssue('Expected object, got array')]); } if (typeof value !== 'object') { return failure([createIssue(`Expected object, got ${typeof value}`)]); } // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: Record = {}; const issues: ReturnType[] = []; for (const [key, schema] of Object.entries(this.shape)) { const fieldValue = (value as Record)[key]; const validation = schema['~standard'].validate(fieldValue); // Only support synchronous validation for now if (validation instanceof Promise) { throw new Error('Async validation not supported'); } if (validation.issues) { for (const issue of validation.issues) { issues.push( createIssue(issue.message, issue.path ? [key, ...issue.path] : [key]) ); } } else { result[key] = validation.value; } } if (issues.length > 0) { return failure(issues); } return success(result as InferObjectShape); }, types: undefined as unknown as { input: InferObjectShape; output: InferObjectShape }, }; describe(description: string): this { this.description = description; return this; } optional() { return optional(this); } nullable() { return nullable(this); } /** * Create a new object schema with only the specified keys. * Similar to TypeScript's Pick utility type. * * @param keys - Array of keys to pick from the schema * * @example * ```typescript * const userSchema = s.object({ name: s.string(), age: s.number(), email: s.string() }); * const nameOnlySchema = userSchema.pick(['name']); * // { name: string } * ``` */ pick(keys: readonly K[]): ObjectSchema> { const newShape = {} as PickShape; for (const key of keys) { if (key in this.shape) { newShape[key] = this.shape[key]; } } return new ObjectSchema(newShape); } /** * Create a new object schema without the specified keys. * Similar to TypeScript's Omit utility type. * * @param keys - Array of keys to omit from the schema * * @example * ```typescript * const userSchema = s.object({ name: s.string(), age: s.number(), password: s.string() }); * const publicUserSchema = userSchema.omit(['password']); * // { name: string; age: number } * ``` */ omit(keys: readonly K[]): ObjectSchema> { const keysSet = new Set(keys); const newShape = {} as OmitShape; for (const [key, schema] of Object.entries(this.shape)) { if (!keysSet.has(key as K)) { (newShape as ObjectShape)[key] = schema; } } return new ObjectSchema(newShape); } /** * Create a new object schema where all properties are optional. * Similar to TypeScript's Partial utility type. * * @example * ```typescript * const userSchema = s.object({ name: s.string(), age: s.number() }); * const partialUserSchema = userSchema.partial(); * // { name?: string; age?: number } * ``` */ partial(): ObjectSchema> { const newShape = {} as PartialShape; for (const [key, schema] of Object.entries(this.shape)) { (newShape as ObjectShape)[key] = optional(schema); } return new ObjectSchema(newShape); } /** * Create a new object schema by extending this schema with additional properties. * Properties in the extension override properties in the base schema. * Similar to TypeScript's intersection types with override semantics. * * @param extension - Object shape to extend with * * @example * ```typescript * const userSchema = s.object({ name: s.string(), age: s.number() }); * const adminSchema = userSchema.extend({ role: s.literal('admin'), permissions: s.array(s.string()) }); * // { name: string; age: number; role: 'admin'; permissions: string[] } * ``` */ extend(extension: U): ObjectSchema> { const newShape = { ...this.shape, ...extension } as ExtendShape; return new ObjectSchema(newShape); } parse = this.parseMethods.parse; safeParse = this.parseMethods.safeParse; } /** * Create an object schema with typed properties. * * @param shape - Object defining the schema for each property * * @example * ```typescript * const userSchema = s.object({ * name: s.string().describe('Full name'), * age: s.number().describe('Age in years'), * email: s.optional(s.string()) * }); * * type User = s.infer; * const user = userSchema.parse(data); * ``` */ export function object(shape: T): ObjectSchema { return new ObjectSchema(shape); }