import { ArrayFormatter, Schema } from "@effect/schema" import type { ParseOptions } from "@effect/schema/AST" import type { DeepKeys, DeepValue, FieldApi, FormApi, FormValidationError, ValidationError, Validator } from "@tanstack/form-core" import { Array, Effect, Either, flow, Layer, ManagedRuntime, pipe } from "effect" import { mapValues } from "remeda" export const makeSchemaFormValidator = ( schema: Schema.Schema, options?: ParseOptions, ) => { const decodeEither = Schema.decodeEither(schema, options) return < TFormData extends I, TFormValidator extends Validator | undefined, >(props: { value: TFormData formApi: FormApi }): FormValidationError => decodeEither(props.value).pipe(result => Either.isLeft(result) ? { form: "Please check the fields", fields: issuesToFieldsRecord(ArrayFormatter.formatErrorSync(result.left), props.formApi) as any, } : null ) } export const makeSchemaAsyncFormValidator = ( schema: Schema.Schema, options?: ParseOptions, ) => { const runtime = ManagedRuntime.make(Layer.empty) const decode = Schema.decode(schema, options) return < TFormData extends I, TFormValidator extends Validator | undefined, >(props: { value: TFormData formApi: FormApi signal: AbortSignal }): Promise> => decode(props.value).pipe( Effect.matchEffect({ onSuccess: () => Effect.succeed(null), onFailure: e => ArrayFormatter.formatError(e).pipe( Effect.map(issues => ({ form: "Please check the fields", fields: issuesToFieldsRecord(issues, props.formApi) as any, })) ), }), prgm => runtime.runPromise(prgm, { signal: props.signal }), ) } /** * TODO: handle nested array fields */ const issuesToFieldsRecord = < TFormData, TFormValidator extends Validator | undefined = undefined, >( issues: readonly ArrayFormatter.Issue[], _formApi: FormApi, ) => pipe(issues, Array.groupBy(issue => issue.path.join(".")), mapValues(flow( Array.map(issue => issue.message), Array.join("\n"), )), ) export const makeSchemaFieldValidator = ( schema: Schema.Schema, options?: ParseOptions, ) => { const decodeEither = Schema.decodeEither(schema, options) return < TParentData, TName extends DeepKeys, TFieldValidator extends Validator, unknown> | undefined, TFormValidator extends Validator | undefined, TData extends I & DeepValue, >(props: { value: TData fieldApi: FieldApi }): ValidationError => decodeEither(props.value).pipe(result => Either.isLeft(result) ? ArrayFormatter.formatErrorSync(result.left) .map(issue => issue.message) .join("\n") : null ) } export const makeSchemaAsyncFieldValidator = ( schema: Schema.Schema, options?: ParseOptions, ) => { const runtime = ManagedRuntime.make(Layer.empty) const decode = Schema.decode(schema, options) return < TParentData, TName extends DeepKeys, TFieldValidator extends Validator, unknown> | undefined, TFormValidator extends Validator | undefined, TData extends I & DeepValue, >(props: { value: TData fieldApi: FieldApi signal: AbortSignal }): Promise => decode(props.value).pipe( Effect.matchEffect({ onSuccess: () => Effect.succeed(null), onFailure: e => ArrayFormatter.formatError(e).pipe( Effect.map(flow( Array.map(issue => issue.message), Array.join("\n"), )) ), }), prgm => runtime.runPromise(prgm, { signal: props.signal }), ) }