@pyreon/validation 0.11.4 → 0.11.6
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/README.md +41 -41
- package/lib/arktype.js.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/valibot.js.map +1 -1
- package/lib/zod.js.map +1 -1
- package/package.json +20 -20
- package/src/arktype.ts +6 -6
- package/src/index.ts +5 -5
- package/src/tests/setup.ts +1 -1
- package/src/tests/validation.test.tsx +165 -165
- package/src/types.ts +1 -1
- package/src/utils.ts +2 -2
- package/src/valibot.ts +5 -5
- package/src/zod.ts +5 -5
package/README.md
CHANGED
|
@@ -11,9 +11,9 @@ bun add @pyreon/validation
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
|
-
import { z } from
|
|
15
|
-
import { useForm } from
|
|
16
|
-
import { zodSchema } from
|
|
14
|
+
import { z } from 'zod'
|
|
15
|
+
import { useForm } from '@pyreon/form'
|
|
16
|
+
import { zodSchema } from '@pyreon/validation'
|
|
17
17
|
|
|
18
18
|
const schema = z.object({
|
|
19
19
|
email: z.string().email(),
|
|
@@ -21,7 +21,7 @@ const schema = z.object({
|
|
|
21
21
|
})
|
|
22
22
|
|
|
23
23
|
const form = useForm({
|
|
24
|
-
initialValues: { email:
|
|
24
|
+
initialValues: { email: '', age: 0 },
|
|
25
25
|
schema: zodSchema(schema),
|
|
26
26
|
onSubmit: async (values) => console.log(values),
|
|
27
27
|
})
|
|
@@ -35,9 +35,9 @@ Each adapter comes in two flavors: **schema-level** (validates the whole form) a
|
|
|
35
35
|
|
|
36
36
|
Create a form-level schema validator from a Zod schema. Uses `safeParseAsync` internally — supports both sync and async refinements. Duck-typed to work with Zod v3 and v4.
|
|
37
37
|
|
|
38
|
-
| Parameter | Type
|
|
39
|
-
|
|
|
40
|
-
| `schema`
|
|
38
|
+
| Parameter | Type | Description |
|
|
39
|
+
| --------- | ---------- | ------------------------------------------------------- |
|
|
40
|
+
| `schema` | Zod schema | Any Zod object schema with `safeParse`/`safeParseAsync` |
|
|
41
41
|
|
|
42
42
|
**Returns:** `SchemaValidateFn<TValues>`
|
|
43
43
|
|
|
@@ -57,9 +57,9 @@ const form = useForm({
|
|
|
57
57
|
|
|
58
58
|
Create a single-field validator from a Zod schema. Returns the first error message on failure.
|
|
59
59
|
|
|
60
|
-
| Parameter | Type
|
|
61
|
-
|
|
|
62
|
-
| `schema`
|
|
60
|
+
| Parameter | Type | Description |
|
|
61
|
+
| --------- | ---------- | ------------------------------------- |
|
|
62
|
+
| `schema` | Zod schema | Any Zod schema (string, number, etc.) |
|
|
63
63
|
|
|
64
64
|
**Returns:** `ValidateFn<T>`
|
|
65
65
|
|
|
@@ -77,10 +77,10 @@ const form = useForm({
|
|
|
77
77
|
|
|
78
78
|
Create a form-level schema validator from a Valibot schema. Valibot uses standalone functions, so you must pass the parse function.
|
|
79
79
|
|
|
80
|
-
| Parameter
|
|
81
|
-
|
|
|
82
|
-
| `schema`
|
|
83
|
-
| `safeParseFn` | `Function`
|
|
80
|
+
| Parameter | Type | Description |
|
|
81
|
+
| ------------- | -------------- | ------------------------------------------------ |
|
|
82
|
+
| `schema` | Valibot schema | Any Valibot object schema |
|
|
83
|
+
| `safeParseFn` | `Function` | `v.safeParse` or `v.safeParseAsync` from valibot |
|
|
84
84
|
|
|
85
85
|
**Returns:** `SchemaValidateFn<TValues>`
|
|
86
86
|
|
|
@@ -103,10 +103,10 @@ const form = useForm({
|
|
|
103
103
|
|
|
104
104
|
Create a single-field validator from a Valibot schema.
|
|
105
105
|
|
|
106
|
-
| Parameter
|
|
107
|
-
|
|
|
108
|
-
| `schema`
|
|
109
|
-
| `safeParseFn` | `Function`
|
|
106
|
+
| Parameter | Type | Description |
|
|
107
|
+
| ------------- | -------------- | ------------------------------------------------ |
|
|
108
|
+
| `schema` | Valibot schema | Any Valibot schema |
|
|
109
|
+
| `safeParseFn` | `Function` | `v.safeParse` or `v.safeParseAsync` from valibot |
|
|
110
110
|
|
|
111
111
|
**Returns:** `ValidateFn<T>`
|
|
112
112
|
|
|
@@ -120,9 +120,9 @@ validators: {
|
|
|
120
120
|
|
|
121
121
|
Create a form-level schema validator from an ArkType schema. ArkType validation is synchronous.
|
|
122
122
|
|
|
123
|
-
| Parameter | Type
|
|
124
|
-
|
|
|
125
|
-
| `schema`
|
|
123
|
+
| Parameter | Type | Description |
|
|
124
|
+
| --------- | -------------- | ------------------------- |
|
|
125
|
+
| `schema` | ArkType `Type` | Any callable ArkType type |
|
|
126
126
|
|
|
127
127
|
**Returns:** `SchemaValidateFn<TValues>`
|
|
128
128
|
|
|
@@ -142,9 +142,9 @@ const form = useForm({
|
|
|
142
142
|
|
|
143
143
|
Create a single-field validator from an ArkType schema.
|
|
144
144
|
|
|
145
|
-
| Parameter | Type
|
|
146
|
-
|
|
|
147
|
-
| `schema`
|
|
145
|
+
| Parameter | Type | Description |
|
|
146
|
+
| --------- | -------------- | ------------------------- |
|
|
147
|
+
| `schema` | ArkType `Type` | Any callable ArkType type |
|
|
148
148
|
|
|
149
149
|
**Returns:** `ValidateFn<T>`
|
|
150
150
|
|
|
@@ -158,17 +158,17 @@ validators: {
|
|
|
158
158
|
|
|
159
159
|
Convert an array of `ValidationIssue` objects into a flat field-to-error record. First error per field wins. Useful for building custom adapters.
|
|
160
160
|
|
|
161
|
-
| Parameter | Type
|
|
162
|
-
|
|
|
163
|
-
| `issues`
|
|
161
|
+
| Parameter | Type | Description |
|
|
162
|
+
| --------- | ------------------- | -------------------------------------------- |
|
|
163
|
+
| `issues` | `ValidationIssue[]` | Array of `{ path: string, message: string }` |
|
|
164
164
|
|
|
165
165
|
**Returns:** `Partial<Record<keyof TValues, ValidationError>>`
|
|
166
166
|
|
|
167
167
|
```ts
|
|
168
168
|
issuesToRecord([
|
|
169
|
-
{ path:
|
|
170
|
-
{ path:
|
|
171
|
-
{ path:
|
|
169
|
+
{ path: 'email', message: 'Required' },
|
|
170
|
+
{ path: 'email', message: 'Invalid' }, // ignored — first wins
|
|
171
|
+
{ path: 'age', message: 'Too young' },
|
|
172
172
|
])
|
|
173
173
|
// => { email: "Required", age: "Too young" }
|
|
174
174
|
```
|
|
@@ -180,9 +180,9 @@ issuesToRecord([
|
|
|
180
180
|
Each adapter is available via subpath import to avoid bundling unused adapters:
|
|
181
181
|
|
|
182
182
|
```ts
|
|
183
|
-
import { zodSchema } from
|
|
184
|
-
import { valibotSchema } from
|
|
185
|
-
import { arktypeSchema } from
|
|
183
|
+
import { zodSchema } from '@pyreon/validation/zod'
|
|
184
|
+
import { valibotSchema } from '@pyreon/validation/valibot'
|
|
185
|
+
import { arktypeSchema } from '@pyreon/validation/arktype'
|
|
186
186
|
```
|
|
187
187
|
|
|
188
188
|
### Mixing Field and Schema Validators
|
|
@@ -205,14 +205,14 @@ const form = useForm({
|
|
|
205
205
|
|
|
206
206
|
## Types
|
|
207
207
|
|
|
208
|
-
| Type
|
|
209
|
-
|
|
|
210
|
-
| `ValidationIssue`
|
|
211
|
-
| `SchemaAdapter<TSchema>`
|
|
212
|
-
| `FieldAdapter<TSchema>`
|
|
213
|
-
| `SchemaValidateFn<TValues>` | Re-exported from `@pyreon/form`
|
|
214
|
-
| `ValidateFn<T>`
|
|
215
|
-
| `ValidationError`
|
|
208
|
+
| Type | Description |
|
|
209
|
+
| --------------------------- | ------------------------------------------------------- |
|
|
210
|
+
| `ValidationIssue` | `{ path: string, message: string }` — normalized issue |
|
|
211
|
+
| `SchemaAdapter<TSchema>` | Generic schema adapter factory type |
|
|
212
|
+
| `FieldAdapter<TSchema>` | Generic field adapter factory type |
|
|
213
|
+
| `SchemaValidateFn<TValues>` | Re-exported from `@pyreon/form` |
|
|
214
|
+
| `ValidateFn<T>` | Re-exported from `@pyreon/form` |
|
|
215
|
+
| `ValidationError` | Re-exported from `@pyreon/form` — `string \| undefined` |
|
|
216
216
|
|
|
217
217
|
## Gotchas
|
|
218
218
|
|
package/lib/arktype.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"arktype.js","names":[],"sources":["../src/utils.ts","../src/arktype.ts"],"sourcesContent":["import type { ValidationError } from
|
|
1
|
+
{"version":3,"file":"arktype.js","names":[],"sources":["../src/utils.ts","../src/arktype.ts"],"sourcesContent":["import type { ValidationError } from '@pyreon/form'\nimport type { ValidationIssue } from './types'\n\n/**\n * Convert an array of validation issues into a flat field → error record.\n * For nested paths like [\"address\", \"city\"], produces \"address.city\".\n * When multiple issues exist for the same path, the first message wins.\n */\nexport function issuesToRecord<TValues extends Record<string, unknown>>(\n issues: ValidationIssue[],\n): Partial<Record<keyof TValues, ValidationError>> {\n const errors = {} as Partial<Record<keyof TValues, ValidationError>>\n for (const issue of issues) {\n const key = issue.path as keyof TValues\n // First error per field wins\n if (errors[key] === undefined) {\n errors[key] = issue.message\n }\n }\n return errors\n}\n","import type { SchemaValidateFn, ValidateFn, ValidationError } from '@pyreon/form'\nimport type { ValidationIssue } from './types'\nimport { issuesToRecord } from './utils'\n\n/**\n * Minimal ArkType-compatible interfaces so we don't require arktype as a hard dep.\n */\ninterface ArkError {\n path: PropertyKey[]\n message: string\n}\n\ninterface ArkErrors extends Array<ArkError> {\n summary: string\n}\n\n/**\n * Internal callable interface matching ArkType's Type.\n * Not exposed publicly — consumers pass their ArkType schema directly.\n */\ntype ArkTypeCallable = (data: unknown) => unknown\n\nfunction isArkErrors(result: unknown): result is ArkErrors {\n return Array.isArray(result) && 'summary' in (result as object)\n}\n\nfunction arkIssuesToGeneric(errors: ArkErrors): ValidationIssue[] {\n return errors.map((err) => ({\n path: err.path.map(String).join('.'),\n message: err.message,\n }))\n}\n\n/**\n * Create a form-level schema validator from an ArkType schema.\n *\n * Accepts any callable ArkType `Type` instance. The schema is duck-typed —\n * no ArkType import required.\n *\n * @example\n * import { type } from 'arktype'\n * import { arktypeSchema } from '@pyreon/validation/arktype'\n *\n * const schema = type({\n * email: 'string.email',\n * password: 'string >= 8',\n * })\n *\n * const form = useForm({\n * initialValues: { email: '', password: '' },\n * schema: arktypeSchema(schema),\n * onSubmit: (values) => { ... },\n * })\n */\nexport function arktypeSchema<TValues extends Record<string, unknown>>(\n schema: ArkTypeCallable,\n): SchemaValidateFn<TValues> {\n return (values: TValues) => {\n try {\n const result = schema(values)\n if (!isArkErrors(result)) return {} as Partial<Record<keyof TValues, ValidationError>>\n return issuesToRecord<TValues>(arkIssuesToGeneric(result))\n } catch (err) {\n return {\n '': err instanceof Error ? err.message : String(err),\n } as Partial<Record<keyof TValues, ValidationError>>\n }\n }\n}\n\n/**\n * Create a single-field validator from an ArkType schema.\n *\n * @example\n * import { type } from 'arktype'\n * import { arktypeField } from '@pyreon/validation/arktype'\n *\n * const form = useForm({\n * initialValues: { email: '' },\n * validators: {\n * email: arktypeField(type('string.email')),\n * },\n * onSubmit: (values) => { ... },\n * })\n */\nexport function arktypeField<T>(schema: ArkTypeCallable): ValidateFn<T> {\n return (value: T) => {\n try {\n const result = schema(value)\n if (!isArkErrors(result)) return undefined\n return result[0]?.message\n } catch (err) {\n return err instanceof Error ? err.message : String(err)\n }\n }\n}\n"],"mappings":";;;;;;AAQA,SAAgB,eACd,QACiD;CACjD,MAAM,SAAS,EAAE;AACjB,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,MAAM,MAAM;AAElB,MAAI,OAAO,SAAS,OAClB,QAAO,OAAO,MAAM;;AAGxB,QAAO;;;;;ACGT,SAAS,YAAY,QAAsC;AACzD,QAAO,MAAM,QAAQ,OAAO,IAAI,aAAc;;AAGhD,SAAS,mBAAmB,QAAsC;AAChE,QAAO,OAAO,KAAK,SAAS;EAC1B,MAAM,IAAI,KAAK,IAAI,OAAO,CAAC,KAAK,IAAI;EACpC,SAAS,IAAI;EACd,EAAE;;;;;;;;;;;;;;;;;;;;;;;AAwBL,SAAgB,cACd,QAC2B;AAC3B,SAAQ,WAAoB;AAC1B,MAAI;GACF,MAAM,SAAS,OAAO,OAAO;AAC7B,OAAI,CAAC,YAAY,OAAO,CAAE,QAAO,EAAE;AACnC,UAAO,eAAwB,mBAAmB,OAAO,CAAC;WACnD,KAAK;AACZ,UAAO,EACL,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,EACrD;;;;;;;;;;;;;;;;;;;AAoBP,SAAgB,aAAgB,QAAwC;AACtE,SAAQ,UAAa;AACnB,MAAI;GACF,MAAM,SAAS,OAAO,MAAM;AAC5B,OAAI,CAAC,YAAY,OAAO,CAAE,QAAO;AACjC,UAAO,OAAO,IAAI;WACX,KAAK;AACZ,UAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI"}
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/utils.ts","../src/arktype.ts","../src/valibot.ts","../src/zod.ts"],"sourcesContent":["import type { ValidationError } from \"@pyreon/form\"\nimport type { ValidationIssue } from \"./types\"\n\n/**\n * Convert an array of validation issues into a flat field → error record.\n * For nested paths like [\"address\", \"city\"], produces \"address.city\".\n * When multiple issues exist for the same path, the first message wins.\n */\nexport function issuesToRecord<TValues extends Record<string, unknown>>(\n issues: ValidationIssue[],\n): Partial<Record<keyof TValues, ValidationError>> {\n const errors = {} as Partial<Record<keyof TValues, ValidationError>>\n for (const issue of issues) {\n const key = issue.path as keyof TValues\n // First error per field wins\n if (errors[key] === undefined) {\n errors[key] = issue.message\n }\n }\n return errors\n}\n","import type { SchemaValidateFn, ValidateFn, ValidationError } from \"@pyreon/form\"\nimport type { ValidationIssue } from \"./types\"\nimport { issuesToRecord } from \"./utils\"\n\n/**\n * Minimal ArkType-compatible interfaces so we don't require arktype as a hard dep.\n */\ninterface ArkError {\n path: PropertyKey[]\n message: string\n}\n\ninterface ArkErrors extends Array<ArkError> {\n summary: string\n}\n\n/**\n * Internal callable interface matching ArkType's Type.\n * Not exposed publicly — consumers pass their ArkType schema directly.\n */\ntype ArkTypeCallable = (data: unknown) => unknown\n\nfunction isArkErrors(result: unknown): result is ArkErrors {\n return Array.isArray(result) && \"summary\" in (result as object)\n}\n\nfunction arkIssuesToGeneric(errors: ArkErrors): ValidationIssue[] {\n return errors.map((err) => ({\n path: err.path.map(String).join(\".\"),\n message: err.message,\n }))\n}\n\n/**\n * Create a form-level schema validator from an ArkType schema.\n *\n * Accepts any callable ArkType `Type` instance. The schema is duck-typed —\n * no ArkType import required.\n *\n * @example\n * import { type } from 'arktype'\n * import { arktypeSchema } from '@pyreon/validation/arktype'\n *\n * const schema = type({\n * email: 'string.email',\n * password: 'string >= 8',\n * })\n *\n * const form = useForm({\n * initialValues: { email: '', password: '' },\n * schema: arktypeSchema(schema),\n * onSubmit: (values) => { ... },\n * })\n */\nexport function arktypeSchema<TValues extends Record<string, unknown>>(\n schema: ArkTypeCallable,\n): SchemaValidateFn<TValues> {\n return (values: TValues) => {\n try {\n const result = schema(values)\n if (!isArkErrors(result)) return {} as Partial<Record<keyof TValues, ValidationError>>\n return issuesToRecord<TValues>(arkIssuesToGeneric(result))\n } catch (err) {\n return {\n \"\": err instanceof Error ? err.message : String(err),\n } as Partial<Record<keyof TValues, ValidationError>>\n }\n }\n}\n\n/**\n * Create a single-field validator from an ArkType schema.\n *\n * @example\n * import { type } from 'arktype'\n * import { arktypeField } from '@pyreon/validation/arktype'\n *\n * const form = useForm({\n * initialValues: { email: '' },\n * validators: {\n * email: arktypeField(type('string.email')),\n * },\n * onSubmit: (values) => { ... },\n * })\n */\nexport function arktypeField<T>(schema: ArkTypeCallable): ValidateFn<T> {\n return (value: T) => {\n try {\n const result = schema(value)\n if (!isArkErrors(result)) return undefined\n return result[0]?.message\n } catch (err) {\n return err instanceof Error ? err.message : String(err)\n }\n }\n}\n","import type { SchemaValidateFn, ValidateFn, ValidationError } from \"@pyreon/form\"\nimport type { ValidationIssue } from \"./types\"\nimport { issuesToRecord } from \"./utils\"\n\n/**\n * Minimal Valibot-compatible interfaces so we don't require valibot as a hard dep.\n */\ninterface ValibotPathItem {\n key: string | number\n}\n\ninterface ValibotIssue {\n path?: ValibotPathItem[]\n message: string\n}\n\ninterface ValibotSafeParseResult {\n success: boolean\n output?: unknown\n issues?: ValibotIssue[]\n}\n\n/**\n * Any function that takes (schema, input, ...rest) and returns a parse result.\n * Valibot's safeParse/safeParseAsync have generic constraints on the schema\n * parameter that can't be expressed without importing Valibot types. We accept\n * any callable and cast internally.\n */\n// biome-ignore lint/complexity/noBannedTypes: must accept any valibot parse function\ntype GenericSafeParseFn = Function\n\nfunction valibotIssuesToGeneric(issues: ValibotIssue[]): ValidationIssue[] {\n return issues.map((issue) => ({\n path: issue.path?.map((p) => String(p.key)).join(\".\") ?? \"\",\n message: issue.message,\n }))\n}\n\ntype InternalParseFn = (\n schema: unknown,\n input: unknown,\n) => ValibotSafeParseResult | Promise<ValibotSafeParseResult>\n\n/**\n * Create a form-level schema validator from a Valibot schema.\n *\n * Valibot uses standalone functions rather than methods, so you must pass\n * the `safeParseAsync` (or `safeParse`) function from valibot.\n *\n * @example\n * import * as v from 'valibot'\n * import { valibotSchema } from '@pyreon/validation/valibot'\n *\n * const schema = v.object({\n * email: v.pipe(v.string(), v.email()),\n * password: v.pipe(v.string(), v.minLength(8)),\n * })\n *\n * const form = useForm({\n * initialValues: { email: '', password: '' },\n * schema: valibotSchema(schema, v.safeParseAsync),\n * onSubmit: (values) => { ... },\n * })\n */\nexport function valibotSchema<TValues extends Record<string, unknown>>(\n schema: unknown,\n safeParseFn: GenericSafeParseFn,\n): SchemaValidateFn<TValues> {\n const parse = safeParseFn as InternalParseFn\n return async (values: TValues) => {\n try {\n const result = await parse(schema, values)\n if (result.success) return {} as Partial<Record<keyof TValues, ValidationError>>\n return issuesToRecord<TValues>(valibotIssuesToGeneric(result.issues ?? []))\n } catch (err) {\n return {\n \"\": err instanceof Error ? err.message : String(err),\n } as Partial<Record<keyof TValues, ValidationError>>\n }\n }\n}\n\n/**\n * Create a single-field validator from a Valibot schema.\n *\n * @example\n * import * as v from 'valibot'\n * import { valibotField } from '@pyreon/validation/valibot'\n *\n * const form = useForm({\n * initialValues: { email: '' },\n * validators: {\n * email: valibotField(v.pipe(v.string(), v.email('Invalid email')), v.safeParseAsync),\n * },\n * onSubmit: (values) => { ... },\n * })\n */\nexport function valibotField<T>(schema: unknown, safeParseFn: GenericSafeParseFn): ValidateFn<T> {\n const parse = safeParseFn as InternalParseFn\n return async (value: T) => {\n try {\n const result = await parse(schema, value)\n if (result.success) return undefined\n return result.issues?.[0]?.message\n } catch (err) {\n return err instanceof Error ? err.message : String(err)\n }\n }\n}\n","import type { SchemaValidateFn, ValidateFn, ValidationError } from \"@pyreon/form\"\nimport type { ValidationIssue } from \"./types\"\nimport { issuesToRecord } from \"./utils\"\n\n/**\n * Minimal Zod-compatible interfaces so we don't require zod as a hard dep.\n * These match Zod v3's public API surface.\n */\ninterface ZodIssue {\n path: PropertyKey[]\n message: string\n}\n\n/**\n * Duck-typed Zod schema interface — works with both Zod v3 and v4.\n * Inlines the result shape to avoid version-specific type mismatches.\n */\ninterface ZodSchema<T = unknown> {\n safeParse(data: unknown): {\n success: boolean\n data?: T\n error?: { issues: ZodIssue[] }\n }\n safeParseAsync(\n data: unknown,\n ): Promise<{ success: boolean; data?: T; error?: { issues: ZodIssue[] } }>\n}\n\nfunction zodIssuesToGeneric(issues: ZodIssue[]): ValidationIssue[] {\n return issues.map((issue) => ({\n path: issue.path.map(String).join(\".\"),\n message: issue.message,\n }))\n}\n\n/**\n * Create a form-level schema validator from a Zod schema.\n * Supports both sync and async Zod schemas (uses `safeParseAsync`).\n *\n * @example\n * import { z } from 'zod'\n * import { zodSchema } from '@pyreon/validation/zod'\n *\n * const schema = z.object({\n * email: z.string().email(),\n * password: z.string().min(8),\n * })\n *\n * const form = useForm({\n * initialValues: { email: '', password: '' },\n * schema: zodSchema(schema),\n * onSubmit: (values) => { ... },\n * })\n */\nexport function zodSchema<TValues extends Record<string, unknown>>(\n schema: ZodSchema<TValues>,\n): SchemaValidateFn<TValues> {\n return async (values: TValues) => {\n try {\n const result = await schema.safeParseAsync(values)\n if (result.success) return {} as Partial<Record<keyof TValues, ValidationError>>\n return issuesToRecord<TValues>(zodIssuesToGeneric(result.error!.issues))\n } catch (err) {\n return {\n \"\": err instanceof Error ? err.message : String(err),\n } as Partial<Record<keyof TValues, ValidationError>>\n }\n }\n}\n\n/**\n * Create a single-field validator from a Zod schema.\n * Supports both sync and async Zod refinements.\n *\n * @example\n * import { z } from 'zod'\n * import { zodField } from '@pyreon/validation/zod'\n *\n * const form = useForm({\n * initialValues: { email: '' },\n * validators: {\n * email: zodField(z.string().email('Invalid email')),\n * },\n * onSubmit: (values) => { ... },\n * })\n */\nexport function zodField<T>(schema: ZodSchema<T>): ValidateFn<T> {\n return async (value: T) => {\n try {\n const result = await schema.safeParseAsync(value)\n if (result.success) return undefined\n return result.error!.issues[0]?.message\n } catch (err) {\n return err instanceof Error ? err.message : String(err)\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/utils.ts","../src/arktype.ts","../src/valibot.ts","../src/zod.ts"],"sourcesContent":["import type { ValidationError } from '@pyreon/form'\nimport type { ValidationIssue } from './types'\n\n/**\n * Convert an array of validation issues into a flat field → error record.\n * For nested paths like [\"address\", \"city\"], produces \"address.city\".\n * When multiple issues exist for the same path, the first message wins.\n */\nexport function issuesToRecord<TValues extends Record<string, unknown>>(\n issues: ValidationIssue[],\n): Partial<Record<keyof TValues, ValidationError>> {\n const errors = {} as Partial<Record<keyof TValues, ValidationError>>\n for (const issue of issues) {\n const key = issue.path as keyof TValues\n // First error per field wins\n if (errors[key] === undefined) {\n errors[key] = issue.message\n }\n }\n return errors\n}\n","import type { SchemaValidateFn, ValidateFn, ValidationError } from '@pyreon/form'\nimport type { ValidationIssue } from './types'\nimport { issuesToRecord } from './utils'\n\n/**\n * Minimal ArkType-compatible interfaces so we don't require arktype as a hard dep.\n */\ninterface ArkError {\n path: PropertyKey[]\n message: string\n}\n\ninterface ArkErrors extends Array<ArkError> {\n summary: string\n}\n\n/**\n * Internal callable interface matching ArkType's Type.\n * Not exposed publicly — consumers pass their ArkType schema directly.\n */\ntype ArkTypeCallable = (data: unknown) => unknown\n\nfunction isArkErrors(result: unknown): result is ArkErrors {\n return Array.isArray(result) && 'summary' in (result as object)\n}\n\nfunction arkIssuesToGeneric(errors: ArkErrors): ValidationIssue[] {\n return errors.map((err) => ({\n path: err.path.map(String).join('.'),\n message: err.message,\n }))\n}\n\n/**\n * Create a form-level schema validator from an ArkType schema.\n *\n * Accepts any callable ArkType `Type` instance. The schema is duck-typed —\n * no ArkType import required.\n *\n * @example\n * import { type } from 'arktype'\n * import { arktypeSchema } from '@pyreon/validation/arktype'\n *\n * const schema = type({\n * email: 'string.email',\n * password: 'string >= 8',\n * })\n *\n * const form = useForm({\n * initialValues: { email: '', password: '' },\n * schema: arktypeSchema(schema),\n * onSubmit: (values) => { ... },\n * })\n */\nexport function arktypeSchema<TValues extends Record<string, unknown>>(\n schema: ArkTypeCallable,\n): SchemaValidateFn<TValues> {\n return (values: TValues) => {\n try {\n const result = schema(values)\n if (!isArkErrors(result)) return {} as Partial<Record<keyof TValues, ValidationError>>\n return issuesToRecord<TValues>(arkIssuesToGeneric(result))\n } catch (err) {\n return {\n '': err instanceof Error ? err.message : String(err),\n } as Partial<Record<keyof TValues, ValidationError>>\n }\n }\n}\n\n/**\n * Create a single-field validator from an ArkType schema.\n *\n * @example\n * import { type } from 'arktype'\n * import { arktypeField } from '@pyreon/validation/arktype'\n *\n * const form = useForm({\n * initialValues: { email: '' },\n * validators: {\n * email: arktypeField(type('string.email')),\n * },\n * onSubmit: (values) => { ... },\n * })\n */\nexport function arktypeField<T>(schema: ArkTypeCallable): ValidateFn<T> {\n return (value: T) => {\n try {\n const result = schema(value)\n if (!isArkErrors(result)) return undefined\n return result[0]?.message\n } catch (err) {\n return err instanceof Error ? err.message : String(err)\n }\n }\n}\n","import type { SchemaValidateFn, ValidateFn, ValidationError } from '@pyreon/form'\nimport type { ValidationIssue } from './types'\nimport { issuesToRecord } from './utils'\n\n/**\n * Minimal Valibot-compatible interfaces so we don't require valibot as a hard dep.\n */\ninterface ValibotPathItem {\n key: string | number\n}\n\ninterface ValibotIssue {\n path?: ValibotPathItem[]\n message: string\n}\n\ninterface ValibotSafeParseResult {\n success: boolean\n output?: unknown\n issues?: ValibotIssue[]\n}\n\n/**\n * Any function that takes (schema, input, ...rest) and returns a parse result.\n * Valibot's safeParse/safeParseAsync have generic constraints on the schema\n * parameter that can't be expressed without importing Valibot types. We accept\n * any callable and cast internally.\n */\n// biome-ignore lint/complexity/noBannedTypes: must accept any valibot parse function\ntype GenericSafeParseFn = Function\n\nfunction valibotIssuesToGeneric(issues: ValibotIssue[]): ValidationIssue[] {\n return issues.map((issue) => ({\n path: issue.path?.map((p) => String(p.key)).join('.') ?? '',\n message: issue.message,\n }))\n}\n\ntype InternalParseFn = (\n schema: unknown,\n input: unknown,\n) => ValibotSafeParseResult | Promise<ValibotSafeParseResult>\n\n/**\n * Create a form-level schema validator from a Valibot schema.\n *\n * Valibot uses standalone functions rather than methods, so you must pass\n * the `safeParseAsync` (or `safeParse`) function from valibot.\n *\n * @example\n * import * as v from 'valibot'\n * import { valibotSchema } from '@pyreon/validation/valibot'\n *\n * const schema = v.object({\n * email: v.pipe(v.string(), v.email()),\n * password: v.pipe(v.string(), v.minLength(8)),\n * })\n *\n * const form = useForm({\n * initialValues: { email: '', password: '' },\n * schema: valibotSchema(schema, v.safeParseAsync),\n * onSubmit: (values) => { ... },\n * })\n */\nexport function valibotSchema<TValues extends Record<string, unknown>>(\n schema: unknown,\n safeParseFn: GenericSafeParseFn,\n): SchemaValidateFn<TValues> {\n const parse = safeParseFn as InternalParseFn\n return async (values: TValues) => {\n try {\n const result = await parse(schema, values)\n if (result.success) return {} as Partial<Record<keyof TValues, ValidationError>>\n return issuesToRecord<TValues>(valibotIssuesToGeneric(result.issues ?? []))\n } catch (err) {\n return {\n '': err instanceof Error ? err.message : String(err),\n } as Partial<Record<keyof TValues, ValidationError>>\n }\n }\n}\n\n/**\n * Create a single-field validator from a Valibot schema.\n *\n * @example\n * import * as v from 'valibot'\n * import { valibotField } from '@pyreon/validation/valibot'\n *\n * const form = useForm({\n * initialValues: { email: '' },\n * validators: {\n * email: valibotField(v.pipe(v.string(), v.email('Invalid email')), v.safeParseAsync),\n * },\n * onSubmit: (values) => { ... },\n * })\n */\nexport function valibotField<T>(schema: unknown, safeParseFn: GenericSafeParseFn): ValidateFn<T> {\n const parse = safeParseFn as InternalParseFn\n return async (value: T) => {\n try {\n const result = await parse(schema, value)\n if (result.success) return undefined\n return result.issues?.[0]?.message\n } catch (err) {\n return err instanceof Error ? err.message : String(err)\n }\n }\n}\n","import type { SchemaValidateFn, ValidateFn, ValidationError } from '@pyreon/form'\nimport type { ValidationIssue } from './types'\nimport { issuesToRecord } from './utils'\n\n/**\n * Minimal Zod-compatible interfaces so we don't require zod as a hard dep.\n * These match Zod v3's public API surface.\n */\ninterface ZodIssue {\n path: PropertyKey[]\n message: string\n}\n\n/**\n * Duck-typed Zod schema interface — works with both Zod v3 and v4.\n * Inlines the result shape to avoid version-specific type mismatches.\n */\ninterface ZodSchema<T = unknown> {\n safeParse(data: unknown): {\n success: boolean\n data?: T\n error?: { issues: ZodIssue[] }\n }\n safeParseAsync(\n data: unknown,\n ): Promise<{ success: boolean; data?: T; error?: { issues: ZodIssue[] } }>\n}\n\nfunction zodIssuesToGeneric(issues: ZodIssue[]): ValidationIssue[] {\n return issues.map((issue) => ({\n path: issue.path.map(String).join('.'),\n message: issue.message,\n }))\n}\n\n/**\n * Create a form-level schema validator from a Zod schema.\n * Supports both sync and async Zod schemas (uses `safeParseAsync`).\n *\n * @example\n * import { z } from 'zod'\n * import { zodSchema } from '@pyreon/validation/zod'\n *\n * const schema = z.object({\n * email: z.string().email(),\n * password: z.string().min(8),\n * })\n *\n * const form = useForm({\n * initialValues: { email: '', password: '' },\n * schema: zodSchema(schema),\n * onSubmit: (values) => { ... },\n * })\n */\nexport function zodSchema<TValues extends Record<string, unknown>>(\n schema: ZodSchema<TValues>,\n): SchemaValidateFn<TValues> {\n return async (values: TValues) => {\n try {\n const result = await schema.safeParseAsync(values)\n if (result.success) return {} as Partial<Record<keyof TValues, ValidationError>>\n return issuesToRecord<TValues>(zodIssuesToGeneric(result.error!.issues))\n } catch (err) {\n return {\n '': err instanceof Error ? err.message : String(err),\n } as Partial<Record<keyof TValues, ValidationError>>\n }\n }\n}\n\n/**\n * Create a single-field validator from a Zod schema.\n * Supports both sync and async Zod refinements.\n *\n * @example\n * import { z } from 'zod'\n * import { zodField } from '@pyreon/validation/zod'\n *\n * const form = useForm({\n * initialValues: { email: '' },\n * validators: {\n * email: zodField(z.string().email('Invalid email')),\n * },\n * onSubmit: (values) => { ... },\n * })\n */\nexport function zodField<T>(schema: ZodSchema<T>): ValidateFn<T> {\n return async (value: T) => {\n try {\n const result = await schema.safeParseAsync(value)\n if (result.success) return undefined\n return result.error!.issues[0]?.message\n } catch (err) {\n return err instanceof Error ? err.message : String(err)\n }\n }\n}\n"],"mappings":"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"}
|
package/lib/valibot.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"valibot.js","names":[],"sources":["../src/utils.ts","../src/valibot.ts"],"sourcesContent":["import type { ValidationError } from
|
|
1
|
+
{"version":3,"file":"valibot.js","names":[],"sources":["../src/utils.ts","../src/valibot.ts"],"sourcesContent":["import type { ValidationError } from '@pyreon/form'\nimport type { ValidationIssue } from './types'\n\n/**\n * Convert an array of validation issues into a flat field → error record.\n * For nested paths like [\"address\", \"city\"], produces \"address.city\".\n * When multiple issues exist for the same path, the first message wins.\n */\nexport function issuesToRecord<TValues extends Record<string, unknown>>(\n issues: ValidationIssue[],\n): Partial<Record<keyof TValues, ValidationError>> {\n const errors = {} as Partial<Record<keyof TValues, ValidationError>>\n for (const issue of issues) {\n const key = issue.path as keyof TValues\n // First error per field wins\n if (errors[key] === undefined) {\n errors[key] = issue.message\n }\n }\n return errors\n}\n","import type { SchemaValidateFn, ValidateFn, ValidationError } from '@pyreon/form'\nimport type { ValidationIssue } from './types'\nimport { issuesToRecord } from './utils'\n\n/**\n * Minimal Valibot-compatible interfaces so we don't require valibot as a hard dep.\n */\ninterface ValibotPathItem {\n key: string | number\n}\n\ninterface ValibotIssue {\n path?: ValibotPathItem[]\n message: string\n}\n\ninterface ValibotSafeParseResult {\n success: boolean\n output?: unknown\n issues?: ValibotIssue[]\n}\n\n/**\n * Any function that takes (schema, input, ...rest) and returns a parse result.\n * Valibot's safeParse/safeParseAsync have generic constraints on the schema\n * parameter that can't be expressed without importing Valibot types. We accept\n * any callable and cast internally.\n */\n// biome-ignore lint/complexity/noBannedTypes: must accept any valibot parse function\ntype GenericSafeParseFn = Function\n\nfunction valibotIssuesToGeneric(issues: ValibotIssue[]): ValidationIssue[] {\n return issues.map((issue) => ({\n path: issue.path?.map((p) => String(p.key)).join('.') ?? '',\n message: issue.message,\n }))\n}\n\ntype InternalParseFn = (\n schema: unknown,\n input: unknown,\n) => ValibotSafeParseResult | Promise<ValibotSafeParseResult>\n\n/**\n * Create a form-level schema validator from a Valibot schema.\n *\n * Valibot uses standalone functions rather than methods, so you must pass\n * the `safeParseAsync` (or `safeParse`) function from valibot.\n *\n * @example\n * import * as v from 'valibot'\n * import { valibotSchema } from '@pyreon/validation/valibot'\n *\n * const schema = v.object({\n * email: v.pipe(v.string(), v.email()),\n * password: v.pipe(v.string(), v.minLength(8)),\n * })\n *\n * const form = useForm({\n * initialValues: { email: '', password: '' },\n * schema: valibotSchema(schema, v.safeParseAsync),\n * onSubmit: (values) => { ... },\n * })\n */\nexport function valibotSchema<TValues extends Record<string, unknown>>(\n schema: unknown,\n safeParseFn: GenericSafeParseFn,\n): SchemaValidateFn<TValues> {\n const parse = safeParseFn as InternalParseFn\n return async (values: TValues) => {\n try {\n const result = await parse(schema, values)\n if (result.success) return {} as Partial<Record<keyof TValues, ValidationError>>\n return issuesToRecord<TValues>(valibotIssuesToGeneric(result.issues ?? []))\n } catch (err) {\n return {\n '': err instanceof Error ? err.message : String(err),\n } as Partial<Record<keyof TValues, ValidationError>>\n }\n }\n}\n\n/**\n * Create a single-field validator from a Valibot schema.\n *\n * @example\n * import * as v from 'valibot'\n * import { valibotField } from '@pyreon/validation/valibot'\n *\n * const form = useForm({\n * initialValues: { email: '' },\n * validators: {\n * email: valibotField(v.pipe(v.string(), v.email('Invalid email')), v.safeParseAsync),\n * },\n * onSubmit: (values) => { ... },\n * })\n */\nexport function valibotField<T>(schema: unknown, safeParseFn: GenericSafeParseFn): ValidateFn<T> {\n const parse = safeParseFn as InternalParseFn\n return async (value: T) => {\n try {\n const result = await parse(schema, value)\n if (result.success) return undefined\n return result.issues?.[0]?.message\n } catch (err) {\n return err instanceof Error ? err.message : String(err)\n }\n }\n}\n"],"mappings":";;;;;;AAQA,SAAgB,eACd,QACiD;CACjD,MAAM,SAAS,EAAE;AACjB,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,MAAM,MAAM;AAElB,MAAI,OAAO,SAAS,OAClB,QAAO,OAAO,MAAM;;AAGxB,QAAO;;;;;ACYT,SAAS,uBAAuB,QAA2C;AACzE,QAAO,OAAO,KAAK,WAAW;EAC5B,MAAM,MAAM,MAAM,KAAK,MAAM,OAAO,EAAE,IAAI,CAAC,CAAC,KAAK,IAAI,IAAI;EACzD,SAAS,MAAM;EAChB,EAAE;;;;;;;;;;;;;;;;;;;;;;;AA6BL,SAAgB,cACd,QACA,aAC2B;CAC3B,MAAM,QAAQ;AACd,QAAO,OAAO,WAAoB;AAChC,MAAI;GACF,MAAM,SAAS,MAAM,MAAM,QAAQ,OAAO;AAC1C,OAAI,OAAO,QAAS,QAAO,EAAE;AAC7B,UAAO,eAAwB,uBAAuB,OAAO,UAAU,EAAE,CAAC,CAAC;WACpE,KAAK;AACZ,UAAO,EACL,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,EACrD;;;;;;;;;;;;;;;;;;;AAoBP,SAAgB,aAAgB,QAAiB,aAAgD;CAC/F,MAAM,QAAQ;AACd,QAAO,OAAO,UAAa;AACzB,MAAI;GACF,MAAM,SAAS,MAAM,MAAM,QAAQ,MAAM;AACzC,OAAI,OAAO,QAAS,QAAO;AAC3B,UAAO,OAAO,SAAS,IAAI;WACpB,KAAK;AACZ,UAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI"}
|
package/lib/zod.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zod.js","names":[],"sources":["../src/utils.ts","../src/zod.ts"],"sourcesContent":["import type { ValidationError } from
|
|
1
|
+
{"version":3,"file":"zod.js","names":[],"sources":["../src/utils.ts","../src/zod.ts"],"sourcesContent":["import type { ValidationError } from '@pyreon/form'\nimport type { ValidationIssue } from './types'\n\n/**\n * Convert an array of validation issues into a flat field → error record.\n * For nested paths like [\"address\", \"city\"], produces \"address.city\".\n * When multiple issues exist for the same path, the first message wins.\n */\nexport function issuesToRecord<TValues extends Record<string, unknown>>(\n issues: ValidationIssue[],\n): Partial<Record<keyof TValues, ValidationError>> {\n const errors = {} as Partial<Record<keyof TValues, ValidationError>>\n for (const issue of issues) {\n const key = issue.path as keyof TValues\n // First error per field wins\n if (errors[key] === undefined) {\n errors[key] = issue.message\n }\n }\n return errors\n}\n","import type { SchemaValidateFn, ValidateFn, ValidationError } from '@pyreon/form'\nimport type { ValidationIssue } from './types'\nimport { issuesToRecord } from './utils'\n\n/**\n * Minimal Zod-compatible interfaces so we don't require zod as a hard dep.\n * These match Zod v3's public API surface.\n */\ninterface ZodIssue {\n path: PropertyKey[]\n message: string\n}\n\n/**\n * Duck-typed Zod schema interface — works with both Zod v3 and v4.\n * Inlines the result shape to avoid version-specific type mismatches.\n */\ninterface ZodSchema<T = unknown> {\n safeParse(data: unknown): {\n success: boolean\n data?: T\n error?: { issues: ZodIssue[] }\n }\n safeParseAsync(\n data: unknown,\n ): Promise<{ success: boolean; data?: T; error?: { issues: ZodIssue[] } }>\n}\n\nfunction zodIssuesToGeneric(issues: ZodIssue[]): ValidationIssue[] {\n return issues.map((issue) => ({\n path: issue.path.map(String).join('.'),\n message: issue.message,\n }))\n}\n\n/**\n * Create a form-level schema validator from a Zod schema.\n * Supports both sync and async Zod schemas (uses `safeParseAsync`).\n *\n * @example\n * import { z } from 'zod'\n * import { zodSchema } from '@pyreon/validation/zod'\n *\n * const schema = z.object({\n * email: z.string().email(),\n * password: z.string().min(8),\n * })\n *\n * const form = useForm({\n * initialValues: { email: '', password: '' },\n * schema: zodSchema(schema),\n * onSubmit: (values) => { ... },\n * })\n */\nexport function zodSchema<TValues extends Record<string, unknown>>(\n schema: ZodSchema<TValues>,\n): SchemaValidateFn<TValues> {\n return async (values: TValues) => {\n try {\n const result = await schema.safeParseAsync(values)\n if (result.success) return {} as Partial<Record<keyof TValues, ValidationError>>\n return issuesToRecord<TValues>(zodIssuesToGeneric(result.error!.issues))\n } catch (err) {\n return {\n '': err instanceof Error ? err.message : String(err),\n } as Partial<Record<keyof TValues, ValidationError>>\n }\n }\n}\n\n/**\n * Create a single-field validator from a Zod schema.\n * Supports both sync and async Zod refinements.\n *\n * @example\n * import { z } from 'zod'\n * import { zodField } from '@pyreon/validation/zod'\n *\n * const form = useForm({\n * initialValues: { email: '' },\n * validators: {\n * email: zodField(z.string().email('Invalid email')),\n * },\n * onSubmit: (values) => { ... },\n * })\n */\nexport function zodField<T>(schema: ZodSchema<T>): ValidateFn<T> {\n return async (value: T) => {\n try {\n const result = await schema.safeParseAsync(value)\n if (result.success) return undefined\n return result.error!.issues[0]?.message\n } catch (err) {\n return err instanceof Error ? err.message : String(err)\n }\n }\n}\n"],"mappings":";;;;;;AAQA,SAAgB,eACd,QACiD;CACjD,MAAM,SAAS,EAAE;AACjB,MAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,MAAM,MAAM;AAElB,MAAI,OAAO,SAAS,OAClB,QAAO,OAAO,MAAM;;AAGxB,QAAO;;;;;ACST,SAAS,mBAAmB,QAAuC;AACjE,QAAO,OAAO,KAAK,WAAW;EAC5B,MAAM,MAAM,KAAK,IAAI,OAAO,CAAC,KAAK,IAAI;EACtC,SAAS,MAAM;EAChB,EAAE;;;;;;;;;;;;;;;;;;;;;AAsBL,SAAgB,UACd,QAC2B;AAC3B,QAAO,OAAO,WAAoB;AAChC,MAAI;GACF,MAAM,SAAS,MAAM,OAAO,eAAe,OAAO;AAClD,OAAI,OAAO,QAAS,QAAO,EAAE;AAC7B,UAAO,eAAwB,mBAAmB,OAAO,MAAO,OAAO,CAAC;WACjE,KAAK;AACZ,UAAO,EACL,IAAI,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,EACrD;;;;;;;;;;;;;;;;;;;;AAqBP,SAAgB,SAAY,QAAqC;AAC/D,QAAO,OAAO,UAAa;AACzB,MAAI;GACF,MAAM,SAAS,MAAM,OAAO,eAAe,MAAM;AACjD,OAAI,OAAO,QAAS,QAAO;AAC3B,UAAO,OAAO,MAAO,OAAO,IAAI;WACzB,KAAK;AACZ,UAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI"}
|
package/package.json
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/validation",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.6",
|
|
4
4
|
"description": "Schema validation adapters for Pyreon forms (Zod, Valibot, ArkType)",
|
|
5
|
+
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/validation#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/pyreon/pyreon/issues"
|
|
8
|
+
},
|
|
5
9
|
"license": "MIT",
|
|
6
10
|
"repository": {
|
|
7
11
|
"type": "git",
|
|
8
12
|
"url": "https://github.com/pyreon/pyreon.git",
|
|
9
13
|
"directory": "packages/fundamentals/validation"
|
|
10
14
|
},
|
|
11
|
-
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/validation#readme",
|
|
12
|
-
"bugs": {
|
|
13
|
-
"url": "https://github.com/pyreon/pyreon/issues"
|
|
14
|
-
},
|
|
15
|
-
"publishConfig": {
|
|
16
|
-
"access": "public"
|
|
17
|
-
},
|
|
18
15
|
"files": [
|
|
19
16
|
"lib",
|
|
20
17
|
"src",
|
|
@@ -48,15 +45,28 @@
|
|
|
48
45
|
"types": "./lib/types/arktype.d.ts"
|
|
49
46
|
}
|
|
50
47
|
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
51
|
"scripts": {
|
|
52
52
|
"build": "vl_rolldown_build",
|
|
53
53
|
"dev": "vl_rolldown_build-watch",
|
|
54
54
|
"test": "vitest run",
|
|
55
55
|
"typecheck": "tsc --noEmit",
|
|
56
|
-
"lint": "
|
|
56
|
+
"lint": "oxlint ."
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@happy-dom/global-registrator": "^20.8.3",
|
|
60
|
+
"@pyreon/core": "^0.11.6",
|
|
61
|
+
"@pyreon/form": "^0.11.6",
|
|
62
|
+
"@pyreon/reactivity": "^0.11.6",
|
|
63
|
+
"@pyreon/runtime-dom": "^0.11.6",
|
|
64
|
+
"arktype": "^2.2.0",
|
|
65
|
+
"valibot": "^1.2.0",
|
|
66
|
+
"zod": "^4.3.6"
|
|
57
67
|
},
|
|
58
68
|
"peerDependencies": {
|
|
59
|
-
"@pyreon/form": "^0.11.
|
|
69
|
+
"@pyreon/form": "^0.11.6"
|
|
60
70
|
},
|
|
61
71
|
"peerDependenciesMeta": {
|
|
62
72
|
"zod": {
|
|
@@ -68,15 +78,5 @@
|
|
|
68
78
|
"arktype": {
|
|
69
79
|
"optional": true
|
|
70
80
|
}
|
|
71
|
-
},
|
|
72
|
-
"devDependencies": {
|
|
73
|
-
"@happy-dom/global-registrator": "^20.8.3",
|
|
74
|
-
"@pyreon/core": "^0.11.4",
|
|
75
|
-
"@pyreon/form": "^0.11.4",
|
|
76
|
-
"@pyreon/reactivity": "^0.11.4",
|
|
77
|
-
"@pyreon/runtime-dom": "^0.11.4",
|
|
78
|
-
"zod": "^4.3.6",
|
|
79
|
-
"valibot": "^1.2.0",
|
|
80
|
-
"arktype": "^2.2.0"
|
|
81
81
|
}
|
|
82
82
|
}
|
package/src/arktype.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { SchemaValidateFn, ValidateFn, ValidationError } from
|
|
2
|
-
import type { ValidationIssue } from
|
|
3
|
-
import { issuesToRecord } from
|
|
1
|
+
import type { SchemaValidateFn, ValidateFn, ValidationError } from '@pyreon/form'
|
|
2
|
+
import type { ValidationIssue } from './types'
|
|
3
|
+
import { issuesToRecord } from './utils'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Minimal ArkType-compatible interfaces so we don't require arktype as a hard dep.
|
|
@@ -21,12 +21,12 @@ interface ArkErrors extends Array<ArkError> {
|
|
|
21
21
|
type ArkTypeCallable = (data: unknown) => unknown
|
|
22
22
|
|
|
23
23
|
function isArkErrors(result: unknown): result is ArkErrors {
|
|
24
|
-
return Array.isArray(result) &&
|
|
24
|
+
return Array.isArray(result) && 'summary' in (result as object)
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
function arkIssuesToGeneric(errors: ArkErrors): ValidationIssue[] {
|
|
28
28
|
return errors.map((err) => ({
|
|
29
|
-
path: err.path.map(String).join(
|
|
29
|
+
path: err.path.map(String).join('.'),
|
|
30
30
|
message: err.message,
|
|
31
31
|
}))
|
|
32
32
|
}
|
|
@@ -62,7 +62,7 @@ export function arktypeSchema<TValues extends Record<string, unknown>>(
|
|
|
62
62
|
return issuesToRecord<TValues>(arkIssuesToGeneric(result))
|
|
63
63
|
} catch (err) {
|
|
64
64
|
return {
|
|
65
|
-
|
|
65
|
+
'': err instanceof Error ? err.message : String(err),
|
|
66
66
|
} as Partial<Record<keyof TValues, ValidationError>>
|
|
67
67
|
}
|
|
68
68
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { arktypeField, arktypeSchema } from
|
|
1
|
+
export { arktypeField, arktypeSchema } from './arktype'
|
|
2
2
|
export type {
|
|
3
3
|
FieldAdapter,
|
|
4
4
|
SchemaAdapter,
|
|
@@ -6,7 +6,7 @@ export type {
|
|
|
6
6
|
ValidateFn,
|
|
7
7
|
ValidationError,
|
|
8
8
|
ValidationIssue,
|
|
9
|
-
} from
|
|
10
|
-
export { issuesToRecord } from
|
|
11
|
-
export { valibotField, valibotSchema } from
|
|
12
|
-
export { zodField, zodSchema } from
|
|
9
|
+
} from './types'
|
|
10
|
+
export { issuesToRecord } from './utils'
|
|
11
|
+
export { valibotField, valibotSchema } from './valibot'
|
|
12
|
+
export { zodField, zodSchema } from './zod'
|
package/src/tests/setup.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import '@happy-dom/global-registrator'
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { useForm } from
|
|
2
|
-
import { mount } from
|
|
3
|
-
import { type } from
|
|
4
|
-
import * as v from
|
|
5
|
-
import { z } from
|
|
6
|
-
import { arktypeField, arktypeSchema } from
|
|
7
|
-
import { issuesToRecord } from
|
|
8
|
-
import { valibotField, valibotSchema } from
|
|
9
|
-
import { zodField, zodSchema } from
|
|
1
|
+
import { useForm } from '@pyreon/form'
|
|
2
|
+
import { mount } from '@pyreon/runtime-dom'
|
|
3
|
+
import { type } from 'arktype'
|
|
4
|
+
import * as v from 'valibot'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
import { arktypeField, arktypeSchema } from '../arktype'
|
|
7
|
+
import { issuesToRecord } from '../utils'
|
|
8
|
+
import { valibotField, valibotSchema } from '../valibot'
|
|
9
|
+
import { zodField, zodSchema } from '../zod'
|
|
10
10
|
|
|
11
11
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
12
12
|
|
|
@@ -17,7 +17,7 @@ function Capture<T>({ fn }: { fn: () => T }) {
|
|
|
17
17
|
|
|
18
18
|
function mountWith<T>(fn: () => T): { result: T; unmount: () => void } {
|
|
19
19
|
let result: T | undefined
|
|
20
|
-
const el = document.createElement(
|
|
20
|
+
const el = document.createElement('div')
|
|
21
21
|
document.body.appendChild(el)
|
|
22
22
|
const unmount = mount(
|
|
23
23
|
<Capture
|
|
@@ -38,85 +38,85 @@ function mountWith<T>(fn: () => T): { result: T; unmount: () => void } {
|
|
|
38
38
|
|
|
39
39
|
// ─── issuesToRecord ──────────────────────────────────────────────────────────
|
|
40
40
|
|
|
41
|
-
describe(
|
|
42
|
-
it(
|
|
41
|
+
describe('issuesToRecord', () => {
|
|
42
|
+
it('converts issues to a flat record', () => {
|
|
43
43
|
const result = issuesToRecord([
|
|
44
|
-
{ path:
|
|
45
|
-
{ path:
|
|
44
|
+
{ path: 'email', message: 'Required' },
|
|
45
|
+
{ path: 'password', message: 'Too short' },
|
|
46
46
|
])
|
|
47
|
-
expect(result).toEqual({ email:
|
|
47
|
+
expect(result).toEqual({ email: 'Required', password: 'Too short' })
|
|
48
48
|
})
|
|
49
49
|
|
|
50
|
-
it(
|
|
50
|
+
it('first error per field wins', () => {
|
|
51
51
|
const result = issuesToRecord([
|
|
52
|
-
{ path:
|
|
53
|
-
{ path:
|
|
52
|
+
{ path: 'email', message: 'Required' },
|
|
53
|
+
{ path: 'email', message: 'Invalid format' },
|
|
54
54
|
])
|
|
55
|
-
expect(result).toEqual({ email:
|
|
55
|
+
expect(result).toEqual({ email: 'Required' })
|
|
56
56
|
})
|
|
57
57
|
|
|
58
|
-
it(
|
|
58
|
+
it('returns empty object for no issues', () => {
|
|
59
59
|
expect(issuesToRecord([])).toEqual({})
|
|
60
60
|
})
|
|
61
61
|
})
|
|
62
62
|
|
|
63
63
|
// ─── Zod Adapter ─────────────────────────────────────────────────────────────
|
|
64
64
|
|
|
65
|
-
describe(
|
|
65
|
+
describe('zodSchema', () => {
|
|
66
66
|
const schema = z.object({
|
|
67
|
-
email: z.string().email(
|
|
68
|
-
password: z.string().min(8,
|
|
67
|
+
email: z.string().email('Invalid email'),
|
|
68
|
+
password: z.string().min(8, 'Min 8 chars'),
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
-
it(
|
|
71
|
+
it('returns empty record for valid data', async () => {
|
|
72
72
|
const validate = zodSchema(schema)
|
|
73
|
-
const result = await validate({ email:
|
|
73
|
+
const result = await validate({ email: 'a@b.com', password: '12345678' })
|
|
74
74
|
expect(result).toEqual({})
|
|
75
75
|
})
|
|
76
76
|
|
|
77
|
-
it(
|
|
77
|
+
it('returns field errors for invalid data', async () => {
|
|
78
78
|
const validate = zodSchema(schema)
|
|
79
|
-
const result = await validate({ email:
|
|
80
|
-
expect(result.email).toBe(
|
|
81
|
-
expect(result.password).toBe(
|
|
79
|
+
const result = await validate({ email: 'bad', password: 'short' })
|
|
80
|
+
expect(result.email).toBe('Invalid email')
|
|
81
|
+
expect(result.password).toBe('Min 8 chars')
|
|
82
82
|
})
|
|
83
83
|
|
|
84
|
-
it(
|
|
84
|
+
it('returns error for single invalid field', async () => {
|
|
85
85
|
const validate = zodSchema(schema)
|
|
86
|
-
const result = await validate({ email:
|
|
86
|
+
const result = await validate({ email: 'a@b.com', password: 'short' })
|
|
87
87
|
expect(result.email).toBeUndefined()
|
|
88
|
-
expect(result.password).toBe(
|
|
88
|
+
expect(result.password).toBe('Min 8 chars')
|
|
89
89
|
})
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
-
describe(
|
|
93
|
-
it(
|
|
94
|
-
const validate = zodField(z.string().email(
|
|
95
|
-
expect(await validate(
|
|
92
|
+
describe('zodField', () => {
|
|
93
|
+
it('returns undefined for valid value', async () => {
|
|
94
|
+
const validate = zodField(z.string().email('Invalid email'))
|
|
95
|
+
expect(await validate('a@b.com', {})).toBeUndefined()
|
|
96
96
|
})
|
|
97
97
|
|
|
98
|
-
it(
|
|
99
|
-
const validate = zodField(z.string().email(
|
|
100
|
-
expect(await validate(
|
|
98
|
+
it('returns error message for invalid value', async () => {
|
|
99
|
+
const validate = zodField(z.string().email('Invalid email'))
|
|
100
|
+
expect(await validate('bad', {})).toBe('Invalid email')
|
|
101
101
|
})
|
|
102
102
|
|
|
103
|
-
it(
|
|
104
|
-
const validate = zodField(z.number().min(0,
|
|
105
|
-
expect(await validate(-1, {})).toBe(
|
|
103
|
+
it('works with number schemas', async () => {
|
|
104
|
+
const validate = zodField(z.number().min(0, 'Must be positive'))
|
|
105
|
+
expect(await validate(-1, {})).toBe('Must be positive')
|
|
106
106
|
expect(await validate(5, {})).toBeUndefined()
|
|
107
107
|
})
|
|
108
108
|
})
|
|
109
109
|
|
|
110
|
-
describe(
|
|
111
|
-
it(
|
|
110
|
+
describe('zod + useForm integration', () => {
|
|
111
|
+
it('validates form with zod schema', async () => {
|
|
112
112
|
const schema = z.object({
|
|
113
|
-
email: z.string().email(
|
|
114
|
-
password: z.string().min(8,
|
|
113
|
+
email: z.string().email('Invalid email'),
|
|
114
|
+
password: z.string().min(8, 'Min 8 chars'),
|
|
115
115
|
})
|
|
116
116
|
|
|
117
117
|
const { result: form, unmount } = mountWith(() =>
|
|
118
118
|
useForm({
|
|
119
|
-
initialValues: { email:
|
|
119
|
+
initialValues: { email: '', password: '' },
|
|
120
120
|
schema: zodSchema(schema),
|
|
121
121
|
onSubmit: () => {
|
|
122
122
|
/* noop */
|
|
@@ -126,18 +126,18 @@ describe("zod + useForm integration", () => {
|
|
|
126
126
|
|
|
127
127
|
const valid = await form.validate()
|
|
128
128
|
expect(valid).toBe(false)
|
|
129
|
-
expect(form.fields.email.error()).toBe(
|
|
130
|
-
expect(form.fields.password.error()).toBe(
|
|
129
|
+
expect(form.fields.email.error()).toBe('Invalid email')
|
|
130
|
+
expect(form.fields.password.error()).toBe('Min 8 chars')
|
|
131
131
|
unmount()
|
|
132
132
|
})
|
|
133
133
|
|
|
134
|
-
it(
|
|
134
|
+
it('validates with field-level zod validators', async () => {
|
|
135
135
|
const { result: form, unmount } = mountWith(() =>
|
|
136
136
|
useForm({
|
|
137
|
-
initialValues: { email:
|
|
137
|
+
initialValues: { email: '', age: 0 },
|
|
138
138
|
validators: {
|
|
139
|
-
email: zodField(z.string().email(
|
|
140
|
-
age: zodField(z.number().min(18,
|
|
139
|
+
email: zodField(z.string().email('Invalid')),
|
|
140
|
+
age: zodField(z.number().min(18, 'Must be 18+')),
|
|
141
141
|
},
|
|
142
142
|
onSubmit: () => {
|
|
143
143
|
/* noop */
|
|
@@ -147,52 +147,52 @@ describe("zod + useForm integration", () => {
|
|
|
147
147
|
|
|
148
148
|
const valid = await form.validate()
|
|
149
149
|
expect(valid).toBe(false)
|
|
150
|
-
expect(form.fields.email.error()).toBe(
|
|
151
|
-
expect(form.fields.age.error()).toBe(
|
|
150
|
+
expect(form.fields.email.error()).toBe('Invalid')
|
|
151
|
+
expect(form.fields.age.error()).toBe('Must be 18+')
|
|
152
152
|
unmount()
|
|
153
153
|
})
|
|
154
154
|
})
|
|
155
155
|
|
|
156
156
|
// ─── Valibot Adapter ─────────────────────────────────────────────────────────
|
|
157
157
|
|
|
158
|
-
describe(
|
|
158
|
+
describe('valibotSchema', () => {
|
|
159
159
|
const schema = v.object({
|
|
160
|
-
email: v.pipe(v.string(), v.email(
|
|
161
|
-
password: v.pipe(v.string(), v.minLength(8,
|
|
160
|
+
email: v.pipe(v.string(), v.email('Invalid email')),
|
|
161
|
+
password: v.pipe(v.string(), v.minLength(8, 'Min 8 chars')),
|
|
162
162
|
})
|
|
163
163
|
|
|
164
|
-
it(
|
|
164
|
+
it('returns empty record for valid data', async () => {
|
|
165
165
|
const validate = valibotSchema(schema, v.safeParseAsync)
|
|
166
|
-
const result = await validate({ email:
|
|
166
|
+
const result = await validate({ email: 'a@b.com', password: '12345678' })
|
|
167
167
|
expect(result).toEqual({})
|
|
168
168
|
})
|
|
169
169
|
|
|
170
|
-
it(
|
|
170
|
+
it('returns field errors for invalid data', async () => {
|
|
171
171
|
const validate = valibotSchema(schema, v.safeParseAsync)
|
|
172
|
-
const result = await validate({ email:
|
|
173
|
-
expect(result.email).toBe(
|
|
174
|
-
expect(result.password).toBe(
|
|
172
|
+
const result = await validate({ email: 'bad', password: 'short' })
|
|
173
|
+
expect(result.email).toBe('Invalid email')
|
|
174
|
+
expect(result.password).toBe('Min 8 chars')
|
|
175
175
|
})
|
|
176
176
|
|
|
177
|
-
it(
|
|
177
|
+
it('works with sync safeParse', async () => {
|
|
178
178
|
const validate = valibotSchema(schema, v.safeParse)
|
|
179
|
-
const result = await validate({ email:
|
|
180
|
-
expect(result.email).toBe(
|
|
179
|
+
const result = await validate({ email: 'bad', password: 'short' })
|
|
180
|
+
expect(result.email).toBe('Invalid email')
|
|
181
181
|
})
|
|
182
182
|
|
|
183
|
-
it(
|
|
183
|
+
it('handles issues without path', async () => {
|
|
184
184
|
// Simulate a safeParse function that returns issues without path
|
|
185
185
|
const mockSafeParse = async () => ({
|
|
186
186
|
success: false as const,
|
|
187
|
-
issues: [{ message:
|
|
187
|
+
issues: [{ message: 'Schema-level error' }],
|
|
188
188
|
})
|
|
189
189
|
const validate = valibotSchema({}, mockSafeParse)
|
|
190
190
|
const result = await validate({})
|
|
191
191
|
// Issue without path maps to empty string key
|
|
192
|
-
expect(result[
|
|
192
|
+
expect(result['' as keyof typeof result]).toBe('Schema-level error')
|
|
193
193
|
})
|
|
194
194
|
|
|
195
|
-
it(
|
|
195
|
+
it('handles result with undefined issues array', async () => {
|
|
196
196
|
const mockSafeParse = async () => ({
|
|
197
197
|
success: false as const,
|
|
198
198
|
// issues is undefined
|
|
@@ -203,36 +203,36 @@ describe("valibotSchema", () => {
|
|
|
203
203
|
})
|
|
204
204
|
})
|
|
205
205
|
|
|
206
|
-
describe(
|
|
207
|
-
it(
|
|
208
|
-
const validate = valibotField(v.pipe(v.string(), v.email(
|
|
209
|
-
expect(await validate(
|
|
206
|
+
describe('valibotField', () => {
|
|
207
|
+
it('returns undefined for valid value', async () => {
|
|
208
|
+
const validate = valibotField(v.pipe(v.string(), v.email('Invalid email')), v.safeParseAsync)
|
|
209
|
+
expect(await validate('a@b.com', {})).toBeUndefined()
|
|
210
210
|
})
|
|
211
211
|
|
|
212
|
-
it(
|
|
213
|
-
const validate = valibotField(v.pipe(v.string(), v.email(
|
|
214
|
-
expect(await validate(
|
|
212
|
+
it('returns error message for invalid value', async () => {
|
|
213
|
+
const validate = valibotField(v.pipe(v.string(), v.email('Invalid email')), v.safeParseAsync)
|
|
214
|
+
expect(await validate('bad', {})).toBe('Invalid email')
|
|
215
215
|
})
|
|
216
216
|
|
|
217
|
-
it(
|
|
217
|
+
it('handles result with undefined issues', async () => {
|
|
218
218
|
const mockSafeParse = async () => ({
|
|
219
219
|
success: false as const,
|
|
220
220
|
})
|
|
221
221
|
const validate = valibotField({}, mockSafeParse)
|
|
222
|
-
expect(await validate(
|
|
222
|
+
expect(await validate('x', {})).toBeUndefined()
|
|
223
223
|
})
|
|
224
224
|
})
|
|
225
225
|
|
|
226
|
-
describe(
|
|
227
|
-
it(
|
|
226
|
+
describe('valibot + useForm integration', () => {
|
|
227
|
+
it('validates form with valibot schema', async () => {
|
|
228
228
|
const schema = v.object({
|
|
229
|
-
email: v.pipe(v.string(), v.email(
|
|
230
|
-
password: v.pipe(v.string(), v.minLength(8,
|
|
229
|
+
email: v.pipe(v.string(), v.email('Invalid email')),
|
|
230
|
+
password: v.pipe(v.string(), v.minLength(8, 'Min 8 chars')),
|
|
231
231
|
})
|
|
232
232
|
|
|
233
233
|
const { result: form, unmount } = mountWith(() =>
|
|
234
234
|
useForm({
|
|
235
|
-
initialValues: { email:
|
|
235
|
+
initialValues: { email: '', password: '' },
|
|
236
236
|
schema: valibotSchema(schema, v.safeParseAsync),
|
|
237
237
|
onSubmit: () => {
|
|
238
238
|
/* noop */
|
|
@@ -242,198 +242,198 @@ describe("valibot + useForm integration", () => {
|
|
|
242
242
|
|
|
243
243
|
const valid = await form.validate()
|
|
244
244
|
expect(valid).toBe(false)
|
|
245
|
-
expect(form.fields.email.error()).toBe(
|
|
246
|
-
expect(form.fields.password.error()).toBe(
|
|
245
|
+
expect(form.fields.email.error()).toBe('Invalid email')
|
|
246
|
+
expect(form.fields.password.error()).toBe('Min 8 chars')
|
|
247
247
|
unmount()
|
|
248
248
|
})
|
|
249
249
|
})
|
|
250
250
|
|
|
251
251
|
// ─── ArkType Adapter ─────────────────────────────────────────────────────────
|
|
252
252
|
|
|
253
|
-
describe(
|
|
253
|
+
describe('arktypeSchema', () => {
|
|
254
254
|
const schema = type({
|
|
255
|
-
email:
|
|
256
|
-
password:
|
|
255
|
+
email: 'string.email',
|
|
256
|
+
password: 'string >= 8',
|
|
257
257
|
})
|
|
258
258
|
|
|
259
|
-
it(
|
|
259
|
+
it('returns empty record for valid data', async () => {
|
|
260
260
|
const validate = arktypeSchema(schema)
|
|
261
|
-
const result = await validate({ email:
|
|
261
|
+
const result = await validate({ email: 'a@b.com', password: '12345678' })
|
|
262
262
|
expect(result).toEqual({})
|
|
263
263
|
})
|
|
264
264
|
|
|
265
|
-
it(
|
|
265
|
+
it('returns field errors for invalid data', async () => {
|
|
266
266
|
const validate = arktypeSchema(schema)
|
|
267
|
-
const result = await validate({ email:
|
|
267
|
+
const result = await validate({ email: 'bad', password: 'short' })
|
|
268
268
|
expect(result.email).toBeDefined()
|
|
269
269
|
expect(result.password).toBeDefined()
|
|
270
270
|
})
|
|
271
271
|
})
|
|
272
272
|
|
|
273
|
-
describe(
|
|
274
|
-
it(
|
|
275
|
-
const validate = arktypeField(type(
|
|
276
|
-
expect(await validate(
|
|
273
|
+
describe('arktypeField', () => {
|
|
274
|
+
it('returns undefined for valid value', async () => {
|
|
275
|
+
const validate = arktypeField(type('string.email'))
|
|
276
|
+
expect(await validate('a@b.com', {})).toBeUndefined()
|
|
277
277
|
})
|
|
278
278
|
|
|
279
|
-
it(
|
|
280
|
-
const validate = arktypeField(type(
|
|
281
|
-
const result = await validate(
|
|
279
|
+
it('returns error message for invalid value', async () => {
|
|
280
|
+
const validate = arktypeField(type('string.email'))
|
|
281
|
+
const result = await validate('bad', {})
|
|
282
282
|
expect(result).toBeDefined()
|
|
283
|
-
expect(typeof result).toBe(
|
|
283
|
+
expect(typeof result).toBe('string')
|
|
284
284
|
})
|
|
285
285
|
})
|
|
286
286
|
|
|
287
|
-
describe(
|
|
288
|
-
it(
|
|
287
|
+
describe('zodSchema catch branch', () => {
|
|
288
|
+
it('captures Error when safeParseAsync throws an Error', async () => {
|
|
289
289
|
const throwingSchema = {
|
|
290
290
|
safeParseAsync: () => {
|
|
291
|
-
throw new Error(
|
|
291
|
+
throw new Error('Zod schema exploded')
|
|
292
292
|
},
|
|
293
293
|
safeParse: () => {
|
|
294
|
-
throw new Error(
|
|
294
|
+
throw new Error('Zod schema exploded')
|
|
295
295
|
},
|
|
296
296
|
}
|
|
297
297
|
const validate = zodSchema(throwingSchema as any)
|
|
298
|
-
const result = await validate({ email:
|
|
299
|
-
expect(result[
|
|
298
|
+
const result = await validate({ email: '', password: '' })
|
|
299
|
+
expect(result['' as keyof typeof result]).toBe('Zod schema exploded')
|
|
300
300
|
})
|
|
301
301
|
|
|
302
|
-
it(
|
|
302
|
+
it('captures non-Error when safeParseAsync throws a string', async () => {
|
|
303
303
|
const throwingSchema = {
|
|
304
304
|
safeParseAsync: () => {
|
|
305
|
-
throw
|
|
305
|
+
throw 'raw string error'
|
|
306
306
|
},
|
|
307
307
|
safeParse: () => {
|
|
308
|
-
throw
|
|
308
|
+
throw 'raw string error'
|
|
309
309
|
},
|
|
310
310
|
}
|
|
311
311
|
const validate = zodSchema(throwingSchema as any)
|
|
312
|
-
const result = await validate({ email:
|
|
313
|
-
expect(result[
|
|
312
|
+
const result = await validate({ email: '', password: '' })
|
|
313
|
+
expect(result['' as keyof typeof result]).toBe('raw string error')
|
|
314
314
|
})
|
|
315
315
|
})
|
|
316
316
|
|
|
317
|
-
describe(
|
|
318
|
-
it(
|
|
317
|
+
describe('zodField catch branch', () => {
|
|
318
|
+
it('captures Error when safeParseAsync throws an Error', async () => {
|
|
319
319
|
const throwingSchema = {
|
|
320
320
|
safeParseAsync: () => {
|
|
321
|
-
throw new Error(
|
|
321
|
+
throw new Error('Zod field exploded')
|
|
322
322
|
},
|
|
323
323
|
safeParse: () => {
|
|
324
|
-
throw new Error(
|
|
324
|
+
throw new Error('Zod field exploded')
|
|
325
325
|
},
|
|
326
326
|
}
|
|
327
327
|
const validate = zodField(throwingSchema as any)
|
|
328
|
-
const result = await validate(
|
|
329
|
-
expect(result).toBe(
|
|
328
|
+
const result = await validate('test', {})
|
|
329
|
+
expect(result).toBe('Zod field exploded')
|
|
330
330
|
})
|
|
331
331
|
|
|
332
|
-
it(
|
|
332
|
+
it('captures non-Error when safeParseAsync throws a string', async () => {
|
|
333
333
|
const throwingSchema = {
|
|
334
334
|
safeParseAsync: () => {
|
|
335
|
-
throw
|
|
335
|
+
throw 'raw zod field error'
|
|
336
336
|
},
|
|
337
337
|
safeParse: () => {
|
|
338
|
-
throw
|
|
338
|
+
throw 'raw zod field error'
|
|
339
339
|
},
|
|
340
340
|
}
|
|
341
341
|
const validate = zodField(throwingSchema as any)
|
|
342
|
-
const result = await validate(
|
|
343
|
-
expect(result).toBe(
|
|
342
|
+
const result = await validate('test', {})
|
|
343
|
+
expect(result).toBe('raw zod field error')
|
|
344
344
|
})
|
|
345
345
|
})
|
|
346
346
|
|
|
347
|
-
describe(
|
|
348
|
-
it(
|
|
347
|
+
describe('valibotSchema catch branch', () => {
|
|
348
|
+
it('captures Error when safeParse function throws an Error', async () => {
|
|
349
349
|
const throwingParse = () => {
|
|
350
|
-
throw new Error(
|
|
350
|
+
throw new Error('Valibot schema exploded')
|
|
351
351
|
}
|
|
352
352
|
const validate = valibotSchema({}, throwingParse)
|
|
353
|
-
const result = await validate({ email:
|
|
354
|
-
expect(result[
|
|
353
|
+
const result = await validate({ email: '', password: '' })
|
|
354
|
+
expect(result['' as keyof typeof result]).toBe('Valibot schema exploded')
|
|
355
355
|
})
|
|
356
356
|
|
|
357
|
-
it(
|
|
357
|
+
it('captures non-Error when safeParse function throws a string', async () => {
|
|
358
358
|
const throwingParse = () => {
|
|
359
|
-
throw
|
|
359
|
+
throw 'raw valibot schema error'
|
|
360
360
|
}
|
|
361
361
|
const validate = valibotSchema({}, throwingParse)
|
|
362
|
-
const result = await validate({ email:
|
|
363
|
-
expect(result[
|
|
362
|
+
const result = await validate({ email: '', password: '' })
|
|
363
|
+
expect(result['' as keyof typeof result]).toBe('raw valibot schema error')
|
|
364
364
|
})
|
|
365
365
|
})
|
|
366
366
|
|
|
367
|
-
describe(
|
|
368
|
-
it(
|
|
367
|
+
describe('valibotField catch branch', () => {
|
|
368
|
+
it('captures Error when safeParse function throws an Error', async () => {
|
|
369
369
|
const throwingParse = () => {
|
|
370
|
-
throw new Error(
|
|
370
|
+
throw new Error('Valibot field exploded')
|
|
371
371
|
}
|
|
372
372
|
const validate = valibotField({}, throwingParse)
|
|
373
|
-
const result = await validate(
|
|
374
|
-
expect(result).toBe(
|
|
373
|
+
const result = await validate('test', {})
|
|
374
|
+
expect(result).toBe('Valibot field exploded')
|
|
375
375
|
})
|
|
376
376
|
|
|
377
|
-
it(
|
|
377
|
+
it('captures non-Error when safeParse function throws a string', async () => {
|
|
378
378
|
const throwingParse = () => {
|
|
379
|
-
throw
|
|
379
|
+
throw 'raw valibot field error'
|
|
380
380
|
}
|
|
381
381
|
const validate = valibotField({}, throwingParse)
|
|
382
|
-
const result = await validate(
|
|
383
|
-
expect(result).toBe(
|
|
382
|
+
const result = await validate('test', {})
|
|
383
|
+
expect(result).toBe('raw valibot field error')
|
|
384
384
|
})
|
|
385
385
|
})
|
|
386
386
|
|
|
387
|
-
describe(
|
|
388
|
-
it(
|
|
387
|
+
describe('arktypeSchema catch branch', () => {
|
|
388
|
+
it('captures Error when schema function throws an Error', async () => {
|
|
389
389
|
const throwingSchema = () => {
|
|
390
|
-
throw new Error(
|
|
390
|
+
throw new Error('ArkType schema exploded')
|
|
391
391
|
}
|
|
392
392
|
const validate = arktypeSchema(throwingSchema)
|
|
393
|
-
const result = await validate({ email:
|
|
394
|
-
expect(result[
|
|
393
|
+
const result = await validate({ email: '', password: '' })
|
|
394
|
+
expect(result['' as keyof typeof result]).toBe('ArkType schema exploded')
|
|
395
395
|
})
|
|
396
396
|
|
|
397
|
-
it(
|
|
397
|
+
it('captures non-Error when schema function throws a string', async () => {
|
|
398
398
|
const throwingSchema = () => {
|
|
399
|
-
throw
|
|
399
|
+
throw 'raw arktype schema error'
|
|
400
400
|
}
|
|
401
401
|
const validate = arktypeSchema(throwingSchema)
|
|
402
|
-
const result = await validate({ email:
|
|
403
|
-
expect(result[
|
|
402
|
+
const result = await validate({ email: '', password: '' })
|
|
403
|
+
expect(result['' as keyof typeof result]).toBe('raw arktype schema error')
|
|
404
404
|
})
|
|
405
405
|
})
|
|
406
406
|
|
|
407
|
-
describe(
|
|
408
|
-
it(
|
|
407
|
+
describe('arktypeField catch branch', () => {
|
|
408
|
+
it('captures Error when schema function throws an Error', async () => {
|
|
409
409
|
const throwingSchema = () => {
|
|
410
|
-
throw new Error(
|
|
410
|
+
throw new Error('ArkType field exploded')
|
|
411
411
|
}
|
|
412
412
|
const validate = arktypeField(throwingSchema)
|
|
413
|
-
const result = await validate(
|
|
414
|
-
expect(result).toBe(
|
|
413
|
+
const result = await validate('test', {})
|
|
414
|
+
expect(result).toBe('ArkType field exploded')
|
|
415
415
|
})
|
|
416
416
|
|
|
417
|
-
it(
|
|
417
|
+
it('captures non-Error when schema function throws a string', async () => {
|
|
418
418
|
const throwingSchema = () => {
|
|
419
|
-
throw
|
|
419
|
+
throw 'raw arktype field error'
|
|
420
420
|
}
|
|
421
421
|
const validate = arktypeField(throwingSchema)
|
|
422
|
-
const result = await validate(
|
|
423
|
-
expect(result).toBe(
|
|
422
|
+
const result = await validate('test', {})
|
|
423
|
+
expect(result).toBe('raw arktype field error')
|
|
424
424
|
})
|
|
425
425
|
})
|
|
426
426
|
|
|
427
|
-
describe(
|
|
428
|
-
it(
|
|
427
|
+
describe('arktype + useForm integration', () => {
|
|
428
|
+
it('validates form with arktype schema', async () => {
|
|
429
429
|
const schema = type({
|
|
430
|
-
email:
|
|
431
|
-
password:
|
|
430
|
+
email: 'string.email',
|
|
431
|
+
password: 'string >= 8',
|
|
432
432
|
})
|
|
433
433
|
|
|
434
434
|
const { result: form, unmount } = mountWith(() =>
|
|
435
435
|
useForm({
|
|
436
|
-
initialValues: { email:
|
|
436
|
+
initialValues: { email: '', password: '' },
|
|
437
437
|
schema: arktypeSchema(schema),
|
|
438
438
|
onSubmit: () => {
|
|
439
439
|
/* noop */
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { SchemaValidateFn, ValidateFn, ValidationError } from
|
|
1
|
+
import type { SchemaValidateFn, ValidateFn, ValidationError } from '@pyreon/form'
|
|
2
2
|
|
|
3
3
|
/** Re-export form types for convenience. */
|
|
4
4
|
export type { SchemaValidateFn, ValidateFn, ValidationError }
|
package/src/utils.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ValidationError } from
|
|
2
|
-
import type { ValidationIssue } from
|
|
1
|
+
import type { ValidationError } from '@pyreon/form'
|
|
2
|
+
import type { ValidationIssue } from './types'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Convert an array of validation issues into a flat field → error record.
|
package/src/valibot.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { SchemaValidateFn, ValidateFn, ValidationError } from
|
|
2
|
-
import type { ValidationIssue } from
|
|
3
|
-
import { issuesToRecord } from
|
|
1
|
+
import type { SchemaValidateFn, ValidateFn, ValidationError } from '@pyreon/form'
|
|
2
|
+
import type { ValidationIssue } from './types'
|
|
3
|
+
import { issuesToRecord } from './utils'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Minimal Valibot-compatible interfaces so we don't require valibot as a hard dep.
|
|
@@ -31,7 +31,7 @@ type GenericSafeParseFn = Function
|
|
|
31
31
|
|
|
32
32
|
function valibotIssuesToGeneric(issues: ValibotIssue[]): ValidationIssue[] {
|
|
33
33
|
return issues.map((issue) => ({
|
|
34
|
-
path: issue.path?.map((p) => String(p.key)).join(
|
|
34
|
+
path: issue.path?.map((p) => String(p.key)).join('.') ?? '',
|
|
35
35
|
message: issue.message,
|
|
36
36
|
}))
|
|
37
37
|
}
|
|
@@ -74,7 +74,7 @@ export function valibotSchema<TValues extends Record<string, unknown>>(
|
|
|
74
74
|
return issuesToRecord<TValues>(valibotIssuesToGeneric(result.issues ?? []))
|
|
75
75
|
} catch (err) {
|
|
76
76
|
return {
|
|
77
|
-
|
|
77
|
+
'': err instanceof Error ? err.message : String(err),
|
|
78
78
|
} as Partial<Record<keyof TValues, ValidationError>>
|
|
79
79
|
}
|
|
80
80
|
}
|
package/src/zod.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { SchemaValidateFn, ValidateFn, ValidationError } from
|
|
2
|
-
import type { ValidationIssue } from
|
|
3
|
-
import { issuesToRecord } from
|
|
1
|
+
import type { SchemaValidateFn, ValidateFn, ValidationError } from '@pyreon/form'
|
|
2
|
+
import type { ValidationIssue } from './types'
|
|
3
|
+
import { issuesToRecord } from './utils'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Minimal Zod-compatible interfaces so we don't require zod as a hard dep.
|
|
@@ -28,7 +28,7 @@ interface ZodSchema<T = unknown> {
|
|
|
28
28
|
|
|
29
29
|
function zodIssuesToGeneric(issues: ZodIssue[]): ValidationIssue[] {
|
|
30
30
|
return issues.map((issue) => ({
|
|
31
|
-
path: issue.path.map(String).join(
|
|
31
|
+
path: issue.path.map(String).join('.'),
|
|
32
32
|
message: issue.message,
|
|
33
33
|
}))
|
|
34
34
|
}
|
|
@@ -62,7 +62,7 @@ export function zodSchema<TValues extends Record<string, unknown>>(
|
|
|
62
62
|
return issuesToRecord<TValues>(zodIssuesToGeneric(result.error!.issues))
|
|
63
63
|
} catch (err) {
|
|
64
64
|
return {
|
|
65
|
-
|
|
65
|
+
'': err instanceof Error ? err.message : String(err),
|
|
66
66
|
} as Partial<Record<keyof TValues, ValidationError>>
|
|
67
67
|
}
|
|
68
68
|
}
|