@gabrielbryk/json-schema-to-zod 2.7.4 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -1
- package/dist/cjs/core/analyzeSchema.js +62 -0
- package/dist/cjs/core/emitZod.js +141 -0
- package/dist/cjs/generators/generateBundle.js +355 -0
- package/dist/cjs/index.js +6 -0
- package/dist/cjs/jsonSchemaToZod.js +5 -73
- package/dist/cjs/parsers/parseArray.js +34 -15
- package/dist/cjs/parsers/parseIfThenElse.js +2 -1
- package/dist/cjs/parsers/parseNumber.js +81 -39
- package/dist/cjs/parsers/parseObject.js +24 -0
- package/dist/cjs/parsers/parseSchema.js +147 -25
- package/dist/cjs/parsers/parseString.js +294 -54
- package/dist/cjs/utils/buildRefRegistry.js +56 -0
- package/dist/cjs/utils/cycles.js +113 -0
- package/dist/cjs/utils/resolveUri.js +16 -0
- package/dist/cjs/utils/withMessage.js +4 -5
- package/dist/esm/core/analyzeSchema.js +58 -0
- package/dist/esm/core/emitZod.js +137 -0
- package/dist/esm/generators/generateBundle.js +351 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/jsonSchemaToZod.js +5 -73
- package/dist/esm/parsers/parseArray.js +34 -15
- package/dist/esm/parsers/parseIfThenElse.js +2 -1
- package/dist/esm/parsers/parseNumber.js +81 -39
- package/dist/esm/parsers/parseObject.js +24 -0
- package/dist/esm/parsers/parseSchema.js +147 -25
- package/dist/esm/parsers/parseString.js +294 -54
- package/dist/esm/utils/buildRefRegistry.js +52 -0
- package/dist/esm/utils/cycles.js +107 -0
- package/dist/esm/utils/resolveUri.js +12 -0
- package/dist/esm/utils/withMessage.js +4 -5
- package/dist/types/Types.d.ts +36 -0
- package/dist/types/core/analyzeSchema.d.ts +24 -0
- package/dist/types/core/emitZod.d.ts +2 -0
- package/dist/types/generators/generateBundle.d.ts +62 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/jsonSchemaToZod.d.ts +1 -1
- package/dist/types/parsers/parseSchema.d.ts +2 -1
- package/dist/types/parsers/parseString.d.ts +2 -2
- package/dist/types/utils/buildRefRegistry.d.ts +12 -0
- package/dist/types/utils/cycles.d.ts +7 -0
- package/dist/types/utils/jsdocs.d.ts +1 -1
- package/dist/types/utils/resolveUri.d.ts +1 -0
- package/dist/types/utils/withMessage.d.ts +6 -1
- package/docs/proposals/bundle-refactor.md +43 -0
- package/docs/proposals/ref-anchor-support.md +65 -0
- package/eslint.config.js +26 -0
- package/package.json +10 -4
- /package/{jest.config.js → jest.config.cjs} +0 -0
- /package/{postcjs.js → postcjs.cjs} +0 -0
- /package/{postesm.js → postesm.cjs} +0 -0
|
@@ -1,73 +1,313 @@
|
|
|
1
1
|
import { withMessage } from "../utils/withMessage.js";
|
|
2
2
|
import { parseSchema } from "./parseSchema.js";
|
|
3
|
-
export const parseString = (schema) => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
3
|
+
export const parseString = (schema, refs) => {
|
|
4
|
+
const formatError = schema.errorMessage?.format;
|
|
5
|
+
const refContext = ensureRefs(refs);
|
|
6
|
+
const topLevelFormatMap = {
|
|
7
|
+
email: "z.email",
|
|
8
|
+
ipv4: "z.ipv4",
|
|
9
|
+
ipv6: "z.ipv6",
|
|
10
|
+
uri: "z.url",
|
|
11
|
+
uuid: "z.uuid",
|
|
12
|
+
cuid: "z.cuid",
|
|
13
|
+
cuid2: "z.cuid2",
|
|
14
|
+
nanoid: "z.nanoid",
|
|
15
|
+
ulid: "z.ulid",
|
|
16
|
+
jwt: "z.jwt",
|
|
17
|
+
e164: "z.e164",
|
|
18
|
+
base64url: "z.base64url",
|
|
19
|
+
base64: "z.base64",
|
|
20
|
+
emoji: "z.emoji",
|
|
21
|
+
"idn-email": "z.email",
|
|
22
|
+
};
|
|
23
|
+
const formatFn = schema.format && topLevelFormatMap[schema.format];
|
|
24
|
+
const formatParam = formatError !== undefined ? `{ error: ${JSON.stringify(formatError)} }` : "";
|
|
25
|
+
let r = formatFn ? `${formatFn}(${formatParam})` : "z.string()";
|
|
26
|
+
const formatHandled = Boolean(formatFn);
|
|
27
|
+
let formatWasHandled = formatHandled;
|
|
28
|
+
if (!formatHandled) {
|
|
29
|
+
r += withMessage(schema, "format", ({ value }) => {
|
|
30
|
+
switch (value) {
|
|
31
|
+
case "email":
|
|
32
|
+
formatWasHandled = true;
|
|
33
|
+
return {
|
|
34
|
+
opener: ".email(",
|
|
35
|
+
closer: ")",
|
|
36
|
+
messagePrefix: "{ error: ",
|
|
37
|
+
messageCloser: " })",
|
|
38
|
+
};
|
|
39
|
+
case "ip":
|
|
40
|
+
formatWasHandled = true;
|
|
41
|
+
return {
|
|
42
|
+
opener: ".ip(",
|
|
43
|
+
closer: ")",
|
|
44
|
+
messagePrefix: "{ error: ",
|
|
45
|
+
messageCloser: " })",
|
|
46
|
+
};
|
|
47
|
+
case "ipv4":
|
|
48
|
+
formatWasHandled = true;
|
|
49
|
+
return {
|
|
50
|
+
opener: '.ip({ version: "v4"',
|
|
51
|
+
closer: " })",
|
|
52
|
+
messagePrefix: ", error: ",
|
|
53
|
+
messageCloser: " })",
|
|
54
|
+
};
|
|
55
|
+
case "ipv6":
|
|
56
|
+
formatWasHandled = true;
|
|
57
|
+
return {
|
|
58
|
+
opener: '.ip({ version: "v6"',
|
|
59
|
+
closer: " })",
|
|
60
|
+
messagePrefix: ", error: ",
|
|
61
|
+
messageCloser: " })",
|
|
62
|
+
};
|
|
63
|
+
case "uri":
|
|
64
|
+
formatWasHandled = true;
|
|
65
|
+
return {
|
|
66
|
+
opener: ".url(",
|
|
67
|
+
closer: ")",
|
|
68
|
+
messagePrefix: "{ error: ",
|
|
69
|
+
messageCloser: " })",
|
|
70
|
+
};
|
|
71
|
+
case "uuid":
|
|
72
|
+
formatWasHandled = true;
|
|
73
|
+
return {
|
|
74
|
+
opener: ".uuid(",
|
|
75
|
+
closer: ")",
|
|
76
|
+
messagePrefix: "{ error: ",
|
|
77
|
+
messageCloser: " })",
|
|
78
|
+
};
|
|
79
|
+
case "cuid":
|
|
80
|
+
formatWasHandled = true;
|
|
81
|
+
return {
|
|
82
|
+
opener: ".cuid(",
|
|
83
|
+
closer: ")",
|
|
84
|
+
messagePrefix: "{ error: ",
|
|
85
|
+
messageCloser: " })",
|
|
86
|
+
};
|
|
87
|
+
case "cuid2":
|
|
88
|
+
formatWasHandled = true;
|
|
89
|
+
return {
|
|
90
|
+
opener: ".cuid2(",
|
|
91
|
+
closer: ")",
|
|
92
|
+
messagePrefix: "{ error: ",
|
|
93
|
+
messageCloser: " })",
|
|
94
|
+
};
|
|
95
|
+
case "nanoid":
|
|
96
|
+
formatWasHandled = true;
|
|
97
|
+
return {
|
|
98
|
+
opener: ".nanoid(",
|
|
99
|
+
closer: ")",
|
|
100
|
+
messagePrefix: "{ error: ",
|
|
101
|
+
messageCloser: " })",
|
|
102
|
+
};
|
|
103
|
+
case "ulid":
|
|
104
|
+
formatWasHandled = true;
|
|
105
|
+
return {
|
|
106
|
+
opener: ".ulid(",
|
|
107
|
+
closer: ")",
|
|
108
|
+
messagePrefix: "{ error: ",
|
|
109
|
+
messageCloser: " })",
|
|
110
|
+
};
|
|
111
|
+
case "jwt":
|
|
112
|
+
formatWasHandled = true;
|
|
113
|
+
return {
|
|
114
|
+
opener: ".jwt(",
|
|
115
|
+
closer: ")",
|
|
116
|
+
messagePrefix: "{ error: ",
|
|
117
|
+
messageCloser: " })",
|
|
118
|
+
};
|
|
119
|
+
case "e164":
|
|
120
|
+
formatWasHandled = true;
|
|
121
|
+
return {
|
|
122
|
+
opener: ".e164(",
|
|
123
|
+
closer: ")",
|
|
124
|
+
messagePrefix: "{ error: ",
|
|
125
|
+
messageCloser: " })",
|
|
126
|
+
};
|
|
127
|
+
case "base64url":
|
|
128
|
+
formatWasHandled = true;
|
|
129
|
+
return {
|
|
130
|
+
opener: ".base64url(",
|
|
131
|
+
closer: ")",
|
|
132
|
+
messagePrefix: "{ error: ",
|
|
133
|
+
messageCloser: " })",
|
|
134
|
+
};
|
|
135
|
+
case "emoji":
|
|
136
|
+
formatWasHandled = true;
|
|
137
|
+
return {
|
|
138
|
+
opener: ".emoji(",
|
|
139
|
+
closer: ")",
|
|
140
|
+
messagePrefix: "{ error: ",
|
|
141
|
+
messageCloser: " })",
|
|
142
|
+
};
|
|
143
|
+
case "date-time":
|
|
144
|
+
formatWasHandled = true;
|
|
145
|
+
return {
|
|
146
|
+
opener: ".datetime({ offset: true",
|
|
147
|
+
closer: " })",
|
|
148
|
+
messagePrefix: ", error: ",
|
|
149
|
+
messageCloser: " })",
|
|
150
|
+
};
|
|
151
|
+
case "time":
|
|
152
|
+
formatWasHandled = true;
|
|
153
|
+
return {
|
|
154
|
+
opener: ".time(",
|
|
155
|
+
closer: ")",
|
|
156
|
+
messagePrefix: "{ error: ",
|
|
157
|
+
messageCloser: " })",
|
|
158
|
+
};
|
|
159
|
+
case "date":
|
|
160
|
+
formatWasHandled = true;
|
|
161
|
+
return {
|
|
162
|
+
opener: ".date(",
|
|
163
|
+
closer: ")",
|
|
164
|
+
messagePrefix: "{ error: ",
|
|
165
|
+
messageCloser: " })",
|
|
166
|
+
};
|
|
167
|
+
case "binary":
|
|
168
|
+
formatWasHandled = true;
|
|
169
|
+
return {
|
|
170
|
+
opener: ".base64(",
|
|
171
|
+
closer: ")",
|
|
172
|
+
messagePrefix: "{ error: ",
|
|
173
|
+
messageCloser: " })",
|
|
174
|
+
};
|
|
175
|
+
case "duration":
|
|
176
|
+
formatWasHandled = true;
|
|
177
|
+
return {
|
|
178
|
+
opener: ".duration(",
|
|
179
|
+
closer: ")",
|
|
180
|
+
messagePrefix: "{ error: ",
|
|
181
|
+
messageCloser: " })",
|
|
182
|
+
};
|
|
183
|
+
case "hostname":
|
|
184
|
+
case "idn-hostname":
|
|
185
|
+
formatWasHandled = true;
|
|
186
|
+
return {
|
|
187
|
+
opener: ".refine((val) => { if (typeof val !== \"string\" || val.length === 0 || val.length > 253) return false; return val.split(\".\").every((label) => label.length > 0 && label.length <= 63 && /^[A-Za-z0-9-]+$/.test(label) && label[0] !== \"-\" && label[label.length - 1] !== \"-\"); }",
|
|
188
|
+
closer: ")",
|
|
189
|
+
messagePrefix: ", { error: ",
|
|
190
|
+
messageCloser: " })",
|
|
191
|
+
};
|
|
192
|
+
case "idn-email":
|
|
193
|
+
formatWasHandled = true;
|
|
194
|
+
return {
|
|
195
|
+
opener: ".email(",
|
|
196
|
+
closer: ")",
|
|
197
|
+
messagePrefix: "{ error: ",
|
|
198
|
+
messageCloser: " })",
|
|
199
|
+
};
|
|
200
|
+
case "uri-reference":
|
|
201
|
+
case "iri":
|
|
202
|
+
case "iri-reference":
|
|
203
|
+
formatWasHandled = true;
|
|
204
|
+
return {
|
|
205
|
+
opener: '.refine((val) => { try { new URL(val, "http://example.com"); return true; } catch { return false; } }',
|
|
206
|
+
closer: ")",
|
|
207
|
+
messagePrefix: ", { error: ",
|
|
208
|
+
messageCloser: " })",
|
|
209
|
+
};
|
|
210
|
+
case "json-pointer":
|
|
211
|
+
formatWasHandled = true;
|
|
212
|
+
return {
|
|
213
|
+
opener: ".refine((val) => typeof val === \"string\" && /^(?:\\/(?:[^/~]|~[01])*)*$/.test(val)",
|
|
214
|
+
closer: ")",
|
|
215
|
+
messagePrefix: ", { error: ",
|
|
216
|
+
messageCloser: " })",
|
|
217
|
+
};
|
|
218
|
+
case "relative-json-pointer":
|
|
219
|
+
formatWasHandled = true;
|
|
220
|
+
return {
|
|
221
|
+
opener: ".refine((val) => typeof val === \"string\" && /^(?:0|[1-9][0-9]*)(?:#|(?:\\/(?:[^/~]|~[01])*))*$/.test(val)",
|
|
222
|
+
closer: ")",
|
|
223
|
+
messagePrefix: ", { error: ",
|
|
224
|
+
messageCloser: " })",
|
|
225
|
+
};
|
|
226
|
+
case "uri-template":
|
|
227
|
+
formatWasHandled = true;
|
|
228
|
+
return {
|
|
229
|
+
opener: ".refine((val) => { if (typeof val !== \"string\") return false; const opens = (val.match(/\\{/g) || []).length; const closes = (val.match(/\\}/g) || []).length; return opens === closes; }",
|
|
230
|
+
closer: ")",
|
|
231
|
+
messagePrefix: ", { error: ",
|
|
232
|
+
messageCloser: " })",
|
|
233
|
+
};
|
|
234
|
+
case "regex":
|
|
235
|
+
formatWasHandled = true;
|
|
236
|
+
return {
|
|
237
|
+
opener: ".refine((val) => { try { new RegExp(val); return true; } catch { return false; } }",
|
|
238
|
+
closer: ")",
|
|
239
|
+
messagePrefix: ", { error: ",
|
|
240
|
+
messageCloser: " })",
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (schema.format && !formatWasHandled) {
|
|
246
|
+
refContext.onUnknownFormat?.(schema.format, refContext.path);
|
|
247
|
+
}
|
|
248
|
+
r += withMessage(schema, "pattern", ({ json }) => ({
|
|
249
|
+
opener: `.regex(new RegExp(${json})`,
|
|
250
|
+
closer: ")",
|
|
251
|
+
messagePrefix: ", { error: ",
|
|
252
|
+
messageCloser: " })",
|
|
253
|
+
}));
|
|
254
|
+
r += withMessage(schema, "minLength", ({ json }) => ({
|
|
255
|
+
opener: `.min(${json}`,
|
|
256
|
+
closer: ")",
|
|
257
|
+
messagePrefix: ", { error: ",
|
|
258
|
+
messageCloser: " })",
|
|
259
|
+
}));
|
|
260
|
+
r += withMessage(schema, "maxLength", ({ json }) => ({
|
|
261
|
+
opener: `.max(${json}`,
|
|
262
|
+
closer: ")",
|
|
263
|
+
messagePrefix: ", { error: ",
|
|
264
|
+
messageCloser: " })",
|
|
265
|
+
}));
|
|
46
266
|
r += withMessage(schema, "contentEncoding", ({ value }) => {
|
|
47
267
|
if (value === "base64") {
|
|
48
|
-
return
|
|
268
|
+
return {
|
|
269
|
+
opener: ".base64(",
|
|
270
|
+
closer: ")",
|
|
271
|
+
messagePrefix: "{ error: ",
|
|
272
|
+
messageCloser: " })",
|
|
273
|
+
};
|
|
49
274
|
}
|
|
50
275
|
});
|
|
51
276
|
const contentMediaType = withMessage(schema, "contentMediaType", ({ value }) => {
|
|
52
277
|
if (value === "application/json") {
|
|
53
|
-
return
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
|
|
278
|
+
return {
|
|
279
|
+
opener: '.transform((str, ctx) => { try { return JSON.parse(str); } catch (err) { ctx.addIssue({ code: "custom", message: "Invalid JSON" }); }}',
|
|
280
|
+
closer: ")",
|
|
281
|
+
messagePrefix: ", { error: ",
|
|
282
|
+
messageCloser: " })",
|
|
283
|
+
};
|
|
58
284
|
}
|
|
59
285
|
});
|
|
60
286
|
if (contentMediaType != "") {
|
|
61
287
|
r += contentMediaType;
|
|
62
288
|
r += withMessage(schema, "contentSchema", ({ value }) => {
|
|
63
289
|
if (value && value instanceof Object) {
|
|
64
|
-
return
|
|
65
|
-
`.pipe(${parseSchema(value)}`,
|
|
66
|
-
|
|
67
|
-
"
|
|
68
|
-
|
|
290
|
+
return {
|
|
291
|
+
opener: `.pipe(${parseSchema(value, refContext)}`,
|
|
292
|
+
closer: ")",
|
|
293
|
+
messagePrefix: ", { error: ",
|
|
294
|
+
messageCloser: " })",
|
|
295
|
+
};
|
|
69
296
|
}
|
|
70
297
|
});
|
|
71
298
|
}
|
|
72
299
|
return r;
|
|
73
300
|
};
|
|
301
|
+
function ensureRefs(refs) {
|
|
302
|
+
if (refs)
|
|
303
|
+
return refs;
|
|
304
|
+
return {
|
|
305
|
+
path: [],
|
|
306
|
+
seen: new Map(),
|
|
307
|
+
declarations: new Map(),
|
|
308
|
+
dependencies: new Map(),
|
|
309
|
+
inProgress: new Set(),
|
|
310
|
+
refNameByPointer: new Map(),
|
|
311
|
+
usedNames: new Set(),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { resolveUri } from "./resolveUri.js";
|
|
2
|
+
export const buildRefRegistry = (schema, rootBaseUri = "root:///", opts = {}) => {
|
|
3
|
+
const registry = new Map();
|
|
4
|
+
const walk = (node, baseUri, path) => {
|
|
5
|
+
if (typeof node !== "object" || node === null)
|
|
6
|
+
return;
|
|
7
|
+
const obj = node;
|
|
8
|
+
const nextBase = obj.$id ? resolveUri(baseUri, obj.$id) : baseUri;
|
|
9
|
+
// Legacy recursive anchor
|
|
10
|
+
if (obj.$recursiveAnchor === true) {
|
|
11
|
+
const name = "__recursive__";
|
|
12
|
+
registry.set(`${nextBase}#${name}`, {
|
|
13
|
+
schema: node,
|
|
14
|
+
path,
|
|
15
|
+
baseUri: nextBase,
|
|
16
|
+
dynamic: true,
|
|
17
|
+
anchor: name,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
// Register base entry
|
|
21
|
+
registry.set(nextBase, { schema: node, path, baseUri: nextBase });
|
|
22
|
+
if (typeof obj.$anchor === "string") {
|
|
23
|
+
registry.set(`${nextBase}#${obj.$anchor}`, {
|
|
24
|
+
schema: node,
|
|
25
|
+
path,
|
|
26
|
+
baseUri: nextBase,
|
|
27
|
+
anchor: obj.$anchor,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (typeof obj.$dynamicAnchor === "string") {
|
|
31
|
+
const name = obj.$dynamicAnchor;
|
|
32
|
+
registry.set(`${nextBase}#${name}`, {
|
|
33
|
+
schema: node,
|
|
34
|
+
path,
|
|
35
|
+
baseUri: nextBase,
|
|
36
|
+
dynamic: true,
|
|
37
|
+
anchor: name,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
for (const key in obj) {
|
|
41
|
+
const value = obj[key];
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
value.forEach((v, i) => walk(v, nextBase, [...path, key, i]));
|
|
44
|
+
}
|
|
45
|
+
else if (typeof value === "object" && value !== null) {
|
|
46
|
+
walk(value, nextBase, [...path, key]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
walk(schema, rootBaseUri, []);
|
|
51
|
+
return { registry, rootBaseUri };
|
|
52
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
export const findRefDependencies = (schema, validDefNames) => {
|
|
2
|
+
const deps = new Set();
|
|
3
|
+
function traverse(obj) {
|
|
4
|
+
if (obj === null || typeof obj !== "object")
|
|
5
|
+
return;
|
|
6
|
+
if (Array.isArray(obj)) {
|
|
7
|
+
obj.forEach(traverse);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
const record = obj;
|
|
11
|
+
if (typeof record["$ref"] === "string") {
|
|
12
|
+
const ref = record["$ref"];
|
|
13
|
+
const match = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/);
|
|
14
|
+
if (match && validDefNames.includes(match[1])) {
|
|
15
|
+
deps.add(match[1]);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
for (const value of Object.values(record)) {
|
|
19
|
+
traverse(value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
traverse(schema);
|
|
23
|
+
return deps;
|
|
24
|
+
};
|
|
25
|
+
export const detectCycles = (defNames, deps) => {
|
|
26
|
+
const cycleNodes = new Set();
|
|
27
|
+
const visited = new Set();
|
|
28
|
+
const recursionStack = new Set();
|
|
29
|
+
function dfs(node, path) {
|
|
30
|
+
if (recursionStack.has(node)) {
|
|
31
|
+
const cycleStart = path.indexOf(node);
|
|
32
|
+
for (let i = cycleStart; i < path.length; i++) {
|
|
33
|
+
cycleNodes.add(path[i]);
|
|
34
|
+
}
|
|
35
|
+
cycleNodes.add(node);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (visited.has(node))
|
|
39
|
+
return;
|
|
40
|
+
visited.add(node);
|
|
41
|
+
recursionStack.add(node);
|
|
42
|
+
const targets = deps.get(node);
|
|
43
|
+
if (targets) {
|
|
44
|
+
for (const dep of targets) {
|
|
45
|
+
dfs(dep, [...path, node]);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
recursionStack.delete(node);
|
|
49
|
+
}
|
|
50
|
+
for (const defName of defNames) {
|
|
51
|
+
if (!visited.has(defName))
|
|
52
|
+
dfs(defName, []);
|
|
53
|
+
}
|
|
54
|
+
return cycleNodes;
|
|
55
|
+
};
|
|
56
|
+
export const computeScc = (defNames, deps) => {
|
|
57
|
+
// Tarjan's algorithm, keeps mapping for quick "is this ref in my cycle?"
|
|
58
|
+
const index = new Map();
|
|
59
|
+
const lowlink = new Map();
|
|
60
|
+
const stack = [];
|
|
61
|
+
const onStack = new Set();
|
|
62
|
+
const componentByName = new Map();
|
|
63
|
+
const cycleMembers = new Set();
|
|
64
|
+
let currentIndex = 0;
|
|
65
|
+
let componentId = 0;
|
|
66
|
+
const strongConnect = (v) => {
|
|
67
|
+
index.set(v, currentIndex);
|
|
68
|
+
lowlink.set(v, currentIndex);
|
|
69
|
+
currentIndex += 1;
|
|
70
|
+
stack.push(v);
|
|
71
|
+
onStack.add(v);
|
|
72
|
+
const neighbors = deps.get(v);
|
|
73
|
+
if (neighbors) {
|
|
74
|
+
for (const w of neighbors) {
|
|
75
|
+
if (!index.has(w)) {
|
|
76
|
+
strongConnect(w);
|
|
77
|
+
lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
|
|
78
|
+
}
|
|
79
|
+
else if (onStack.has(w)) {
|
|
80
|
+
lowlink.set(v, Math.min(lowlink.get(v), index.get(w)));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (lowlink.get(v) === index.get(v)) {
|
|
85
|
+
const component = [];
|
|
86
|
+
let w;
|
|
87
|
+
do {
|
|
88
|
+
w = stack.pop();
|
|
89
|
+
if (w === undefined)
|
|
90
|
+
break;
|
|
91
|
+
onStack.delete(w);
|
|
92
|
+
component.push(w);
|
|
93
|
+
componentByName.set(w, componentId);
|
|
94
|
+
} while (w !== v);
|
|
95
|
+
if (component.length > 1) {
|
|
96
|
+
component.forEach((name) => cycleMembers.add(name));
|
|
97
|
+
}
|
|
98
|
+
componentId += 1;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
for (const name of defNames) {
|
|
102
|
+
if (!index.has(name)) {
|
|
103
|
+
strongConnect(name);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { cycleMembers, componentByName };
|
|
107
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const resolveUri = (base, ref) => {
|
|
2
|
+
try {
|
|
3
|
+
// If ref is absolute, new URL will accept it; otherwise resolves against base
|
|
4
|
+
return new URL(ref, base).toString();
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
// Fallback: simple concatenation to avoid throwing; keep ref as-is
|
|
8
|
+
if (ref.startsWith("#"))
|
|
9
|
+
return `${base}${ref}`;
|
|
10
|
+
return ref;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
@@ -4,14 +4,13 @@ export function withMessage(schema, key, get) {
|
|
|
4
4
|
if (value !== undefined) {
|
|
5
5
|
const got = get({ value, json: JSON.stringify(value) });
|
|
6
6
|
if (got) {
|
|
7
|
-
const opener = got
|
|
8
|
-
const prefix = got.length === 3 ? got[1] : "";
|
|
9
|
-
const closer = got.length === 3 ? got[2] : got[1];
|
|
7
|
+
const { opener, closer, messagePrefix = "", messageCloser } = got;
|
|
10
8
|
r += opener;
|
|
11
9
|
if (schema.errorMessage?.[key] !== undefined) {
|
|
12
|
-
r +=
|
|
10
|
+
r += messagePrefix + JSON.stringify(schema.errorMessage[key]);
|
|
11
|
+
r += messageCloser ?? closer;
|
|
12
|
+
return r;
|
|
13
13
|
}
|
|
14
|
-
r;
|
|
15
14
|
r += closer;
|
|
16
15
|
}
|
|
17
16
|
}
|
package/dist/types/Types.d.ts
CHANGED
|
@@ -94,6 +94,21 @@ export type Options = {
|
|
|
94
94
|
* @default false
|
|
95
95
|
*/
|
|
96
96
|
strictOneOf?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Called when a string format is encountered that has no built-in mapping.
|
|
99
|
+
* Can be used to log or throw on unknown formats.
|
|
100
|
+
*/
|
|
101
|
+
onUnknownFormat?: (format: string, path: (string | number)[]) => void;
|
|
102
|
+
/**
|
|
103
|
+
* Called when a $ref/$dynamicRef cannot be resolved.
|
|
104
|
+
* Can be used to log or throw on unknown references.
|
|
105
|
+
*/
|
|
106
|
+
onUnresolvedRef?: (ref: string, path: (string | number)[]) => void;
|
|
107
|
+
/**
|
|
108
|
+
* Optional resolver for external $ref URIs.
|
|
109
|
+
* Return a JsonSchema to register, or undefined if not found.
|
|
110
|
+
*/
|
|
111
|
+
resolveExternalRef?: (uri: string) => JsonSchema | Promise<JsonSchema> | undefined;
|
|
97
112
|
};
|
|
98
113
|
export type Refs = Options & {
|
|
99
114
|
path: (string | number)[];
|
|
@@ -103,10 +118,31 @@ export type Refs = Options & {
|
|
|
103
118
|
}>;
|
|
104
119
|
root?: JsonSchema;
|
|
105
120
|
declarations?: Map<string, string>;
|
|
121
|
+
dependencies?: Map<string, Set<string>>;
|
|
106
122
|
inProgress?: Set<string>;
|
|
107
123
|
refNameByPointer?: Map<string, string>;
|
|
108
124
|
usedNames?: Set<string>;
|
|
109
125
|
currentSchemaName?: string;
|
|
126
|
+
cycleRefNames?: Set<string>;
|
|
127
|
+
cycleComponentByName?: Map<string, number>;
|
|
128
|
+
/** Base URI in scope while traversing */
|
|
129
|
+
currentBaseUri?: string;
|
|
130
|
+
/** Root/base URI for the document */
|
|
131
|
+
rootBaseUri?: string;
|
|
132
|
+
/** Prebuilt registry of resolved URIs/anchors */
|
|
133
|
+
refRegistry?: Map<string, {
|
|
134
|
+
schema: JsonSchema;
|
|
135
|
+
path: (string | number)[];
|
|
136
|
+
baseUri: string;
|
|
137
|
+
dynamic?: boolean;
|
|
138
|
+
anchor?: string;
|
|
139
|
+
}>;
|
|
140
|
+
/** Stack of active dynamic anchors (nearest last) */
|
|
141
|
+
dynamicAnchors?: {
|
|
142
|
+
name: string;
|
|
143
|
+
uri: string;
|
|
144
|
+
path: (string | number)[];
|
|
145
|
+
}[];
|
|
110
146
|
};
|
|
111
147
|
export type SimpleDiscriminatedOneOfSchema<D extends string = string> = JsonSchemaObject & {
|
|
112
148
|
oneOf: (JsonSchemaObject & {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Options, JsonSchema } from "../Types.js";
|
|
2
|
+
export type NormalizedOptions = Options & {
|
|
3
|
+
exportRefs: boolean;
|
|
4
|
+
withMeta: boolean;
|
|
5
|
+
};
|
|
6
|
+
export type AnalysisResult = {
|
|
7
|
+
schema: JsonSchema;
|
|
8
|
+
options: NormalizedOptions;
|
|
9
|
+
refNameByPointer: Map<string, string>;
|
|
10
|
+
usedNames: Set<string>;
|
|
11
|
+
declarations: Map<string, string>;
|
|
12
|
+
dependencies: Map<string, Set<string>>;
|
|
13
|
+
cycleRefNames: Set<string>;
|
|
14
|
+
cycleComponentByName: Map<string, number>;
|
|
15
|
+
refRegistry: Map<string, {
|
|
16
|
+
schema: JsonSchema;
|
|
17
|
+
path: (string | number)[];
|
|
18
|
+
baseUri: string;
|
|
19
|
+
dynamic?: boolean;
|
|
20
|
+
anchor?: string;
|
|
21
|
+
}>;
|
|
22
|
+
rootBaseUri: string;
|
|
23
|
+
};
|
|
24
|
+
export declare const analyzeSchema: (schema: JsonSchema, options?: Options) => AnalysisResult;
|