@moluoxixi/zod3-to-rule 0.1.2 → 0.1.3
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 +27 -27
- package/index.ts +7 -7
- package/package.json +1 -1
- package/src/constants.ts +1 -1
- package/src/diagnostics.ts +27 -27
- package/src/from-zod.ts +213 -213
- package/src/parse.ts +82 -82
- package/src/rules.ts +65 -65
- package/src/to-zod.ts +127 -127
- package/src/types.ts +102 -102
package/README.md
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
# Zod 3 to Rule
|
|
2
|
-
|
|
3
|
-
`@moluoxixi/zod3-to-rule` 提供可序列化校验规则与 Zod 3 之间的桥接,不依赖 Vue 或任何表单 UI。
|
|
4
|
-
|
|
5
|
-
规则 JSON 是稳定协议。外部输入先通过 `parseRuleSet()` 严格解析,再使用 `compileRules()` 编译为 Zod schema、required 元数据和跨字段/custom validator。`rulesToZod()` 只编译单字段 Zod 能力。
|
|
6
|
-
|
|
7
|
-
```ts
|
|
8
|
-
import { compileRules, parseRuleSet } from '@moluoxixi/zod3-to-rule'
|
|
9
|
-
|
|
10
|
-
const parsed = parseRuleSet({
|
|
11
|
-
version: 1,
|
|
12
|
-
base: { type: 'string' },
|
|
13
|
-
rules: [
|
|
14
|
-
{ kind: 'required', message: '请输入邮箱' },
|
|
15
|
-
{ kind: 'email', message: '邮箱格式不正确' },
|
|
16
|
-
],
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
if (parsed.success) {
|
|
20
|
-
const compiled = compileRules(parsed.data)
|
|
21
|
-
compiled.schema.safeParse('user@example.com')
|
|
22
|
-
}
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
支持的基础类型包括 string、number、boolean、date、enum 和 JSON primitive literal。内置规则覆盖字符串长度、regex、email、URL、UUID、数值范围、integer、finite、multipleOf、日期范围、required、基础跨字段比较和命名 custom validator。
|
|
26
|
-
|
|
27
|
-
`zodToRules()` 是受限的最佳努力导出器。它支持上述基础类型和 Zod 3 内置 checks,并返回结构化 diagnostics。`refine`、`superRefine`、transform、preprocess、coerce、default 和其他无法由规则 JSON 等价表达的行为不会被静默丢弃;存在不可恢复的 effects 或 coercion 时不会返回 `ruleSet`。
|
|
1
|
+
# Zod 3 to Rule
|
|
2
|
+
|
|
3
|
+
`@moluoxixi/zod3-to-rule` 提供可序列化校验规则与 Zod 3 之间的桥接,不依赖 Vue 或任何表单 UI。
|
|
4
|
+
|
|
5
|
+
规则 JSON 是稳定协议。外部输入先通过 `parseRuleSet()` 严格解析,再使用 `compileRules()` 编译为 Zod schema、required 元数据和跨字段/custom validator。`rulesToZod()` 只编译单字段 Zod 能力。
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { compileRules, parseRuleSet } from '@moluoxixi/zod3-to-rule'
|
|
9
|
+
|
|
10
|
+
const parsed = parseRuleSet({
|
|
11
|
+
version: 1,
|
|
12
|
+
base: { type: 'string' },
|
|
13
|
+
rules: [
|
|
14
|
+
{ kind: 'required', message: '请输入邮箱' },
|
|
15
|
+
{ kind: 'email', message: '邮箱格式不正确' },
|
|
16
|
+
],
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
if (parsed.success) {
|
|
20
|
+
const compiled = compileRules(parsed.data)
|
|
21
|
+
compiled.schema.safeParse('user@example.com')
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
支持的基础类型包括 string、number、boolean、date、enum 和 JSON primitive literal。内置规则覆盖字符串长度、regex、email、URL、UUID、数值范围、integer、finite、multipleOf、日期范围、required、基础跨字段比较和命名 custom validator。
|
|
26
|
+
|
|
27
|
+
`zodToRules()` 是受限的最佳努力导出器。它支持上述基础类型和 Zod 3 内置 checks,并返回结构化 diagnostics。`refine`、`superRefine`、transform、preprocess、coerce、default 和其他无法由规则 JSON 等价表达的行为不会被静默丢弃;存在不可恢复的 effects 或 coercion 时不会返回 `ruleSet`。
|
package/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export * from './src/constants'
|
|
2
|
-
export * from './src/diagnostics'
|
|
3
|
-
export * from './src/from-zod'
|
|
4
|
-
export * from './src/parse'
|
|
5
|
-
export * from './src/rules'
|
|
6
|
-
export * from './src/to-zod'
|
|
7
|
-
export type * from './src/types'
|
|
1
|
+
export * from './src/constants'
|
|
2
|
+
export * from './src/diagnostics'
|
|
3
|
+
export * from './src/from-zod'
|
|
4
|
+
export * from './src/parse'
|
|
5
|
+
export * from './src/rules'
|
|
6
|
+
export * from './src/to-zod'
|
|
7
|
+
export type * from './src/types'
|
package/package.json
CHANGED
package/src/constants.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const RULE_SET_VERSION = 1 as const
|
|
1
|
+
export const RULE_SET_VERSION = 1 as const
|
package/src/diagnostics.ts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
import type { ZodIssue } from 'zod'
|
|
2
|
-
import type { RuleDiagnostic, RuleDiagnosticSeverity } from './types'
|
|
3
|
-
|
|
4
|
-
export function ruleDiagnostic(
|
|
5
|
-
code: string,
|
|
6
|
-
message: string,
|
|
7
|
-
path: (string | number)[] = [],
|
|
8
|
-
severity: RuleDiagnosticSeverity = 'error',
|
|
9
|
-
ruleIndex?: number,
|
|
10
|
-
): RuleDiagnostic {
|
|
11
|
-
return { code, severity, path, message, ...(ruleIndex === undefined ? {} : { ruleIndex }) }
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function formatRuleZodIssues(issues: ZodIssue[]): RuleDiagnostic[] {
|
|
15
|
-
return issues.map(issue => ruleDiagnostic(
|
|
16
|
-
'RULE_DOCUMENT_INVALID',
|
|
17
|
-
issue.message,
|
|
18
|
-
issue.path,
|
|
19
|
-
))
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export class RuleCompileError extends Error {
|
|
23
|
-
constructor(public readonly diagnostics: RuleDiagnostic[]) {
|
|
24
|
-
super(diagnostics.map(diagnostic => diagnostic.message).join('; '))
|
|
25
|
-
this.name = 'RuleCompileError'
|
|
26
|
-
}
|
|
27
|
-
}
|
|
1
|
+
import type { ZodIssue } from 'zod'
|
|
2
|
+
import type { RuleDiagnostic, RuleDiagnosticSeverity } from './types'
|
|
3
|
+
|
|
4
|
+
export function ruleDiagnostic(
|
|
5
|
+
code: string,
|
|
6
|
+
message: string,
|
|
7
|
+
path: (string | number)[] = [],
|
|
8
|
+
severity: RuleDiagnosticSeverity = 'error',
|
|
9
|
+
ruleIndex?: number,
|
|
10
|
+
): RuleDiagnostic {
|
|
11
|
+
return { code, severity, path, message, ...(ruleIndex === undefined ? {} : { ruleIndex }) }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatRuleZodIssues(issues: ZodIssue[]): RuleDiagnostic[] {
|
|
15
|
+
return issues.map(issue => ruleDiagnostic(
|
|
16
|
+
'RULE_DOCUMENT_INVALID',
|
|
17
|
+
issue.message,
|
|
18
|
+
issue.path,
|
|
19
|
+
))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class RuleCompileError extends Error {
|
|
23
|
+
constructor(public readonly diagnostics: RuleDiagnostic[]) {
|
|
24
|
+
super(diagnostics.map(diagnostic => diagnostic.message).join('; '))
|
|
25
|
+
this.name = 'RuleCompileError'
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/from-zod.ts
CHANGED
|
@@ -1,213 +1,213 @@
|
|
|
1
|
-
import type { RuleDescriptor, RuleSet, ZodToRulesResult } from './types'
|
|
2
|
-
import { z } from 'zod'
|
|
3
|
-
import { RULE_SET_VERSION } from './constants'
|
|
4
|
-
import { ruleDiagnostic } from './diagnostics'
|
|
5
|
-
|
|
6
|
-
function messageFromCheck(check: { message?: string }): string | undefined {
|
|
7
|
-
return typeof check.message === 'string' ? check.message : undefined
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function withMessage<T extends RuleDescriptor>(rule: T, check: { message?: string }): T {
|
|
11
|
-
const message = messageFromCheck(check)
|
|
12
|
-
return message ? { ...rule, message } as T : rule
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function unsupported(message: string, path: (string | number)[] = []): ReturnType<typeof ruleDiagnostic> {
|
|
16
|
-
return ruleDiagnostic('ZOD_UNSUPPORTED', message, path, 'warning')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function unwrapSchema(schema: z.ZodTypeAny): {
|
|
20
|
-
schema: z.ZodTypeAny
|
|
21
|
-
optional: boolean
|
|
22
|
-
nullable: boolean
|
|
23
|
-
unsupported: boolean
|
|
24
|
-
diagnostics: ReturnType<typeof ruleDiagnostic>[]
|
|
25
|
-
} {
|
|
26
|
-
let current = schema
|
|
27
|
-
let optional = false
|
|
28
|
-
let nullable = false
|
|
29
|
-
const diagnostics: ReturnType<typeof ruleDiagnostic>[] = []
|
|
30
|
-
|
|
31
|
-
while (true) {
|
|
32
|
-
if (current instanceof z.ZodOptional) {
|
|
33
|
-
optional = true
|
|
34
|
-
current = current.unwrap()
|
|
35
|
-
continue
|
|
36
|
-
}
|
|
37
|
-
if (current instanceof z.ZodNullable) {
|
|
38
|
-
nullable = true
|
|
39
|
-
current = current.unwrap()
|
|
40
|
-
continue
|
|
41
|
-
}
|
|
42
|
-
if (current instanceof z.ZodDefault) {
|
|
43
|
-
diagnostics.push(unsupported('Zod default values are not part of the rule format'))
|
|
44
|
-
current = current.removeDefault()
|
|
45
|
-
continue
|
|
46
|
-
}
|
|
47
|
-
if (current instanceof z.ZodEffects) {
|
|
48
|
-
diagnostics.push(unsupported('Zod effects/refine/transform cannot be represented as rules'))
|
|
49
|
-
return { schema: current, optional, nullable, unsupported: true, diagnostics }
|
|
50
|
-
}
|
|
51
|
-
break
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return { schema: current, optional, nullable, unsupported: false, diagnostics }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function exportString(schema: z.ZodString): { rules: RuleDescriptor[], diagnostics: ReturnType<typeof ruleDiagnostic>[] } {
|
|
58
|
-
const rules: RuleDescriptor[] = []
|
|
59
|
-
const diagnostics: ReturnType<typeof ruleDiagnostic>[] = []
|
|
60
|
-
const checks = schema._def.checks
|
|
61
|
-
for (const check of checks) {
|
|
62
|
-
const kind = check.kind
|
|
63
|
-
switch (kind) {
|
|
64
|
-
case 'min':
|
|
65
|
-
rules.push(withMessage({ kind: 'minLength', value: check.value }, check))
|
|
66
|
-
break
|
|
67
|
-
case 'max':
|
|
68
|
-
rules.push(withMessage({ kind: 'maxLength', value: check.value }, check))
|
|
69
|
-
break
|
|
70
|
-
case 'length':
|
|
71
|
-
rules.push(withMessage({ kind: 'length', value: check.value }, check))
|
|
72
|
-
break
|
|
73
|
-
case 'email':
|
|
74
|
-
rules.push(withMessage({ kind: 'email' }, check))
|
|
75
|
-
break
|
|
76
|
-
case 'url':
|
|
77
|
-
rules.push(withMessage({ kind: 'url' }, check))
|
|
78
|
-
break
|
|
79
|
-
case 'uuid':
|
|
80
|
-
rules.push(withMessage({ kind: 'uuid' }, check))
|
|
81
|
-
break
|
|
82
|
-
case 'regex': {
|
|
83
|
-
const regex = check.regex
|
|
84
|
-
if (regex instanceof RegExp)
|
|
85
|
-
rules.push(withMessage({ kind: 'regex', source: regex.source, flags: regex.flags }, check))
|
|
86
|
-
else
|
|
87
|
-
diagnostics.push(unsupported('Zod regex check does not contain a RegExp'))
|
|
88
|
-
break
|
|
89
|
-
}
|
|
90
|
-
default:
|
|
91
|
-
diagnostics.push(unsupported(`Zod string check is not supported: ${String(kind)}`))
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return { rules, diagnostics }
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function exportNumber(schema: z.ZodNumber): { rules: RuleDescriptor[], diagnostics: ReturnType<typeof ruleDiagnostic>[] } {
|
|
98
|
-
const rules: RuleDescriptor[] = []
|
|
99
|
-
const diagnostics: ReturnType<typeof ruleDiagnostic>[] = []
|
|
100
|
-
const checks = schema._def.checks
|
|
101
|
-
for (const check of checks) {
|
|
102
|
-
const kind = check.kind
|
|
103
|
-
switch (kind) {
|
|
104
|
-
case 'min':
|
|
105
|
-
rules.push(withMessage({ kind: 'min', value: check.value, inclusive: check.inclusive }, check))
|
|
106
|
-
break
|
|
107
|
-
case 'max':
|
|
108
|
-
rules.push(withMessage({ kind: 'max', value: check.value, inclusive: check.inclusive }, check))
|
|
109
|
-
break
|
|
110
|
-
case 'int':
|
|
111
|
-
rules.push(withMessage({ kind: 'integer' }, check))
|
|
112
|
-
break
|
|
113
|
-
case 'finite':
|
|
114
|
-
rules.push(withMessage({ kind: 'finite' }, check))
|
|
115
|
-
break
|
|
116
|
-
case 'multipleOf':
|
|
117
|
-
rules.push(withMessage({ kind: 'multipleOf', value: check.value }, check))
|
|
118
|
-
break
|
|
119
|
-
default:
|
|
120
|
-
diagnostics.push(unsupported(`Zod number check is not supported: ${String(kind)}`))
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return { rules, diagnostics }
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function exportDate(schema: z.ZodDate): { rules: RuleDescriptor[], diagnostics: ReturnType<typeof ruleDiagnostic>[] } {
|
|
127
|
-
const rules: RuleDescriptor[] = []
|
|
128
|
-
const diagnostics: ReturnType<typeof ruleDiagnostic>[] = []
|
|
129
|
-
const checks = schema._def.checks
|
|
130
|
-
for (const check of checks) {
|
|
131
|
-
const value = new Date(check.value)
|
|
132
|
-
if (Number.isNaN(value.getTime())) {
|
|
133
|
-
diagnostics.push(unsupported('Zod date check does not contain a valid date value'))
|
|
134
|
-
continue
|
|
135
|
-
}
|
|
136
|
-
if (check.kind === 'min')
|
|
137
|
-
rules.push(withMessage({ kind: 'dateMin', value: value.toISOString() }, check))
|
|
138
|
-
else
|
|
139
|
-
rules.push(withMessage({ kind: 'dateMax', value: value.toISOString() }, check))
|
|
140
|
-
}
|
|
141
|
-
return { rules, diagnostics }
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export function zodToRules(schema: z.ZodTypeAny): ZodToRulesResult {
|
|
145
|
-
const unwrapped = unwrapSchema(schema)
|
|
146
|
-
const diagnostics = [...unwrapped.diagnostics]
|
|
147
|
-
if (unwrapped.unsupported)
|
|
148
|
-
return { diagnostics }
|
|
149
|
-
|
|
150
|
-
if (
|
|
151
|
-
(unwrapped.schema instanceof z.ZodString
|
|
152
|
-
|| unwrapped.schema instanceof z.ZodNumber
|
|
153
|
-
|| unwrapped.schema instanceof z.ZodBoolean
|
|
154
|
-
|| unwrapped.schema instanceof z.ZodDate)
|
|
155
|
-
&& unwrapped.schema._def.coerce
|
|
156
|
-
) {
|
|
157
|
-
diagnostics.push(unsupported('Zod coercion cannot be represented as rules'))
|
|
158
|
-
return { diagnostics }
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
let base: RuleSet['base']
|
|
162
|
-
let rules: RuleDescriptor[] = []
|
|
163
|
-
|
|
164
|
-
if (unwrapped.schema instanceof z.ZodString) {
|
|
165
|
-
base = { type: 'string' }
|
|
166
|
-
const exported = exportString(unwrapped.schema)
|
|
167
|
-
rules = exported.rules
|
|
168
|
-
diagnostics.push(...exported.diagnostics)
|
|
169
|
-
}
|
|
170
|
-
else if (unwrapped.schema instanceof z.ZodNumber) {
|
|
171
|
-
base = { type: 'number' }
|
|
172
|
-
const exported = exportNumber(unwrapped.schema)
|
|
173
|
-
rules = exported.rules
|
|
174
|
-
diagnostics.push(...exported.diagnostics)
|
|
175
|
-
}
|
|
176
|
-
else if (unwrapped.schema instanceof z.ZodBoolean) {
|
|
177
|
-
base = { type: 'boolean' }
|
|
178
|
-
}
|
|
179
|
-
else if (unwrapped.schema instanceof z.ZodDate) {
|
|
180
|
-
base = { type: 'date' }
|
|
181
|
-
const exported = exportDate(unwrapped.schema)
|
|
182
|
-
rules = exported.rules
|
|
183
|
-
diagnostics.push(...exported.diagnostics)
|
|
184
|
-
}
|
|
185
|
-
else if (unwrapped.schema instanceof z.ZodEnum) {
|
|
186
|
-
const values = unwrapped.schema.options
|
|
187
|
-
base = { type: 'enum', values: values as [string, ...string[]] }
|
|
188
|
-
}
|
|
189
|
-
else if (unwrapped.schema instanceof z.ZodLiteral) {
|
|
190
|
-
const value = unwrapped.schema.value
|
|
191
|
-
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean' && value !== null) {
|
|
192
|
-
diagnostics.push(unsupported('Zod literal value is not JSON-safe'))
|
|
193
|
-
return { diagnostics }
|
|
194
|
-
}
|
|
195
|
-
base = { type: 'literal', value }
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
const definition = unwrapped.schema._def as unknown as { typeName?: unknown }
|
|
199
|
-
diagnostics.push(unsupported(`Zod schema type is not supported: ${String(definition.typeName ?? 'unknown')}`))
|
|
200
|
-
return { diagnostics }
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
ruleSet: {
|
|
205
|
-
version: RULE_SET_VERSION,
|
|
206
|
-
base,
|
|
207
|
-
rules,
|
|
208
|
-
...(unwrapped.optional ? { optional: true } : {}),
|
|
209
|
-
...(unwrapped.nullable ? { nullable: true } : {}),
|
|
210
|
-
},
|
|
211
|
-
diagnostics,
|
|
212
|
-
}
|
|
213
|
-
}
|
|
1
|
+
import type { RuleDescriptor, RuleSet, ZodToRulesResult } from './types'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { RULE_SET_VERSION } from './constants'
|
|
4
|
+
import { ruleDiagnostic } from './diagnostics'
|
|
5
|
+
|
|
6
|
+
function messageFromCheck(check: { message?: string }): string | undefined {
|
|
7
|
+
return typeof check.message === 'string' ? check.message : undefined
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function withMessage<T extends RuleDescriptor>(rule: T, check: { message?: string }): T {
|
|
11
|
+
const message = messageFromCheck(check)
|
|
12
|
+
return message ? { ...rule, message } as T : rule
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function unsupported(message: string, path: (string | number)[] = []): ReturnType<typeof ruleDiagnostic> {
|
|
16
|
+
return ruleDiagnostic('ZOD_UNSUPPORTED', message, path, 'warning')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function unwrapSchema(schema: z.ZodTypeAny): {
|
|
20
|
+
schema: z.ZodTypeAny
|
|
21
|
+
optional: boolean
|
|
22
|
+
nullable: boolean
|
|
23
|
+
unsupported: boolean
|
|
24
|
+
diagnostics: ReturnType<typeof ruleDiagnostic>[]
|
|
25
|
+
} {
|
|
26
|
+
let current = schema
|
|
27
|
+
let optional = false
|
|
28
|
+
let nullable = false
|
|
29
|
+
const diagnostics: ReturnType<typeof ruleDiagnostic>[] = []
|
|
30
|
+
|
|
31
|
+
while (true) {
|
|
32
|
+
if (current instanceof z.ZodOptional) {
|
|
33
|
+
optional = true
|
|
34
|
+
current = current.unwrap()
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
if (current instanceof z.ZodNullable) {
|
|
38
|
+
nullable = true
|
|
39
|
+
current = current.unwrap()
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
if (current instanceof z.ZodDefault) {
|
|
43
|
+
diagnostics.push(unsupported('Zod default values are not part of the rule format'))
|
|
44
|
+
current = current.removeDefault()
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
if (current instanceof z.ZodEffects) {
|
|
48
|
+
diagnostics.push(unsupported('Zod effects/refine/transform cannot be represented as rules'))
|
|
49
|
+
return { schema: current, optional, nullable, unsupported: true, diagnostics }
|
|
50
|
+
}
|
|
51
|
+
break
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { schema: current, optional, nullable, unsupported: false, diagnostics }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function exportString(schema: z.ZodString): { rules: RuleDescriptor[], diagnostics: ReturnType<typeof ruleDiagnostic>[] } {
|
|
58
|
+
const rules: RuleDescriptor[] = []
|
|
59
|
+
const diagnostics: ReturnType<typeof ruleDiagnostic>[] = []
|
|
60
|
+
const checks = schema._def.checks
|
|
61
|
+
for (const check of checks) {
|
|
62
|
+
const kind = check.kind
|
|
63
|
+
switch (kind) {
|
|
64
|
+
case 'min':
|
|
65
|
+
rules.push(withMessage({ kind: 'minLength', value: check.value }, check))
|
|
66
|
+
break
|
|
67
|
+
case 'max':
|
|
68
|
+
rules.push(withMessage({ kind: 'maxLength', value: check.value }, check))
|
|
69
|
+
break
|
|
70
|
+
case 'length':
|
|
71
|
+
rules.push(withMessage({ kind: 'length', value: check.value }, check))
|
|
72
|
+
break
|
|
73
|
+
case 'email':
|
|
74
|
+
rules.push(withMessage({ kind: 'email' }, check))
|
|
75
|
+
break
|
|
76
|
+
case 'url':
|
|
77
|
+
rules.push(withMessage({ kind: 'url' }, check))
|
|
78
|
+
break
|
|
79
|
+
case 'uuid':
|
|
80
|
+
rules.push(withMessage({ kind: 'uuid' }, check))
|
|
81
|
+
break
|
|
82
|
+
case 'regex': {
|
|
83
|
+
const regex = check.regex
|
|
84
|
+
if (regex instanceof RegExp)
|
|
85
|
+
rules.push(withMessage({ kind: 'regex', source: regex.source, flags: regex.flags }, check))
|
|
86
|
+
else
|
|
87
|
+
diagnostics.push(unsupported('Zod regex check does not contain a RegExp'))
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
default:
|
|
91
|
+
diagnostics.push(unsupported(`Zod string check is not supported: ${String(kind)}`))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { rules, diagnostics }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function exportNumber(schema: z.ZodNumber): { rules: RuleDescriptor[], diagnostics: ReturnType<typeof ruleDiagnostic>[] } {
|
|
98
|
+
const rules: RuleDescriptor[] = []
|
|
99
|
+
const diagnostics: ReturnType<typeof ruleDiagnostic>[] = []
|
|
100
|
+
const checks = schema._def.checks
|
|
101
|
+
for (const check of checks) {
|
|
102
|
+
const kind = check.kind
|
|
103
|
+
switch (kind) {
|
|
104
|
+
case 'min':
|
|
105
|
+
rules.push(withMessage({ kind: 'min', value: check.value, inclusive: check.inclusive }, check))
|
|
106
|
+
break
|
|
107
|
+
case 'max':
|
|
108
|
+
rules.push(withMessage({ kind: 'max', value: check.value, inclusive: check.inclusive }, check))
|
|
109
|
+
break
|
|
110
|
+
case 'int':
|
|
111
|
+
rules.push(withMessage({ kind: 'integer' }, check))
|
|
112
|
+
break
|
|
113
|
+
case 'finite':
|
|
114
|
+
rules.push(withMessage({ kind: 'finite' }, check))
|
|
115
|
+
break
|
|
116
|
+
case 'multipleOf':
|
|
117
|
+
rules.push(withMessage({ kind: 'multipleOf', value: check.value }, check))
|
|
118
|
+
break
|
|
119
|
+
default:
|
|
120
|
+
diagnostics.push(unsupported(`Zod number check is not supported: ${String(kind)}`))
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { rules, diagnostics }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function exportDate(schema: z.ZodDate): { rules: RuleDescriptor[], diagnostics: ReturnType<typeof ruleDiagnostic>[] } {
|
|
127
|
+
const rules: RuleDescriptor[] = []
|
|
128
|
+
const diagnostics: ReturnType<typeof ruleDiagnostic>[] = []
|
|
129
|
+
const checks = schema._def.checks
|
|
130
|
+
for (const check of checks) {
|
|
131
|
+
const value = new Date(check.value)
|
|
132
|
+
if (Number.isNaN(value.getTime())) {
|
|
133
|
+
diagnostics.push(unsupported('Zod date check does not contain a valid date value'))
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
if (check.kind === 'min')
|
|
137
|
+
rules.push(withMessage({ kind: 'dateMin', value: value.toISOString() }, check))
|
|
138
|
+
else
|
|
139
|
+
rules.push(withMessage({ kind: 'dateMax', value: value.toISOString() }, check))
|
|
140
|
+
}
|
|
141
|
+
return { rules, diagnostics }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function zodToRules(schema: z.ZodTypeAny): ZodToRulesResult {
|
|
145
|
+
const unwrapped = unwrapSchema(schema)
|
|
146
|
+
const diagnostics = [...unwrapped.diagnostics]
|
|
147
|
+
if (unwrapped.unsupported)
|
|
148
|
+
return { diagnostics }
|
|
149
|
+
|
|
150
|
+
if (
|
|
151
|
+
(unwrapped.schema instanceof z.ZodString
|
|
152
|
+
|| unwrapped.schema instanceof z.ZodNumber
|
|
153
|
+
|| unwrapped.schema instanceof z.ZodBoolean
|
|
154
|
+
|| unwrapped.schema instanceof z.ZodDate)
|
|
155
|
+
&& unwrapped.schema._def.coerce
|
|
156
|
+
) {
|
|
157
|
+
diagnostics.push(unsupported('Zod coercion cannot be represented as rules'))
|
|
158
|
+
return { diagnostics }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let base: RuleSet['base']
|
|
162
|
+
let rules: RuleDescriptor[] = []
|
|
163
|
+
|
|
164
|
+
if (unwrapped.schema instanceof z.ZodString) {
|
|
165
|
+
base = { type: 'string' }
|
|
166
|
+
const exported = exportString(unwrapped.schema)
|
|
167
|
+
rules = exported.rules
|
|
168
|
+
diagnostics.push(...exported.diagnostics)
|
|
169
|
+
}
|
|
170
|
+
else if (unwrapped.schema instanceof z.ZodNumber) {
|
|
171
|
+
base = { type: 'number' }
|
|
172
|
+
const exported = exportNumber(unwrapped.schema)
|
|
173
|
+
rules = exported.rules
|
|
174
|
+
diagnostics.push(...exported.diagnostics)
|
|
175
|
+
}
|
|
176
|
+
else if (unwrapped.schema instanceof z.ZodBoolean) {
|
|
177
|
+
base = { type: 'boolean' }
|
|
178
|
+
}
|
|
179
|
+
else if (unwrapped.schema instanceof z.ZodDate) {
|
|
180
|
+
base = { type: 'date' }
|
|
181
|
+
const exported = exportDate(unwrapped.schema)
|
|
182
|
+
rules = exported.rules
|
|
183
|
+
diagnostics.push(...exported.diagnostics)
|
|
184
|
+
}
|
|
185
|
+
else if (unwrapped.schema instanceof z.ZodEnum) {
|
|
186
|
+
const values = unwrapped.schema.options
|
|
187
|
+
base = { type: 'enum', values: values as [string, ...string[]] }
|
|
188
|
+
}
|
|
189
|
+
else if (unwrapped.schema instanceof z.ZodLiteral) {
|
|
190
|
+
const value = unwrapped.schema.value
|
|
191
|
+
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'boolean' && value !== null) {
|
|
192
|
+
diagnostics.push(unsupported('Zod literal value is not JSON-safe'))
|
|
193
|
+
return { diagnostics }
|
|
194
|
+
}
|
|
195
|
+
base = { type: 'literal', value }
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const definition = unwrapped.schema._def as unknown as { typeName?: unknown }
|
|
199
|
+
diagnostics.push(unsupported(`Zod schema type is not supported: ${String(definition.typeName ?? 'unknown')}`))
|
|
200
|
+
return { diagnostics }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
ruleSet: {
|
|
205
|
+
version: RULE_SET_VERSION,
|
|
206
|
+
base,
|
|
207
|
+
rules,
|
|
208
|
+
...(unwrapped.optional ? { optional: true } : {}),
|
|
209
|
+
...(unwrapped.nullable ? { nullable: true } : {}),
|
|
210
|
+
},
|
|
211
|
+
diagnostics,
|
|
212
|
+
}
|
|
213
|
+
}
|