@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/dist/Field.d.ts +10 -10
- package/dist/Field.d.ts.map +1 -1
- package/dist/Field.js +20 -15
- package/dist/Field.js.map +1 -1
- package/dist/FieldState.d.ts +2 -1
- package/dist/FieldState.d.ts.map +1 -1
- package/dist/FormAtoms.d.ts +10 -9
- package/dist/FormAtoms.d.ts.map +1 -1
- package/dist/FormAtoms.js +9 -8
- package/dist/FormAtoms.js.map +1 -1
- package/dist/FormBuilder.d.ts +12 -16
- package/dist/FormBuilder.d.ts.map +1 -1
- package/dist/FormBuilder.js +2 -6
- package/dist/FormBuilder.js.map +1 -1
- package/dist/Mode.d.ts.map +1 -1
- package/dist/Mode.js +1 -1
- package/dist/Mode.js.map +1 -1
- package/dist/Validation.d.ts +4 -4
- package/dist/Validation.d.ts.map +1 -1
- package/dist/Validation.js +58 -41
- package/dist/Validation.js.map +1 -1
- package/dist/internal/dirty.d.ts.map +1 -1
- package/dist/internal/dirty.js +5 -2
- package/dist/internal/dirty.js.map +1 -1
- package/package.json +3 -2
- package/src/Field.ts +35 -27
- package/src/FieldState.ts +2 -1
- package/src/FormAtoms.ts +29 -28
- package/src/FormBuilder.ts +22 -33
- package/src/Mode.ts +1 -3
- package/src/Validation.ts +68 -53
- package/src/internal/dirty.ts +5 -2
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,
|
|
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.
|
|
57
|
+
? FormBuilder.FieldRef<Schema.Schema.Encoded<S>>
|
|
57
58
|
: TFields[K] extends Field.ArrayFieldDef<any, infer S>
|
|
58
|
-
? FormBuilder.FieldRef<ReadonlyArray<Schema.
|
|
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 |
|
|
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.
|
|
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,
|
|
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.
|
|
91
|
-
) => Atom.AtomResultFn<unknown, void,
|
|
91
|
+
schema: Schema.Schema.Any
|
|
92
|
+
) => Atom.AtomResultFn<unknown, void, ParseResult.ParseError>
|
|
92
93
|
|
|
93
|
-
readonly getOrCreateFieldAtoms: (fieldPath: string, schema: Schema.
|
|
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.
|
|
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,
|
|
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.
|
|
265
|
-
const fieldSchemaRegistry = new Map<string, Schema.
|
|
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.
|
|
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.
|
|
280
|
-
): Atom.AtomResultFn<unknown, void,
|
|
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.
|
|
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,
|
|
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.
|
|
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.
|
|
367
|
-
if (Option.isSome(parseError) &&
|
|
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.
|
|
473
|
+
Schema.decodeUnknown(combinedSchema, { errors: "all" })(values) as Effect.Effect<
|
|
473
474
|
Field.DecodedFromFields<TFields>,
|
|
474
|
-
|
|
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 |
|
|
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.
|
|
513
|
+
Schema.decodeUnknown(combinedSchema, { errors: "all" })(values) as Effect.Effect<
|
|
513
514
|
Field.DecodedFromFields<TFields>,
|
|
514
|
-
|
|
515
|
+
ParseResult.ParseError,
|
|
515
516
|
R
|
|
516
517
|
>,
|
|
517
|
-
Effect.catchTag("
|
|
518
|
+
Effect.catchTag("ParseError", (parseError) =>
|
|
518
519
|
Effect.sync(() => {
|
|
519
520
|
const routedErrors = Validation.routeErrorsWithSource(parseError)
|
|
520
521
|
get.set(errorsAtom, routedErrors)
|
package/src/FormBuilder.ts
CHANGED
|
@@ -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) =>
|
|
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<
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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>) =>
|
|
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<
|
|
105
|
-
): FormBuilder<TFields, R | Exclude<RD,
|
|
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.
|
|
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>) =>
|
|
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<
|
|
147
|
-
): FormBuilder<TFields, R | Exclude<RD,
|
|
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.
|
|
171
|
-
const schemaFields: Record<string, Schema.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
3
|
-
import * as
|
|
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:
|
|
16
|
+
readonly issue: ParseResult.ParseIssue
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
const
|
|
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:
|
|
43
|
+
const collectIssueSources = (error: ParseResult.ParseError): ReadonlyArray<IssueSourceEntry> => {
|
|
22
44
|
const entries: Array<IssueSourceEntry> = []
|
|
23
45
|
|
|
24
|
-
const walk = (issue:
|
|
46
|
+
const walk = (issue: ParseResult.ParseIssue, path: ReadonlyArray<PropertyKey>, source: ErrorSource): void => {
|
|
25
47
|
switch (issue._tag) {
|
|
26
|
-
case "
|
|
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 "
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
case "
|
|
55
|
-
case "
|
|
56
|
-
case "
|
|
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:
|
|
69
|
-
const formatted =
|
|
70
|
-
return formatted
|
|
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:
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
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(
|
|
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:
|
|
105
|
+
export const routeErrors = (error: ParseResult.ParseError): Map<string, string> => {
|
|
91
106
|
const result = new Map<string, string>()
|
|
92
|
-
const
|
|
107
|
+
const issues = ParseResult.ArrayFormatter.formatErrorSync(error)
|
|
93
108
|
|
|
94
|
-
for (const issue of
|
|
95
|
-
const fieldPath = schemaPathToFieldPath(
|
|
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:
|
|
119
|
+
export const routeErrorsWithSource = (error: ParseResult.ParseError): Map<string, ErrorEntry> => {
|
|
105
120
|
const result = new Map<string, ErrorEntry>()
|
|
106
|
-
const formattedIssues =
|
|
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(
|
|
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(
|
|
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"
|
package/src/internal/dirty.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
100
|
+
const isEqual = Utils.structuralRegion(() => Equal.equals(current, initial))
|
|
101
|
+
if (!isEqual && path) nextDirty.add(path)
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
|