@hookform/resolvers 5.9.1 → 6.0.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.
@@ -1 +1 @@
1
- {"version":3,"file":"effect-ts.umd.js","sources":["../src/effect-ts.ts"],"sourcesContent":["import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers';\nimport { Effect, Schema } from 'effect';\nimport { ArrayFormatter, decodeUnknown } from 'effect/ParseResult';\nimport { ParseOptions } from 'effect/SchemaAST';\nimport {\n type FieldError,\n FieldValues,\n Resolver,\n appendErrors,\n} from 'react-hook-form';\n\nexport function effectTsResolver<Input extends FieldValues, Context, Output>(\n schema: Schema.Schema<Output, Input>,\n schemaOptions?: ParseOptions,\n resolverOptions?: {\n mode?: 'async' | 'sync';\n raw?: false;\n },\n): Resolver<Input, Context, Output>;\n\nexport function effectTsResolver<Input extends FieldValues, Context, Output>(\n schema: Schema.Schema<Output, Input>,\n schemaOptions: ParseOptions | undefined,\n resolverOptions: {\n mode?: 'async' | 'sync';\n raw: true;\n },\n): Resolver<Input, Context, Input>;\n\n/**\n * Creates a resolver for react-hook-form using Effect.ts schema validation\n * @param {Schema.Schema<TFieldValues, I>} schema - The Effect.ts schema to validate against\n * @param {ParseOptions} [schemaOptions] - Optional Effect.ts validation options\n * @returns {Resolver<Schema.Schema.Type<typeof schema>>} A resolver function compatible with react-hook-form\n * @example\n * const schema = Schema.Struct({\n * name: Schema.String,\n * age: Schema.Number\n * });\n *\n * useForm({\n * resolver: effectTsResolver(schema)\n * });\n */\nexport function effectTsResolver<Input extends FieldValues, Context, Output>(\n schema: Schema.Schema<Output, Input>,\n schemaOptions: ParseOptions = { errors: 'all', onExcessProperty: 'ignore' },\n): Resolver<Input, Context, Output | Input> {\n return (values, _, options) => {\n return decodeUnknown(\n schema,\n schemaOptions,\n )(values).pipe(\n Effect.catchAll((parseIssue) =>\n Effect.flip(ArrayFormatter.formatIssue(parseIssue)),\n ),\n Effect.mapError((issues) => {\n const validateAllFieldCriteria =\n !options.shouldUseNativeValidation && options.criteriaMode === 'all';\n\n // A null-prototype accumulator avoids false-positive hits from\n // inherited members (e.g. `toString`, `constructor`) when a field\n // is named after one of them — a plain `{}` would make `!acc[key]`\n // truthy-skip those paths and silently drop their errors.\n const errors = issues.reduce(\n (acc, error) => {\n const key = error.path.join('.');\n\n if (!acc[key]) {\n acc[key] = { message: error.message, type: error._tag };\n }\n\n if (validateAllFieldCriteria) {\n const types = acc[key].types;\n const messages = types && types[String(error._tag)];\n\n acc[key] = appendErrors(\n key,\n validateAllFieldCriteria,\n acc,\n error._tag,\n messages\n ? ([] as string[]).concat(messages as string[], error.message)\n : error.message,\n ) as FieldError;\n }\n\n return acc;\n },\n Object.create(null) as Record<string, FieldError>,\n );\n\n return toNestErrors(errors, options);\n }),\n Effect.tap(() =>\n Effect.sync(\n () =>\n options.shouldUseNativeValidation &&\n validateFieldsNatively({}, options),\n ),\n ),\n Effect.match({\n onFailure: (errors) => ({ errors, values: {} }),\n onSuccess: (result) => ({ errors: {}, values: result }),\n }),\n Effect.runPromise,\n );\n };\n}\n"],"names":["schema","schemaOptions","errors","onExcessProperty","values","_","options","decodeUnknown","pipe","Effect","catchAll","parseIssue","flip","ArrayFormatter","formatIssue","mapError","issues","validateAllFieldCriteria","shouldUseNativeValidation","criteriaMode","reduce","acc","error","key","path","join","message","type","_tag","types","messages","String","appendErrors","concat","Object","create","toNestErrors","tap","sync","validateFieldsNatively","match","onFailure","onSuccess","result","runPromise"],"mappings":"wgBA6CEA,EACAC,GAEA,YAF8B,IAA9BA,IAAAA,EAA8B,CAAEC,OAAQ,MAAOC,iBAAkB,oBAEzDC,EAAQC,EAAGC,GACjB,OAAOC,EAAaA,cAClBP,EACAC,EAFKM,CAGLH,GAAQI,KACRC,EAAMA,OAACC,SAAS,SAACC,GACf,OAAAF,EAAAA,OAAOG,KAAKC,EAAcA,eAACC,YAAYH,GAAY,GAErDF,EAAMA,OAACM,SAAS,SAACC,GACf,IAAMC,GACHX,EAAQY,2BAAsD,QAAzBZ,EAAQa,aAM1CjB,EAASc,EAAOI,OACpB,SAACC,EAAKC,GACJ,IAAMC,EAAMD,EAAME,KAAKC,KAAK,KAM5B,GAJKJ,EAAIE,KACPF,EAAIE,GAAO,CAAEG,QAASJ,EAAMI,QAASC,KAAML,EAAMM,OAG/CX,EAA0B,CAC5B,IAAMY,EAAQR,EAAIE,GAAKM,MACjBC,EAAWD,GAASA,EAAME,OAAOT,EAAMM,OAE7CP,EAAIE,GAAOS,eACTT,EACAN,EACAI,EACAC,EAAMM,KACNE,EACK,GAAgBG,OAAOH,EAAsBR,EAAMI,SACpDJ,EAAMI,QAEd,CAEA,OAAOL,CACT,EACAa,OAAOC,OAAO,OAGhB,OAAOC,eAAalC,EAAQI,EAC9B,GACAG,SAAO4B,IAAI,WACT,OAAA5B,EAAAA,OAAO6B,KACL,WACE,OAAAhC,EAAQY,2BACRqB,EAAsBA,uBAAC,GAAIjC,EAAQ,EACtC,GAEHG,EAAAA,OAAO+B,MAAM,CACXC,UAAW,SAACvC,GAAM,MAAM,CAAEA,OAAAA,EAAQE,OAAQ,GAAI,EAC9CsC,UAAW,SAACC,GAAM,MAAM,CAAEzC,OAAQ,CAAE,EAAEE,OAAQuC,EAAQ,IAExDlC,EAAMA,OAACmC,WAEX,CACF"}
1
+ {"version":3,"file":"effect-ts.umd.js","sources":["../src/effect-ts.ts"],"sourcesContent":["import { toNestErrors, validateFieldsNatively } from '@hookform/resolvers';\nimport { Effect, Schema, SchemaIssue } from 'effect';\nimport type { ParseOptions } from 'effect/SchemaAST';\nimport {\n type FieldError,\n FieldValues,\n Resolver,\n appendErrors,\n} from 'react-hook-form';\n\ntype Issue = { path: PropertyKey[]; message: string; type: string };\n\nconst formatIssue = SchemaIssue.makeFormatterStandardSchemaV1();\n\n// Flattens the issue tree like `makeFormatterStandardSchemaV1` does, but keeps\n// the `_tag` of the failing node so it can be reported as the error `type`.\nconst flattenIssue = (\n issue: SchemaIssue.Issue,\n path: PropertyKey[],\n): Issue[] => {\n switch (issue._tag) {\n case 'Pointer':\n return flattenIssue(issue.issue, [...path, ...issue.path]);\n case 'Encoding':\n return flattenIssue(issue.issue, path);\n case 'Composite':\n case 'AnyOf':\n if (issue.issues.length) {\n return issue.issues.flatMap((issue) => flattenIssue(issue, path));\n }\n }\n\n return formatIssue(issue).issues.map((error) => ({\n path: [...path, ...(error.path || [])] as PropertyKey[],\n message: error.message,\n type: issue._tag,\n }));\n};\n\nexport function effectTsResolver<Input extends FieldValues, Context, Output>(\n schema: Schema.Codec<Output, Input, never, unknown>,\n schemaOptions?: ParseOptions,\n resolverOptions?: {\n mode?: 'async' | 'sync';\n raw?: false;\n },\n): Resolver<Input, Context, Output>;\n\nexport function effectTsResolver<Input extends FieldValues, Context, Output>(\n schema: Schema.Codec<Output, Input, never, unknown>,\n schemaOptions: ParseOptions | undefined,\n resolverOptions: {\n mode?: 'async' | 'sync';\n raw: true;\n },\n): Resolver<Input, Context, Input>;\n\n/**\n * Creates a resolver for react-hook-form using Effect Schema validation\n * @param {Schema.Codec<Output, Input>} schema - The Effect schema to validate against\n * @param {ParseOptions} [schemaOptions] - Optional Effect Schema parse options\n * @returns {Resolver<Schema.Schema.Type<typeof schema>>} A resolver function compatible with react-hook-form\n * @example\n * const schema = Schema.Struct({\n * name: Schema.String,\n * age: Schema.Number\n * });\n *\n * useForm({\n * resolver: effectTsResolver(schema)\n * });\n */\nexport function effectTsResolver<Input extends FieldValues, Context, Output>(\n schema: Schema.Codec<Output, Input, never, unknown>,\n schemaOptions: ParseOptions = { errors: 'all', onExcessProperty: 'ignore' },\n): Resolver<Input, Context, Output | Input> {\n return (values, _, options) =>\n Schema.decodeUnknownEffect(schema)(values, schemaOptions).pipe(\n Effect.match({\n onFailure: ({ issue }) => {\n const validateAllFieldCriteria =\n !options.shouldUseNativeValidation &&\n options.criteriaMode === 'all';\n\n // A null-prototype accumulator avoids false-positive hits from\n // inherited members (e.g. `toString`, `constructor`) when a field\n // is named after one of them — a plain `{}` would make `!acc[key]`\n // truthy-skip those paths and silently drop their errors.\n const errors = flattenIssue(issue, []).reduce(\n (acc, error) => {\n const key = error.path.join('.');\n\n if (!acc[key]) {\n acc[key] = { message: error.message, type: error.type };\n }\n\n if (validateAllFieldCriteria) {\n const types = acc[key].types;\n const messages = types && types[error.type];\n\n acc[key] = appendErrors(\n key,\n validateAllFieldCriteria,\n acc,\n error.type,\n messages\n ? ([] as string[]).concat(\n messages as string[],\n error.message,\n )\n : error.message,\n ) as FieldError;\n }\n\n return acc;\n },\n Object.create(null) as Record<string, FieldError>,\n );\n\n return { errors: toNestErrors(errors, options), values: {} };\n },\n onSuccess: (result) => {\n options.shouldUseNativeValidation &&\n validateFieldsNatively({}, options);\n\n return { errors: {}, values: result };\n },\n }),\n Effect.runPromise,\n );\n}\n"],"names":["formatIssue","SchemaIssue","makeFormatterStandardSchemaV1","flattenIssue","issue","path","_tag","concat","issues","length","flatMap","map","error","message","type","schema","schemaOptions","errors","onExcessProperty","values","_","options","Schema","decodeUnknownEffect","pipe","Effect","match","onFailure","_ref","validateAllFieldCriteria","shouldUseNativeValidation","criteriaMode","reduce","acc","key","join","types","messages","appendErrors","Object","create","toNestErrors","onSuccess","result","validateFieldsNatively","runPromise"],"mappings":"maAYA,IAAMA,EAAcC,EAAWA,YAACC,gCAI1BC,EAAe,SACnBC,EACAC,GAEA,OAAQD,EAAME,MACZ,IAAK,UACH,OAAOH,EAAaC,EAAMA,MAAK,GAAAG,OAAMF,EAASD,EAAMC,OACtD,IAAK,WACH,OAAOF,EAAaC,EAAMA,MAAOC,GACnC,IAAK,YACL,IAAK,QACH,GAAID,EAAMI,OAAOC,OACf,OAAOL,EAAMI,OAAOE,QAAQ,SAACN,GAAK,OAAKD,EAAaC,EAAOC,EAAK,GAItE,OAAOL,EAAYI,GAAOI,OAAOG,IAAI,SAACC,GAAW,MAAA,CAC/CP,KAAI,GAAAE,OAAMF,EAAUO,EAAMP,MAAQ,IAClCQ,QAASD,EAAMC,QACfC,KAAMV,EAAME,KACb,EACH,qBAmCgB,SACdS,EACAC,GAEA,YAF8B,IAA9BA,IAAAA,EAA8B,CAAEC,OAAQ,MAAOC,iBAAkB,WAE1D,SAACC,EAAQC,EAAGC,GACjB,OAAAC,EAAMA,OAACC,oBAAoBR,EAA3BO,CAAmCH,EAAQH,GAAeQ,KACxDC,EAAAA,OAAOC,MAAM,CACXC,UAAW,SAAFC,GAAgB,IACjBC,GACHR,EAAQS,2BACgB,QAAzBT,EAAQU,aAMJd,EAASd,EATEyB,EAALxB,MASuB,IAAI4B,OACrC,SAACC,EAAKrB,GACJ,IAAMsB,EAAMtB,EAAMP,KAAK8B,KAAK,KAM5B,GAJKF,EAAIC,KACPD,EAAIC,GAAO,CAAErB,QAASD,EAAMC,QAASC,KAAMF,EAAME,OAG/Ce,EAA0B,CAC5B,IAAMO,EAAQH,EAAIC,GAAKE,MACjBC,EAAWD,GAASA,EAAMxB,EAAME,MAEtCmB,EAAIC,GAAOI,eACTJ,EACAL,EACAI,EACArB,EAAME,KACNuB,EACK,GAAgB9B,OACf8B,EACAzB,EAAMC,SAERD,EAAMC,QAEd,CAEA,OAAOoB,CACT,EACAM,OAAOC,OAAO,OAGhB,MAAO,CAAEvB,OAAQwB,EAAYA,aAACxB,EAAQI,GAAUF,OAAQ,GAC1D,EACAuB,UAAW,SAACC,GAIV,OAHAtB,EAAQS,2BACNc,EAAsBA,uBAAC,GAAIvB,GAEtB,CAAEJ,OAAQ,CAAE,EAAEE,OAAQwB,EAC/B,IAEFlB,SAAOoB,WACR,CACL"}
@@ -12,7 +12,7 @@
12
12
  "license": "MIT",
