import type * as Registry from "@effect-atom/atom/Registry" import type * as Effect from "effect/Effect" import type * as Option from "effect/Option" import * as Predicate from "effect/Predicate" import * as Schema from "effect/Schema" import type { AnyFieldDef, ArrayFieldDef, DecodedFromFields, EncodedFromFields, FieldDef, FieldsRecord } from "./Field.ts" import { isArrayFieldDef, isFieldDef, makeField } from "./Field.ts" export interface SubmittedValues { readonly encoded: EncodedFromFields readonly decoded: DecodedFromFields } export const FieldTypeId: unique symbol = Symbol.for("@lucas-barake/effect-form/Field") export type FieldTypeId = typeof FieldTypeId export interface FieldRef { readonly [FieldTypeId]: FieldTypeId readonly _S: S readonly key: string } export const makeFieldRef = (key: string): FieldRef => ({ [FieldTypeId]: FieldTypeId, _S: undefined as any, key }) export const TypeId: unique symbol = Symbol.for("@lucas-barake/effect-form/Form") export type TypeId = typeof TypeId export interface FormState { readonly values: EncodedFromFields readonly initialValues: EncodedFromFields readonly lastSubmittedValues: Option.Option> readonly touched: { readonly [K in keyof TFields]: boolean } readonly submitCount: number readonly validationCount: number readonly dirtyFields: ReadonlySet } interface SyncRefinement { readonly _tag: "sync" readonly fn: (values: unknown) => Schema.FilterOutput } interface AsyncRefinement { readonly _tag: "async" readonly fn: (values: unknown) => Effect.Effect } type Refinement = SyncRefinement | AsyncRefinement export interface FormBuilder { readonly [TypeId]: TypeId readonly fields: TFields readonly refinements: ReadonlyArray readonly _R?: R addField( this: FormBuilder, field: FieldDef ): FormBuilder }, R | Schema.Schema.Context> addField( this: FormBuilder, field: ArrayFieldDef ): FormBuilder }, R | Schema.Schema.Context> addField( this: FormBuilder, key: K, schema: S ): FormBuilder }, R | Schema.Schema.Context> merge( this: FormBuilder, other: FormBuilder ): FormBuilder refine( this: FormBuilder, predicate: (values: DecodedFromFields) => Schema.FilterOutput ): FormBuilder refineEffect( this: FormBuilder, predicate: (values: DecodedFromFields) => Effect.Effect ): FormBuilder> } const FormBuilderProto = { [TypeId]: TypeId, addField( this: FormBuilder, keyOrField: string | AnyFieldDef, schema?: Schema.Schema.Any ): FormBuilder { const field = typeof keyOrField === "string" ? makeField(keyOrField, schema!) : keyOrField const newSelf = Object.create(FormBuilderProto) newSelf.fields = { ...this.fields, [field.key]: field } newSelf.refinements = this.refinements return newSelf }, merge( this: FormBuilder, other: FormBuilder ): FormBuilder { const newSelf = Object.create(FormBuilderProto) newSelf.fields = { ...this.fields, ...other.fields } newSelf.refinements = [...this.refinements, ...other.refinements] return newSelf }, refine( this: FormBuilder, predicate: (values: DecodedFromFields) => Schema.FilterOutput ): FormBuilder { const newSelf = Object.create(FormBuilderProto) newSelf.fields = this.fields newSelf.refinements = [ ...this.refinements, { _tag: "sync" as const, fn: (values: unknown) => predicate(values as DecodedFromFields) } ] return newSelf }, refineEffect( this: FormBuilder, predicate: (values: DecodedFromFields) => Effect.Effect ): FormBuilder> { const newSelf = Object.create(FormBuilderProto) newSelf.fields = this.fields newSelf.refinements = [ ...this.refinements, { _tag: "async" as const, fn: (values: unknown) => predicate(values as DecodedFromFields) } ] return newSelf } } export const isFormBuilder = (u: unknown): u is FormBuilder => Predicate.hasProperty(u, TypeId) // eslint-disable-next-line @typescript-eslint/no-empty-object-type export const empty: FormBuilder<{}, never> = (() => { const self = Object.create(FormBuilderProto) self.fields = {} self.refinements = [] return self })() export const buildSchema = ( self: FormBuilder ): Schema.Schema, EncodedFromFields, R> => { const schemaFields: Record = {} for (const [key, def] of Object.entries(self.fields)) { if (isArrayFieldDef(def)) { schemaFields[key] = Schema.Array(def.itemSchema) } else if (isFieldDef(def)) { schemaFields[key] = def.schema } } let schema: Schema.Schema = Schema.Struct(schemaFields) for (const refinement of self.refinements) { if (refinement._tag === "sync") { schema = schema.pipe(Schema.filter(refinement.fn)) } else { schema = schema.pipe(Schema.filterEffect(refinement.fn)) } } return schema as Schema.Schema< DecodedFromFields, EncodedFromFields, R > }