@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 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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@moluoxixi/zod3-to-rule",
3
3
  "type": "module",
4
- "version": "0.1.2",
4
+ "version": "0.1.3",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/moluoxixi/vue-components",
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
@@ -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
+ }