@lucas-barake/effect-form 0.25.0-beta.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/FormAtoms.ts CHANGED
@@ -1,9 +1,10 @@
1
+ import * as Atom from "@effect-atom/atom/Atom"
1
2
  import * as Cause from "effect/Cause"
2
3
  import * as Effect from "effect/Effect"
3
4
  import { pipe } from "effect/Function"
4
5
  import * as Option from "effect/Option"
6
+ import * as ParseResult from "effect/ParseResult"
5
7
  import * as Schema from "effect/Schema"
6
- import * as Atom from "effect/unstable/reactivity/Atom"
7
8
  import * as Field from "./Field.ts"
8
9
  import * as FormBuilder from "./FormBuilder.ts"
9
10
  import { recalculateDirtyFieldsForArray, recalculateDirtySubtree } from "./internal/dirty.ts"
@@ -18,7 +19,7 @@ export interface FieldAtoms {
18
19
  readonly touchedAtom: Atom.Writable<boolean, boolean>
19
20
  readonly errorAtom: Atom.Atom<Option.Option<Validation.ErrorEntry>>
20
21
  readonly isDirtyAtom: Atom.Atom<boolean>
21
- readonly validationAtom: Atom.AtomResultFn<unknown, void, Schema.SchemaError>
22
+ readonly validationAtom: Atom.AtomResultFn<unknown, void, ParseResult.ParseError>
22
23
  readonly displayErrorAtom: Atom.Atom<Option.Option<string>>
23
24
  readonly fieldValidationCountAtom: Atom.Writable<number, number>
24
25
  readonly shouldValidateAtom: Atom.Atom<boolean>
@@ -53,9 +54,9 @@ export interface FormAtomsConfig<TFields extends Field.FieldsRecord, R, A, E, Su
53
54
 
54
55
  export type FieldRefs<TFields extends Field.FieldsRecord,> = {
55
56
  readonly [K in keyof TFields]: TFields[K] extends Field.FieldDef<any, infer S>
56
- ? FormBuilder.FieldRef<Schema.Codec.Encoded<S>>
57
+ ? FormBuilder.FieldRef<Schema.Schema.Encoded<S>>
57
58
  : TFields[K] extends Field.ArrayFieldDef<any, infer S>
58
- ? FormBuilder.FieldRef<ReadonlyArray<Schema.Codec.Encoded<S>>>
59
+ ? FormBuilder.FieldRef<ReadonlyArray<Schema.Schema.Encoded<S>>>
59
60
  : never
60
61
  }
61
62
 
@@ -75,22 +76,22 @@ export interface FormAtoms<TFields extends Field.FieldsRecord, R, A = void, E =
75
76
  readonly changedSinceSubmitFieldsAtom: Atom.Atom<ReadonlySet<string>>
76
77
  readonly hasChangedSinceSubmitAtom: Atom.Atom<boolean>
77
78
 
78
- readonly submitAtom: Atom.AtomResultFn<SubmitArgs, A, E | Schema.SchemaError>
79
+ readonly submitAtom: Atom.AtomResultFn<SubmitArgs, A, E | ParseResult.ParseError>
79
80
  readonly validateAtom: Atom.AtomResultFn<void, void, never>
80
81
 
81
- readonly combinedSchema: Schema.Codec<Field.DecodedFromFields<TFields>, Field.EncodedFromFields<TFields>, R>
82
+ readonly combinedSchema: Schema.Schema<Field.DecodedFromFields<TFields>, Field.EncodedFromFields<TFields>, R>
82
83
 
83
84
  readonly fieldRefs: FieldRefs<TFields>
84
85
 
85
- readonly validationAtomsRegistry: WeakRegistry<Atom.AtomResultFn<unknown, void, Schema.SchemaError>>
86
+ readonly validationAtomsRegistry: WeakRegistry<Atom.AtomResultFn<unknown, void, ParseResult.ParseError>>
86
87
  readonly fieldAtomsRegistry: WeakRegistry<FieldAtoms>
87
88
 
88
89
  readonly getOrCreateValidationAtom: (
89
90
  fieldPath: string,
90
- schema: Schema.Top
91
- ) => Atom.AtomResultFn<unknown, void, Schema.SchemaError>
91
+ schema: Schema.Schema.Any
92
+ ) => Atom.AtomResultFn<unknown, void, ParseResult.ParseError>
92
93
 
