@webergency-utils/typechecker 0.1.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/LICENSE +21 -0
- package/README.md +263 -0
- package/dist/engine/generators.d.ts +23 -0
- package/dist/engine/generators.js +283 -0
- package/dist/engine/hoister.d.ts +2 -0
- package/dist/engine/hoister.js +106 -0
- package/dist/engine/resolver.d.ts +7 -0
- package/dist/engine/resolver.js +882 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +2 -0
- package/dist/runtime/tags/constraint.d.ts +61 -0
- package/dist/runtime/tags/constraint.js +1 -0
- package/dist/runtime/tags/format.d.ts +15 -0
- package/dist/runtime/tags/format.js +1 -0
- package/dist/runtime/tags/tag.d.ts +3 -0
- package/dist/runtime/tags/tag.js +1 -0
- package/dist/runtime/tags/transform.d.ts +24 -0
- package/dist/runtime/tags/transform.js +1 -0
- package/dist/runtime/tags.d.ts +44 -0
- package/dist/runtime/tags.js +12 -0
- package/dist/runtime/validators.d.ts +89 -0
- package/dist/runtime/validators.js +768 -0
- package/dist/transformer.d.ts +3 -0
- package/dist/transformer.js +134 -0
- package/package.json +62 -0
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
const report = (ctx, path, expected, value, message) => {
|
|
2
|
+
ctx.success = false;
|
|
3
|
+
ctx.errors.push({ path, value, error: message || expected });
|
|
4
|
+
};
|
|
5
|
+
export const validators = {
|
|
6
|
+
string: (v, path, ctx) => {
|
|
7
|
+
if (typeof v !== 'string') {
|
|
8
|
+
report(ctx, path, 'Type<string>', v);
|
|
9
|
+
}
|
|
10
|
+
return v;
|
|
11
|
+
},
|
|
12
|
+
number: (v, path, ctx) => {
|
|
13
|
+
if (typeof v !== 'number') {
|
|
14
|
+
if (ctx.tryConvert && typeof v === 'string' && v.trim() !== '') {
|
|
15
|
+
const parsed = parseFloat(v);
|
|
16
|
+
if (!isNaN(parsed)) {
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
report(ctx, path, 'Type<number>', v);
|
|
21
|
+
}
|
|
22
|
+
return v;
|
|
23
|
+
},
|
|
24
|
+
bigint: (v, path, ctx) => {
|
|
25
|
+
if (typeof v !== 'bigint') {
|
|
26
|
+
if (ctx.tryConvert && typeof v === 'string' && v.trim() !== '') {
|
|
27
|
+
try {
|
|
28
|
+
return BigInt(v);
|
|
29
|
+
}
|
|
30
|
+
catch (e) { /* ignore */ }
|
|
31
|
+
}
|
|
32
|
+
report(ctx, path, 'Type<bigint>', v);
|
|
33
|
+
}
|
|
34
|
+
return v;
|
|
35
|
+
},
|
|
36
|
+
boolean: (v, path, ctx) => {
|
|
37
|
+
if (typeof v !== 'boolean') {
|
|
38
|
+
if (ctx.tryConvert && (v === undefined || v === null)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
if (ctx.tryConvert && (typeof v === 'string' || typeof v === 'number')) {
|
|
42
|
+
const s = String(v).toLowerCase();
|
|
43
|
+
if (s === 'true' || s === '1' || s === 'yes' || s === 'on') {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (s === 'false' || s === '0' || s === 'no' || s === 'off') {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
report(ctx, path, 'Type<boolean>', v);
|
|
51
|
+
}
|
|
52
|
+
return v;
|
|
53
|
+
},
|
|
54
|
+
date: (v, path, ctx) => {
|
|
55
|
+
if (!(v instanceof Date) || isNaN(v.getTime())) {
|
|
56
|
+
if (ctx.tryConvert && typeof v === 'string') {
|
|
57
|
+
const parsed = new Date(v);
|
|
58
|
+
if (!isNaN(parsed.getTime())) {
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
report(ctx, path, 'Type<Date>', v);
|
|
63
|
+
}
|
|
64
|
+
return v;
|
|
65
|
+
},
|
|
66
|
+
regexp: (v, path, ctx) => {
|
|
67
|
+
if (!(v instanceof RegExp)) {
|
|
68
|
+
if (ctx.tryConvert && typeof v === 'string') {
|
|
69
|
+
const match = v.match(/^\/(.*)\/([gimuy]*)$/);
|
|
70
|
+
if (match) {
|
|
71
|
+
try {
|
|
72
|
+
return new RegExp(match[1], match[2]);
|
|
73
|
+
}
|
|
74
|
+
catch (e) { }
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
try {
|
|
78
|
+
return new RegExp(v);
|
|
79
|
+
}
|
|
80
|
+
catch (e) { }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
report(ctx, path, 'Type<RegExp>', v);
|
|
84
|
+
}
|
|
85
|
+
return v;
|
|
86
|
+
},
|
|
87
|
+
null: (v, path, ctx) => {
|
|
88
|
+
if (v !== null) {
|
|
89
|
+
report(ctx, path, 'Type<null>', v);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
},
|
|
93
|
+
undefined: (v, path, ctx) => {
|
|
94
|
+
if (v !== undefined) {
|
|
95
|
+
report(ctx, path, 'Type<undefined>', v);
|
|
96
|
+
}
|
|
97
|
+
return undefined;
|
|
98
|
+
},
|
|
99
|
+
literal: (v, path, ctx, expected) => {
|
|
100
|
+
if (v !== expected) {
|
|
101
|
+
if (ctx.tryConvert && (v === undefined || v === null)) {
|
|
102
|
+
if (typeof expected === 'boolean') {
|
|
103
|
+
if (expected === false) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (ctx.tryConvert && typeof v === 'string') {
|
|
109
|
+
if (typeof expected === 'number') {
|
|
110
|
+
const p = parseFloat(v);
|
|
111
|
+
if (p === expected) {
|
|
112
|
+
return p;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (typeof expected === 'boolean') {
|
|
116
|
+
const s = v.toLowerCase();
|
|
117
|
+
let val;
|
|
118
|
+
if (s === 'true' || s === '1' || s === 'yes' || s === 'on') {
|
|
119
|
+
val = true;
|
|
120
|
+
}
|
|
121
|
+
else if (s === 'false' || s === '0' || s === 'no' || s === 'off') {
|
|
122
|
+
val = false;
|
|
123
|
+
}
|
|
124
|
+
if (val === expected) {
|
|
125
|
+
return val;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const expStr = typeof expected === 'string' ? `'${expected}'` : expected;
|
|
130
|
+
report(ctx, path, `Literal<${expStr}>`, v);
|
|
131
|
+
}
|
|
132
|
+
return v;
|
|
133
|
+
},
|
|
134
|
+
array: (v, path, ctx, childValidator) => {
|
|
135
|
+
if (!Array.isArray(v)) {
|
|
136
|
+
if (ctx.wrapArrays && v !== undefined && v !== null) {
|
|
137
|
+
v = [v];
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
report(ctx, path, 'Type<Array>', v);
|
|
141
|
+
return v;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const data = ctx.mode === 'strip' ? [] : v;
|
|
145
|
+
for (let i = 0; i < v.length; i++) {
|
|
146
|
+
const val = childValidator(v[i], path + '[' + i + ']', ctx);
|
|
147
|
+
if (ctx.mode === 'strip') {
|
|
148
|
+
data.push(val);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return data;
|
|
152
|
+
},
|
|
153
|
+
props: (v, data, path, ctx, props) => {
|
|
154
|
+
for (const [key, isOptional, validator] of props) {
|
|
155
|
+
const val = v[key];
|
|
156
|
+
const oldErrors = ctx.errors.length;
|
|
157
|
+
const result = validator(val, path + '.' + key, ctx);
|
|
158
|
+
if (ctx.success) {
|
|
159
|
+
data[key] = result;
|
|
160
|
+
}
|
|
161
|
+
else if (isOptional && val === undefined) {
|
|
162
|
+
ctx.success = true;
|
|
163
|
+
ctx.errors.length = oldErrors;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
object: (v, path, ctx, allowedKeys, expected = 'Type<Object>') => {
|
|
168
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) {
|
|
169
|
+
report(ctx, path, expected, v);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
if (ctx.mode === 'strict' && allowedKeys) {
|
|
173
|
+
for (const k of Object.keys(v)) {
|
|
174
|
+
if (!allowedKeys.includes(k)) {
|
|
175
|
+
report(ctx, path, `PropertyNotAllowed<${k}>`, v[k]);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return true;
|
|
180
|
+
},
|
|
181
|
+
templateLiteral: (v, path, ctx, regex, expected) => {
|
|
182
|
+
if (typeof v !== 'string' || !regex.test(v)) {
|
|
183
|
+
report(ctx, path, expected, v);
|
|
184
|
+
}
|
|
185
|
+
return v;
|
|
186
|
+
},
|
|
187
|
+
minLength: (v, path, ctx, min, message) => {
|
|
188
|
+
if (v.length < min) {
|
|
189
|
+
report(ctx, path, `MinLength<${min}>`, v, message);
|
|
190
|
+
}
|
|
191
|
+
return v;
|
|
192
|
+
},
|
|
193
|
+
maxLength: (v, path, ctx, max, message) => {
|
|
194
|
+
if (v.length > max) {
|
|
195
|
+
report(ctx, path, `MaxLength<${max}>`, v, message);
|
|
196
|
+
}
|
|
197
|
+
return v;
|
|
198
|
+
},
|
|
199
|
+
minimum: (v, path, ctx, min, message) => {
|
|
200
|
+
if (v < min) {
|
|
201
|
+
report(ctx, path, `Minimum<${min}>`, v, message);
|
|
202
|
+
}
|
|
203
|
+
return v;
|
|
204
|
+
},
|
|
205
|
+
maximum: (v, path, ctx, max, message) => {
|
|
206
|
+
if (v > max) {
|
|
207
|
+
report(ctx, path, `Maximum<${max}>`, v, message);
|
|
208
|
+
}
|
|
209
|
+
return v;
|
|
210
|
+
},
|
|
211
|
+
exclusiveMinimum: (v, path, ctx, min, message) => {
|
|
212
|
+
if (v <= min) {
|
|
213
|
+
report(ctx, path, `ExclusiveMinimum<${min}>`, v, message);
|
|
214
|
+
}
|
|
215
|
+
return v;
|
|
216
|
+
},
|
|
217
|
+
exclusiveMaximum: (v, path, ctx, max, message) => {
|
|
218
|
+
if (v >= max) {
|
|
219
|
+
report(ctx, path, `ExclusiveMaximum<${max}>`, v, message);
|
|
220
|
+
}
|
|
221
|
+
return v;
|
|
222
|
+
},
|
|
223
|
+
multipleOf: (v, path, ctx, n, message) => {
|
|
224
|
+
if (typeof v === 'bigint' || typeof n === 'bigint') {
|
|
225
|
+
if (BigInt(v) % BigInt(n) !== 0n) {
|
|
226
|
+
report(ctx, path, `MultipleOf<${n}>`, v, message);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
if (v % n !== 0) {
|
|
231
|
+
report(ctx, path, `MultipleOf<${n}>`, v, message);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return v;
|
|
235
|
+
},
|
|
236
|
+
pattern: (v, path, ctx, regex, expected, message) => {
|
|
237
|
+
if (!regex.test(v)) {
|
|
238
|
+
report(ctx, path, expected, v, message);
|
|
239
|
+
}
|
|
240
|
+
return v;
|
|
241
|
+
},
|
|
242
|
+
format: (v, path, ctx, format, message) => {
|
|
243
|
+
let regex;
|
|
244
|
+
let isValid = true;
|
|
245
|
+
switch (format) {
|
|
246
|
+
case 'email':
|
|
247
|
+
regex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
|
|
248
|
+
break;
|
|
249
|
+
case 'uuid':
|
|
250
|
+
regex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
251
|
+
break;
|
|
252
|
+
case 'url':
|
|
253
|
+
regex = /^(?:https?|ftp):\/\/[^\s/$.?#].[^\s]*$/i;
|
|
254
|
+
break;
|
|
255
|
+
case 'ipv4':
|
|
256
|
+
regex = /^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
|
257
|
+
break;
|
|
258
|
+
case 'ipv6':
|
|
259
|
+
regex = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/;
|
|
260
|
+
break;
|
|
261
|
+
case 'date':
|
|
262
|
+
regex = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$/;
|
|
263
|
+
break;
|
|
264
|
+
case 'date-time':
|
|
265
|
+
regex = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])[tT ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[zZ]|[+-]\d{2}:\d{2})$/;
|
|
266
|
+
break;
|
|
267
|
+
case 'byte':
|
|
268
|
+
regex = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
269
|
+
break;
|
|
270
|
+
case 'password': break; // Anything is a password
|
|
271
|
+
case 'regex':
|
|
272
|
+
try {
|
|
273
|
+
new RegExp(v);
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
isValid = false;
|
|
277
|
+
}
|
|
278
|
+
;
|
|
279
|
+
break;
|
|
280
|
+
case 'hostname':
|
|
281
|
+
regex = /^(?=.{1,253}$)(?:(?!-)[A-Za-z0-9-]{1,63}(?<!-)\.)+[A-Za-z]{2,63}$/;
|
|
282
|
+
break;
|
|
283
|
+
case 'uri':
|
|
284
|
+
regex = /^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]*$/;
|
|
285
|
+
break;
|
|
286
|
+
case 'time':
|
|
287
|
+
regex = /^\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[zZ]|[+-]\d{2}:\d{2})$/;
|
|
288
|
+
break;
|
|
289
|
+
case 'duration':
|
|
290
|
+
regex = /^P(?!$)(?:\d+Y)?(?:\d+M)?(?:\d+W)?(?:\d+D)?(?:T(?=\d)(?:\d+H)?(?:\d+M)?(?:\d+S)?)?$/;
|
|
291
|
+
break;
|
|
292
|
+
case 'objectId':
|
|
293
|
+
regex = /^[0-9a-fA-F]{24}$/;
|
|
294
|
+
break;
|
|
295
|
+
default: break;
|
|
296
|
+
}
|
|
297
|
+
if (regex && !regex.test(v)) {
|
|
298
|
+
isValid = false;
|
|
299
|
+
}
|
|
300
|
+
if (!isValid) {
|
|
301
|
+
report(ctx, path, `Format<${format}>`, v, message);
|
|
302
|
+
}
|
|
303
|
+
return v;
|
|
304
|
+
},
|
|
305
|
+
minItems: (v, path, ctx, min, message) => {
|
|
306
|
+
if (v.length < min) {
|
|
307
|
+
report(ctx, path, `MinItems<${min}>`, v, message);
|
|
308
|
+
}
|
|
309
|
+
return v;
|
|
310
|
+
},
|
|
311
|
+
maxItems: (v, path, ctx, max, message) => {
|
|
312
|
+
if (v.length > max) {
|
|
313
|
+
report(ctx, path, `MaxItems<${max}>`, v, message);
|
|
314
|
+
}
|
|
315
|
+
return v;
|
|
316
|
+
},
|
|
317
|
+
uniqueItems: (v, path, ctx, message) => {
|
|
318
|
+
const seen = new Set();
|
|
319
|
+
for (let i = 0; i < v.length; i++) {
|
|
320
|
+
const item = v[i];
|
|
321
|
+
const key = typeof item === 'object' && item !== null ? JSON.stringify(item) : item;
|
|
322
|
+
if (seen.has(key)) {
|
|
323
|
+
report(ctx, path, 'UniqueItems', v, message);
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
seen.add(key);
|
|
327
|
+
}
|
|
328
|
+
return v;
|
|
329
|
+
},
|
|
330
|
+
custom: (v, path, ctx, fn, message) => {
|
|
331
|
+
const parentPath = path.includes('.') ? path.substring(0, path.lastIndexOf('.')) : '';
|
|
332
|
+
const parent = getValueAtPath(ctx.root, parentPath);
|
|
333
|
+
const indexMatch = path.match(/\[(\d+)\]$/);
|
|
334
|
+
const index = indexMatch ? parseInt(indexMatch[1], 10) : undefined;
|
|
335
|
+
if (!fn(v, { parent, root: ctx.root, path, index })) {
|
|
336
|
+
report(ctx, path, fn.name ? `Custom<${fn.name}>` : 'Custom', v, message);
|
|
337
|
+
}
|
|
338
|
+
return v;
|
|
339
|
+
},
|
|
340
|
+
union: (v, path, ctx, checks, expected = 'Type<Union>') => {
|
|
341
|
+
// Pass 1: No conversion
|
|
342
|
+
for (const check of checks) {
|
|
343
|
+
const subCtx = { ...ctx, success: true, errors: [], tryConvert: false };
|
|
344
|
+
const val = check(v, path, subCtx);
|
|
345
|
+
if (subCtx.success) {
|
|
346
|
+
return val;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// Pass 2: With conversion
|
|
350
|
+
const unionErrors = [];
|
|
351
|
+
for (const check of checks) {
|
|
352
|
+
const subCtx = { ...ctx, success: true, errors: [], tryConvert: true };
|
|
353
|
+
const val = check(v, path, subCtx);
|
|
354
|
+
if (subCtx.success) {
|
|
355
|
+
return val;
|
|
356
|
+
}
|
|
357
|
+
unionErrors.push(...subCtx.errors);
|
|
358
|
+
}
|
|
359
|
+
ctx.success = false;
|
|
360
|
+
ctx.errors.push({
|
|
361
|
+
path,
|
|
362
|
+
value: v,
|
|
363
|
+
error: expected
|
|
364
|
+
});
|
|
365
|
+
ctx.errors.push(...unionErrors);
|
|
366
|
+
return v;
|
|
367
|
+
},
|
|
368
|
+
tuple: (v, path, ctx, checks) => {
|
|
369
|
+
if (!Array.isArray(v) || v.length !== checks.length) {
|
|
370
|
+
report(ctx, path, `Tuple<${checks.length}>`, v);
|
|
371
|
+
return v;
|
|
372
|
+
}
|
|
373
|
+
const data = ctx.mode === 'strip' ? [] : v;
|
|
374
|
+
for (let i = 0; i < checks.length; i++) {
|
|
375
|
+
const val = checks[i](v[i], path + '[' + i + ']', ctx);
|
|
376
|
+
if (ctx.mode === 'strip') {
|
|
377
|
+
data.push(val);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return data;
|
|
381
|
+
},
|
|
382
|
+
any: (v) => v,
|
|
383
|
+
requires: (v, path, ctx, reqs, message) => {
|
|
384
|
+
if (v === undefined || v === null) {
|
|
385
|
+
return v;
|
|
386
|
+
}
|
|
387
|
+
for (const r of reqs) {
|
|
388
|
+
const resolved = resolvePath(path, r);
|
|
389
|
+
if (!hasPath(ctx.root, resolved)) {
|
|
390
|
+
report(ctx, path, `Requires<${r}>`, v, message);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return v;
|
|
394
|
+
},
|
|
395
|
+
record: (v, path, ctx, childValidator) => {
|
|
396
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) {
|
|
397
|
+
report(ctx, path, 'Type<Object>', v);
|
|
398
|
+
return v;
|
|
399
|
+
}
|
|
400
|
+
const data = ctx.mode === 'strip' ? {} : v;
|
|
401
|
+
for (const key of Object.keys(v)) {
|
|
402
|
+
const val = childValidator(v[key], path + '.' + key, ctx);
|
|
403
|
+
if (ctx.mode === 'strip') {
|
|
404
|
+
data[key] = val;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return data;
|
|
408
|
+
},
|
|
409
|
+
set: (v, path, ctx, childValidator, message) => {
|
|
410
|
+
if (!(v instanceof Set)) {
|
|
411
|
+
if (ctx.tryConvert && Array.isArray(v)) {
|
|
412
|
+
v = new Set(v);
|
|
413
|
+
}
|
|
414
|
+
else if (ctx.tryConvert && v !== undefined && v !== null) {
|
|
415
|
+
v = new Set([v]);
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
report(ctx, path, 'Type<Set>', v, message);
|
|
419
|
+
return v;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
const data = ctx.mode === 'strip' ? new Set() : v;
|
|
423
|
+
let index = 0;
|
|
424
|
+
for (const item of v) {
|
|
425
|
+
const val = childValidator(item, `${path}[${index}]`, ctx);
|
|
426
|
+
if (ctx.mode === 'strip') {
|
|
427
|
+
data.add(val);
|
|
428
|
+
}
|
|
429
|
+
index++;
|
|
430
|
+
}
|
|
431
|
+
return data;
|
|
432
|
+
},
|
|
433
|
+
map: (v, path, ctx, keyValidator, valueValidator, message) => {
|
|
434
|
+
if (!(v instanceof Map)) {
|
|
435
|
+
if (ctx.tryConvert && typeof v === 'object' && v !== null && !Array.isArray(v)) {
|
|
436
|
+
v = new Map(Object.entries(v));
|
|
437
|
+
}
|
|
438
|
+
else {
|
|
439
|
+
report(ctx, path, 'Type<Map>', v, message);
|
|
440
|
+
return v;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const data = ctx.mode === 'strip' ? new Map() : v;
|
|
444
|
+
for (const [key, val] of v.entries()) {
|
|
445
|
+
const validatedKey = keyValidator(key, `${path}.key(${JSON.stringify(key)})`, ctx);
|
|
446
|
+
const validatedVal = valueValidator(val, `${path}[${JSON.stringify(key)}]`, ctx);
|
|
447
|
+
if (ctx.mode === 'strip') {
|
|
448
|
+
data.set(validatedKey, validatedVal);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return data;
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
function resolvePath(currentPath, targetPath) {
|
|
455
|
+
if (!targetPath.startsWith('.')) {
|
|
456
|
+
return targetPath;
|
|
457
|
+
}
|
|
458
|
+
const dotsMatch = targetPath.match(/^\.+/);
|
|
459
|
+
const dots = dotsMatch ? dotsMatch[0].length : 0;
|
|
460
|
+
const targetClean = targetPath.substring(dots);
|
|
461
|
+
const cleanCurrentPath = currentPath.startsWith('.') ? currentPath.substring(1) : currentPath;
|
|
462
|
+
const currentParts = cleanCurrentPath ? cleanCurrentPath.split('.') : [];
|
|
463
|
+
const baseParts = currentParts.slice(0, currentParts.length - dots);
|
|
464
|
+
if (targetClean) {
|
|
465
|
+
baseParts.push(targetClean);
|
|
466
|
+
}
|
|
467
|
+
return baseParts.join('.');
|
|
468
|
+
}
|
|
469
|
+
function getValueAtPath(obj, path) {
|
|
470
|
+
if (!obj || typeof obj !== 'object') {
|
|
471
|
+
return undefined;
|
|
472
|
+
}
|
|
473
|
+
const cleanPath = path.startsWith('.') ? path.substring(1) : path;
|
|
474
|
+
if (!cleanPath) {
|
|
475
|
+
return obj;
|
|
476
|
+
}
|
|
477
|
+
const parts = cleanPath.split('.');
|
|
478
|
+
let current = obj;
|
|
479
|
+
for (const part of parts) {
|
|
480
|
+
if (current === null || current === undefined || typeof current !== 'object') {
|
|
481
|
+
return undefined;
|
|
482
|
+
}
|
|
483
|
+
const cleanPart = part.replace(/\[\d+\]/g, '');
|
|
484
|
+
current = current[cleanPart];
|
|
485
|
+
}
|
|
486
|
+
return current;
|
|
487
|
+
}
|
|
488
|
+
function hasPath(obj, path) {
|
|
489
|
+
const val = getValueAtPath(obj, path);
|
|
490
|
+
return val !== undefined && val !== null;
|
|
491
|
+
}
|
|
492
|
+
export class MetadataStoreClass {
|
|
493
|
+
validators = new Map();
|
|
494
|
+
schemas = new Map();
|
|
495
|
+
compiledSchemas = new WeakMap();
|
|
496
|
+
registerValidator(hash, validator) {
|
|
497
|
+
this.validators.set(hash, validator);
|
|
498
|
+
}
|
|
499
|
+
getValidator(hash) {
|
|
500
|
+
const val = this.validators.get(hash);
|
|
501
|
+
if (!val) {
|
|
502
|
+
throw new Error(`Validator not found for hash: ${hash}`);
|
|
503
|
+
}
|
|
504
|
+
return val;
|
|
505
|
+
}
|
|
506
|
+
registerSchema(hash, schema) {
|
|
507
|
+
this.schemas.set(hash, schema);
|
|
508
|
+
}
|
|
509
|
+
getSchema(hash) {
|
|
510
|
+
const schema = this.schemas.get(hash);
|
|
511
|
+
if (!schema) {
|
|
512
|
+
throw new Error(`Schema not found for hash: ${hash}`);
|
|
513
|
+
}
|
|
514
|
+
return schema;
|
|
515
|
+
}
|
|
516
|
+
getOrCompileSchema(schema) {
|
|
517
|
+
if (typeof schema !== 'object' || schema === null) {
|
|
518
|
+
throw new Error('Invalid JSON Schema: must be a non-null object');
|
|
519
|
+
}
|
|
520
|
+
let compiled = this.compiledSchemas.get(schema);
|
|
521
|
+
if (!compiled) {
|
|
522
|
+
compiled = compileSchema(schema);
|
|
523
|
+
this.compiledSchemas.set(schema, compiled);
|
|
524
|
+
}
|
|
525
|
+
return compiled;
|
|
526
|
+
}
|
|
527
|
+
is(validator, value, options) {
|
|
528
|
+
const opt = options;
|
|
529
|
+
const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
|
|
530
|
+
const tryConvert = typeof opt === 'object' ? opt?.tryConvert : undefined;
|
|
531
|
+
const wrapArrays = typeof opt === 'object' ? opt?.wrapArrays : undefined;
|
|
532
|
+
const ctx = { success: true, errors: [], mode, tryConvert, wrapArrays, root: value };
|
|
533
|
+
validator(value, '', ctx);
|
|
534
|
+
return ctx.success;
|
|
535
|
+
}
|
|
536
|
+
assert(validator, value, options) {
|
|
537
|
+
const opt = options;
|
|
538
|
+
const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
|
|
539
|
+
const tryConvert = typeof opt === 'object' ? opt?.tryConvert : undefined;
|
|
540
|
+
const wrapArrays = typeof opt === 'object' ? opt?.wrapArrays : undefined;
|
|
541
|
+
const ctx = { success: true, errors: [], mode, tryConvert, wrapArrays, root: value };
|
|
542
|
+
const res = validator(value, '', ctx);
|
|
543
|
+
if (!ctx.success) {
|
|
544
|
+
if (typeof opt === 'object' && opt?.errorFactory) {
|
|
545
|
+
throw opt.errorFactory(ctx.errors);
|
|
546
|
+
}
|
|
547
|
+
throw new Error('Validation Error: ' + ctx.errors.map(e => e.path ? `${e.path}: ${e.error}` : e.error).join(', '));
|
|
548
|
+
}
|
|
549
|
+
return res;
|
|
550
|
+
}
|
|
551
|
+
validate(validator, value, options) {
|
|
552
|
+
const opt = options;
|
|
553
|
+
const mode = typeof opt === 'string' ? opt : (opt?.mode || 'strict');
|
|
554
|
+
const tryConvert = typeof opt === 'object' ? opt?.tryConvert : undefined;
|
|
555
|
+
const wrapArrays = typeof opt === 'object' ? opt?.wrapArrays : undefined;
|
|
556
|
+
const ctx = { success: true, errors: [], mode, tryConvert, wrapArrays, root: value };
|
|
557
|
+
const res = validator(value, '', ctx);
|
|
558
|
+
return { success: ctx.success, errors: ctx.errors, data: res };
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
export function groupErrorsByPath(errors) {
|
|
562
|
+
const grouped = {};
|
|
563
|
+
for (const err of errors) {
|
|
564
|
+
if (!grouped[err.path]) {
|
|
565
|
+
grouped[err.path] = { value: err.value, errors: [] };
|
|
566
|
+
}
|
|
567
|
+
if (!grouped[err.path].errors.includes(err.error)) {
|
|
568
|
+
grouped[err.path].errors.push(err.error);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return grouped;
|
|
572
|
+
}
|
|
573
|
+
export const MetadataStore = new MetadataStoreClass();
|
|
574
|
+
export function compileSchema(schema) {
|
|
575
|
+
const rootDefs = schema.$defs || schema.definitions || {};
|
|
576
|
+
const compiledDefs = new Map();
|
|
577
|
+
function build(subSchema) {
|
|
578
|
+
if (!subSchema || typeof subSchema !== 'object') {
|
|
579
|
+
return (v) => v;
|
|
580
|
+
}
|
|
581
|
+
if (subSchema.$ref) {
|
|
582
|
+
const refPath = subSchema.$ref;
|
|
583
|
+
if (compiledDefs.has(refPath)) {
|
|
584
|
+
return (v, path, ctx) => compiledDefs.get(refPath)(v, path, ctx);
|
|
585
|
+
}
|
|
586
|
+
const parts = refPath.split('/');
|
|
587
|
+
const defName = parts[parts.length - 1];
|
|
588
|
+
const targetSchema = rootDefs[defName];
|
|
589
|
+
if (!targetSchema) {
|
|
590
|
+
throw new Error(`Schema reference not found: ${refPath}`);
|
|
591
|
+
}
|
|
592
|
+
let resolved = null;
|
|
593
|
+
const proxy = (v, path, ctx) => {
|
|
594
|
+
if (!resolved) {
|
|
595
|
+
resolved = build(targetSchema);
|
|
596
|
+
}
|
|
597
|
+
return resolved(v, path, ctx);
|
|
598
|
+
};
|
|
599
|
+
compiledDefs.set(refPath, proxy);
|
|
600
|
+
return proxy;
|
|
601
|
+
}
|
|
602
|
+
if (subSchema.type === 'string') {
|
|
603
|
+
const minLength = subSchema.minLength;
|
|
604
|
+
const maxLength = subSchema.maxLength;
|
|
605
|
+
const pattern = subSchema.pattern ? new RegExp(subSchema.pattern) : undefined;
|
|
606
|
+
const patternStr = subSchema.pattern;
|
|
607
|
+
const format = subSchema.format;
|
|
608
|
+
return (v, path, ctx) => {
|
|
609
|
+
v = validators.string(v, path, ctx);
|
|
610
|
+
if (v === undefined || v === null) {
|
|
611
|
+
return v;
|
|
612
|
+
}
|
|
613
|
+
if (minLength !== undefined) {
|
|
614
|
+
validators.minLength(v, path, ctx, minLength);
|
|
615
|
+
}
|
|
616
|
+
if (maxLength !== undefined) {
|
|
617
|
+
validators.maxLength(v, path, ctx, maxLength);
|
|
618
|
+
}
|
|
619
|
+
if (pattern !== undefined) {
|
|
620
|
+
validators.pattern(v, path, ctx, pattern, patternStr);
|
|
621
|
+
}
|
|
622
|
+
if (format !== undefined) {
|
|
623
|
+
validators.format(v, path, ctx, format);
|
|
624
|
+
}
|
|
625
|
+
return v;
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
if (subSchema.type === 'number' || subSchema.type === 'integer') {
|
|
629
|
+
const isInt = subSchema.type === 'integer';
|
|
630
|
+
const minimum = subSchema.minimum;
|
|
631
|
+
const maximum = subSchema.maximum;
|
|
632
|
+
const multipleOf = subSchema.multipleOf;
|
|
633
|
+
return (v, path, ctx) => {
|
|
634
|
+
v = validators.number(v, path, ctx);
|
|
635
|
+
if (v === undefined || v === null) {
|
|
636
|
+
return v;
|
|
637
|
+
}
|
|
638
|
+
if (isInt && typeof v === 'number' && !Number.isInteger(v)) {
|
|
639
|
+
report(ctx, path, 'Type<integer>', v);
|
|
640
|
+
}
|
|
641
|
+
if (minimum !== undefined) {
|
|
642
|
+
validators.minimum(v, path, ctx, minimum);
|
|
643
|
+
}
|
|
644
|
+
if (maximum !== undefined) {
|
|
645
|
+
validators.maximum(v, path, ctx, maximum);
|
|
646
|
+
}
|
|
647
|
+
if (multipleOf !== undefined) {
|
|
648
|
+
validators.multipleOf(v, path, ctx, multipleOf);
|
|
649
|
+
}
|
|
650
|
+
return v;
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
if (subSchema.type === 'boolean') {
|
|
654
|
+
return (v, path, ctx) => validators.boolean(v, path, ctx);
|
|
655
|
+
}
|
|
656
|
+
if (subSchema.type === 'null') {
|
|
657
|
+
return (v, path, ctx) => validators.null(v, path, ctx);
|
|
658
|
+
}
|
|
659
|
+
if (subSchema.anyOf) {
|
|
660
|
+
const checks = subSchema.anyOf.map((s) => build(s));
|
|
661
|
+
return (v, path, ctx) => validators.union(v, path, ctx, checks);
|
|
662
|
+
}
|
|
663
|
+
if (subSchema.type === 'array') {
|
|
664
|
+
if (Array.isArray(subSchema.items)) {
|
|
665
|
+
const checks = subSchema.items.map((s) => build(s));
|
|
666
|
+
return (v, path, ctx) => validators.tuple(v, path, ctx, checks);
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
const check = build(subSchema.items);
|
|
670
|
+
const minItems = subSchema.minItems;
|
|
671
|
+
const maxItems = subSchema.maxItems;
|
|
672
|
+
const uniqueItems = subSchema.uniqueItems;
|
|
673
|
+
return (v, path, ctx) => {
|
|
674
|
+
v = validators.array(v, path, ctx, check);
|
|
675
|
+
if (Array.isArray(v)) {
|
|
676
|
+
if (minItems !== undefined) {
|
|
677
|
+
validators.minItems(v, path, ctx, minItems);
|
|
678
|
+
}
|
|
679
|
+
if (maxItems !== undefined) {
|
|
680
|
+
validators.maxItems(v, path, ctx, maxItems);
|
|
681
|
+
}
|
|
682
|
+
if (uniqueItems) {
|
|
683
|
+
validators.uniqueItems(v, path, ctx);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return v;
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (subSchema.type === 'object') {
|
|
691
|
+
const props = Object.entries(subSchema.properties || {});
|
|
692
|
+
const required = subSchema.required || [];
|
|
693
|
+
const propVals = props.map(([key, s]) => {
|
|
694
|
+
const isOptional = !required.includes(key);
|
|
695
|
+
const check = build(s);
|
|
696
|
+
return [key, isOptional, check];
|
|
697
|
+
});
|
|
698
|
+
const allowedKeys = Object.keys(subSchema.properties || {});
|
|
699
|
+
return (v, path, ctx) => {
|
|
700
|
+
if (!validators.object(v, path, ctx, allowedKeys, 'Object')) {
|
|
701
|
+
return v;
|
|
702
|
+
}
|
|
703
|
+
let data = v;
|
|
704
|
+
if (ctx.mode === 'strip') {
|
|
705
|
+
let hasAdditional = false;
|
|
706
|
+
const keys = Object.keys(v);
|
|
707
|
+
if (keys.length > allowedKeys.length) {
|
|
708
|
+
hasAdditional = true;
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
for (let i = 0; i < keys.length; i++) {
|
|
712
|
+
if (!allowedKeys.includes(keys[i])) {
|
|
713
|
+
hasAdditional = true;
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
if (hasAdditional) {
|
|
719
|
+
data = {};
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
validators.props(v, data, path, ctx, propVals);
|
|
723
|
+
return data;
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
if (subSchema.const !== undefined) {
|
|
727
|
+
const expected = subSchema.const;
|
|
728
|
+
return (v, path, ctx) => {
|
|
729
|
+
if (v !== expected) {
|
|
730
|
+
report(ctx, path, `Const<${JSON.stringify(expected)}>`, v);
|
|
731
|
+
}
|
|
732
|
+
return v;
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
return (v) => v;
|
|
736
|
+
}
|
|
737
|
+
return build(schema);
|
|
738
|
+
}
|
|
739
|
+
export function toZodIssues(errors) {
|
|
740
|
+
return errors.map((err) => {
|
|
741
|
+
const zodPath = err.path
|
|
742
|
+
.split(/\.|\[|\]/)
|
|
743
|
+
.filter(Boolean)
|
|
744
|
+
.map((segment) => {
|
|
745
|
+
if (isNaN(Number(segment))) {
|
|
746
|
+
return segment;
|
|
747
|
+
}
|
|
748
|
+
return Number(segment);
|
|
749
|
+
});
|
|
750
|
+
const issue = {
|
|
751
|
+
code: 'custom',
|
|
752
|
+
path: zodPath,
|
|
753
|
+
message: err.error,
|
|
754
|
+
received: err.value
|
|
755
|
+
};
|
|
756
|
+
return issue;
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
export class ZodLikeError extends Error {
|
|
760
|
+
issues;
|
|
761
|
+
constructor(errors) {
|
|
762
|
+
super('Validation failed');
|
|
763
|
+
this.name = 'ZodError';
|
|
764
|
+
this.issues = toZodIssues(errors);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
globalThis.__WEBERGENCY_TYPECHECKER_METADATA_STORE__ = MetadataStore;
|
|
768
|
+
globalThis.__WEBERGENCY_TYPECHECKER_VALIDATORS__ = validators;
|