13
13
  "peerDependencies": {
14
14
  "@hookform/resolvers": "^2.0.0",
15
- "effect": "^3.10.3",
15
+ "effect": "^4.0.0",
16
16
  "react-hook-form": "^7.55.0"
17
17
  }
18
18
  }
@@ -9,11 +9,11 @@ const USERNAME_REQUIRED_MESSAGE = 'username field is required';
9
9
  const PASSWORD_REQUIRED_MESSAGE = 'password field is required';
10
10
 
11
11
  const schema = Schema.Struct({
12
- username: Schema.String.pipe(
13
- Schema.nonEmptyString({ message: () => USERNAME_REQUIRED_MESSAGE }),
12
+ username: Schema.String.check(
13
+ Schema.isNonEmpty({ message: USERNAME_REQUIRED_MESSAGE }),
14
14
  ),
15
- password: Schema.String.pipe(
16
- Schema.nonEmptyString({ message: () => PASSWORD_REQUIRED_MESSAGE }),
15
+ password: Schema.String.check(
16
+ Schema.isNonEmpty({ message: PASSWORD_REQUIRED_MESSAGE }),
17
17
  ),
18
18
  });
19
19
 
@@ -9,11 +9,11 @@ const USERNAME_REQUIRED_MESSAGE = 'username field is required';
9
9
  const PASSWORD_REQUIRED_MESSAGE = 'password field is required';
10
10
 
11
11
  const schema = Schema.Struct({
12
- username: Schema.String.pipe(
13
- Schema.nonEmptyString({ message: () => USERNAME_REQUIRED_MESSAGE }),
12
+ username: Schema.String.check(
13
+ Schema.isNonEmpty({ message: USERNAME_REQUIRED_MESSAGE }),
14
14
  ),
15
- password: Schema.String.pipe(
16
- Schema.nonEmptyString({ message: () => PASSWORD_REQUIRED_MESSAGE }),
15
+ password: Schema.String.check(
16
+ Schema.isNonEmpty({ message: PASSWORD_REQUIRED_MESSAGE }),
17
17
  ),
18
18
  });
19
19
 
@@ -2,43 +2,43 @@ import { Schema } from 'effect';
2
2
  import { Field, InternalFieldName } from 'react-hook-form';
3
3
 
4
4
  export const schema = Schema.Struct({
5
- username: Schema.String.pipe(
6
- Schema.nonEmptyString({ message: () => 'A username is required' }),
5
+ username: Schema.String.check(
6
+ Schema.isNonEmpty({ message: 'A username is required' }),
7
7
  ),
8
- password: Schema.String.pipe(
9
- Schema.pattern(new RegExp('.*[A-Z].*'), {
10
- message: () => 'At least 1 uppercase letter.',
8
+ password: Schema.String.check(
9
+ Schema.isPattern(new RegExp('.*[A-Z].*'), {
10
+ message: 'At least 1 uppercase letter.',
11
11
  }),
12
- Schema.pattern(new RegExp('.*[a-z].*'), {
13
- message: () => 'At least 1 lowercase letter.',
12
+ Schema.isPattern(new RegExp('.*[a-z].*'), {
13
+ message: 'At least 1 lowercase letter.',
14
14
  }),
15
- Schema.pattern(new RegExp('.*\\d.*'), {
16
- message: () => 'At least 1 number.',
15
+ Schema.isPattern(new RegExp('.*\\d.*'), {
16
+ message: 'At least 1 number.',
17
17
  }),
18
- Schema.pattern(
18
+ Schema.isPattern(
19
19
  new RegExp('.*[`~<>?,./!@#$%^&*()\\-_+="\'|{}\\[\\];:\\\\].*'),
20
20
  {
21
- message: () => 'At least 1 special character.',
21
+ message: 'At least 1 special character.',
22
22
  },
23
23
  ),
24
- Schema.minLength(8, { message: () => 'Must be at least 8 characters.' }),
24
+ Schema.isMinLength(8, { message: 'Must be at least 8 characters.' }),
25
25
  ),
26
- accessToken: Schema.Union(Schema.String, Schema.Number),
27
- birthYear: Schema.Number.pipe(
28
- Schema.greaterThan(1900, {
29
- message: () => 'Must be greater than the year 1900',
26
+ accessToken: Schema.Union([Schema.String, Schema.Number]),
27
+ birthYear: Schema.Number.check(
28
+ Schema.isGreaterThan(1900, {
29
+ message: 'Must be greater than the year 1900',
30
30
  }),
31
- Schema.filter((value) => value < new Date().getFullYear(), {
32
- message: () => 'Must be before the current year.',
31
+ Schema.makeFilter((value: number) => value < new Date().getFullYear(), {
32
+ message: 'Must be before the current year.',
33
33
  }),
34
34
  ),
35
- email: Schema.String.pipe(
36
- Schema.pattern(
35
+ email: Schema.String.check(
36
+ Schema.isPattern(
37
37
  new RegExp(
38
38
  /^(?!\.)(?!.*\.\.)([A-Z0-9_+-.]*)[A-Z0-9_+-]@([A-Z0-9][A-Z0-9-]*\.)+[A-Z]{2,}$/i,
39
39
  ),
40
40
  {
41
- message: () => 'A valid email address is required.',
41
+ message: 'A valid email address is required.',
42
42
  },
43
43
  ),
44
44
  ),
@@ -49,9 +49,9 @@ export const schema = Schema.Struct({
49
49
  ),
50
50
  luckyNumbers: Schema.Array(Schema.Number),
51
51
  enabled: Schema.Boolean,
52
- animal: Schema.Union(Schema.String, Schema.Literal('bird', 'snake')),
52
+ animal: Schema.Union([Schema.String, Schema.Literals(['bird', 'snake'])]),
53
53
  vehicles: Schema.Array(
54
- Schema.Union(
54
+ Schema.Union([
55
55
  Schema.Struct({
56
56
  type: Schema.Literal('car'),
57
57
  brand: Schema.String,
@@ -61,7 +61,7 @@ export const schema = Schema.Struct({
61
61
  type: Schema.Literal('bike'),
62
62
  speed: Schema.Number,
63
63
  }),
64
- ),
64
+ ]),
65
65
  ),
66
66
  });
67
67
 
@@ -4,17 +4,17 @@ exports[`effectTsResolver > should return a single error from effectTsResolver w
4
4
  {
5
5
  "errors": {
6
6
  "animal": {
7
- "message": "Expected string, actual ["dog"]",
7
+ "message": "Expected string | "bird" | "snake"",
8
8
  "ref": undefined,
9
- "type": "Type",
9
+ "type": "AnyOf",
10
10
  },
11
11
  "luckyNumbers": [
12
12
  ,
13
13
  ,
14
14
  {
15
- "message": "Expected number, actual "3"",
15
+ "message": "Expected number",
16
16
  "ref": undefined,
17
- "type": "Type",
17
+ "type": "InvalidType",
18
18
  },
19
19
  ],
20
20
  "password": {
@@ -22,15 +22,15 @@ exports[`effectTsResolver > should return a single error from effectTsResolver w
22
22
  "ref": {
23
23
  "name": "password",
24
24
  },
25
- "type": "Refinement",
25
+ "type": "Filter",
26
26
  },
27
27
  "vehicles": [
28
28
  ,
29
29
  {
30
30
  "horsepower": {
31
- "message": "is missing",
31
+ "message": "Missing key",
32
32
  "ref": undefined,
33
- "type": "Missing",
33
+ "type": "MissingKey",
34
34
  },
35
35
  },
36
36
  ],
@@ -47,10 +47,9 @@ exports[`effectTsResolver > should return all the errors from effectTsResolver w
47
47
  "ref": {
48
48
  "name": "phoneNumber",
49
49
  },
50
- "type": "Refinement",
50
+ "type": "Filter",
51
51
  "types": {
52
- "Refinement": "Please enter a valid phone number in international format.",
53
- "Type": "Expected undefined, actual "123"",
52
+ "Filter": "Please enter a valid phone number in international format.",
54
53
  },
55
54
  },
56
55
  },
@@ -66,7 +65,7 @@ exports[`effectTsResolver > should return the first error from effectTsResolver
66
65
  "ref": {
67
66
  "name": "phoneNumber",
68
67
  },
69
- "type": "Refinement",
68
+ "type": "Filter",
70
69
  },
71
70
  },
72
71
  "values": {},
@@ -1,5 +1,11 @@
1
1
  import { renderHook } from '@testing-library/react';
2
- import { Schema } from 'effect';
2
+ import {
3
+ Effect,
4
+ Schema,
5
+ SchemaGetter,
6
+ SchemaIssue,
7
+ SchemaTransformation,
8
+ } from 'effect';
3
9
  import { Resolver, useForm } from 'react-hook-form';
4
10
  import { SubmitHandler } from 'react-hook-form';
5
11
  import { effectTsResolver } from '..';
@@ -29,9 +35,9 @@ describe('effectTsResolver', () => {
29
35
  it('should return the first error from effectTsResolver when validation fails with `validateAllFieldCriteria` set to firstError', async () => {
30
36
  const SignupSchema = Schema.Struct({
31
37
  phoneNumber: Schema.optional(
32
- Schema.String.pipe(
33
- Schema.pattern(/^\+\d{7,15}$/, {
34
- message: () =>
38
+ Schema.String.check(
39
+ Schema.isPattern(/^\+\d{7,15}$/, {
40
+ message:
35
41
  'Please enter a valid phone number in international format.',
36
42
  }),
37
43
  ),
@@ -59,9 +65,9 @@ describe('effectTsResolver', () => {
59
65
  it('should return all the errors from effectTsResolver when validation fails with `validateAllFieldCriteria` set to true', async () => {
60
66
  const SignupSchema = Schema.Struct({
61
67
  phoneNumber: Schema.optional(
62
- Schema.String.pipe(
63
- Schema.pattern(/^\+\d{7,15}$/, {
64
- message: () =>
68
+ Schema.String.check(
69
+ Schema.isPattern(/^\+\d{7,15}$/, {
70
+ message:
65
71
  'Please enter a valid phone number in international format.',
66
72
  }),
67
73
  ),
@@ -86,6 +92,171 @@ describe('effectTsResolver', () => {
86
92
  expect(result).toMatchSnapshot();
87
93
  });
88
94
 
95
+ it('should group all messages of the same issue type when `criteriaMode` is "all"', async () => {
96
+ const schema = Schema.Struct({
97
+ password: Schema.String.check(
98
+ Schema.isMinLength(8, { message: 'Too short' }),
99
+ Schema.isPattern(/\d/, { message: 'Needs a number' }),
100
+ ),
101
+ });
102
+
103
+ const result = await effectTsResolver(schema)(
104
+ { password: 'abc' },
105
+ undefined,
106
+ { fields, criteriaMode: 'all', shouldUseNativeValidation },
107
+ );
108
+
109
+ expect(result.errors.password).toMatchObject({
110
+ message: 'Too short',
111
+ type: 'Filter',
112
+ types: { Filter: ['Too short', 'Needs a number'] },
113
+ });
114
+ });
115
+
116
+ it('should return nested and indexed errors with their issue type', async () => {
117
+ const schema = Schema.Struct({
118
+ user: Schema.Struct({ name: Schema.String }),
119
+ tags: Schema.Array(Schema.String),
120
+ });
121
+
122
+ const result = await effectTsResolver(schema)(
123
+ { user: {}, tags: ['a', 1] } as any,
124
+ undefined,
125
+ { fields, shouldUseNativeValidation },
126
+ );
127
+
128
+ expect(result).toEqual({
129
+ values: {},
130
+ errors: {
131
+ user: { name: { message: 'Missing key', type: 'MissingKey' } },
132
+ tags: [undefined, { message: 'Expected string', type: 'InvalidType' }],
133
+ },
134
+ });
135
+ });
136
+
137
+ it('should only report the first error when `schemaOptions.errors` is "first"', async () => {
138
+ const schema = Schema.Struct({ a: Schema.String, b: Schema.String });
139
+
140
+ const result = await effectTsResolver(schema, { errors: 'first' })(
141
+ {} as any,
142
+ undefined,
143
+ { fields, shouldUseNativeValidation },
144
+ );
145
+
146
+ expect(Object.keys(result.errors)).toEqual(['a']);
147
+ });
148
+
149
+ it('should ignore excess properties by default and report them when `onExcessProperty` is "error"', async () => {
150
+ const schema = Schema.Struct({ a: Schema.String });
151
+ const values = { a: 'a', b: 'b' } as any;
152
+
153
+ const ignored = await effectTsResolver(schema)(values, undefined, {
154
+ fields,
155
+ shouldUseNativeValidation,
156
+ });
157
+ expect(ignored).toEqual({ errors: {}, values: { a: 'a' } });
158
+
159
+ const rejected = await effectTsResolver(schema, {
160
+ errors: 'all',
161
+ onExcessProperty: 'error',
162
+ })(values, undefined, { fields, shouldUseNativeValidation });
163
+ expect(rejected.errors).toMatchObject({
164
+ b: { type: 'UnexpectedKey' },
165
+ });
166
+ });
167
+
168
+ it('should return the decoded output when a transformation is used', async () => {
169
+ const schema = Schema.Struct({ age: Schema.NumberFromString });
170
+
171
+ const result = await effectTsResolver(schema)({ age: '42' }, undefined, {
172
+ fields,
173
+ shouldUseNativeValidation,
174
+ });
175
+
176
+ expect(result).toEqual({ errors: {}, values: { age: 42 } });
177
+ });
178
+
179
+ it('should report errors raised by a failing transformation', async () => {
180
+ const schema = Schema.Struct({
181
+ age: Schema.String.pipe(
182
+ Schema.decodeTo(Schema.Number, {
183
+ decode: SchemaGetter.transformEffect((s: string) =>
184
+ Number.isNaN(Number(s))
185
+ ? Effect.fail(
186
+ new SchemaIssue.InvalidValue({ message: 'Not a number' }),
187
+ )
188
+ : Effect.succeed(Number(s)),
189
+ ),
190
+ encode: SchemaGetter.String(),
191
+ }),
192
+ ),
193
+ });
194
+
195
+ const result = await effectTsResolver(schema)({ age: 'abc' }, undefined, {
196
+ fields,
197
+ shouldUseNativeValidation,
198
+ });
199
+
200
+ expect(result).toEqual({
201
+ values: {},
202
+ errors: { age: { message: 'Not a number', type: 'InvalidValue' } },
203
+ });
204
+ });
205
+
206
+ it('should support asynchronous validation', async () => {
207
+ const schema = Schema.Struct({
208
+ username: Schema.String.pipe(
209
+ Schema.decodeTo(Schema.String, {
210
+ decode: SchemaGetter.checkEffect((s: string) =>
211
+ Effect.sleep('1 millis').pipe(
212
+ Effect.as(s === 'taken' ? 'Username is taken' : undefined),
213
+ ),
214
+ ),
215
+ encode: SchemaGetter.passthrough(),
216
+ }),
217
+ ),
218
+ });
219
+
220
+ const valid = await effectTsResolver(schema)(
221
+ { username: 'free' },
222
+ undefined,
223
+ { fields, shouldUseNativeValidation },
224
+ );
225
+ expect(valid).toEqual({ errors: {}, values: { username: 'free' } });
226
+
227
+ const invalid = await effectTsResolver(schema)(
228
+ { username: 'taken' },
229
+ undefined,
230
+ { fields, shouldUseNativeValidation },
231
+ );
232
+ expect(invalid.errors).toMatchObject({
233
+ username: { message: 'Username is taken' },
234
+ });
235
+ });
236
+
237
+ it('should validate fields natively when `shouldUseNativeValidation` is true', async () => {
238
+ const setCustomValidity = vi.fn();
239
+ const reportValidity = vi.fn();
240
+ const nativeFields = {
241
+ username: {
242
+ name: 'username',
243
+ ref: { name: 'username', setCustomValidity, reportValidity },
244
+ },
245
+ } as any;
246
+
247
+ const result = await effectTsResolver(
248
+ Schema.Struct({ username: Schema.String }),
249
+ )({ username: 'john' }, undefined, {
250
+ fields: nativeFields,
251
+ names: ['username'],
252
+ shouldUseNativeValidation: true,
253
+ });
254
+
255
+ expect(result).toEqual({ errors: {}, values: { username: 'john' } });
256
+ expect(setCustomValidity).toHaveBeenCalledWith('');
257
+ expect(reportValidity).toHaveBeenCalled();
258
+ });
259
+
89
260
  /**
90
261
  * Type inference tests
91
262
  */
@@ -100,10 +271,15 @@ describe('effectTsResolver', () => {
100
271
  it('should correctly infer the output type from a effectTs schema using a transform', () => {
101
272
  const resolver = effectTsResolver(
102
273
  Schema.Struct({
103
- id: Schema.transform(Schema.Number, Schema.String, {
104
- decode: (val) => String(val),
105
- encode: (val) => Number(val),
106
- }),
274
+ id: Schema.Number.pipe(
275
+ Schema.decodeTo(
276
+ Schema.String,
277
+ SchemaTransformation.transform({
278
+ decode: (val) => String(val),
279
+ encode: (val) => Number(val),
280
+ }),
281
+ ),
282
+ ),
107
283
  }),
108
284
  );
109
285
 
@@ -132,10 +308,15 @@ describe('effectTsResolver', () => {
132
308
 
133
309
  it('should correctly infer the output type from a effectTs schema with a transform for the handleSubmit function in useForm', () => {
134
310
  const schema = Schema.Struct({
135
- id: Schema.transform(Schema.Number, Schema.String, {
136
- decode: (val) => String(val),
137
- encode: (val) => Number(val),
138
- }),
311
+ id: Schema.Number.pipe(
312
+ Schema.decodeTo(
313
+ Schema.String,
314
+ SchemaTransformation.transform({
315
+ decode: (val) => String(val),
316
+ encode: (val) => Number(val),
317
+ }),
318
+ ),
319
+ ),
139
320
  });
140
321
 
141
322
  const {
@@ -8,7 +8,7 @@ describe('effectTsResolver special path names', () => {
8
8
  'reports a validation error for a field named "%s"',
9
9
  async (name) => {
10
10
  const schema = Schema.Struct({
11
- [name]: Schema.String.pipe(Schema.minLength(1)),
11
+ [name]: Schema.String.check(Schema.isMinLength(1)),
12
12
  });
13
13
 
14
14
  const result = await effectTsResolver(schema as any)(