93
- readonly getOrCreateFieldAtoms: (fieldPath: string, schema: Schema.Top) => FieldAtoms
94
+ readonly getOrCreateFieldAtoms: (fieldPath: string, schema: Schema.Schema.Any) => FieldAtoms
94
95
 
95
96
  readonly resetValidationAtoms: (ctx: { set: <R, W,>(atom: Atom.Writable<R, W>, value: W) => void }) => void
96
97
 
@@ -155,7 +156,7 @@ export interface FormOperations<TFields extends Field.FieldsRecord,> {
155
156
  readonly appendArrayItem: (
156
157
  state: FormBuilder.FormState<TFields>,
157
158
  arrayPath: string,
158
- itemSchema: Schema.Top,
159
+ itemSchema: Schema.Schema.Any,
159
160
  value?: unknown
160
161
  ) => FormBuilder.FormState<TFields>
161
162
 
@@ -258,14 +259,14 @@ export const make = <TFields extends Field.FieldsRecord, R, A, E, SubmitArgs = v
258
259
  })
259
260
  ).pipe(Atom.setIdleTTL(0))
260
261
 
261
- const validationAtomsRegistry = createWeakRegistry<Atom.AtomResultFn<unknown, void, Schema.SchemaError>>()
262
+ const validationAtomsRegistry = createWeakRegistry<Atom.AtomResultFn<unknown, void, ParseResult.ParseError>>()
262
263
  const fieldAtomsRegistry = createWeakRegistry<FieldAtoms>()
263
264
  const publicFieldAtomsRegistry = createWeakRegistry<PublicFieldAtoms<unknown>>()
264
- const validationSchemaRegistry = new Map<string, Schema.Top>()
265
- const fieldSchemaRegistry = new Map<string, Schema.Top>()
265
+ const validationSchemaRegistry = new Map<string, Schema.Schema.Any>()
266
+ const fieldSchemaRegistry = new Map<string, Schema.Schema.Any>()
266
267
  const isDirtyAtomsRegistry = createWeakRegistry<Atom.Atom<boolean>>()
267
268
 
