import type { Schema, Infer } from '../base.ts'; import { createIssue, failure, success, createParseMethods, SCHEMA_KIND } from '../base.ts'; import { optional } from '../utils/optional.ts'; import { nullable } from '../utils/nullable.ts'; /** * Schema for validating records (objects with string keys and typed values). * Like TypeScript's Record type. * * @template K - The key schema (must be string) * @template V - The value schema * * @example * ```typescript * const schema = s.record(s.string(), s.number()); * schema.parse({ a: 1, b: 2 }); // { a: 1, b: 2 } * schema.parse({ a: 'invalid' }); // throws ValidationError * ``` */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export class RecordSchema, V extends Schema> implements Schema, Infer>, Record, Infer>> { readonly [SCHEMA_KIND] = 'RecordSchema'; description?: string; private recordParseMethods = createParseMethods, Infer>>(); constructor( private keySchema: K, private valueSchema: V ) {} readonly '~standard' = { version: 1 as const, vendor: 'agentuity', validate: (value: unknown) => { if (value === null) { return failure([createIssue('Expected record, got null')]); } if (Array.isArray(value)) { return failure([createIssue('Expected record, got array')]); } if (typeof value !== 'object') { return failure([createIssue(`Expected record, got ${typeof value}`)]); } // eslint-disable-next-line @typescript-eslint/no-explicit-any const result: Record = {}; const issues: ReturnType[] = []; for (const [key, val] of Object.entries(value as Record)) { // Validate key const keyValidation = this.keySchema['~standard'].validate(key); if (keyValidation instanceof Promise) { throw new Error('Async validation not supported'); } if (keyValidation.issues) { for (const issue of keyValidation.issues) { issues.push(createIssue(`Invalid key "${key}": ${issue.message}`, [key])); } continue; } // Validate value const valueValidation = this.valueSchema['~standard'].validate(val); if (valueValidation instanceof Promise) { throw new Error('Async validation not supported'); } if (valueValidation.issues) { for (const issue of valueValidation.issues) { issues.push( createIssue(issue.message, issue.path ? [key, ...issue.path] : [key]) ); } } else { result[key] = valueValidation.value; } } if (issues.length > 0) { return failure(issues); } return success(result as Record, Infer>); }, types: undefined as unknown as { input: Record, Infer>; output: Record, Infer>; }, }; describe(description: string): this { this.description = description; return this; } optional() { return optional(this); } nullable() { return nullable(this); } parse = this.recordParseMethods.parse; safeParse = this.recordParseMethods.safeParse; } /** * Create a record schema for objects with string keys and typed values. * * @param keySchema - Schema for keys (typically s.string()) * @param valueSchema - Schema for values * * @example * ```typescript * const configSchema = s.record(s.string(), s.number()); * const config = configSchema.parse({ timeout: 30, retries: 3 }); * * const metadataSchema = s.record(s.string(), s.unknown()); * const metadata = metadataSchema.parse({ any: 'data', here: 123 }); * ``` */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function record, V extends Schema>( keySchema: K, valueSchema: V ): RecordSchema { return new RecordSchema(keySchema, valueSchema); }