@amritk/generate-validators 0.11.12 → 0.12.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.
@@ -0,0 +1,29 @@
1
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
2
+ /**
3
+ * Builds the boolean expression that is TRUE when `accessor` matches `schema`.
4
+ * Supplied by the caller so this module stays independent of how the validator
5
+ * spells a match; `depth` lets the caller mint collision-free locals.
6
+ */
7
+ export type UnevaluatedMatchFn = (accessor: string, schema: JSONSchema, depth: number) => string;
8
+ /** The statements to emit before {@link UnevaluatedExpression.expr}, and the test itself. */
9
+ export type UnevaluatedExpression = {
10
+ /** `const` declarations for the branch conditions the test reads. */
11
+ readonly setup: readonly string[];
12
+ /** TRUE when the value has no unevaluated key / index left over. */
13
+ readonly expr: string;
14
+ };
15
+ /**
16
+ * The `unevaluatedProperties` test for an object accessor: every own key the
17
+ * schema's other keywords did not evaluate must satisfy the unevaluated
18
+ * subschema (and `false` means there must be no such key).
19
+ *
20
+ * Returns `undefined` when the keyword is absent or constrains nothing (a `true`
21
+ * schema, or a node whose other keywords already sweep every key), and `null`
22
+ * when it cannot be proven inline.
23
+ */
24
+ export declare const unevaluatedPropertiesExpr: (acc: string, schema: JSONSchema, rootSchema: Record<string, unknown> | undefined, depth: number, match: UnevaluatedMatchFn) => UnevaluatedExpression | null | undefined;
25
+ /**
26
+ * The `unevaluatedItems` test for an array accessor — the index counterpart of
27
+ * {@link unevaluatedPropertiesExpr}.
28
+ */
29
+ export declare const unevaluatedItemsExpr: (acc: string, schema: JSONSchema, rootSchema: Record<string, unknown> | undefined, depth: number, match: UnevaluatedMatchFn) => UnevaluatedExpression | null | undefined;
@@ -0,0 +1,209 @@
1
+ import { regexLiteral } from "@amritk/helpers/escape-regex-pattern";
2
+ import { resolveRef } from "@amritk/helpers/resolve-ref";
3
+ import { isSchemaObject } from "@amritk/helpers/schema-guards";
4
+ const NONE = { all: false, terms: [] };
5
+ const ALL = { all: true, terms: [] };
6
+ const MAX_COVERAGE_DEPTH = 8;
7
+ const orJoin = (terms) => terms.length === 1 ? terms[0] : `(${terms.join(" || ")})`;
8
+ const nest = (ctx) => ({ ...ctx, depth: ctx.depth + 1 });
9
+ const hoistCondition = (sink, condition) => {
10
+ const name = `${sink.prefix}${sink.setup.length}`;
11
+ sink.setup.push(`const ${name} = ${condition}`);
12
+ return name;
13
+ };
14
+ const conditioned = (coverage, condition) => {
15
+ if (coverage.all)
16
+ return { all: false, terms: [condition] };
17
+ if (coverage.terms.length === 0)
18
+ return NONE;
19
+ return { all: false, terms: [`(${condition} && ${orJoin(coverage.terms)})`] };
20
+ };
21
+ const merge = (into, from) => into.all || from.all ? ALL : { all: false, terms: [...into.terms, ...from.terms] };
22
+ const ownPropertyCoverage = (schema, keyVar, ignoreOwnUnevaluated) => {
23
+ if ("additionalProperties" in schema)
24
+ return ALL;
25
+ if (!ignoreOwnUnevaluated && "unevaluatedProperties" in schema && schema["unevaluatedProperties"] !== false) {
26
+ return ALL;
27
+ }
28
+ const terms = [];
29
+ const properties = schema["properties"];
30
+ if (typeof properties === "object" && properties !== null && !Array.isArray(properties)) {
31
+ const keys = Object.keys(properties);
32
+ if (keys.length > 0)
33
+ terms.push(`${JSON.stringify(keys)}.includes(${keyVar})`);
34
+ }
35
+ const patterns = schema["patternProperties"];
36
+ if (typeof patterns === "object" && patterns !== null && !Array.isArray(patterns)) {
37
+ for (const pattern of Object.keys(patterns)) {
38
+ terms.push(`${regexLiteral(pattern)}.test(${keyVar})`);
39
+ }
40
+ }
41
+ return { all: false, terms };
42
+ };
43
+ const ownItemCoverage = (schema, indexVar, itemVar, ignoreOwnUnevaluated, ctx, match) => {
44
+ const tupleItems = Array.isArray(schema["items"]);
45
+ if (!tupleItems && "items" in schema)
46
+ return ALL;
47
+ if (tupleItems && "additionalItems" in schema)
48
+ return ALL;
49
+ if (!ignoreOwnUnevaluated && "unevaluatedItems" in schema && schema["unevaluatedItems"] !== false) {
50
+ return ALL;
51
+ }
52
+ const terms = [];
53
+ const prefix = Array.isArray(schema["prefixItems"]) ? schema["prefixItems"] : tupleItems ? schema["items"] : null;
54
+ if (prefix && prefix.length > 0)
55
+ terms.push(`${indexVar} < ${prefix.length}`);
56
+ if ("contains" in schema) {
57
+ terms.push(match(itemVar, schema["contains"], ctx.depth));
58
+ }
59
+ return { all: false, terms };
60
+ };
61
+ const coverageOf = (kind, schema, indexVar, itemVar, acc, ctx, sink, match, ignoreOwnUnevaluated = false) => {
62
+ if (typeof schema === "boolean")
63
+ return NONE;
64
+ if (!isSchemaObject(schema))
65
+ return null;
66
+ if (ctx.depth > MAX_COVERAGE_DEPTH)
67
+ return null;
68
+ const s = schema;
69
+ if (typeof s["$dynamicRef"] === "string")
70
+ return null;
71
+ let coverage = kind === "properties" ? ownPropertyCoverage(s, indexVar, ignoreOwnUnevaluated) : ownItemCoverage(s, indexVar, itemVar, ignoreOwnUnevaluated, ctx, match);
72
+ const ref = s["$ref"];
73
+ if (typeof ref === "string") {
74
+ if (ctx.rootSchema === void 0 || ctx.seen.has(ref))
75
+ return null;
76
+ const target = resolveRef(ref, ctx.rootSchema);
77
+ if (target === void 0)
78
+ return null;
79
+ const seen = new Set(ctx.seen);
80
+ seen.add(ref);
81
+ const refCoverage = coverageOf(kind, target, indexVar, itemVar, acc, { ...nest(ctx), seen }, sink, match);
82
+ if (refCoverage === null)
83
+ return null;
84
+ coverage = merge(coverage, refCoverage);
85
+ }
86
+ if (Array.isArray(s["allOf"])) {
87
+ for (const member of s["allOf"]) {
88
+ const memberCoverage = coverageOf(kind, member, indexVar, itemVar, acc, nest(ctx), sink, match);
89
+ if (memberCoverage === null)
90
+ return null;
91
+ coverage = merge(coverage, memberCoverage);
92
+ }
93
+ }
94
+ for (const keyword of ["anyOf", "oneOf"]) {
95
+ if (!Array.isArray(s[keyword]))
96
+ continue;
97
+ for (const branch of s[keyword]) {
98
+ const branchCoverage = coverageOf(kind, branch, indexVar, itemVar, acc, nest(ctx), sink, match);
99
+ if (branchCoverage === null)
100
+ return null;
101
+ if (!branchCoverage.all && branchCoverage.terms.length === 0)
102
+ continue;
103
+ coverage = merge(coverage, conditioned(branchCoverage, hoistCondition(sink, match(acc, branch, ctx.depth))));
104
+ }
105
+ }
106
+ if ("if" in s) {
107
+ const branches = [];
108
+ for (const [keyword, negated] of [
109
+ ["if", false],
110
+ ["then", false],
111
+ ["else", true]
112
+ ]) {
113
+ if (!(keyword in s))
114
+ continue;
115
+ const branchCoverage = coverageOf(kind, s[keyword], indexVar, itemVar, acc, nest(ctx), sink, match);
116
+ if (branchCoverage === null)
117
+ return null;
118
+ if (!branchCoverage.all && branchCoverage.terms.length === 0)
119
+ continue;
120
+ branches.push({ coverage: branchCoverage, negated });
121
+ }
122
+ if (branches.length > 0) {
123
+ const ifMatch = hoistCondition(sink, match(acc, s["if"], ctx.depth));
124
+ for (const branch of branches) {
125
+ coverage = merge(coverage, conditioned(branch.coverage, branch.negated ? `!${ifMatch}` : ifMatch));
126
+ }
127
+ }
128
+ }
129
+ for (const keyword of ["dependentSchemas", "dependencies"]) {
130
+ const dependents = s[keyword];
131
+ if (typeof dependents !== "object" || dependents === null || Array.isArray(dependents))
132
+ continue;
133
+ for (const [trigger, sub] of Object.entries(dependents)) {
134
+ if (Array.isArray(sub))
135
+ continue;
136
+ const subCoverage = coverageOf(kind, sub, indexVar, itemVar, acc, nest(ctx), sink, match);
137
+ if (subCoverage === null)
138
+ return null;
139
+ if (!subCoverage.all && subCoverage.terms.length === 0)
140
+ continue;
141
+ const present = `Object.hasOwn(${acc} as object, ${JSON.stringify(trigger)})`;
142
+ coverage = merge(coverage, conditioned(subCoverage, hoistCondition(sink, present)));
143
+ }
144
+ }
145
+ return coverage;
146
+ };
147
+ const unevaluatedPropertiesExpr = (acc, schema, rootSchema, depth, match) => {
148
+ if (!isSchemaObject(schema))
149
+ return void 0;
150
+ const s = schema;
151
+ if (!("unevaluatedProperties" in s))
152
+ return void 0;
153
+ const unevaluated = s["unevaluatedProperties"];
154
+ if (unevaluated === true)
155
+ return void 0;
156
+ const keyVar = `_uk${depth}`;
157
+ const sink = { setup: [], prefix: `_uc${depth}_` };
158
+ const ctx = { rootSchema, depth: 0, seen: /* @__PURE__ */ new Set() };
159
+ const coverage = coverageOf("properties", schema, keyVar, "", acc, ctx, sink, match, true);
160
+ if (coverage === null)
161
+ return null;
162
+ if (coverage.all)
163
+ return void 0;
164
+ const record = `(${acc} as Record<string, unknown>)`;
165
+ const covered = coverage.terms.length > 0 ? orJoin(coverage.terms) : null;
166
+ if (unevaluated === false) {
167
+ const expr2 = covered === null ? `Object.keys(${record}).length === 0` : `Object.keys(${record}).every((${keyVar}) => ${covered})`;
168
+ return { setup: sink.setup, expr: expr2 };
169
+ }
170
+ const valueMatch = match(`${record}[${keyVar}]`, unevaluated, depth);
171
+ if (valueMatch === "true")
172
+ return void 0;
173
+ const expr = covered === null ? `Object.keys(${record}).every((${keyVar}) => ${valueMatch})` : `Object.keys(${record}).every((${keyVar}) => ${covered} || ${valueMatch})`;
174
+ return { setup: sink.setup, expr };
175
+ };
176
+ const unevaluatedItemsExpr = (acc, schema, rootSchema, depth, match) => {
177
+ if (!isSchemaObject(schema))
178
+ return void 0;
179
+ const s = schema;
180
+ if (!("unevaluatedItems" in s))
181
+ return void 0;
182
+ const unevaluated = s["unevaluatedItems"];
183
+ if (unevaluated === true)
184
+ return void 0;
185
+ const indexVar = `_un${depth}`;
186
+ const itemVar = `_ue${depth}`;
187
+ const sink = { setup: [], prefix: `_ud${depth}_` };
188
+ const ctx = { rootSchema, depth: 0, seen: /* @__PURE__ */ new Set() };
189
+ const coverage = coverageOf("items", schema, indexVar, itemVar, acc, ctx, sink, match, true);
190
+ if (coverage === null)
191
+ return null;
192
+ if (coverage.all)
193
+ return void 0;
194
+ const elements = `(${acc} as unknown[])`;
195
+ const covered = coverage.terms.length > 0 ? orJoin(coverage.terms) : null;
196
+ if (unevaluated === false) {
197
+ const expr2 = covered === null ? `${elements}.length === 0` : `${elements}.every((${itemVar}, ${indexVar}) => ${covered})`;
198
+ return { setup: sink.setup, expr: expr2 };
199
+ }
200
+ const valueMatch = match(itemVar, unevaluated, depth);
201
+ if (valueMatch === "true")
202
+ return void 0;
203
+ const expr = covered === null ? `${elements}.every((${itemVar}) => ${valueMatch})` : `${elements}.every((${itemVar}, ${indexVar}) => ${covered} || ${valueMatch})`;
204
+ return { setup: sink.setup, expr };
205
+ };
206
+ export {
207
+ unevaluatedItemsExpr,
208
+ unevaluatedPropertiesExpr
209
+ };
package/package.json CHANGED
@@ -1,8 +1,10 @@
1
1
  {
2
2
  "name": "@amritk/generate-validators",
3
- "version": "0.11.12",
3
+ "version": "0.12.0",
4
4
  "description": "Generate TypeScript validation functions from JSON Schemas.",
5
+ "main": "./dist/index.js",
5
6
  "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
6
8
  "type": "module",
7
9
  "sideEffects": false,
8
10
  "engines": {
@@ -51,10 +53,10 @@
51
53
  },
52
54
  "dependencies": {
53
55
  "json-schema-typed": "^8.0.1",
54
- "@amritk/helpers": "0.14.0"
56
+ "@amritk/helpers": "^0.15.0"
55
57
  },
56
58
  "devDependencies": {
57
- "@amritk/runtime-validators": "0.9.1",
59
+ "@amritk/runtime-validators": "^0.10.0",
58
60
  "@ryoppippi/unplugin-typia": "^2.6.5",
59
61
  "@scalar/openapi-parser": "^0.26.1",
60
62
  "@sinclair/typebox": "^0.34.49",