@pyreon/validation 0.10.0 → 0.11.1
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/lib/arktype.js.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/types/arktype.d.ts.map +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/valibot.d.ts.map +1 -1
- package/lib/types/zod.d.ts.map +1 -1
- package/lib/valibot.js.map +1 -1
- package/lib/zod.js.map +1 -1
- package/package.json +17 -6
- package/src/arktype.ts +7 -12
- package/src/index.ts +5 -5
- package/src/tests/setup.ts +1 -1
- package/src/tests/validation.test.tsx +165 -171
- package/src/types.ts +1 -5
- package/src/utils.ts +2 -2
- package/src/valibot.ts +8 -18
- package/src/zod.ts +6 -11
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 {\n SchemaValidateFn,\n ValidateFn,\n ValidationError,\n} 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))\n 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 {\n SchemaValidateFn,\n ValidateFn,\n ValidationError,\n} 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)\n return {} as Partial<Record<keyof TValues, ValidationError>>\n return issuesToRecord<TValues>(\n valibotIssuesToGeneric(result.issues ?? []),\n )\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>(\n schema: unknown,\n safeParseFn: GenericSafeParseFn,\n): 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 {\n SchemaValidateFn,\n ValidateFn,\n ValidationError,\n} 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)\n 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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"arktype2.d.ts","names":[],"sources":["../../../src/arktype.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"arktype2.d.ts","names":[],"sources":["../../../src/arktype.ts"],"mappings":";;;;;AAAiF;;KAoB5E,eAAA,IAAmB,IAAA;;;AAkCxB;;;;;;;;;;;;;;;;;AA+BA;;iBA/BgB,aAAA,iBAA8B,MAAA,kBAAA,CAC5C,MAAA,EAAQ,eAAA,GACP,gBAAA,CAAiB,OAAA;;;;;;;;;;;;;;;;iBA6BJ,YAAA,GAAA,CAAgB,MAAA,EAAQ,eAAA,GAAkB,UAAA,CAAW,CAAA"}
|
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/arktype.ts","../../../src/types.ts","../../../src/utils.ts","../../../src/valibot.ts","../../../src/zod.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"index2.d.ts","names":[],"sources":["../../../src/arktype.ts","../../../src/types.ts","../../../src/utils.ts","../../../src/valibot.ts","../../../src/zod.ts"],"mappings":";;;;;AAAiF;;KAoB5E,eAAA,IAAmB,IAAA;;;AAkCxB;;;;;;;;;;;;;;;;;AA+BA;;iBA/BgB,aAAA,iBAA8B,MAAA,kBAAA,CAC5C,MAAA,EAAQ,eAAA,GACP,kBAAA,CAAiB,OAAA;;;;;;;;;;;;;;;;iBA6BJ,YAAA,GAAA,CAAgB,MAAA,EAAQ,eAAA,GAAkB,YAAA,CAAW,CAAA;;;AArFY;;;;AAAA,UCShE,eAAA;ED6CD;EC3Cd,IAAA;ED2C2B;ECzC3B,OAAA;AAAA;;;;;KAOU,aAAA,6BAA0C,MAAA,mBACpD,MAAA,EAAQ,OAAA,KACL,gBAAA,CAAiB,OAAA;;;;;KAMV,YAAA,gBAA4B,MAAA,EAAQ,OAAA,KAAY,UAAA,CAAW,CAAA;;;;AD5BU;;;;iBEQjE,cAAA,iBAA+B,MAAA,kBAAA,CAC7C,MAAA,EAAQ,eAAA,KACP,OAAA,CAAQ,MAAA,OAAa,OAAA,EAAS,iBAAA;;;;;AFVgD;;;;KG6B5E,kBAAA,GAAqB,QAAA;AHyB1B;;;;;;;;;;;;;;;;;AA+BA;;;;AA/BA,iBGUgB,aAAA,iBAA8B,MAAA,kBAAA,CAC5C,MAAA,WACA,WAAA,EAAa,kBAAA,GACZ,kBAAA,CAAiB,OAAA;;;;;;;;;;;;;;AF1DpB;;iBEwFgB,YAAA,GAAA,CAAgB,MAAA,WAAiB,WAAA,EAAa,kBAAA,GAAqB,YAAA,CAAW,CAAA;;;;;AHjGb;;UIQvE,QAAA;EACR,IAAA,EAAM,WAAA;EACN,OAAA;AAAA;;;;;UAOQ,SAAA;EACR,SAAA,CAAU,IAAA;IACR,OAAA;IACA,IAAA,GAAO,CAAA;IACP,KAAA;MAAU,MAAA,EAAQ,QAAA;IAAA;EAAA;EAEpB,cAAA,CACE,IAAA,YACC,OAAA;IAAU,OAAA;IAAkB,IAAA,GAAO,CAAA;IAAG,KAAA;MAAU,MAAA,EAAQ,QAAA;IAAA;EAAA;AAAA;;;;;;;;;;;;;;;AHhB7D;;;;;iBG6CgB,SAAA,iBAA0B,MAAA,kBAAA,CACxC,MAAA,EAAQ,SAAA,CAAU,OAAA,IACjB,kBAAA,CAAiB,OAAA;;;;;;;;;;;;;;;;;iBA8BJ,QAAA,GAAA,CAAY,MAAA,EAAQ,SAAA,CAAU,CAAA,IAAK,YAAA,CAAW,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"valibot2.d.ts","names":[],"sources":["../../../src/valibot.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"valibot2.d.ts","names":[],"sources":["../../../src/valibot.ts"],"mappings":";;;;;AAAiF;;;;KA6B5E,kBAAA,GAAqB,QAAA;AAmC1B;;;;;;;;;;;;;;;;;;AAiCA;;;AAjCA,iBAAgB,aAAA,iBAA8B,MAAA,kBAAA,CAC5C,MAAA,WACA,WAAA,EAAa,kBAAA,GACZ,gBAAA,CAAiB,OAAA;;;;;;;;;;;;;;;;iBA8BJ,YAAA,GAAA,CAAgB,MAAA,WAAiB,WAAA,EAAa,kBAAA,GAAqB,UAAA,CAAW,CAAA"}
|
package/lib/types/zod.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zod2.d.ts","names":[],"sources":["../../../src/zod.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"zod2.d.ts","names":[],"sources":["../../../src/zod.ts"],"mappings":";;;;;AAAiF;;UAQvE,QAAA;EACR,IAAA,EAAM,WAAA;EACN,OAAA;AAAA;;;;AAAO;UAOC,SAAA;EACR,SAAA,CAAU,IAAA;IACR,OAAA;IACA,IAAA,GAAO,CAAA;IACP,KAAA;MAAU,MAAA,EAAQ,QAAA;IAAA;EAAA;EAEpB,cAAA,CACE,IAAA,YACC,OAAA;IAAU,OAAA;IAAkB,IAAA,GAAO,CAAA;IAAG,KAAA;MAAU,MAAA,EAAQ,QAAA;IAAA;EAAA;AAAA;;;;;;;;;;;;;;AA6B7D;;;;;;iBAAgB,SAAA,iBAA0B,MAAA,kBAAA,CACxC,MAAA,EAAQ,SAAA,CAAU,OAAA,IACjB,gBAAA,CAAiB,OAAA;;;;;;;;;;;;;AA8BpB;;;;iBAAgB,QAAA,GAAA,CAAY,MAAA,EAAQ,SAAA,CAAU,CAAA,IAAK,UAAA,CAAW,CAAA"}
|
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,16 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/validation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Schema validation adapters for Pyreon forms (Zod, Valibot, ArkType)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/pyreon/
|
|
9
|
-
"directory": "packages/validation"
|
|
8
|
+
"url": "https://github.com/pyreon/pyreon.git",
|
|
9
|
+
"directory": "packages/fundamentals/validation"
|
|
10
10
|
},
|
|
11
11
|
"homepage": "https://github.com/pyreon/fundamentals/tree/main/packages/validation#readme",
|
|
12
12
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/pyreon/
|
|
13
|
+
"url": "https://github.com/pyreon/pyreon/issues"
|
|
14
14
|
},
|
|
15
15
|
"publishConfig": {
|
|
16
16
|
"access": "public"
|
|
@@ -52,10 +52,11 @@
|
|
|
52
52
|
"build": "vl_rolldown_build",
|
|
53
53
|
"dev": "vl_rolldown_build-watch",
|
|
54
54
|
"test": "vitest run",
|
|
55
|
-
"typecheck": "tsc --noEmit"
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"lint": "biome check ."
|
|
56
57
|
},
|
|
57
58
|
"peerDependencies": {
|
|
58
|
-
"@pyreon/form": "^0.
|
|
59
|
+
"@pyreon/form": "^0.11.1"
|
|
59
60
|
},
|
|
60
61
|
"peerDependenciesMeta": {
|
|
61
62
|
"zod": {
|
|
@@ -67,5 +68,15 @@
|
|
|
67
68
|
"arktype": {
|
|
68
69
|
"optional": true
|
|
69
70
|
}
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@happy-dom/global-registrator": "^20.8.3",
|
|
74
|
+
"@pyreon/core": "^0.11.1",
|
|
75
|
+
"@pyreon/form": "^0.11.1",
|
|
76
|
+
"@pyreon/reactivity": "^0.11.1",
|
|
77
|
+
"@pyreon/runtime-dom": "^0.11.1",
|
|
78
|
+
"zod": "^4.3.6",
|
|
79
|
+
"valibot": "^1.2.0",
|
|
80
|
+
"arktype": "^2.2.0"
|
|
70
81
|
}
|
|
71
82
|
}
|
package/src/arktype.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
ValidationError,
|
|
5
|
-
} from '@pyreon/form'
|
|
6
|
-
import type { ValidationIssue } from './types'
|
|
7
|
-
import { issuesToRecord } from './utils'
|
|
1
|
+
import type { SchemaValidateFn, ValidateFn, ValidationError } from "@pyreon/form"
|
|
2
|
+
import type { ValidationIssue } from "./types"
|
|
3
|
+
import { issuesToRecord } from "./utils"
|
|
8
4
|
|
|
9
5
|
/**
|
|
10
6
|
* Minimal ArkType-compatible interfaces so we don't require arktype as a hard dep.
|
|
@@ -25,12 +21,12 @@ interface ArkErrors extends Array<ArkError> {
|
|
|
25
21
|
type ArkTypeCallable = (data: unknown) => unknown
|
|
26
22
|
|
|
27
23
|
function isArkErrors(result: unknown): result is ArkErrors {
|
|
28
|
-
return Array.isArray(result) &&
|
|
24
|
+
return Array.isArray(result) && "summary" in (result as object)
|
|
29
25
|
}
|
|
30
26
|
|
|
31
27
|
function arkIssuesToGeneric(errors: ArkErrors): ValidationIssue[] {
|
|
32
28
|
return errors.map((err) => ({
|
|
33
|
-
path: err.path.map(String).join(
|
|
29
|
+
path: err.path.map(String).join("."),
|
|
34
30
|
message: err.message,
|
|
35
31
|
}))
|
|
36
32
|
}
|
|
@@ -62,12 +58,11 @@ export function arktypeSchema<TValues extends Record<string, unknown>>(
|
|
|
62
58
|
return (values: TValues) => {
|
|
63
59
|
try {
|
|
64
60
|
const result = schema(values)
|
|
65
|
-
if (!isArkErrors(result))
|
|
66
|
-
return {} as Partial<Record<keyof TValues, ValidationError>>
|
|
61
|
+
if (!isArkErrors(result)) return {} as Partial<Record<keyof TValues, ValidationError>>
|
|
67
62
|
return issuesToRecord<TValues>(arkIssuesToGeneric(result))
|
|
68
63
|
} catch (err) {
|
|
69
64
|
return {
|
|
70
|
-
|
|
65
|
+
"": err instanceof Error ? err.message : String(err),
|
|
71
66
|
} as Partial<Record<keyof TValues, ValidationError>>
|
|
72
67
|
}
|
|
73
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,42 +203,36 @@ describe('valibotSchema', () => {
|
|
|
203
203
|
})
|
|
204
204
|
})
|
|
205
205
|
|
|
206
|
-
describe(
|
|
207
|
-
it(
|
|
208
|
-
const validate = valibotField(
|
|
209
|
-
|
|
210
|
-
v.safeParseAsync,
|
|
211
|
-
)
|
|
212
|
-
expect(await validate('a@b.com', {})).toBeUndefined()
|
|
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()
|
|
213
210
|
})
|
|
214
211
|
|
|
215
|
-
it(
|
|
216
|
-
const validate = valibotField(
|
|
217
|
-
|
|
218
|
-
v.safeParseAsync,
|
|
219
|
-
)
|
|
220
|
-
expect(await validate('bad', {})).toBe('Invalid email')
|
|
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")
|
|
221
215
|
})
|
|
222
216
|
|
|
223
|
-
it(
|
|
217
|
+
it("handles result with undefined issues", async () => {
|
|
224
218
|
const mockSafeParse = async () => ({
|
|
225
219
|
success: false as const,
|
|
226
220
|
})
|
|
227
221
|
const validate = valibotField({}, mockSafeParse)
|
|
228
|
-
expect(await validate(
|
|
222
|
+
expect(await validate("x", {})).toBeUndefined()
|
|
229
223
|
})
|
|
230
224
|
})
|
|
231
225
|
|
|
232
|
-
describe(
|
|
233
|
-
it(
|
|
226
|
+
describe("valibot + useForm integration", () => {
|
|
227
|
+
it("validates form with valibot schema", async () => {
|
|
234
228
|
const schema = v.object({
|
|
235
|
-
email: v.pipe(v.string(), v.email(
|
|
236
|
-
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")),
|
|
237
231
|
})
|
|
238
232
|
|
|
239
233
|
const { result: form, unmount } = mountWith(() =>
|
|
240
234
|
useForm({
|
|
241
|
-
initialValues: { email:
|
|
235
|
+
initialValues: { email: "", password: "" },
|
|
242
236
|
schema: valibotSchema(schema, v.safeParseAsync),
|
|
243
237
|
onSubmit: () => {
|
|
244
238
|
/* noop */
|
|
@@ -248,198 +242,198 @@ describe('valibot + useForm integration', () => {
|
|
|
248
242
|
|
|
249
243
|
const valid = await form.validate()
|
|
250
244
|
expect(valid).toBe(false)
|
|
251
|
-
expect(form.fields.email.error()).toBe(
|
|
252
|
-
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")
|
|
253
247
|
unmount()
|
|
254
248
|
})
|
|
255
249
|
})
|
|
256
250
|
|
|
257
251
|
// ─── ArkType Adapter ─────────────────────────────────────────────────────────
|
|
258
252
|
|
|
259
|
-
describe(
|
|
253
|
+
describe("arktypeSchema", () => {
|
|
260
254
|
const schema = type({
|
|
261
|
-
email:
|
|
262
|
-
password:
|
|
255
|
+
email: "string.email",
|
|
256
|
+
password: "string >= 8",
|
|
263
257
|
})
|
|
264
258
|
|
|
265
|
-
it(
|
|
259
|
+
it("returns empty record for valid data", async () => {
|
|
266
260
|
const validate = arktypeSchema(schema)
|
|
267
|
-
const result = await validate({ email:
|
|
261
|
+
const result = await validate({ email: "a@b.com", password: "12345678" })
|
|
268
262
|
expect(result).toEqual({})
|
|
269
263
|
})
|
|
270
264
|
|
|
271
|
-
it(
|
|
265
|
+
it("returns field errors for invalid data", async () => {
|
|
272
266
|
const validate = arktypeSchema(schema)
|
|
273
|
-
const result = await validate({ email:
|
|
267
|
+
const result = await validate({ email: "bad", password: "short" })
|
|
274
268
|
expect(result.email).toBeDefined()
|
|
275
269
|
expect(result.password).toBeDefined()
|
|
276
270
|
})
|
|
277
271
|
})
|
|
278
272
|
|
|
279
|
-
describe(
|
|
280
|
-
it(
|
|
281
|
-
const validate = arktypeField(type(
|
|
282
|
-
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()
|
|
283
277
|
})
|
|
284
278
|
|
|
285
|
-
it(
|
|
286
|
-
const validate = arktypeField(type(
|
|
287
|
-
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", {})
|
|
288
282
|
expect(result).toBeDefined()
|
|
289
|
-
expect(typeof result).toBe(
|
|
283
|
+
expect(typeof result).toBe("string")
|
|
290
284
|
})
|
|
291
285
|
})
|
|
292
286
|
|
|
293
|
-
describe(
|
|
294
|
-
it(
|
|
287
|
+
describe("zodSchema catch branch", () => {
|
|
288
|
+
it("captures Error when safeParseAsync throws an Error", async () => {
|
|
295
289
|
const throwingSchema = {
|
|
296
290
|
safeParseAsync: () => {
|
|
297
|
-
throw new Error(
|
|
291
|
+
throw new Error("Zod schema exploded")
|
|
298
292
|
},
|
|
299
293
|
safeParse: () => {
|
|
300
|
-
throw new Error(
|
|
294
|
+
throw new Error("Zod schema exploded")
|
|
301
295
|
},
|
|
302
296
|
}
|
|
303
297
|
const validate = zodSchema(throwingSchema as any)
|
|
304
|
-
const result = await validate({ email:
|
|
305
|
-
expect(result[
|
|
298
|
+
const result = await validate({ email: "", password: "" })
|
|
299
|
+
expect(result["" as keyof typeof result]).toBe("Zod schema exploded")
|
|
306
300
|
})
|
|
307
301
|
|
|
308
|
-
it(
|
|
302
|
+
it("captures non-Error when safeParseAsync throws a string", async () => {
|
|
309
303
|
const throwingSchema = {
|
|
310
304
|
safeParseAsync: () => {
|
|
311
|
-
throw
|
|
305
|
+
throw "raw string error"
|
|
312
306
|
},
|
|
313
307
|
safeParse: () => {
|
|
314
|
-
throw
|
|
308
|
+
throw "raw string error"
|
|
315
309
|
},
|
|
316
310
|
}
|
|
317
311
|
const validate = zodSchema(throwingSchema as any)
|
|
318
|
-
const result = await validate({ email:
|
|
319
|
-
expect(result[
|
|
312
|
+
const result = await validate({ email: "", password: "" })
|
|
313
|
+
expect(result["" as keyof typeof result]).toBe("raw string error")
|
|
320
314
|
})
|
|
321
315
|
})
|
|
322
316
|
|
|
323
|
-
describe(
|
|
324
|
-
it(
|
|
317
|
+
describe("zodField catch branch", () => {
|
|
318
|
+
it("captures Error when safeParseAsync throws an Error", async () => {
|
|
325
319
|
const throwingSchema = {
|
|
326
320
|
safeParseAsync: () => {
|
|
327
|
-
throw new Error(
|
|
321
|
+
throw new Error("Zod field exploded")
|
|
328
322
|
},
|
|
329
323
|
safeParse: () => {
|
|
330
|
-
throw new Error(
|
|
324
|
+
throw new Error("Zod field exploded")
|
|
331
325
|
},
|
|
332
326
|
}
|
|
333
327
|
const validate = zodField(throwingSchema as any)
|
|
334
|
-
const result = await validate(
|
|
335
|
-
expect(result).toBe(
|
|
328
|
+
const result = await validate("test", {})
|
|
329
|
+
expect(result).toBe("Zod field exploded")
|
|
336
330
|
})
|
|
337
331
|
|
|
338
|
-
it(
|
|
332
|
+
it("captures non-Error when safeParseAsync throws a string", async () => {
|
|
339
333
|
const throwingSchema = {
|
|
340
334
|
safeParseAsync: () => {
|
|
341
|
-
throw
|
|
335
|
+
throw "raw zod field error"
|
|
342
336
|
},
|
|
343
337
|
safeParse: () => {
|
|
344
|
-
throw
|
|
338
|
+
throw "raw zod field error"
|
|
345
339
|
},
|
|
346
340
|
}
|
|
347
341
|
const validate = zodField(throwingSchema as any)
|
|
348
|
-
const result = await validate(
|
|
349
|
-
expect(result).toBe(
|
|
342
|
+
const result = await validate("test", {})
|
|
343
|
+
expect(result).toBe("raw zod field error")
|
|
350
344
|
})
|
|
351
345
|
})
|
|
352
346
|
|
|
353
|
-
describe(
|
|
354
|
-
it(
|
|
347
|
+
describe("valibotSchema catch branch", () => {
|
|
348
|
+
it("captures Error when safeParse function throws an Error", async () => {
|
|
355
349
|
const throwingParse = () => {
|
|
356
|
-
throw new Error(
|
|
350
|
+
throw new Error("Valibot schema exploded")
|
|
357
351
|
}
|
|
358
352
|
const validate = valibotSchema({}, throwingParse)
|
|
359
|
-
const result = await validate({ email:
|
|
360
|
-
expect(result[
|
|
353
|
+
const result = await validate({ email: "", password: "" })
|
|
354
|
+
expect(result["" as keyof typeof result]).toBe("Valibot schema exploded")
|
|
361
355
|
})
|
|
362
356
|
|
|
363
|
-
it(
|
|
357
|
+
it("captures non-Error when safeParse function throws a string", async () => {
|
|
364
358
|
const throwingParse = () => {
|
|
365
|
-
throw
|
|
359
|
+
throw "raw valibot schema error"
|
|
366
360
|
}
|
|
367
361
|
const validate = valibotSchema({}, throwingParse)
|
|
368
|
-
const result = await validate({ email:
|
|
369
|
-
expect(result[
|
|
362
|
+
const result = await validate({ email: "", password: "" })
|
|
363
|
+
expect(result["" as keyof typeof result]).toBe("raw valibot schema error")
|
|
370
364
|
})
|
|
371
365
|
})
|
|
372
366
|
|
|
373
|
-
describe(
|
|
374
|
-
it(
|
|
367
|
+
describe("valibotField catch branch", () => {
|
|
368
|
+
it("captures Error when safeParse function throws an Error", async () => {
|
|
375
369
|
const throwingParse = () => {
|
|
376
|
-
throw new Error(
|
|
370
|
+
throw new Error("Valibot field exploded")
|
|
377
371
|
}
|
|
378
372
|
const validate = valibotField({}, throwingParse)
|
|
379
|
-
const result = await validate(
|
|
380
|
-
expect(result).toBe(
|
|
373
|
+
const result = await validate("test", {})
|
|
374
|
+
expect(result).toBe("Valibot field exploded")
|
|
381
375
|
})
|
|
382
376
|
|
|
383
|
-
it(
|
|
377
|
+
it("captures non-Error when safeParse function throws a string", async () => {
|
|
384
378
|
const throwingParse = () => {
|
|
385
|
-
throw
|
|
379
|
+
throw "raw valibot field error"
|
|
386
380
|
}
|
|
387
381
|
const validate = valibotField({}, throwingParse)
|
|
388
|
-
const result = await validate(
|
|
389
|
-
expect(result).toBe(
|
|
382
|
+
const result = await validate("test", {})
|
|
383
|
+
expect(result).toBe("raw valibot field error")
|
|
390
384
|
})
|
|
391
385
|
})
|
|
392
386
|
|
|
393
|
-
describe(
|
|
394
|
-
it(
|
|
387
|
+
describe("arktypeSchema catch branch", () => {
|
|
388
|
+
it("captures Error when schema function throws an Error", async () => {
|
|
395
389
|
const throwingSchema = () => {
|
|
396
|
-
throw new Error(
|
|
390
|
+
throw new Error("ArkType schema exploded")
|
|
397
391
|
}
|
|
398
392
|
const validate = arktypeSchema(throwingSchema)
|
|
399
|
-
const result = await validate({ email:
|
|
400
|
-
expect(result[
|
|
393
|
+
const result = await validate({ email: "", password: "" })
|
|
394
|
+
expect(result["" as keyof typeof result]).toBe("ArkType schema exploded")
|
|
401
395
|
})
|
|
402
396
|
|
|
403
|
-
it(
|
|
397
|
+
it("captures non-Error when schema function throws a string", async () => {
|
|
404
398
|
const throwingSchema = () => {
|
|
405
|
-
throw
|
|
399
|
+
throw "raw arktype schema error"
|
|
406
400
|
}
|
|
407
401
|
const validate = arktypeSchema(throwingSchema)
|
|
408
|
-
const result = await validate({ email:
|
|
409
|
-
expect(result[
|
|
402
|
+
const result = await validate({ email: "", password: "" })
|
|
403
|
+
expect(result["" as keyof typeof result]).toBe("raw arktype schema error")
|
|
410
404
|
})
|
|
411
405
|
})
|
|
412
406
|
|
|
413
|
-
describe(
|
|
414
|
-
it(
|
|
407
|
+
describe("arktypeField catch branch", () => {
|
|
408
|
+
it("captures Error when schema function throws an Error", async () => {
|
|
415
409
|
const throwingSchema = () => {
|
|
416
|
-
throw new Error(
|
|
410
|
+
throw new Error("ArkType field exploded")
|
|
417
411
|
}
|
|
418
412
|
const validate = arktypeField(throwingSchema)
|
|
419
|
-
const result = await validate(
|
|
420
|
-
expect(result).toBe(
|
|
413
|
+
const result = await validate("test", {})
|
|
414
|
+
expect(result).toBe("ArkType field exploded")
|
|
421
415
|
})
|
|
422
416
|
|
|
423
|
-
it(
|
|
417
|
+
it("captures non-Error when schema function throws a string", async () => {
|
|
424
418
|
const throwingSchema = () => {
|
|
425
|
-
throw
|
|
419
|
+
throw "raw arktype field error"
|
|
426
420
|
}
|
|
427
421
|
const validate = arktypeField(throwingSchema)
|
|
428
|
-
const result = await validate(
|
|
429
|
-
expect(result).toBe(
|
|
422
|
+
const result = await validate("test", {})
|
|
423
|
+
expect(result).toBe("raw arktype field error")
|
|
430
424
|
})
|
|
431
425
|
})
|
|
432
426
|
|
|
433
|
-
describe(
|
|
434
|
-
it(
|
|
427
|
+
describe("arktype + useForm integration", () => {
|
|
428
|
+
it("validates form with arktype schema", async () => {
|
|
435
429
|
const schema = type({
|
|
436
|
-
email:
|
|
437
|
-
password:
|
|
430
|
+
email: "string.email",
|
|
431
|
+
password: "string >= 8",
|
|
438
432
|
})
|
|
439
433
|
|
|
440
434
|
const { result: form, unmount } = mountWith(() =>
|
|
441
435
|
useForm({
|
|
442
|
-
initialValues: { email:
|
|
436
|
+
initialValues: { email: "", password: "" },
|
|
443
437
|
schema: arktypeSchema(schema),
|
|
444
438
|
onSubmit: () => {
|
|
445
439
|
/* noop */
|
package/src/types.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
SchemaValidateFn,
|
|
3
|
-
ValidateFn,
|
|
4
|
-
ValidationError,
|
|
5
|
-
} from '@pyreon/form'
|
|
1
|
+
import type { SchemaValidateFn, ValidateFn, ValidationError } from "@pyreon/form"
|
|
6
2
|
|
|
7
3
|
/** Re-export form types for convenience. */
|
|
8
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,10 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
ValidationError,
|
|
5
|
-
} from '@pyreon/form'
|
|
6
|
-
import type { ValidationIssue } from './types'
|
|
7
|
-
import { issuesToRecord } from './utils'
|
|
1
|
+
import type { SchemaValidateFn, ValidateFn, ValidationError } from "@pyreon/form"
|
|
2
|
+
import type { ValidationIssue } from "./types"
|
|
3
|
+
import { issuesToRecord } from "./utils"
|
|
8
4
|
|
|
9
5
|
/**
|
|
10
6
|
* Minimal Valibot-compatible interfaces so we don't require valibot as a hard dep.
|
|
@@ -35,7 +31,7 @@ type GenericSafeParseFn = Function
|
|
|
35
31
|
|
|
36
32
|
function valibotIssuesToGeneric(issues: ValibotIssue[]): ValidationIssue[] {
|
|
37
33
|
return issues.map((issue) => ({
|
|
38
|
-
path: issue.path?.map((p) => String(p.key)).join(
|
|
34
|
+
path: issue.path?.map((p) => String(p.key)).join(".") ?? "",
|
|
39
35
|
message: issue.message,
|
|
40
36
|
}))
|
|
41
37
|
}
|
|
@@ -74,14 +70,11 @@ export function valibotSchema<TValues extends Record<string, unknown>>(
|
|
|
74
70
|
return async (values: TValues) => {
|
|
75
71
|
try {
|
|
76
72
|
const result = await parse(schema, values)
|
|
77
|
-
if (result.success)
|
|
78
|
-
|
|
79
|
-
return issuesToRecord<TValues>(
|
|
80
|
-
valibotIssuesToGeneric(result.issues ?? []),
|
|
81
|
-
)
|
|
73
|
+
if (result.success) return {} as Partial<Record<keyof TValues, ValidationError>>
|
|
74
|
+
return issuesToRecord<TValues>(valibotIssuesToGeneric(result.issues ?? []))
|
|
82
75
|
} catch (err) {
|
|
83
76
|
return {
|
|
84
|
-
|
|
77
|
+
"": err instanceof Error ? err.message : String(err),
|
|
85
78
|
} as Partial<Record<keyof TValues, ValidationError>>
|
|
86
79
|
}
|
|
87
80
|
}
|
|
@@ -102,10 +95,7 @@ export function valibotSchema<TValues extends Record<string, unknown>>(
|
|
|
102
95
|
* onSubmit: (values) => { ... },
|
|
103
96
|
* })
|
|
104
97
|
*/
|
|
105
|
-
export function valibotField<T>(
|
|
106
|
-
schema: unknown,
|
|
107
|
-
safeParseFn: GenericSafeParseFn,
|
|
108
|
-
): ValidateFn<T> {
|
|
98
|
+
export function valibotField<T>(schema: unknown, safeParseFn: GenericSafeParseFn): ValidateFn<T> {
|
|
109
99
|
const parse = safeParseFn as InternalParseFn
|
|
110
100
|
return async (value: T) => {
|
|
111
101
|
try {
|
package/src/zod.ts
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
ValidationError,
|
|
5
|
-
} from '@pyreon/form'
|
|
6
|
-
import type { ValidationIssue } from './types'
|
|
7
|
-
import { issuesToRecord } from './utils'
|
|
1
|
+
import type { SchemaValidateFn, ValidateFn, ValidationError } from "@pyreon/form"
|
|
2
|
+
import type { ValidationIssue } from "./types"
|
|
3
|
+
import { issuesToRecord } from "./utils"
|
|
8
4
|
|
|
9
5
|
/**
|
|
10
6
|
* Minimal Zod-compatible interfaces so we don't require zod as a hard dep.
|
|
@@ -32,7 +28,7 @@ interface ZodSchema<T = unknown> {
|
|
|
32
28
|
|
|
33
29
|
function zodIssuesToGeneric(issues: ZodIssue[]): ValidationIssue[] {
|
|
34
30
|
return issues.map((issue) => ({
|
|
35
|
-
path: issue.path.map(String).join(
|
|
31
|
+
path: issue.path.map(String).join("."),
|
|
36
32
|
message: issue.message,
|
|
37
33
|
}))
|
|
38
34
|
}
|
|
@@ -62,12 +58,11 @@ export function zodSchema<TValues extends Record<string, unknown>>(
|
|
|
62
58
|
return async (values: TValues) => {
|
|
63
59
|
try {
|
|
64
60
|
const result = await schema.safeParseAsync(values)
|
|
65
|
-
if (result.success)
|
|
66
|
-
return {} as Partial<Record<keyof TValues, ValidationError>>
|
|
61
|
+
if (result.success) return {} as Partial<Record<keyof TValues, ValidationError>>
|
|
67
62
|
return issuesToRecord<TValues>(zodIssuesToGeneric(result.error!.issues))
|
|
68
63
|
} catch (err) {
|
|
69
64
|
return {
|
|
70
|
-
|
|
65
|
+
"": err instanceof Error ? err.message : String(err),
|
|
71
66
|
} as Partial<Record<keyof TValues, ValidationError>>
|
|
72
67
|
}
|
|
73
68
|
}
|