268
- const fieldSchemasByKey = new Map<string, Schema.Top>()
269
+ const fieldSchemasByKey = new Map<string, Schema.Schema.Any>()
269
270
  for (const [key, def] of Object.entries(fields)) {
270
271
  if (Field.isArrayFieldDef(def)) {
271
272
  fieldSchemasByKey.set(key, Schema.Array(def.itemSchema))
@@ -276,24 +277,24 @@ export const make = <TFields extends Field.FieldsRecord, R, A, E, SubmitArgs = v
276
277
 
277
278
  const getOrCreateValidationAtom = (
278
279
  fieldPath: string,
279
- schema: Schema.Top
280
- ): Atom.AtomResultFn<unknown, void, Schema.SchemaError> => {
280
+ schema: Schema.Schema.Any
281
+ ): Atom.AtomResultFn<unknown, void, ParseResult.ParseError> => {
281
282
  const existing = validationAtomsRegistry.get(fieldPath)
282
283
  const existingSchema = validationSchemaRegistry.get(fieldPath)
283
284
  if (existing && existingSchema === schema) return existing
284
285
 
285
286
  const validationAtom = runtime
286
287
  .fn<unknown>()((value: unknown) =>
287
- pipe(Schema.decodeUnknownEffect(schema)(value) as Effect.Effect<unknown, Schema.SchemaError, R>, Effect.asVoid)
288
+ pipe(Schema.decodeUnknown(schema)(value) as Effect.Effect<unknown, ParseResult.ParseError, R>, Effect.asVoid)
288
289
  )
289
- .pipe(Atom.setIdleTTL(0)) as Atom.AtomResultFn<unknown, void, Schema.SchemaError>
290
+ .pipe(Atom.setIdleTTL(0)) as Atom.AtomResultFn<unknown, void, ParseResult.ParseError>
290
291
 
291
292
  validationAtomsRegistry.set(fieldPath, validationAtom)
292
293
  validationSchemaRegistry.set(fieldPath, schema)
293
294
  return validationAtom
294
295
  }
295
296
 
296
- const getOrCreateFieldAtoms = (fieldPath: string, schema: Schema.Top): FieldAtoms => {
297
+ const getOrCreateFieldAtoms = (fieldPath: string, schema: Schema.Schema.Any): FieldAtoms => {
297
298
  const existing = fieldAtomsRegistry.get(fieldPath)
298
299
  const existingSchema = fieldSchemaRegistry.get(fieldPath)
299
300
  if (existing && existingSchema === schema) return existing
@@ -363,8 +364,8 @@ export const make = <TFields extends Field.FieldsRecord, R, A, E, SubmitArgs = v
363
364
 
364
365
  let livePerFieldError: Option.Option<string> = Option.none()
365
366
  if (validationResult._tag === "Failure") {
366
- const parseError = Cause.findErrorOption(validationResult.cause)
367
- if (Option.isSome(parseError) && Schema.isSchemaError(parseError.value)) {
367
+ const parseError = Cause.failureOption(validationResult.cause)
368
+ if (Option.isSome(parseError) && ParseResult.isParseError(parseError.value)) {
368
369
  livePerFieldError = Validation.extractFirstError(parseError.value)
369
370
  }
370
371
  }
@@ -469,9 +470,9 @@ export const make = <TFields extends Field.FieldsRecord, R, A, E, SubmitArgs = v
469
470
  const values = state.value.values
470
471
  get.set(errorsAtom, new Map())
471
472
  const decoded = yield* pipe(
472
- Schema.decodeUnknownEffect(combinedSchema)(values, { errors: "all" }) as Effect.Effect<
473
+ Schema.decodeUnknown(combinedSchema, { errors: "all" })(values) as Effect.Effect<
473
474
  Field.DecodedFromFields<TFields>,
474
- Schema.SchemaError,
475
+ ParseResult.ParseError,
475
476
  R
476
477
  >,
477
478
  Effect.tapError((parseError) =>
@@ -498,7 +499,7 @@ export const make = <TFields extends Field.FieldsRecord, R, A, E, SubmitArgs = v
498
499
  }),
499
500
  config.reactivityKeys ? { reactivityKeys: config.reactivityKeys } : undefined
500
501
  )
501
- .pipe(Atom.setIdleTTL(0)) as Atom.AtomResultFn<SubmitArgs, A, E | Schema.SchemaError>
502
+ .pipe(Atom.setIdleTTL(0)) as Atom.AtomResultFn<SubmitArgs, A, E | ParseResult.ParseError>
502
503
 
503
504
  const validateAtom = runtime
504
505
  .fn<void>()(
@@ -509,12 +510,12 @@ export const make = <TFields extends Field.FieldsRecord, R, A, E, SubmitArgs = v
509
510
  const values = state.value.values
510
511
  get.set(errorsAtom, new Map())
511
512
  yield* pipe(
512
- Schema.decodeUnknownEffect(combinedSchema)(values, { errors: "all" }) as Effect.Effect<
513
+ Schema.decodeUnknown(combinedSchema, { errors: "all" })(values) as Effect.Effect<
513
514
  Field.DecodedFromFields<TFields>,
514
- Schema.SchemaError,
515
+ ParseResult.ParseError,
515
516
  R
516
517
  >,
517
- Effect.catchTag("SchemaError", (parseError) =>
518
+ Effect.catchTag("ParseError", (parseError) =>
518
519
  Effect.sync(() => {
519
520
  const routedErrors = Validation.routeErrorsWithSource(parseError)
520
521
  get.set(errorsAtom, routedErrors)
@@ -1,9 +1,8 @@
1
+ import type * as Registry from "@effect-atom/atom/Registry"
1
2
  import type * as Effect from "effect/Effect"
2
3
  import type * as Option from "effect/Option"
3
4
  import * as Predicate from "effect/Predicate"
4
5
  import * as Schema from "effect/Schema"
5
- import * as SchemaGetter from "effect/SchemaGetter"
6
- import type * as AtomRegistry from "effect/unstable/reactivity/AtomRegistry"
7
6
 
8
7
  import type {
9
8
  AnyFieldDef,
@@ -15,11 +14,6 @@ import type {
15
14
  } from "./Field.ts"
16
15
  import { isArrayFieldDef, isFieldDef, makeField } from "./Field.ts"
17
16
 
18
- type FilterResult = undefined | boolean | string | {
19
- readonly path: ReadonlyArray<PropertyKey>
20
- readonly message: string
21
- }
22
-
23
17
  export interface SubmittedValues<TFields extends FieldsRecord,> {
24
18
  readonly encoded: EncodedFromFields<TFields>
25
19
  readonly decoded: DecodedFromFields<TFields>
@@ -57,12 +51,12 @@ export interface FormState<TFields extends FieldsRecord,> {
57
51
 
58
52
  interface SyncRefinement {
59
53
  readonly _tag: "sync"
60
- readonly fn: (values: unknown) => FilterResult
54
+ readonly fn: (values: unknown) => Schema.FilterOutput
61
55
  }
62
56
 
63
57
  interface AsyncRefinement {
64
58
  readonly _tag: "async"
65
- readonly fn: (values: unknown) => Effect.Effect<FilterResult, never, unknown>
59
+ readonly fn: (values: unknown) => Effect.Effect<Schema.FilterOutput, never, unknown>
66
60
  }
67
61
 
68
62
  type Refinement = SyncRefinement | AsyncRefinement
@@ -73,21 +67,21 @@ export interface FormBuilder<TFields extends FieldsRecord, R,> {
73
67
  readonly refinements: ReadonlyArray<Refinement>
74
68
  readonly _R?: R
75
69
 
76
- addField<K extends string, S extends Schema.Top,>(
70
+ addField<K extends string, S extends Schema.Schema.Any,>(
77
71
  this: FormBuilder<TFields, R>,
78
72
  field: FieldDef<K, S>
79
- ): FormBuilder<TFields & { readonly [key in K]: FieldDef<K, S> }, R | Schema.Codec.DecodingServices<S>>
73
+ ): FormBuilder<TFields & { readonly [key in K]: FieldDef<K, S> }, R | Schema.Schema.Context<S>>
80
74
 
81
- addField<K extends string, S extends Schema.Top,>(
75
+ addField<K extends string, S extends Schema.Schema.Any,>(
82
76
  this: FormBuilder<TFields, R>,
83
77
  field: ArrayFieldDef<K, S>
84
- ): FormBuilder<TFields & { readonly [key in K]: ArrayFieldDef<K, S> }, R | Schema.Codec.DecodingServices<S>>
78
+ ): FormBuilder<TFields & { readonly [key in K]: ArrayFieldDef<K, S> }, R | Schema.Schema.Context<S>>
85
79
 
86
- addField<K extends string, S extends Schema.Top,>(
80
+ addField<K extends string, S extends Schema.Schema.Any,>(
87
81
  this: FormBuilder<TFields, R>,
88
82
  key: K,
89
83
  schema: S
90
- ): FormBuilder<TFields & { readonly [key in K]: FieldDef<K, S> }, R | Schema.Codec.DecodingServices<S>>
84
+ ): FormBuilder<TFields & { readonly [key in K]: FieldDef<K, S> }, R | Schema.Schema.Context<S>>
91
85
 
92
86
  merge<TFields2 extends FieldsRecord, R2,>(
93
87
  this: FormBuilder<TFields, R>,
@@ -96,13 +90,13 @@ export interface FormBuilder<TFields extends FieldsRecord, R,> {
96
90
 
97
91
  refine(
98
92
  this: FormBuilder<TFields, R>,
99
- predicate: (values: DecodedFromFields<TFields>) => FilterResult
93
+ predicate: (values: DecodedFromFields<TFields>) => Schema.FilterOutput
100
94
  ): FormBuilder<TFields, R>
101
95
 
102
96
  refineEffect<RD,>(
103
97
  this: FormBuilder<TFields, R>,
104
- predicate: (values: DecodedFromFields<TFields>) => Effect.Effect<FilterResult, never, RD>
105
- ): FormBuilder<TFields, R | Exclude<RD, AtomRegistry.AtomRegistry>>
98
+ predicate: (values: DecodedFromFields<TFields>) => Effect.Effect<Schema.FilterOutput, never, RD>
99
+ ): FormBuilder<TFields, R | Exclude<RD, Registry.AtomRegistry>>
106
100
  }
107
101
 
108
102
  const FormBuilderProto = {
@@ -110,7 +104,7 @@ const FormBuilderProto = {
110
104
  addField<TFields extends FieldsRecord, R,>(
111
105
  this: FormBuilder<TFields, R>,
112
106
  keyOrField: string | AnyFieldDef,
113
- schema?: Schema.Top
107
+ schema?: Schema.Schema.Any
114
108
  ): FormBuilder<any, any> {
115
109
  const field = typeof keyOrField === "string"
116
110
  ? makeField(keyOrField, schema!)
@@ -131,7 +125,7 @@ const FormBuilderProto = {
131
125
  },
132
126
  refine<TFields extends FieldsRecord, R,>(
133
127
  this: FormBuilder<TFields, R>,
134
- predicate: (values: DecodedFromFields<TFields>) => FilterResult
128
+ predicate: (values: DecodedFromFields<TFields>) => Schema.FilterOutput
135
129
  ): FormBuilder<TFields, R> {
136
130
  const newSelf = Object.create(FormBuilderProto)
137
131
  newSelf.fields = this.fields
@@ -143,8 +137,8 @@ const FormBuilderProto = {
143
137
  },
144
138
  refineEffect<TFields extends FieldsRecord, R, RD,>(
145
139
  this: FormBuilder<TFields, R>,
146
- predicate: (values: DecodedFromFields<TFields>) => Effect.Effect<FilterResult, never, RD>
147
- ): FormBuilder<TFields, R | Exclude<RD, AtomRegistry.AtomRegistry>> {
140
+ predicate: (values: DecodedFromFields<TFields>) => Effect.Effect<Schema.FilterOutput, never, RD>
141
+ ): FormBuilder<TFields, R | Exclude<RD, Registry.AtomRegistry>> {
148
142
  const newSelf = Object.create(FormBuilderProto)
149
143
  newSelf.fields = this.fields
150
144
  newSelf.refinements = [
@@ -167,8 +161,8 @@ export const empty: FormBuilder<{}, never> = (() => {
167
161
 
168
162
  export const buildSchema = <TFields extends FieldsRecord, R,>(
169
163
  self: FormBuilder<TFields, R>
170
- ): Schema.Codec<DecodedFromFields<TFields>, EncodedFromFields<TFields>, R> => {
171
- const schemaFields: Record<string, Schema.Top> = {}
164
+ ): Schema.Schema<DecodedFromFields<TFields>, EncodedFromFields<TFields>, R> => {
165
+ const schemaFields: Record<string, Schema.Schema.Any> = {}
172
166
  for (const [key, def] of Object.entries(self.fields)) {
173
167
  if (isArrayFieldDef(def)) {
174
168
  schemaFields[key] = Schema.Array(def.itemSchema)
@@ -177,22 +171,17 @@ export const buildSchema = <TFields extends FieldsRecord, R,>(
177
171
  }
178
172
  }
179
173
 
180
- let schema: Schema.Codec<any, any, any, any> = Schema.Struct(schemaFields)
174
+ let schema: Schema.Schema<any, any, any> = Schema.Struct(schemaFields)
181
175
 
182
176
  for (const refinement of self.refinements) {
183
177
  if (refinement._tag === "sync") {
184
- schema = schema.pipe(Schema.check(Schema.makeFilter((input) => refinement.fn(input))))
178
+ schema = schema.pipe(Schema.filter(refinement.fn))
185
179
  } else {
186
- schema = schema.pipe(
187
- Schema.decode({
188
- decode: SchemaGetter.checkEffect((input) => refinement.fn(input)),
189
- encode: SchemaGetter.passthrough()
190
- })
191
- )
180
+ schema = schema.pipe(Schema.filterEffect(refinement.fn))
192
181
  }
193
182
  }
194
183
 
195
- return schema as Schema.Codec<
184
+ return schema as Schema.Schema<
196
185
  DecodedFromFields<TFields>,
197
186
  EncodedFromFields<TFields>,
198
187
  R
package/src/Mode.ts CHANGED
@@ -24,9 +24,7 @@ export const parse = (mode?: FormMode): ParsedMode => {
24
24
  }
25
25
 
26
26
  if (validation === "onChange") {
27
- const debounceMs = mode?.debounce === undefined
28
- ? null
29
- : Duration.toMillis(Duration.fromDurationInputUnsafe(mode.debounce))
27
+ const debounceMs = mode?.debounce === undefined ? null : Duration.toMillis(mode.debounce)
30
28
  const autoSubmit = mode?.autoSubmit === true
31
29
  return { validation: "onChange", debounce: debounceMs, autoSubmit }
32
30
  }
package/src/Validation.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as Option from "effect/Option"
2
- import type * as Schema from "effect/Schema"
3
- import * as SchemaIssue from "effect/SchemaIssue"
2
+ import * as ParseResult from "effect/ParseResult"
3
+ import type * as AST from "effect/SchemaAST"
4
4
  import { schemaPathToFieldPath } from "./Path.ts"
5
5
 
6
6
  export type ErrorSource = "field" | "refinement"
@@ -13,51 +13,75 @@ export interface ErrorEntry {
13
13
  interface IssueSourceEntry {
14
14
  readonly path: ReadonlyArray<PropertyKey>
15
15
  readonly source: ErrorSource
16
- readonly issue: SchemaIssue.Issue
16
+ readonly issue: ParseResult.ParseIssue
17
17
  }
18
18
 
19
- const standardFormatter = SchemaIssue.makeFormatterStandardSchemaV1()
19
+ const getBaseAST = (ast: AST.AST): AST.AST => {
20
+ switch (ast._tag) {
21
+ case "Refinement":
22
+ case "Transformation":
23
+ return getBaseAST(ast.from)
24
+ default:
25
+ return ast
26
+ }
27
+ }
28
+
29
+ const isCompositeType = (ast: AST.AST): boolean => {
30
+ const base = getBaseAST(ast)
31
+ switch (base._tag) {
32
+ case "TypeLiteral": // Schema.Struct
33
+ case "TupleType": // Schema.Tuple
34
+ case "Declaration": // Schema.Class, Schema.TaggedClass
35
+ case "Union": // Schema.Union
36
+ case "Suspend": // Recursive schemas
37
+ return true
38
+ default:
39
+ return false
40
+ }
41
+ }
20
42
 
21
- const collectIssueSources = (error: Schema.SchemaError): ReadonlyArray<IssueSourceEntry> => {
43
+ const collectIssueSources = (error: ParseResult.ParseError): ReadonlyArray<IssueSourceEntry> => {
22
44
  const entries: Array<IssueSourceEntry> = []
23
45
 
24
- const walk = (issue: SchemaIssue.Issue, path: ReadonlyArray<PropertyKey>, source: ErrorSource): void => {
46
+ const walk = (issue: ParseResult.ParseIssue, path: ReadonlyArray<PropertyKey>, source: ErrorSource): void => {
25
47
  switch (issue._tag) {
26
- case "Filter":
27
- if (path.length === 0) {
48
+ case "Refinement":
49
+ if (issue.kind === "Predicate" && isCompositeType(issue.ast.from) && path.length === 0) {
28
50
  walk(issue.issue, path, "refinement")
29
51
  } else {
30
52
  walk(issue.issue, path, source)
31
53
  }
32
54
  break
33
- case "Encoding":
34
- if (path.length === 0) {
35
- walk(issue.issue, path, "refinement")
36
- } else {
37
- walk(issue.issue, path, source)
38
- }
39
- break
40
- case "Pointer":
41
- walk(issue.issue, [...path, ...issue.path], source)
42
- break
43
- case "Composite":
44
- for (const sub of issue.issues) {
45
- walk(sub, path, source)
46
- }
55
+ case "Pointer": {
56
+ const pointerPath = Array.isArray(issue.path) ? issue.path : [issue.path]
57
+ walk(issue.issue, [...path, ...pointerPath], source)
47
58
  break
48
- case "AnyOf":
49
- for (const sub of issue.issues) {
59
+ }
60
+ case "Composite": {
61
+ const issues = Array.isArray(issue.issues) ? issue.issues : [issue.issues]
62
+ for (const sub of issues) {
50
63
  walk(sub, path, source)
51
64
  }
52
65
  break
53
- case "InvalidType":
54
- case "InvalidValue":
55
- case "MissingKey":
56
- case "UnexpectedKey":
66
+ }
67
+ case "Type":
68
+ case "Missing":
69
+ case "Unexpected":
57
70
  case "Forbidden":
58
- case "OneOf":
59
71
  entries.push({ path, source, issue })
60
72
  break
73
+ case "Transformation":
74
+ if (
75
+ issue.kind === "Transformation" &&
76
+ issue.ast.transformation._tag === "FinalTransformation" &&
77
+ isCompositeType(issue.ast.from) &&
78
+ path.length === 0
79
+ ) {
80
+ walk(issue.issue, path, "refinement")
81
+ } else {
82
+ walk(issue.issue, path, source)
83
+ }
84
+ break
61
85
  }
62
86
  }
63
87
 
@@ -65,34 +89,25 @@ const collectIssueSources = (error: Schema.SchemaError): ReadonlyArray<IssueSour
65
89
  return entries
66
90
  }
67
91
 
68
- const getIssueMessage = (issue: SchemaIssue.Issue): string | undefined => {
69
- const formatted = standardFormatter(issue)
70
- return formatted.issues[0]?.message
92
+ const getIssueMessage = (issue: ParseResult.ParseIssue): string | undefined => {
93
+ const formatted = ParseResult.ArrayFormatter.formatIssueSync(issue)
94
+ return formatted[0]?.message
71
95
  }
72
96
 
73
- export const extractFirstError = (error: Schema.SchemaError): Option.Option<string> => {
74
- const formatted = standardFormatter(error.issue)
75
- if (formatted.issues.length === 0) {
97
+ export const extractFirstError = (error: ParseResult.ParseError): Option.Option<string> => {
98
+ const issues = ParseResult.ArrayFormatter.formatErrorSync(error)
99
+ if (issues.length === 0) {
76
100
  return Option.none()
77
101
  }
78
- return Option.some(formatted.issues[0].message)
79
- }
80
-
81
- const normalizePath = (
82
- path: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }> | undefined
83
- ): ReadonlyArray<PropertyKey> => {
84
- if (!path) return []
85
- return path.map((segment) =>
86
- typeof segment === "object" && segment !== null && "key" in segment ? segment.key : segment as PropertyKey
87
- )
102
+ return Option.some(issues[0].message)
88
103
  }
89
104
 
90
- export const routeErrors = (error: Schema.SchemaError): Map<string, string> => {
105
+ export const routeErrors = (error: ParseResult.ParseError): Map<string, string> => {
91
106
  const result = new Map<string, string>()
92
- const formatted = standardFormatter(error.issue)
107
+ const issues = ParseResult.ArrayFormatter.formatErrorSync(error)
93
108
 
94
- for (const issue of formatted.issues) {
95
- const fieldPath = schemaPathToFieldPath(normalizePath(issue.path))
109
+ for (const issue of issues) {
110
+ const fieldPath = schemaPathToFieldPath(issue.path)
96
111
  if (fieldPath && !result.has(fieldPath)) {
97
112
  result.set(fieldPath, issue.message)
98
113
  }
@@ -101,9 +116,9 @@ export const routeErrors = (error: Schema.SchemaError): Map<string, string> => {
101
116
  return result
102
117
  }
103
118
 
104
- export const routeErrorsWithSource = (error: Schema.SchemaError): Map<string, ErrorEntry> => {
119
+ export const routeErrorsWithSource = (error: ParseResult.ParseError): Map<string, ErrorEntry> => {
105
120
  const result = new Map<string, ErrorEntry>()
106
- const formattedIssues = standardFormatter(error.issue).issues
121
+ const formattedIssues = ParseResult.ArrayFormatter.formatErrorSync(error)
107
122
  const issueSources = collectIssueSources(error)
108
123
  const messageSources = new Map<string, ErrorSource>()
109
124
  const refinementPaths = new Set<string>()
@@ -124,7 +139,7 @@ export const routeErrorsWithSource = (error: Schema.SchemaError): Map<string, Er
124
139
  }
125
140
 
126
141
  for (const issue of formattedIssues) {
127
- const fieldPath = schemaPathToFieldPath(normalizePath(issue.path)) ?? ""
142
+ const fieldPath = schemaPathToFieldPath(issue.path) ?? ""
128
143
  if (result.has(fieldPath)) continue
129
144
  const preferredSource: ErrorSource = refinementPaths.has(fieldPath) ? "refinement" : "field"
130
145
  const messageKey = `${fieldPath}::${issue.message}`
@@ -137,7 +152,7 @@ export const routeErrorsWithSource = (error: Schema.SchemaError): Map<string, Er
137
152
 
138
153
  if (result.size < formattedIssues.length) {
139
154
  for (const issue of formattedIssues) {
140
- const fieldPath = schemaPathToFieldPath(normalizePath(issue.path)) ?? ""
155
+ const fieldPath = schemaPathToFieldPath(issue.path) ?? ""
141
156
  if (result.has(fieldPath)) continue
142
157
  const messageKey = `${fieldPath}::${issue.message}`
143
158
  const issueSource = messageSources.get(messageKey) ?? "field"
@@ -1,4 +1,5 @@
1
1
  import * as Equal from "effect/Equal"
2
+ import * as Utils from "effect/Utils"
2
3
  import { getNestedValue, isPathUnderRoot } from "../Path.ts"
3
4
 
4
5
  export const recalculateDirtyFieldsForArray = (
@@ -25,7 +26,8 @@ export const recalculateDirtyFieldsForArray = (
25
26
 
26
27
  if (newItem === initialItem) continue
27
28
 
28
- if (!Equal.equals(newItem, initialItem)) {
29
+ const isEqual = Utils.structuralRegion(() => Equal.equals(newItem, initialItem))
30
+ if (!isEqual) {
29
31
  nextDirty.add(itemPath)
30
32
  }
31
33
  }
@@ -95,7 +97,8 @@ export const recalculateDirtySubtree = (
95
97
  }
96
98
  }
97
99
  } else {
98
- if (!Equal.equals(current, initial) && path) nextDirty.add(path)
100
+ const isEqual = Utils.structuralRegion(() => Equal.equals(current, initial))
101
+ if (!isEqual && path) nextDirty.add(path)
99
102
  }
100
103
  }
101
104