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 }),
)
}