ascertain 3.0.0 → 3.2.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/build/index.cjs +188 -0
- package/build/index.cjs.map +1 -1
- package/build/index.d.ts +52 -0
- package/build/index.js +152 -0
- package/build/index.js.map +1 -1
- package/package.json +16 -15
- package/src/index.ts +183 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ascertain",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "0-Deps, simple, fast, for browser and node js object schema validator",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -39,30 +39,31 @@
|
|
|
39
39
|
"homepage": "https://github.com/3axap4eHko/ascertain#readme",
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@eslint/js": "^10.0.1",
|
|
42
|
-
"@types/node": "^25.
|
|
43
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
44
|
-
"@typescript-eslint/parser": "^8.
|
|
45
|
-
"@typescript-eslint/typescript-estree": "^8.
|
|
46
|
-
"@vitest/coverage-v8": "^4.0
|
|
47
|
-
"@vuepress/bundler-vite": "2.0.0-rc.
|
|
48
|
-
"@vuepress/theme-default": "2.0.0-rc.
|
|
42
|
+
"@types/node": "^25.5.0",
|
|
43
|
+
"@typescript-eslint/eslint-plugin": "^8.57.1",
|
|
44
|
+
"@typescript-eslint/parser": "^8.57.1",
|
|
45
|
+
"@typescript-eslint/typescript-estree": "^8.57.1",
|
|
46
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
47
|
+
"@vuepress/bundler-vite": "2.0.0-rc.27",
|
|
48
|
+
"@vuepress/theme-default": "2.0.0-rc.125",
|
|
49
|
+
"ajv": "^8.18.0",
|
|
49
50
|
"ascertain": "latest",
|
|
50
|
-
"eslint": "^10.0.
|
|
51
|
+
"eslint": "^10.0.3",
|
|
51
52
|
"eslint-config-prettier": "^10.1.8",
|
|
52
53
|
"eslint-plugin-prettier": "^5.5.5",
|
|
53
54
|
"handlebars": "^4.7.8",
|
|
54
55
|
"husky": "^9.1.7",
|
|
55
56
|
"inop": "^0.9.0",
|
|
56
|
-
"overtake": "^1.
|
|
57
|
+
"overtake": "^1.4.0",
|
|
57
58
|
"prettier": "^3.8.1",
|
|
58
59
|
"recast": "^0.23.11",
|
|
59
|
-
"sass-embedded": "^1.
|
|
60
|
+
"sass-embedded": "^1.98.0",
|
|
60
61
|
"ts-node": "^10.9.2",
|
|
61
62
|
"typescript": "^5.9.3",
|
|
62
|
-
"typescript-eslint": "^8.
|
|
63
|
-
"vitest": "^4.0
|
|
64
|
-
"vue": "^3.5.
|
|
65
|
-
"vuepress": "2.0.0-rc.
|
|
63
|
+
"typescript-eslint": "^8.57.1",
|
|
64
|
+
"vitest": "^4.1.0",
|
|
65
|
+
"vue": "^3.5.30",
|
|
66
|
+
"vuepress": "2.0.0-rc.27",
|
|
66
67
|
"zod": "^4.3.6"
|
|
67
68
|
},
|
|
68
69
|
"scripts": {
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,7 @@ const AND = Symbol.for('@@and');
|
|
|
18
18
|
const OPTIONAL = Symbol.for('@@optional');
|
|
19
19
|
const TUPLE = Symbol.for('@@tuple');
|
|
20
20
|
const DISCRIMINATED = Symbol.for('@@discriminated');
|
|
21
|
+
const CHECK = Symbol.for('@@check');
|
|
21
22
|
|
|
22
23
|
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
|
|
23
24
|
|
|
@@ -42,8 +43,15 @@ interface DiscriminatedShape<T> {
|
|
|
42
43
|
readonly [$op]: typeof DISCRIMINATED;
|
|
43
44
|
readonly key: string;
|
|
44
45
|
}
|
|
46
|
+
export interface CheckContext {
|
|
47
|
+
ref(value: unknown): string;
|
|
48
|
+
}
|
|
49
|
+
interface CheckShape {
|
|
50
|
+
readonly [$op]: typeof CHECK;
|
|
51
|
+
readonly compile: (value: string, ctx: CheckContext) => { check: string; message: string };
|
|
52
|
+
}
|
|
45
53
|
|
|
46
|
-
type Tagged<T> = OrShape<T> | AndShape<T> | OptionalShape<T> | TupleShape<T> | DiscriminatedShape<T
|
|
54
|
+
type Tagged<T> = OrShape<T> | AndShape<T> | OptionalShape<T> | TupleShape<T> | DiscriminatedShape<T> | CheckShape;
|
|
47
55
|
|
|
48
56
|
const OrCtor = function <T>(this: OrShape<T>, schemas: Schema<T>[]) {
|
|
49
57
|
(this as Mutable<OrShape<T>>).schemas = schemas;
|
|
@@ -71,6 +79,11 @@ const DiscriminatedCtor = function <T>(this: DiscriminatedShape<T>, schemas: Sch
|
|
|
71
79
|
} as unknown as { new <T>(schemas: Schema<T>[], key: string): DiscriminatedShape<T>; prototype: { [$op]: typeof DISCRIMINATED } };
|
|
72
80
|
DiscriminatedCtor.prototype[$op] = DISCRIMINATED;
|
|
73
81
|
|
|
82
|
+
const CheckCtor = function (this: CheckShape, compileFn: CheckShape['compile']) {
|
|
83
|
+
(this as Mutable<CheckShape>).compile = compileFn;
|
|
84
|
+
} as unknown as { new (compileFn: CheckShape['compile']): CheckShape; prototype: { [$op]: typeof CHECK } };
|
|
85
|
+
CheckCtor.prototype[$op] = CHECK;
|
|
86
|
+
|
|
74
87
|
/**
|
|
75
88
|
* Represents a schema for validating data.
|
|
76
89
|
*
|
|
@@ -138,6 +151,86 @@ export const discriminated = <T>(schemas: Schema<T>[], key: string): Discriminat
|
|
|
138
151
|
return new DiscriminatedCtor(schemas, key);
|
|
139
152
|
};
|
|
140
153
|
|
|
154
|
+
export const check = (
|
|
155
|
+
fnOrOpts: ((v: unknown) => boolean) | { compile: (value: string, ctx: CheckContext) => { check: string; message: string } },
|
|
156
|
+
message?: string,
|
|
157
|
+
): CheckShape => {
|
|
158
|
+
if (typeof fnOrOpts === 'function') {
|
|
159
|
+
return new CheckCtor((v, ctx) => {
|
|
160
|
+
const fnRef = ctx.ref(fnOrOpts);
|
|
161
|
+
return {
|
|
162
|
+
check: `!${fnRef}(${v})`,
|
|
163
|
+
message: message ? JSON.stringify(message) : `\`check failed for value \${${v}}\``,
|
|
164
|
+
};
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
return new CheckCtor(fnOrOpts.compile);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const min = (n: number, message?: string): CheckShape =>
|
|
171
|
+
new CheckCtor((v) => ({
|
|
172
|
+
check: `${v} < ${n}`,
|
|
173
|
+
message: message ? JSON.stringify(message) : `\`must be >= ${n}, got \${${v}}\``,
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
export const max = (n: number, message?: string): CheckShape =>
|
|
177
|
+
new CheckCtor((v) => ({
|
|
178
|
+
check: `${v} > ${n}`,
|
|
179
|
+
message: message ? JSON.stringify(message) : `\`must be <= ${n}, got \${${v}}\``,
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
export const integer = (message?: string): CheckShape =>
|
|
183
|
+
new CheckCtor((v) => ({
|
|
184
|
+
check: `!Number.isInteger(${v})`,
|
|
185
|
+
message: message ? JSON.stringify(message) : `\`must be an integer, got \${${v}}\``,
|
|
186
|
+
}));
|
|
187
|
+
|
|
188
|
+
export const minLength = (n: number, message?: string): CheckShape =>
|
|
189
|
+
new CheckCtor((v) => ({
|
|
190
|
+
check: `${v}.length < ${n}`,
|
|
191
|
+
message: message ? JSON.stringify(message) : `\`length must be >= ${n}, got \${${v}.length}\``,
|
|
192
|
+
}));
|
|
193
|
+
|
|
194
|
+
export const maxLength = (n: number, message?: string): CheckShape =>
|
|
195
|
+
new CheckCtor((v) => ({
|
|
196
|
+
check: `${v}.length > ${n}`,
|
|
197
|
+
message: message ? JSON.stringify(message) : `\`length must be <= ${n}, got \${${v}.length}\``,
|
|
198
|
+
}));
|
|
199
|
+
|
|
200
|
+
export const gt = (n: number, message?: string): CheckShape =>
|
|
201
|
+
new CheckCtor((v) => ({
|
|
202
|
+
check: `${v} <= ${n}`,
|
|
203
|
+
message: message ? JSON.stringify(message) : `\`must be > ${n}, got \${${v}}\``,
|
|
204
|
+
}));
|
|
205
|
+
|
|
206
|
+
export const lt = (n: number, message?: string): CheckShape =>
|
|
207
|
+
new CheckCtor((v) => ({
|
|
208
|
+
check: `${v} >= ${n}`,
|
|
209
|
+
message: message ? JSON.stringify(message) : `\`must be < ${n}, got \${${v}}\``,
|
|
210
|
+
}));
|
|
211
|
+
|
|
212
|
+
export const multipleOf = (n: number, message?: string): CheckShape =>
|
|
213
|
+
new CheckCtor((v) => ({
|
|
214
|
+
check: `${v} % ${n} !== 0`,
|
|
215
|
+
message: message ? JSON.stringify(message) : `\`must be a multiple of ${n}, got \${${v}}\``,
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
export const uniqueItems = (message?: string): CheckShape =>
|
|
219
|
+
new CheckCtor((v) => ({
|
|
220
|
+
check: `new Set(${v}).size !== ${v}.length`,
|
|
221
|
+
message: message ? JSON.stringify(message) : `\`must have unique items\``,
|
|
222
|
+
}));
|
|
223
|
+
|
|
224
|
+
type EnumLike = { [k: string]: string | number; [n: number]: string };
|
|
225
|
+
|
|
226
|
+
export const oneOf = <T extends EnumLike>(values: (string | number)[] | T, message?: string): CheckShape => {
|
|
227
|
+
const set = new Set(Array.isArray(values) ? values : Object.values(values));
|
|
228
|
+
return new CheckCtor((v, ctx) => ({
|
|
229
|
+
check: `!${ctx.ref(set)}.has(${v})`,
|
|
230
|
+
message: message ? JSON.stringify(message) : `\`must be one of [${[...set].map(toLiteral).join(', ')}], got \${${v}}\``,
|
|
231
|
+
}));
|
|
232
|
+
};
|
|
233
|
+
|
|
141
234
|
/**
|
|
142
235
|
* Decodes a base64-encoded string to UTF-8.
|
|
143
236
|
*
|
|
@@ -295,6 +388,87 @@ export const as = {
|
|
|
295
388
|
},
|
|
296
389
|
};
|
|
297
390
|
|
|
391
|
+
const DATETIME_RE = /^\d{4}-[01]\d-[0-3]\d[t\s](?:[0-2]\d:[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d{2}(?::?\d{2})?)$/i;
|
|
392
|
+
const TIME_FMT_RE = /^(?:(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d|23:59:60)(?:\.\d+)?(?:z|[+-]\d{2}(?::?\d{2})?)$/i;
|
|
393
|
+
const DURATION_RE = /^P(?!$)(\d+Y)?(\d+M)?(\d+W)?(\d+D)?(T(?=\d)(\d+H)?(\d+M)?(\d+S)?)?$/;
|
|
394
|
+
const EMAIL_RE = /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i;
|
|
395
|
+
const IDN_EMAIL_RE =
|
|
396
|
+
/^[a-z0-9!#$%&'*+/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF-]+)*@(?:[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF](?:[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF-]*[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?\.)+[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF](?:[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF-]*[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?$/i;
|
|
397
|
+
const HOSTNAME_RE = /^(?=.{1,253}\.?$)[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*\.?$/i;
|
|
398
|
+
const IDN_HOSTNAME_RE =
|
|
399
|
+
/^(?=.{1,253}\.?$)[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF](?:[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF-]{0,61}[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?(?:\.[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF](?:[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF-]{0,61}[a-z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])?)*\.?$/i;
|
|
400
|
+
const IPV4_RE = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
|
|
401
|
+
const IPV6_RE =
|
|
402
|
+
/^((([0-9a-f]{1,4}:){7}([0-9a-f]{1,4}|:))|(([0-9a-f]{1,4}:){6}(:[0-9a-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){5}(((:[0-9a-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9a-f]{1,4}:){4}(((:[0-9a-f]{1,4}){1,3})|((:[0-9a-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){3}(((:[0-9a-f]{1,4}){1,4})|((:[0-9a-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){2}(((:[0-9a-f]{1,4}){1,5})|((:[0-9a-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9a-f]{1,4}:){1}(((:[0-9a-f]{1,4}){1,6})|((:[0-9a-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9a-f]{1,4}){1,7})|((:[0-9a-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?$/i;
|
|
403
|
+
const URI_RE =
|
|
404
|
+
/^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?]|%[0-9a-f]{2})*)?$/i;
|
|
405
|
+
const isUriRef = (s: string): boolean => URI_RE.test(s) || /^[a-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]*$/i.test(s);
|
|
406
|
+
const IRI_RE =
|
|
407
|
+
/^(?:[a-z][a-z0-9+\-.]*:)(?:\/?\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|%[0-9a-f]{2})*@)?(?:\[(?:(?:(?:(?:[0-9a-f]{1,4}:){6}|::(?:[0-9a-f]{1,4}:){5}|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}|(?:(?:[0-9a-f]{1,4}:){0,1}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::)(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?))|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|[Vv][0-9a-f]+\.[a-z0-9\-._~!$&'()*+,;=:]+)\]|(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)|(?:[a-z0-9\-._~!$&'()*+,;=\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|%[0-9a-f]{2})*)(?::\d*)?(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|%[0-9a-f]{2})*)*|\/(?:(?:[a-z0-9\-._~!$&'()*+,;=:@\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|%[0-9a-f]{2})*)*)?|(?:[a-z0-9\-._~!$&'()*+,;=:@\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|%[0-9a-f]{2})+(?:\/(?:[a-z0-9\-._~!$&'()*+,;=:@\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|%[0-9a-f]{2})*)*)(?:\?(?:[a-z0-9\-._~!$&'()*+,;=:@/?\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|%[0-9a-f]{2})*)?(?:#(?:[a-z0-9\-._~!$&'()*+,;=:@/?\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]|%[0-9a-f]{2})*)?$/i;
|
|
408
|
+
const isIriRef = (s: string): boolean => IRI_RE.test(s) || /^[a-z0-9\-._~:/?#\[\]@!$&'()*+,;=\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF%]*$/i.test(s);
|
|
409
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
410
|
+
const URI_TEMPLATE_RE =
|
|
411
|
+
/^(?:(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i;
|
|
412
|
+
const JSON_POINTER_RE = /^(?:\/(?:[^~/]|~0|~1)*)*$/;
|
|
413
|
+
const REL_JSON_POINTER_RE = /^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/;
|
|
414
|
+
|
|
415
|
+
const DAYS = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
416
|
+
const isValidDate = (s: string): boolean => {
|
|
417
|
+
const m = /^\d{4}-(\d{2})-(\d{2})$/.exec(s);
|
|
418
|
+
if (!m) return false;
|
|
419
|
+
const month = +m[1],
|
|
420
|
+
day = +m[2];
|
|
421
|
+
if (month < 1 || month > 12 || day < 1) return false;
|
|
422
|
+
if (month === 2) {
|
|
423
|
+
const y = +s.slice(0, 4);
|
|
424
|
+
return day <= (y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0) ? 29 : 28);
|
|
425
|
+
}
|
|
426
|
+
return day <= DAYS[month];
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const isValidRegex = (s: string): boolean => {
|
|
430
|
+
try {
|
|
431
|
+
new RegExp(s);
|
|
432
|
+
return true;
|
|
433
|
+
} catch {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const regexFormat = (re: RegExp, name: string, message?: string): CheckShape =>
|
|
439
|
+
new CheckCtor((v, ctx) => ({
|
|
440
|
+
check: `!${ctx.ref(re)}.test(${v})`,
|
|
441
|
+
message: message ? JSON.stringify(message) : `\`must be a valid ${name}, got \${${v}}\``,
|
|
442
|
+
}));
|
|
443
|
+
|
|
444
|
+
const fnFormat = (fn: (s: string) => boolean, name: string, message?: string): CheckShape =>
|
|
445
|
+
new CheckCtor((v, ctx) => ({
|
|
446
|
+
check: `!${ctx.ref(fn)}(${v})`,
|
|
447
|
+
message: message ? JSON.stringify(message) : `\`must be a valid ${name}, got \${${v}}\``,
|
|
448
|
+
}));
|
|
449
|
+
|
|
450
|
+
export const format = {
|
|
451
|
+
dateTime: (message?: string): CheckShape => regexFormat(DATETIME_RE, 'date-time', message),
|
|
452
|
+
date: (message?: string): CheckShape => fnFormat(isValidDate, 'date', message),
|
|
453
|
+
time: (message?: string): CheckShape => regexFormat(TIME_FMT_RE, 'time', message),
|
|
454
|
+
duration: (message?: string): CheckShape => regexFormat(DURATION_RE, 'duration', message),
|
|
455
|
+
email: (message?: string): CheckShape => regexFormat(EMAIL_RE, 'email', message),
|
|
456
|
+
idnEmail: (message?: string): CheckShape => regexFormat(IDN_EMAIL_RE, 'idn-email', message),
|
|
457
|
+
hostname: (message?: string): CheckShape => regexFormat(HOSTNAME_RE, 'hostname', message),
|
|
458
|
+
idnHostname: (message?: string): CheckShape => regexFormat(IDN_HOSTNAME_RE, 'idn-hostname', message),
|
|
459
|
+
ipv4: (message?: string): CheckShape => regexFormat(IPV4_RE, 'ipv4', message),
|
|
460
|
+
ipv6: (message?: string): CheckShape => regexFormat(IPV6_RE, 'ipv6', message),
|
|
461
|
+
uri: (message?: string): CheckShape => regexFormat(URI_RE, 'uri', message),
|
|
462
|
+
uriReference: (message?: string): CheckShape => fnFormat(isUriRef, 'uri-reference', message),
|
|
463
|
+
iri: (message?: string): CheckShape => regexFormat(IRI_RE, 'iri', message),
|
|
464
|
+
iriReference: (message?: string): CheckShape => fnFormat(isIriRef, 'iri-reference', message),
|
|
465
|
+
uuid: (message?: string): CheckShape => regexFormat(UUID_RE, 'uuid', message),
|
|
466
|
+
uriTemplate: (message?: string): CheckShape => regexFormat(URI_TEMPLATE_RE, 'uri-template', message),
|
|
467
|
+
jsonPointer: (message?: string): CheckShape => regexFormat(JSON_POINTER_RE, 'json-pointer', message),
|
|
468
|
+
relativeJsonPointer: (message?: string): CheckShape => regexFormat(REL_JSON_POINTER_RE, 'relative-json-pointer', message),
|
|
469
|
+
regex: (message?: string): CheckShape => fnFormat(isValidRegex, 'regex', message),
|
|
470
|
+
};
|
|
471
|
+
|
|
298
472
|
/**
|
|
299
473
|
* A class representing the context for schema validation.
|
|
300
474
|
*
|
|
@@ -419,6 +593,14 @@ const codeGen = <T>(schema: Schema<T>, context: Context, valuePath: string, mode
|
|
|
419
593
|
`else { ${schema.schemas.map((s, idx) => codeGen(s, context, `${valueAlias}[${idx}]`, childMode(mode, idx))).join('\n')} }`,
|
|
420
594
|
].join('\n');
|
|
421
595
|
}
|
|
596
|
+
} else if (tag === CHECK) {
|
|
597
|
+
const valueAlias = context.unique('v');
|
|
598
|
+
const ref = (v: unknown) => `ctx.registry[${context.register(v)}]`;
|
|
599
|
+
const { check: cond, message } = (schema as CheckShape).compile(valueAlias, { ref });
|
|
600
|
+
if (mode.fast) {
|
|
601
|
+
return `const ${valueAlias} = ${valuePath};\nif (${cond}) { ${fail} }`;
|
|
602
|
+
}
|
|
603
|
+
return `const ${valueAlias} = ${valuePath};\nif (${cond}) { ${emit!(message)} }`;
|
|
422
604
|
} else {
|
|
423
605
|
const { key, schemas } = schema as DiscriminatedShape<T>;
|
|
424
606
|
const valueAlias = context.unique('v');
|