import type { ValidationSource } from './types' export type TStandardSchemaValidatorValue< TData, TSource extends ValidationSource = ValidationSource, > = { value: TData validationSource: TSource } export type TStandardSchemaValidatorIssue< TSource extends ValidationSource = ValidationSource, > = TSource extends 'form' ? { form: Record fields: Record } : TSource extends 'field' ? StandardSchemaV1Issue[] : never function prefixSchemaToErrors( issues: readonly StandardSchemaV1Issue[], formValue: unknown, ) { const schema = new Map() for (const issue of issues) { const issuePath = issue.path ?? [] let currentFormValue = formValue let path = '' for (let i = 0; i < issuePath.length; i++) { const pathSegment = issuePath[i] if (pathSegment === undefined) continue const segment = typeof pathSegment === 'object' ? pathSegment.key : pathSegment // Standard Schema doesn't specify if paths should use numbers or stringified numbers for array access. // However, if we follow the path it provides and encounter an array, then we can assume it's intended for array access. const segmentAsNumber = Number(segment) if (Array.isArray(currentFormValue) && !Number.isNaN(segmentAsNumber)) { path += `[${segmentAsNumber}]` } else { path += (i > 0 ? '.' : '') + String(segment) } if (typeof currentFormValue === 'object' && currentFormValue !== null) { currentFormValue = currentFormValue[segment as never] } else { currentFormValue = undefined } } schema.set(path, (schema.get(path) ?? []).concat(issue)) } return Object.fromEntries(schema) } const transformFormIssues = ( issues: readonly StandardSchemaV1Issue[], formValue: unknown, ): TStandardSchemaValidatorIssue => { const schemaErrors = prefixSchemaToErrors(issues, formValue) return { form: schemaErrors, fields: schemaErrors, } as TStandardSchemaValidatorIssue } export const standardSchemaValidators = { validate( { value, validationSource, }: TStandardSchemaValidatorValue, schema: StandardSchemaV1, ): TStandardSchemaValidatorIssue | undefined { const result = schema['~standard'].validate(value) if (result instanceof Promise) { throw new Error('async function passed to sync validator') } if (!result.issues) return if (validationSource === 'field') return result.issues as TStandardSchemaValidatorIssue return transformFormIssues(result.issues, value) }, async validateAsync( { value, validationSource, }: TStandardSchemaValidatorValue, schema: StandardSchemaV1, ): Promise | undefined> { const result = await schema['~standard'].validate(value) if (!result.issues) return if (validationSource === 'field') return result.issues as TStandardSchemaValidatorIssue return transformFormIssues(result.issues, value) }, } export const isStandardSchemaValidator = ( validator: unknown, ): validator is StandardSchemaV1 => !!validator && '~standard' in (validator as object) /** * The Standard Schema interface. */ export type StandardSchemaV1 = { /** * The Standard Schema properties. */ readonly '~standard': StandardSchemaV1Props } /** * The Standard Schema properties interface. */ interface StandardSchemaV1Props { /** * The version number of the standard. */ readonly version: 1 /** * The vendor name of the schema library. */ readonly vendor: string /** * Validates unknown input values. */ readonly validate: ( value: unknown, ) => StandardSchemaV1Result | Promise> /** * Inferred types associated with the schema. */ readonly types?: StandardSchemaV1Types | undefined } /** * The result interface of the validate function. */ type StandardSchemaV1Result = | StandardSchemaV1SuccessResult | StandardSchemaV1FailureResult /** * The result interface if validation succeeds. */ interface StandardSchemaV1SuccessResult { /** * The typed output value. */ readonly value: Output /** * The non-existent issues. */ readonly issues?: undefined } /** * The result interface if validation fails. */ interface StandardSchemaV1FailureResult { /** * The issues of failed validation. */ readonly issues: ReadonlyArray } /** * The issue interface of the failure output. */ export interface StandardSchemaV1Issue { /** * The error message of the issue. */ readonly message: string /** * The path of the issue, if any. */ readonly path?: | ReadonlyArray | undefined } /** * The path segment interface of the issue. */ interface StandardSchemaV1PathSegment { /** * The key representing a path segment. */ readonly key: PropertyKey } /** * The Standard Schema types interface. */ interface StandardSchemaV1Types { /** * The input type of the schema. */ readonly input: Input /** * The output type of the schema. */ readonly output: Output }