@amritk/validation 0.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/AI.md +92 -0
- package/LICENSE +21 -0
- package/README.md +184 -0
- package/dist/generate.d.ts +135 -0
- package/dist/generate.js +74 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -0
- package/dist/parsers/generators/assert-supported-keywords.d.ts +30 -0
- package/dist/parsers/generators/assert-supported-keywords.js +87 -0
- package/dist/parsers/generators/build-schema.d.ts +111 -0
- package/dist/parsers/generators/build-schema.js +74 -0
- package/dist/parsers/generators/generate-deep-equal-check.d.ts +24 -0
- package/dist/parsers/generators/generate-deep-equal-check.js +35 -0
- package/dist/parsers/generators/generate-discriminated-union-validation.d.ts +15 -0
- package/dist/parsers/generators/generate-discriminated-union-validation.js +28 -0
- package/dist/parsers/generators/generate-enum-check.d.ts +35 -0
- package/dist/parsers/generators/generate-enum-check.js +31 -0
- package/dist/parsers/generators/generate-files.d.ts +138 -0
- package/dist/parsers/generators/generate-files.js +89 -0
- package/dist/parsers/generators/generate-non-discriminated-union-validation.d.ts +6 -0
- package/dist/parsers/generators/generate-non-discriminated-union-validation.js +26 -0
- package/dist/parsers/generators/generate-parser-function.d.ts +120 -0
- package/dist/parsers/generators/generate-parser-function.js +1973 -0
- package/dist/parsers/generators/generate-schema-checks.d.ts +5 -0
- package/dist/parsers/generators/generate-schema-checks.js +260 -0
- package/dist/parsers/generators/generate-strict-assertion.d.ts +155 -0
- package/dist/parsers/generators/generate-strict-assertion.js +759 -0
- package/dist/parsers/generators/generate-type-checks.d.ts +148 -0
- package/dist/parsers/generators/generate-type-checks.js +454 -0
- package/dist/parsers/generators/generate-unique-items-check.d.ts +16 -0
- package/dist/parsers/generators/generate-unique-items-check.js +32 -0
- package/dist/parsers/generators/generate-validation-expression.d.ts +79 -0
- package/dist/parsers/generators/generate-validation-expression.js +416 -0
- package/dist/parsers/generators/subschema-match.d.ts +48 -0
- package/dist/parsers/generators/subschema-match.js +485 -0
- package/dist/parsers/generators/unevaluated-match.d.ts +19 -0
- package/dist/parsers/generators/unevaluated-match.js +206 -0
- package/dist/parsers/helpers/apply-schema-extensions.d.ts +35 -0
- package/dist/parsers/helpers/apply-schema-extensions.js +20 -0
- package/dist/parsers/helpers/collect-helpers.d.ts +15 -0
- package/dist/parsers/helpers/collect-helpers.js +64 -0
- package/dist/parsers/helpers/collect-imports.d.ts +104 -0
- package/dist/parsers/helpers/collect-imports.js +227 -0
- package/dist/parsers/helpers/find-discriminator.d.ts +6 -0
- package/dist/parsers/helpers/find-discriminator.js +27 -0
- package/dist/parsers/helpers/get-discriminator-value.d.ts +6 -0
- package/dist/parsers/helpers/get-discriminator-value.js +18 -0
- package/dist/parsers/index.d.ts +2 -0
- package/dist/parsers/index.js +4 -0
- package/dist/parsers/types/schema-extensions.d.ts +34 -0
- package/dist/parsers/types/schema-extensions.js +0 -0
- package/dist/rehome-parser-file.d.ts +25 -0
- package/dist/rehome-parser-file.js +74 -0
- package/dist/validators/generators/assert-generatable-refs.d.ts +19 -0
- package/dist/validators/generators/assert-generatable-refs.js +12 -0
- package/dist/validators/generators/assert-unevaluated-generatable.d.ts +30 -0
- package/dist/validators/generators/assert-unevaluated-generatable.js +71 -0
- package/dist/validators/generators/build-schema.d.ts +61 -0
- package/dist/validators/generators/build-schema.js +666 -0
- package/dist/validators/generators/collect-emitted-refs.d.ts +26 -0
- package/dist/validators/generators/collect-emitted-refs.js +112 -0
- package/dist/validators/generators/collect-validator-imports.d.ts +86 -0
- package/dist/validators/generators/collect-validator-imports.js +62 -0
- package/dist/validators/generators/emit-format-checks.d.ts +50 -0
- package/dist/validators/generators/emit-format-checks.js +248 -0
- package/dist/validators/generators/enforced-keywords.d.ts +33 -0
- package/dist/validators/generators/enforced-keywords.js +70 -0
- package/dist/validators/generators/folds-to-constant.d.ts +15 -0
- package/dist/validators/generators/folds-to-constant.js +43 -0
- package/dist/validators/generators/generate-coerce-function.d.ts +22 -0
- package/dist/validators/generators/generate-coerce-function.js +208 -0
- package/dist/validators/generators/generate-files.d.ts +92 -0
- package/dist/validators/generators/generate-files.js +86 -0
- package/dist/validators/generators/generate-repair-function.d.ts +27 -0
- package/dist/validators/generators/generate-repair-function.js +102 -0
- package/dist/validators/generators/generate-validator-function.d.ts +58 -0
- package/dist/validators/generators/generate-validator-function.js +1691 -0
- package/dist/validators/generators/tuple-shape.d.ts +31 -0
- package/dist/validators/generators/tuple-shape.js +20 -0
- package/dist/validators/generators/unevaluated-match.d.ts +29 -0
- package/dist/validators/generators/unevaluated-match.js +223 -0
- package/dist/validators/index.d.ts +2 -0
- package/dist/validators/index.js +4 -0
- package/package.json +92 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recursively walks a schema and yields every `$ref` the validator emitter turns
|
|
3
|
+
* into a `validateX(...)` call, in traversal order (duplicates included — the
|
|
4
|
+
* callers dedupe on what they key by).
|
|
5
|
+
*
|
|
6
|
+
* The emitter recurses into far more than properties/items/additionalProperties
|
|
7
|
+
* and the top-level combinators: it also delegates for `patternProperties`,
|
|
8
|
+
* `propertyNames`, `if`/`then`/`else`, `contains`, `prefixItems`,
|
|
9
|
+
* `dependentSchemas`, `not`, the `unevaluated*` subschemas, and objects nested
|
|
10
|
+
* inside any combinator branch. A `$ref` reached by *any* of those paths becomes
|
|
11
|
+
* a call in the output, so both the import collector and the generatability check
|
|
12
|
+
* have to see exactly this set — which is why the traversal lives here rather
|
|
13
|
+
* than in either of them.
|
|
14
|
+
*
|
|
15
|
+
* `$defs` / `definitions` are deliberately skipped: they are split into their own
|
|
16
|
+
* generated files rather than inlined into this one.
|
|
17
|
+
*
|
|
18
|
+
* Pass `rootSchema` to also pick up the refs an `unevaluated*` keyword reaches.
|
|
19
|
+
* Those are the one case where a call can appear in *this* file for a branch
|
|
20
|
+
* written in *another* definition: working out what a node's `$ref` target
|
|
21
|
+
* already evaluated means reading through that target's own `anyOf` / `oneOf` /
|
|
22
|
+
* `if`, and each of those branches is emitted here as a condition. Without the
|
|
23
|
+
* root document there is nothing to resolve the target against, so the extra
|
|
24
|
+
* refs are simply not collected.
|
|
25
|
+
*/
|
|
26
|
+
export declare const collectEmittedRefs: (value: unknown, refs?: string[], rootSchema?: Record<string, unknown>, includeTypeOnly?: boolean, inlinedRefs?: Set<string>) => string[];
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { declaresKey, readKey } from "@amritk/helpers/read-key";
|
|
2
|
+
import { referencedConditional } from "@amritk/helpers/referenced-conditional";
|
|
3
|
+
import { isSchemaObject } from "@amritk/helpers/schema-guards";
|
|
4
|
+
import { foldsToConstant } from "./folds-to-constant.js";
|
|
5
|
+
import { tupleShapeOf } from "./tuple-shape.js";
|
|
6
|
+
import { unevaluatedItemsExpr, unevaluatedPropertiesExpr } from "./unevaluated-match.js";
|
|
7
|
+
const armsOf = (schema) => {
|
|
8
|
+
if (!declaresKey(schema, "if"))
|
|
9
|
+
return [];
|
|
10
|
+
const condition = readKey(schema, "if");
|
|
11
|
+
if (!isSchemaObject(condition) && typeof condition !== "boolean")
|
|
12
|
+
return [];
|
|
13
|
+
const decided = foldsToConstant(condition);
|
|
14
|
+
if (decided === true)
|
|
15
|
+
return ["then"];
|
|
16
|
+
if (decided === false)
|
|
17
|
+
return ["else"];
|
|
18
|
+
return ["then", "else"];
|
|
19
|
+
};
|
|
20
|
+
const collectEmittedRefs = (value, refs = [], rootSchema, includeTypeOnly = false, inlinedRefs = /* @__PURE__ */ new Set()) => {
|
|
21
|
+
if (typeof value !== "object" || value === null)
|
|
22
|
+
return refs;
|
|
23
|
+
if (Array.isArray(value)) {
|
|
24
|
+
for (const item of value)
|
|
25
|
+
collectEmittedRefs(item, refs, rootSchema, includeTypeOnly, inlinedRefs);
|
|
26
|
+
return refs;
|
|
27
|
+
}
|
|
28
|
+
const schema = value;
|
|
29
|
+
if (rootSchema !== void 0 && (declaresKey(schema, "unevaluatedProperties") || declaresKey(schema, "unevaluatedItems"))) {
|
|
30
|
+
collectCoverageRefs(schema, refs, rootSchema);
|
|
31
|
+
}
|
|
32
|
+
const ref = readKey(schema, "$ref");
|
|
33
|
+
if (typeof ref === "string") {
|
|
34
|
+
refs.push(ref);
|
|
35
|
+
}
|
|
36
|
+
for (const mapKey of ["properties", "patternProperties", "dependentSchemas", "dependencies"]) {
|
|
37
|
+
const map = readKey(schema, mapKey);
|
|
38
|
+
if (typeof map === "object" && map !== null && !Array.isArray(map)) {
|
|
39
|
+
for (const sub of Object.values(map))
|
|
40
|
+
collectEmittedRefs(sub, refs, rootSchema, includeTypeOnly, inlinedRefs);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const { tuple, tail } = tupleShapeOf(schema);
|
|
44
|
+
if (tail !== void 0)
|
|
45
|
+
collectEmittedRefs(tail, refs, rootSchema, includeTypeOnly, inlinedRefs);
|
|
46
|
+
if (includeTypeOnly && Array.isArray(readKey(schema, "items"))) {
|
|
47
|
+
const additional = readKey(schema, "additionalItems");
|
|
48
|
+
if (additional !== void 0 && additional !== tail) {
|
|
49
|
+
collectEmittedRefs(additional, refs, rootSchema, includeTypeOnly, inlinedRefs);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
for (const key of [
|
|
53
|
+
"additionalProperties",
|
|
54
|
+
"propertyNames",
|
|
55
|
+
"contains",
|
|
56
|
+
"if",
|
|
57
|
+
// `then` / `else` only apply when there is an `if` to branch on, and neither
|
|
58
|
+
// emitter reads them without one — so a `$ref` there is never referenced,
|
|
59
|
+
// and collecting it refused schemas whose ref happened to be unresolvable.
|
|
60
|
+
//
|
|
61
|
+
// An `if` the emitter can decide picks its arm here too, and unlike `anyOf`
|
|
62
|
+
// the type generator reads neither arm — it types the whole node `unknown` —
|
|
63
|
+
// so the dropped arm is referenced by nobody. See {@link armsOf}.
|
|
64
|
+
...armsOf(schema),
|
|
65
|
+
"not",
|
|
66
|
+
// The `unevaluated*` subschemas are validated against the leftover keys /
|
|
67
|
+
// indices, so a `$ref` inside one becomes a `validateX(...)` call like any
|
|
68
|
+
// other — and without it the generated file would call an import it never asked for.
|
|
69
|
+
"unevaluatedProperties",
|
|
70
|
+
"unevaluatedItems"
|
|
71
|
+
]) {
|
|
72
|
+
if (declaresKey(schema, key))
|
|
73
|
+
collectEmittedRefs(schema[key], refs, rootSchema, includeTypeOnly, inlinedRefs);
|
|
74
|
+
}
|
|
75
|
+
for (const key of ["oneOf", "anyOf", "allOf"]) {
|
|
76
|
+
const list = readKey(schema, key);
|
|
77
|
+
if (!Array.isArray(list))
|
|
78
|
+
continue;
|
|
79
|
+
for (const sub of list) {
|
|
80
|
+
collectEmittedRefs(sub, refs, rootSchema, includeTypeOnly, inlinedRefs);
|
|
81
|
+
if (key !== "allOf" || !includeTypeOnly)
|
|
82
|
+
continue;
|
|
83
|
+
const memberRef = isSchemaObject(sub) ? readKey(sub, "$ref") : void 0;
|
|
84
|
+
if (typeof memberRef === "string" && inlinedRefs.has(memberRef))
|
|
85
|
+
continue;
|
|
86
|
+
const inlined = referencedConditional(sub, rootSchema);
|
|
87
|
+
if (inlined === void 0)
|
|
88
|
+
continue;
|
|
89
|
+
if (typeof memberRef === "string")
|
|
90
|
+
inlinedRefs.add(memberRef);
|
|
91
|
+
for (const arm of ["if", "then", "else"]) {
|
|
92
|
+
if (declaresKey(inlined, arm))
|
|
93
|
+
collectEmittedRefs(readKey(inlined, arm), refs, rootSchema, includeTypeOnly, inlinedRefs);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (tuple !== void 0)
|
|
98
|
+
for (const sub of tuple)
|
|
99
|
+
collectEmittedRefs(sub, refs, rootSchema, includeTypeOnly, inlinedRefs);
|
|
100
|
+
return refs;
|
|
101
|
+
};
|
|
102
|
+
const collectCoverageRefs = (schema, refs, rootSchema) => {
|
|
103
|
+
const record = (_accessor, sub) => {
|
|
104
|
+
collectEmittedRefs(sub, refs);
|
|
105
|
+
return "true";
|
|
106
|
+
};
|
|
107
|
+
unevaluatedPropertiesExpr("_refs", schema, rootSchema, 0, record);
|
|
108
|
+
unevaluatedItemsExpr("_refs", schema, rootSchema, 0, record);
|
|
109
|
+
};
|
|
110
|
+
export {
|
|
111
|
+
collectEmittedRefs
|
|
112
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
2
|
+
/**
|
|
3
|
+
* Options for controlling how validator imports are collected.
|
|
4
|
+
*/
|
|
5
|
+
type CollectValidatorImportsOptions = {
|
|
6
|
+
/**
|
|
7
|
+
* The $ref path of the schema being generated (e.g. `#/$defs/encoding`).
|
|
8
|
+
* Prevents a file from importing itself.
|
|
9
|
+
*/
|
|
10
|
+
readonly selfRef?: string | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* The root schema document. URI refs that cannot be resolved within it
|
|
13
|
+
* are excluded from the import list (they were never generated as files).
|
|
14
|
+
*/
|
|
15
|
+
readonly rootSchema?: Record<string, unknown> | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Suffix appended to every type/validator name derived from a `$ref`. Must
|
|
18
|
+
* match the suffix used when generating the referenced files. Defaults to `''`.
|
|
19
|
+
*/
|
|
20
|
+
readonly typeSuffix?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Extension on every emitted relative specifier. `'js'` is the NodeNext form a
|
|
23
|
+
* compiled consumer needs; `'ts'` is the literal on-disk path, which is what
|
|
24
|
+
* runs under Node's type stripping with no build step.
|
|
25
|
+
*/
|
|
26
|
+
readonly importExt?: 'js' | 'ts';
|
|
27
|
+
/**
|
|
28
|
+
* Whether the file being generated reads each half of a `$ref`'s import — the
|
|
29
|
+
* type, the validator, or both.
|
|
30
|
+
*
|
|
31
|
+
* The two halves come apart in both directions. A `$ref` in a position the type
|
|
32
|
+
* generator does not read (an `if` arm, whose whole node it types `unknown`) is
|
|
33
|
+
* called and never named; one in a position only the *type* reads (a tuple's
|
|
34
|
+
* rest, taken from `additionalItems`) is named and never called; and a ref
|
|
35
|
+
* inside a branch that folded away is neither. Importing a half nothing reads
|
|
36
|
+
* leaves it unused, which is `TS6133` in the generated file for any consumer
|
|
37
|
+
* with `noUnusedLocals` — this repo, and anything inheriting its flags.
|
|
38
|
+
*
|
|
39
|
+
* Defaults to "both", which is what every caller wanted before anyone asked the
|
|
40
|
+
* question.
|
|
41
|
+
*/
|
|
42
|
+
readonly reads?: (names: {
|
|
43
|
+
readonly typeName: string;
|
|
44
|
+
readonly validatorName: string;
|
|
45
|
+
readonly checkerName: string;
|
|
46
|
+
readonly coercerName: string;
|
|
47
|
+
readonly repairerName: string;
|
|
48
|
+
}) => {
|
|
49
|
+
readonly type: boolean;
|
|
50
|
+
readonly validator: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* The `$ref` target's fail-fast validator. Only a file generated with the
|
|
53
|
+
* check half on names it, and then only from inside its own `checkX` — the
|
|
54
|
+
* accumulating body still calls `validateX`, so the two halves of one import
|
|
55
|
+
* come apart exactly like the coercing and repairing ones do.
|
|
56
|
+
*/
|
|
57
|
+
readonly checker?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* The `$ref` target's value-coercing walk. Only a file generated with
|
|
60
|
+
* coercion on names it, and even then only where the ref sits somewhere the
|
|
61
|
+
* coercion pass walks — so it comes apart from the other two halves the same
|
|
62
|
+
* way they come apart from each other.
|
|
63
|
+
*/
|
|
64
|
+
readonly coercer?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* The `$ref` target's position lookup, which answers what a repair at some
|
|
67
|
+
* position under it repairs to. Only a file generated with repair on names
|
|
68
|
+
* it, and a repairing file always coerces too — so this half never appears
|
|
69
|
+
* without {@link coercer}, though the reverse is ordinary.
|
|
70
|
+
*/
|
|
71
|
+
readonly repairer?: boolean;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Collects import statements for all $ref dependencies of a schema.
|
|
76
|
+
* Each import brings in both the generated TypeScript type and validator function.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```typescript
|
|
80
|
+
* const schema = { properties: { contact: { $ref: '#/$defs/contact' } } }
|
|
81
|
+
* collectValidatorImports(schema)
|
|
82
|
+
* // ["import { type Contact, validateContact } from './contact'"]
|
|
83
|
+
* ```
|
|
84
|
+
*/
|
|
85
|
+
export declare const collectValidatorImports: (schema: JSONSchema, options?: CollectValidatorImportsOptions) => string[];
|
|
86
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { refToFilename } from "@amritk/helpers/ref-to-filename";
|
|
2
|
+
import { refToName } from "@amritk/helpers/ref-to-name";
|
|
3
|
+
import { resolveRef } from "@amritk/helpers/resolve-ref";
|
|
4
|
+
import { collectEmittedRefs } from "./collect-emitted-refs.js";
|
|
5
|
+
const buildImport = (ref, suffix, reads, importExt) => {
|
|
6
|
+
const filename = refToFilename(ref);
|
|
7
|
+
const typeName = refToName(ref, suffix);
|
|
8
|
+
const validatorName = `validate${typeName}`;
|
|
9
|
+
const checkerName = `check${typeName}`;
|
|
10
|
+
const coercerName = `coerce${typeName}Value`;
|
|
11
|
+
const repairerName = `repair${typeName}At`;
|
|
12
|
+
const { type, validator, checker = false, coercer = false, repairer = false } = reads({
|
|
13
|
+
typeName,
|
|
14
|
+
validatorName,
|
|
15
|
+
checkerName,
|
|
16
|
+
coercerName,
|
|
17
|
+
repairerName
|
|
18
|
+
});
|
|
19
|
+
const values = [
|
|
20
|
+
...validator ? [validatorName] : [],
|
|
21
|
+
...checker ? [checkerName] : [],
|
|
22
|
+
...coercer ? [coercerName] : [],
|
|
23
|
+
...repairer ? [repairerName] : []
|
|
24
|
+
];
|
|
25
|
+
if (type && values.length > 0)
|
|
26
|
+
return `import { type ${typeName}, ${values.join(", ")} } from './${filename}.${importExt}'`;
|
|
27
|
+
if (values.length > 0)
|
|
28
|
+
return `import { ${values.join(", ")} } from './${filename}.${importExt}'`;
|
|
29
|
+
if (type)
|
|
30
|
+
return `import type { ${typeName} } from './${filename}.${importExt}'`;
|
|
31
|
+
return null;
|
|
32
|
+
};
|
|
33
|
+
const collectValidatorImports = (schema, options) => {
|
|
34
|
+
const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null;
|
|
35
|
+
const rootSchema = options?.rootSchema;
|
|
36
|
+
const typeSuffix = options?.typeSuffix ?? "";
|
|
37
|
+
const reads = options?.reads ?? (() => ({ type: true, validator: true }));
|
|
38
|
+
const importExt = options?.importExt ?? "js";
|
|
39
|
+
const refs = collectEmittedRefs(schema, [], rootSchema, true);
|
|
40
|
+
const seen = /* @__PURE__ */ new Set();
|
|
41
|
+
const imports = [];
|
|
42
|
+
for (const ref of refs) {
|
|
43
|
+
const filename = refToFilename(ref);
|
|
44
|
+
if (seen.has(filename))
|
|
45
|
+
continue;
|
|
46
|
+
if (selfFilename && filename === selfFilename)
|
|
47
|
+
continue;
|
|
48
|
+
if (rootSchema) {
|
|
49
|
+
const resolved = resolveRef(ref, rootSchema);
|
|
50
|
+
if (!resolved)
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const statement = buildImport(ref, typeSuffix, reads, importExt);
|
|
54
|
+
seen.add(filename);
|
|
55
|
+
if (statement !== null)
|
|
56
|
+
imports.push(statement);
|
|
57
|
+
}
|
|
58
|
+
return imports;
|
|
59
|
+
};
|
|
60
|
+
export {
|
|
61
|
+
collectValidatorImports
|
|
62
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `format` checks, as emittable source.
|
|
3
|
+
*
|
|
4
|
+
* Generated validators are dependency-free straight-line TypeScript — that is
|
|
5
|
+
* most of what makes them fast and all of what makes them readable — so a
|
|
6
|
+
* generated validator cannot import `@amritk/runtime-validators`' format table.
|
|
7
|
+
* It gets its own copy, and only of the formats the schema actually names.
|
|
8
|
+
*
|
|
9
|
+
* Two implementations of one rule is exactly the situation that drifts, so it is
|
|
10
|
+
* pinned rather than trusted: `emit-format-checks.test.ts` runs this source and
|
|
11
|
+
* the interpreter's table over the official suite's whole optional/format corpus
|
|
12
|
+
* and requires the same verdict on every case. A divergence fails the build.
|
|
13
|
+
*
|
|
14
|
+
* Each entry is a source *fragment*: a `const` declaration whose name is
|
|
15
|
+
* {@link formatCheckName}. Fragments may depend on shared helpers, named in
|
|
16
|
+
* `needs`, which are emitted once ahead of them.
|
|
17
|
+
*
|
|
18
|
+
* Nearly every string here is code for the *generated* file, so a `${…}` in one
|
|
19
|
+
* is a template hole in the emitted source rather than an interpolation that was
|
|
20
|
+
* meant to happen here — which is what `noTemplateCurlyInString` exists to catch
|
|
21
|
+
* and what makes it a false positive throughout this file.
|
|
22
|
+
*/
|
|
23
|
+
/** The identifier a format's check is emitted under. */
|
|
24
|
+
export declare const formatCheckName: (format: string) => string;
|
|
25
|
+
type FormatFragment = {
|
|
26
|
+
/** The JSON type the check takes — a format asserts about one and is silent about the rest. */
|
|
27
|
+
readonly family: 'string' | 'number';
|
|
28
|
+
/** Names of {@link SHARED} helpers this fragment reads. */
|
|
29
|
+
readonly needs?: readonly string[];
|
|
30
|
+
/** The `const <name> = …` declaration, with `<name>` already substituted. */
|
|
31
|
+
readonly source: (name: string) => string;
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Every format the generator can emit a check for — the same set
|
|
35
|
+
* `@amritk/runtime-validators` enforces, and deliberately not a subset: a
|
|
36
|
+
* generator that silently skipped some would be the exact footgun this closes.
|
|
37
|
+
*/
|
|
38
|
+
export declare const FORMAT_FRAGMENTS: Readonly<Record<string, FormatFragment>>;
|
|
39
|
+
/** The JSON type a format asserts about, or `undefined` when nothing defines it. */
|
|
40
|
+
export declare const formatFamily: (format: string) => 'string' | 'number' | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* The source of a `formats.ts` module defining a check for each format in
|
|
43
|
+
* `formats`, or `''` when none of them is one this generator knows.
|
|
44
|
+
*
|
|
45
|
+
* Only the formats the schema actually names are emitted, along with whichever
|
|
46
|
+
* shared helpers those need — so a schema declaring one `uuid` gets one regex
|
|
47
|
+
* rather than the whole table.
|
|
48
|
+
*/
|
|
49
|
+
export declare const emitFormatModule: (formats: Iterable<string>) => string;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
const formatCheckName = (format) => `isFormat${format.split(/[^A-Za-z0-9]+/).filter((part) => part !== "").map((part) => part[0]?.toUpperCase() + part.slice(1)).join("")}`;
|
|
2
|
+
const SHARED = {
|
|
3
|
+
_asciiLabel: `const _asciiLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'`,
|
|
4
|
+
_idnLabel: `const _idnLabel = '[\\\\p{L}\\\\p{N}](?:[\\\\p{L}\\\\p{N}\\\\p{M}-]{0,61}[\\\\p{L}\\\\p{N}\\\\p{M}])?'`,
|
|
5
|
+
_ipv4Octets: [
|
|
6
|
+
`const _ipv4Octet = '(?:25[0-5]|2[0-4]\\\\d|1\\\\d\\\\d|[1-9]?\\\\d)'`,
|
|
7
|
+
"const _ipv4Octets = `(?:${_ipv4Octet}\\\\.){3}${_ipv4Octet}`"
|
|
8
|
+
].join("\n"),
|
|
9
|
+
_ipv6Body: [
|
|
10
|
+
`const _h16 = '[0-9a-fA-F]{1,4}'`,
|
|
11
|
+
"const _ls32 = `(?:${_h16}:${_h16}|${_ipv4Octets})`",
|
|
12
|
+
"const _ipv6Body =",
|
|
13
|
+
" `(?:(?:${_h16}:){6}${_ls32}` +",
|
|
14
|
+
" `|::(?:${_h16}:){5}${_ls32}` +",
|
|
15
|
+
" `|(?:${_h16})?::(?:${_h16}:){4}${_ls32}` +",
|
|
16
|
+
" `|(?:(?:${_h16}:){0,1}${_h16})?::(?:${_h16}:){3}${_ls32}` +",
|
|
17
|
+
" `|(?:(?:${_h16}:){0,2}${_h16})?::(?:${_h16}:){2}${_ls32}` +",
|
|
18
|
+
" `|(?:(?:${_h16}:){0,3}${_h16})?::(?:${_h16}:)${_ls32}` +",
|
|
19
|
+
" `|(?:(?:${_h16}:){0,4}${_h16})?::${_ls32}` +",
|
|
20
|
+
" `|(?:(?:${_h16}:){0,5}${_h16})?::${_h16}` +",
|
|
21
|
+
" `|(?:(?:${_h16}:){0,6}${_h16})?::)`"
|
|
22
|
+
].join("\n"),
|
|
23
|
+
_uriGrammar: [
|
|
24
|
+
`const _subDelims = "!$&'()*+,;="`,
|
|
25
|
+
`const _pctEncoded = '%[0-9A-Fa-f]{2}'`,
|
|
26
|
+
"const _ucschar =",
|
|
27
|
+
" '\\\\u00A0-\\\\uD7FF\\\\uF900-\\\\uFDCF\\\\uFDF0-\\\\uFFEF' +",
|
|
28
|
+
" '\\\\u{10000}-\\\\u{1FFFD}\\\\u{20000}-\\\\u{2FFFD}\\\\u{30000}-\\\\u{3FFFD}\\\\u{40000}-\\\\u{4FFFD}' +",
|
|
29
|
+
" '\\\\u{50000}-\\\\u{5FFFD}\\\\u{60000}-\\\\u{6FFFD}\\\\u{70000}-\\\\u{7FFFD}\\\\u{80000}-\\\\u{8FFFD}' +",
|
|
30
|
+
" '\\\\u{90000}-\\\\u{9FFFD}\\\\u{A0000}-\\\\u{AFFFD}\\\\u{B0000}-\\\\u{BFFFD}\\\\u{C0000}-\\\\u{CFFFD}' +",
|
|
31
|
+
" '\\\\u{D0000}-\\\\u{DFFFD}\\\\u{E1000}-\\\\u{EFFFD}'",
|
|
32
|
+
`const _iprivate = '\\\\uE000-\\\\uF8FF\\\\u{F0000}-\\\\u{FFFFD}\\\\u{100000}-\\\\u{10FFFD}'`,
|
|
33
|
+
'const _uriGrammar = (extraUnreserved: string, privateQuery = ""): { uri: string; reference: string } => {',
|
|
34
|
+
" const unreserved = `A-Za-z0-9\\\\-._~${extraUnreserved}`",
|
|
35
|
+
" const scheme = '[A-Za-z][A-Za-z0-9+\\\\-.]*'",
|
|
36
|
+
" const pchar = `(?:[${unreserved}${_subDelims}:@]|${_pctEncoded})`",
|
|
37
|
+
" const segment = `${pchar}*`",
|
|
38
|
+
" const segmentNz = `${pchar}+`",
|
|
39
|
+
" const segmentNzNc = `(?:[${unreserved}${_subDelims}@]|${_pctEncoded})+`",
|
|
40
|
+
" const userinfo = `(?:[${unreserved}${_subDelims}:]|${_pctEncoded})*`",
|
|
41
|
+
" const ipvFuture = `[Vv][0-9A-Fa-f]+\\\\.[${unreserved}${_subDelims}:]+`",
|
|
42
|
+
" const ipLiteral = `\\\\[(?:${_ipv6Body}|${ipvFuture})\\\\]`",
|
|
43
|
+
" const regName = `(?:[${unreserved}${_subDelims}]|${_pctEncoded})*`",
|
|
44
|
+
" const host = `(?:${ipLiteral}|${_ipv4Octets}|${regName})`",
|
|
45
|
+
" const authority = `(?:${userinfo}@)?${host}(?::\\\\d*)?`",
|
|
46
|
+
" const pathAbempty = `(?:/${segment})*`",
|
|
47
|
+
" const pathAbsolute = `/(?:${segmentNz}(?:/${segment})*)?`",
|
|
48
|
+
" const pathRootless = `${segmentNz}(?:/${segment})*`",
|
|
49
|
+
" const pathNoscheme = `${segmentNzNc}(?:/${segment})*`",
|
|
50
|
+
" const hierPart = `(?://${authority}${pathAbempty}|${pathAbsolute}|${pathRootless}|)`",
|
|
51
|
+
" const relativePart = `(?://${authority}${pathAbempty}|${pathAbsolute}|${pathNoscheme}|)`",
|
|
52
|
+
" const query = `(?:\\\\?(?:${pchar}|[/?${privateQuery}])*)?`",
|
|
53
|
+
" const fragment = `(?:#(?:${pchar}|[/?])*)?`",
|
|
54
|
+
" return {",
|
|
55
|
+
" uri: `^${scheme}:${hierPart}${query}${fragment}$`,",
|
|
56
|
+
" reference: `^(?:${scheme}:${hierPart}|${relativePart})${query}${fragment}$`,",
|
|
57
|
+
" }",
|
|
58
|
+
"}",
|
|
59
|
+
'const _asciiUri = _uriGrammar("")',
|
|
60
|
+
"const _iriUri = _uriGrammar(_ucschar, _iprivate)"
|
|
61
|
+
].join("\n"),
|
|
62
|
+
_isDate: [
|
|
63
|
+
"const _dateShape = /^(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)$/",
|
|
64
|
+
"const _daysInMonth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]",
|
|
65
|
+
"const _isLeapYear = (year: number): boolean => year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)",
|
|
66
|
+
"const _isDate = (value: string): boolean => {",
|
|
67
|
+
" const matches = _dateShape.exec(value)",
|
|
68
|
+
" if (matches === null) return false",
|
|
69
|
+
" const year = +(matches[1] as string)",
|
|
70
|
+
" const month = +(matches[2] as string)",
|
|
71
|
+
" const day = +(matches[3] as string)",
|
|
72
|
+
" if (month < 1 || month > 12 || day < 1) return false",
|
|
73
|
+
" return day <= (month === 2 && _isLeapYear(year) ? 29 : (_daysInMonth[month] as number))",
|
|
74
|
+
"}"
|
|
75
|
+
].join("\n"),
|
|
76
|
+
_timeCheck: [
|
|
77
|
+
"const _timeShape = /^(\\d\\d):(\\d\\d):(\\d\\d)(?:\\.\\d+)?(?:([Zz])|([+-])(\\d\\d):(\\d\\d))?$/",
|
|
78
|
+
"const _timeCheck =",
|
|
79
|
+
" (requireOffset: boolean) =>",
|
|
80
|
+
" (value: string): boolean => {",
|
|
81
|
+
" const matches = _timeShape.exec(value)",
|
|
82
|
+
" if (matches === null) return false",
|
|
83
|
+
" const hour = +(matches[1] as string)",
|
|
84
|
+
" const minute = +(matches[2] as string)",
|
|
85
|
+
" const second = +(matches[3] as string)",
|
|
86
|
+
" const zulu = matches[4] !== undefined",
|
|
87
|
+
' const offsetSign = matches[5] === "-" ? -1 : 1',
|
|
88
|
+
" const hasOffset = zulu || matches[5] !== undefined",
|
|
89
|
+
" if (requireOffset && !hasOffset) return false",
|
|
90
|
+
" const offsetHours = +(matches[6] ?? 0)",
|
|
91
|
+
" const offsetMinutes = +(matches[7] ?? 0)",
|
|
92
|
+
" if (hour > 23 || minute > 59 || second > 60 || offsetHours > 23 || offsetMinutes > 59) return false",
|
|
93
|
+
" if (second < 60) return true",
|
|
94
|
+
" const utcMinute = minute - offsetMinutes * offsetSign",
|
|
95
|
+
" const utcHour = hour - offsetHours * offsetSign - (utcMinute < 0 ? 1 : 0)",
|
|
96
|
+
" return (utcHour === 23 || utcHour === -1) && (utcMinute === 59 || utcMinute === -1)",
|
|
97
|
+
" }",
|
|
98
|
+
"const _dateTimeSeparator = /t|\\s/i",
|
|
99
|
+
"const _dateTimeCheck = (time: (value: string) => boolean) => (value: string) => {",
|
|
100
|
+
" const parts = value.split(_dateTimeSeparator)",
|
|
101
|
+
" return parts.length === 2 && _isDate(parts[0] as string) && time(parts[1] as string)",
|
|
102
|
+
"}"
|
|
103
|
+
].join("\n"),
|
|
104
|
+
_emailPattern: [
|
|
105
|
+
`const _asciiAtom = "[a-zA-Z0-9!#$%&'*+/=?^_\`{|}~-]"`,
|
|
106
|
+
`const _idnAtom = "[\\\\p{L}\\\\p{N}\\\\p{M}!#$%&'*+/=?^_\`{|}~-]"`,
|
|
107
|
+
"const _emailPattern = (atom: string, label: string, unicode: boolean): RegExp =>",
|
|
108
|
+
' new RegExp(`^${atom}+(?:\\\\.${atom}+)*@(?:${label}\\\\.)+${label}$`, unicode ? "u" : "")'
|
|
109
|
+
].join("\n")
|
|
110
|
+
};
|
|
111
|
+
const SHARED_NEEDS = {
|
|
112
|
+
_ipv6Body: ["_ipv4Octets"],
|
|
113
|
+
_uriGrammar: ["_ipv4Octets", "_ipv6Body"],
|
|
114
|
+
_timeCheck: ["_isDate"]
|
|
115
|
+
};
|
|
116
|
+
const regexFragment = (family, pattern, needs) => ({
|
|
117
|
+
family,
|
|
118
|
+
...needs === void 0 ? {} : { needs },
|
|
119
|
+
source: (name) => `const ${name} = (value: string): boolean => ${pattern}.test(value)`
|
|
120
|
+
});
|
|
121
|
+
const builtFragment = (family, expression, needs) => ({
|
|
122
|
+
family,
|
|
123
|
+
needs,
|
|
124
|
+
source: (name) => `const ${name} = ${expression}`
|
|
125
|
+
});
|
|
126
|
+
const FORMAT_FRAGMENTS = {
|
|
127
|
+
email: builtFragment("string", "_emailPattern(_asciiAtom, _asciiLabel, false).test.bind(_emailPattern(_asciiAtom, _asciiLabel, false))", ["_emailPattern", "_asciiLabel"]),
|
|
128
|
+
"idn-email": builtFragment("string", "_emailPattern(_idnAtom, _idnLabel, true).test.bind(_emailPattern(_idnAtom, _idnLabel, true))", ["_emailPattern", "_idnLabel"]),
|
|
129
|
+
date: builtFragment("string", "_isDate", ["_isDate"]),
|
|
130
|
+
time: builtFragment("string", "_timeCheck(true)", ["_timeCheck"]),
|
|
131
|
+
"iso-time": builtFragment("string", "_timeCheck(false)", ["_timeCheck"]),
|
|
132
|
+
"date-time": builtFragment("string", "_dateTimeCheck(_timeCheck(true))", ["_timeCheck"]),
|
|
133
|
+
"iso-date-time": builtFragment("string", "_dateTimeCheck(_timeCheck(false))", ["_timeCheck"]),
|
|
134
|
+
duration: builtFragment("string", [
|
|
135
|
+
"((): ((value: string) => boolean) => {",
|
|
136
|
+
" const second = '\\\\d+S'",
|
|
137
|
+
" const minute = `\\\\d+M(?:${second})?`",
|
|
138
|
+
" const hour = `\\\\d+H(?:${minute})?`",
|
|
139
|
+
" const time = `T(?:${hour}|${minute}|${second})`",
|
|
140
|
+
" const day = '\\\\d+D'",
|
|
141
|
+
" const month = `\\\\d+M(?:${day})?`",
|
|
142
|
+
" const year = `\\\\d+Y(?:${month})?`",
|
|
143
|
+
" const date = `(?:${year}|${month}|${day})(?:${time})?`",
|
|
144
|
+
" const pattern = new RegExp(`^P(?:${date}|${time}|\\\\d+W)$`)",
|
|
145
|
+
" return (value) => pattern.test(value)",
|
|
146
|
+
"})()"
|
|
147
|
+
].join("\n"), []),
|
|
148
|
+
uuid: regexFragment("string", "/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/"),
|
|
149
|
+
uri: builtFragment("string", "((re) => (value: string) => re.test(value))(new RegExp(_asciiUri.uri))", [
|
|
150
|
+
"_uriGrammar"
|
|
151
|
+
]),
|
|
152
|
+
"uri-reference": builtFragment("string", "((re) => (value: string) => re.test(value))(new RegExp(_asciiUri.reference))", ["_uriGrammar"]),
|
|
153
|
+
iri: builtFragment("string", '((re) => (value: string) => re.test(value))(new RegExp(_iriUri.uri, "u"))', [
|
|
154
|
+
"_uriGrammar"
|
|
155
|
+
]),
|
|
156
|
+
"iri-reference": builtFragment("string", '((re) => (value: string) => re.test(value))(new RegExp(_iriUri.reference, "u"))', ["_uriGrammar"]),
|
|
157
|
+
url: builtFragment("string", "((re) => (value: string) => /^(?:https?|ftp):\\/\\/[^/?#]/.test(value) && re.test(value))(new RegExp(_asciiUri.uri))", ["_uriGrammar"]),
|
|
158
|
+
"uri-template": builtFragment("string", [
|
|
159
|
+
"((): ((value: string) => boolean) => {",
|
|
160
|
+
" const literal = `(?:[!#$&(-;=?-\\\\[\\\\]_a-z~${_ucschar}${_iprivate}]|${_pctEncoded})`",
|
|
161
|
+
" const varchar = `(?:[A-Za-z0-9_]|${_pctEncoded})`",
|
|
162
|
+
" const varname = `${varchar}(?:\\\\.?${varchar})*`",
|
|
163
|
+
" const modifier = '(?::[1-9]\\\\d{0,3}|\\\\*)?'",
|
|
164
|
+
" const varspec = `${varname}${modifier}`",
|
|
165
|
+
" const expression = `\\\\{[+#./;?&=,!@|]?${varspec}(?:,${varspec})*\\\\}`",
|
|
166
|
+
' const pattern = new RegExp(`^(?:${literal}|${expression})*$`, "iu")',
|
|
167
|
+
" return (value) => pattern.test(value)",
|
|
168
|
+
"})()"
|
|
169
|
+
].join("\n"), ["_uriGrammar"]),
|
|
170
|
+
"json-pointer": regexFragment("string", "/^(?:\\/(?:[^~/]|~[01])*)*$/"),
|
|
171
|
+
"json-pointer-uri-fragment": regexFragment("string", "/^#(?:\\/(?:[^~/]|~[01])*)*$/"),
|
|
172
|
+
"relative-json-pointer": regexFragment("string", "/^(?:0|[1-9]\\d*)(?:#|(?:\\/(?:[^~/]|~[01])*)*)$/"),
|
|
173
|
+
hostname: builtFragment("string", "((re) => (value: string) => re.test(value))(new RegExp(`^(?=.{1,253}$)${_asciiLabel}(?:\\\\.${_asciiLabel})*$`))", ["_asciiLabel"]),
|
|
174
|
+
"idn-hostname": builtFragment("string", '((re) => (value: string) => re.test(value))(new RegExp(`^(?=.{1,253}$)${_idnLabel}(?:\\\\.${_idnLabel})*$`, "u"))', ["_idnLabel"]),
|
|
175
|
+
ipv4: builtFragment("string", "((re) => (value: string) => re.test(value))(new RegExp(`^${_ipv4Octets}$`))", [
|
|
176
|
+
"_ipv4Octets"
|
|
177
|
+
]),
|
|
178
|
+
ipv6: builtFragment("string", "((re) => (value: string) => re.test(value))(new RegExp(`^${_ipv6Body}$`))", [
|
|
179
|
+
"_ipv6Body"
|
|
180
|
+
]),
|
|
181
|
+
regex: {
|
|
182
|
+
family: "string",
|
|
183
|
+
source: (name) => `const ${name} = (value: string): boolean => {
|
|
184
|
+
try {
|
|
185
|
+
new RegExp(value)
|
|
186
|
+
return true
|
|
187
|
+
} catch {
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
}`
|
|
191
|
+
},
|
|
192
|
+
byte: regexFragment("string", "/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/"),
|
|
193
|
+
binary: { family: "string", source: (name) => `const ${name} = (_value: string): boolean => true` },
|
|
194
|
+
password: { family: "string", source: (name) => `const ${name} = (_value: string): boolean => true` },
|
|
195
|
+
int32: {
|
|
196
|
+
family: "number",
|
|
197
|
+
source: (name) => `const ${name} = (value: number): boolean => Number.isInteger(value) && value >= -(2 ** 31) && value <= 2 ** 31 - 1`
|
|
198
|
+
},
|
|
199
|
+
int64: { family: "number", source: (name) => `const ${name} = (value: number): boolean => Number.isInteger(value)` },
|
|
200
|
+
float: { family: "number", source: (name) => `const ${name} = (_value: number): boolean => true` },
|
|
201
|
+
double: { family: "number", source: (name) => `const ${name} = (_value: number): boolean => true` }
|
|
202
|
+
};
|
|
203
|
+
const formatFamily = (format) => Object.hasOwn(FORMAT_FRAGMENTS, format) ? FORMAT_FRAGMENTS[format].family : void 0;
|
|
204
|
+
const emitFormatModule = (formats) => {
|
|
205
|
+
const wanted = [...new Set(formats)].filter((format) => Object.hasOwn(FORMAT_FRAGMENTS, format)).sort();
|
|
206
|
+
if (wanted.length === 0)
|
|
207
|
+
return "";
|
|
208
|
+
const needed = /* @__PURE__ */ new Set();
|
|
209
|
+
const require2 = (helper) => {
|
|
210
|
+
if (needed.has(helper))
|
|
211
|
+
return;
|
|
212
|
+
needed.add(helper);
|
|
213
|
+
for (const nested of SHARED_NEEDS[helper] ?? [])
|
|
214
|
+
require2(nested);
|
|
215
|
+
};
|
|
216
|
+
for (const format of wanted) {
|
|
217
|
+
for (const helper of FORMAT_FRAGMENTS[format].needs ?? [])
|
|
218
|
+
require2(helper);
|
|
219
|
+
}
|
|
220
|
+
const helpers = Object.keys(SHARED).filter((helper) => needed.has(helper));
|
|
221
|
+
const checks = wanted.map((format) => {
|
|
222
|
+
const fragment = FORMAT_FRAGMENTS[format];
|
|
223
|
+
const name = formatCheckName(format);
|
|
224
|
+
return `${fragment.source(name)}
|
|
225
|
+
export { ${name} }`;
|
|
226
|
+
});
|
|
227
|
+
return [
|
|
228
|
+
"/**",
|
|
229
|
+
" * `format` checks for the formats this schema declares.",
|
|
230
|
+
" *",
|
|
231
|
+
" * Generated alongside the validators that call them, so the emitted code",
|
|
232
|
+
" * stays dependency-free. Behaviour matches `@amritk/runtime-validators` run",
|
|
233
|
+
" * with the same formats enabled \u2014 a differential test over the official",
|
|
234
|
+
" * suite's optional/format corpus holds the two together.",
|
|
235
|
+
" */",
|
|
236
|
+
"",
|
|
237
|
+
...helpers.map((helper) => SHARED[helper]),
|
|
238
|
+
"",
|
|
239
|
+
...checks,
|
|
240
|
+
""
|
|
241
|
+
].join("\n");
|
|
242
|
+
};
|
|
243
|
+
export {
|
|
244
|
+
FORMAT_FRAGMENTS,
|
|
245
|
+
emitFormatModule,
|
|
246
|
+
formatCheckName,
|
|
247
|
+
formatFamily
|
|
248
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
2
|
+
/**
|
|
3
|
+
* Every keyword this generator turns into a runtime check. Annotations
|
|
4
|
+
* (`title`, `description`, `default`, `$defs`, …) are deliberately absent: they
|
|
5
|
+
* change no verdict, so a node carrying only those is still "just" whatever its
|
|
6
|
+
* one validation keyword says.
|
|
7
|
+
*
|
|
8
|
+
* `format` is the one keyword whose membership is not fixed. It is an annotation
|
|
9
|
+
* by default — the 2020-12 reading, and the interpreter's — and an assertion
|
|
10
|
+
* when the caller asks for formats to be enforced, so every predicate here takes
|
|
11
|
+
* the enforced set rather than reading a constant.
|
|
12
|
+
*/
|
|
13
|
+
export declare const ENFORCED_KEYWORDS: Set<string>;
|
|
14
|
+
/**
|
|
15
|
+
* True when a node declares an enforced keyword outside the set an emitter
|
|
16
|
+
* `owns`.
|
|
17
|
+
*
|
|
18
|
+
* The specialised emitters — the top-level `$ref` delegation, the `const` and
|
|
19
|
+
* `enum` roots, the flat boolean guards — each recognise one keyword and write
|
|
20
|
+
* out a shape built around it. That shape has nowhere to put a sibling, so every
|
|
21
|
+
* one of them used to drop the siblings silently: `{ $ref, minLength: 3 }`
|
|
22
|
+
* accepted `"q"`, `{ type: 'string', const: 1 }` accepted `1`. Asking this first
|
|
23
|
+
* lets each of them keep its tight output for the node it really does describe,
|
|
24
|
+
* and hand anything richer to the general path.
|
|
25
|
+
*/
|
|
26
|
+
export declare const declaresKeywordOutside: (schema: JSONSchema, owned: readonly string[], formats?: ReadonlySet<string>) => boolean;
|
|
27
|
+
/** No formats enforced — the default, and what makes `format` an annotation. */
|
|
28
|
+
export declare const NO_FORMATS: ReadonlySet<string>;
|
|
29
|
+
/**
|
|
30
|
+
* Whether this node's `format` is one the caller asked to enforce, making it an
|
|
31
|
+
* assertion rather than an annotation.
|
|
32
|
+
*/
|
|
33
|
+
export declare const enforcesFormat: (schema: Record<string, unknown>, formats: ReadonlySet<string>) => boolean;
|