@amritk/generate-parsers 0.17.2 → 0.19.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.
Files changed (32) hide show
  1. package/AI.md +10 -0
  2. package/README.md +147 -19
  3. package/dist/generators/assert-supported-keywords.d.ts +13 -9
  4. package/dist/generators/assert-supported-keywords.js +58 -18
  5. package/dist/generators/build-schema.d.ts +9 -1
  6. package/dist/generators/build-schema.js +5 -2
  7. package/dist/generators/generate-deep-equal-check.d.ts +24 -0
  8. package/dist/generators/generate-deep-equal-check.js +35 -0
  9. package/dist/generators/generate-discriminated-union-validation.js +1 -2
  10. package/dist/generators/generate-enum-check.d.ts +8 -3
  11. package/dist/generators/generate-enum-check.js +8 -6
  12. package/dist/generators/generate-files.d.ts +7 -0
  13. package/dist/generators/generate-files.js +23 -4
  14. package/dist/generators/generate-parser-function.d.ts +1 -1
  15. package/dist/generators/generate-parser-function.js +212 -65
  16. package/dist/generators/generate-schema-checks.js +10 -8
  17. package/dist/generators/generate-strict-assertion.d.ts +89 -5
  18. package/dist/generators/generate-strict-assertion.js +429 -58
  19. package/dist/generators/generate-type-checks.d.ts +46 -10
  20. package/dist/generators/generate-type-checks.js +112 -16
  21. package/dist/generators/generate-unique-items-check.d.ts +16 -0
  22. package/dist/generators/generate-unique-items-check.js +32 -0
  23. package/dist/generators/generate-validation-expression.d.ts +29 -4
  24. package/dist/generators/generate-validation-expression.js +15 -6
  25. package/dist/generators/subschema-match.d.ts +23 -4
  26. package/dist/generators/subschema-match.js +312 -48
  27. package/dist/generators/unevaluated-match.d.ts +19 -0
  28. package/dist/generators/unevaluated-match.js +206 -0
  29. package/dist/helpers/collect-imports.d.ts +8 -0
  30. package/dist/helpers/collect-imports.js +1 -1
  31. package/dist/helpers/get-default-value.js +45 -2
  32. package/package.json +9 -3
@@ -0,0 +1,206 @@
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 orJoin = (terms) => terms.length === 1 ? terms[0] : `(${terms.join(" || ")})`;
6
+ const depthOf = (ctx) => ctx.depth ?? 0;
7
+ const nest = (ctx) => ({ ...ctx, depth: depthOf(ctx) + 1 });
8
+ const conditioned = (coverage, cond) => {
9
+ if (coverage.all)
10
+ return { all: false, terms: [cond] };
11
+ if (coverage.terms.length === 0)
12
+ return NONE;
13
+ return { all: false, terms: [`(${cond} && ${orJoin(coverage.terms)})`] };
14
+ };
15
+ const merge = (into, from) => into.all || from.all ? { all: true, terms: [] } : { all: false, terms: [...into.terms, ...from.terms] };
16
+ const ownPropertyCoverage = (schema, keyVar, ignoreOwnUnevaluated) => {
17
+ if ("additionalProperties" in schema)
18
+ return { all: true, terms: [] };
19
+ if (!ignoreOwnUnevaluated && "unevaluatedProperties" in schema && schema["unevaluatedProperties"] !== false) {
20
+ return { all: true, terms: [] };
21
+ }
22
+ const terms = [];
23
+ const properties = schema["properties"];
24
+ if (typeof properties === "object" && properties !== null && !Array.isArray(properties)) {
25
+ const keys = Object.keys(properties);
26
+ if (keys.length > 0)
27
+ terms.push(`${JSON.stringify(keys)}.includes(${keyVar})`);
28
+ }
29
+ const patterns = schema["patternProperties"];
30
+ if (typeof patterns === "object" && patterns !== null && !Array.isArray(patterns)) {
31
+ for (const pattern of Object.keys(patterns)) {
32
+ terms.push(`${regexLiteral(pattern)}.test(${keyVar})`);
33
+ }
34
+ }
35
+ return { all: false, terms };
36
+ };
37
+ const ownItemCoverage = (schema, indexVar, ignoreOwnUnevaluated) => {
38
+ const tupleItems = Array.isArray(schema["items"]);
39
+ if (!tupleItems && "items" in schema)
40
+ return { all: true, terms: [] };
41
+ if (tupleItems && "additionalItems" in schema)
42
+ return { all: true, terms: [] };
43
+ if (!ignoreOwnUnevaluated && "unevaluatedItems" in schema && schema["unevaluatedItems"] !== false) {
44
+ return { all: true, terms: [] };
45
+ }
46
+ const terms = [];
47
+ const prefix = Array.isArray(schema["prefixItems"]) ? schema["prefixItems"] : tupleItems ? schema["items"] : null;
48
+ if (prefix && prefix.length > 0)
49
+ terms.push(`${indexVar} < ${prefix.length}`);
50
+ return { all: false, terms };
51
+ };
52
+ const containsCoverage = (schema, indexVar, acc, ctx, match) => {
53
+ if (!("contains" in schema))
54
+ return NONE;
55
+ const itemMatch = match(`(${acc} as unknown[])[${indexVar}]`, schema["contains"], nest(ctx));
56
+ if (itemMatch === null)
57
+ return null;
58
+ if (itemMatch === "true")
59
+ return { all: true, terms: [] };
60
+ return { all: false, terms: [itemMatch] };
61
+ };
62
+ const coverageOf = (kind, schema, varName, acc, ctx, match, ignoreOwnUnevaluated = false) => {
63
+ if (typeof schema === "boolean")
64
+ return NONE;
65
+ if (!isSchemaObject(schema))
66
+ return null;
67
+ if (depthOf(ctx) > 8)
68
+ return null;
69
+ const s = schema;
70
+ let coverage = kind === "properties" ? ownPropertyCoverage(s, varName, ignoreOwnUnevaluated) : ownItemCoverage(s, varName, ignoreOwnUnevaluated);
71
+ if (kind === "items") {
72
+ const contains = containsCoverage(s, varName, acc, ctx, match);
73
+ if (contains === null)
74
+ return null;
75
+ coverage = merge(coverage, contains);
76
+ }
77
+ const ref = s["$ref"];
78
+ if (typeof ref === "string") {
79
+ if (ctx.rootSchema === void 0 || ctx.seen?.has(ref))
80
+ return null;
81
+ const target = resolveRef(ref, ctx.rootSchema);
82
+ if (target === void 0)
83
+ return null;
84
+ const seen = new Set(ctx.seen ?? []);
85
+ seen.add(ref);
86
+ const refCoverage = coverageOf(kind, target, varName, acc, { ...nest(ctx), seen }, match);
87
+ if (refCoverage === null)
88
+ return null;
89
+ coverage = merge(coverage, refCoverage);
90
+ }
91
+ if (Array.isArray(s["allOf"])) {
92
+ for (const member of s["allOf"]) {
93
+ const memberCoverage = coverageOf(kind, member, varName, acc, nest(ctx), match);
94
+ if (memberCoverage === null)
95
+ return null;
96
+ coverage = merge(coverage, memberCoverage);
97
+ }
98
+ }
99
+ for (const keyword of ["anyOf", "oneOf"]) {
100
+ if (!Array.isArray(s[keyword]))
101
+ continue;
102
+ for (const branch of s[keyword]) {
103
+ const branchCoverage = coverageOf(kind, branch, varName, acc, nest(ctx), match);
104
+ if (branchCoverage === null)
105
+ return null;
106
+ if (branchCoverage.all === false && branchCoverage.terms.length === 0)
107
+ continue;
108
+ const branchMatch = match(acc, branch, nest(ctx));
109
+ if (branchMatch === null)
110
+ return null;
111
+ coverage = merge(coverage, conditioned(branchCoverage, branchMatch));
112
+ }
113
+ }
114
+ if ("if" in s) {
115
+ const ifMatch = match(acc, s["if"], nest(ctx));
116
+ if (ifMatch === null)
117
+ return null;
118
+ for (const [keyword, cond] of [
119
+ ["if", ifMatch],
120
+ ["then", ifMatch],
121
+ ["else", `!(${ifMatch})`]
122
+ ]) {
123
+ if (!(keyword in s))
124
+ continue;
125
+ const branchCoverage = coverageOf(kind, s[keyword], varName, acc, nest(ctx), match);
126
+ if (branchCoverage === null)
127
+ return null;
128
+ coverage = merge(coverage, conditioned(branchCoverage, cond));
129
+ }
130
+ }
131
+ const dependentSchemas = s["dependentSchemas"];
132
+ if (typeof dependentSchemas === "object" && dependentSchemas !== null && !Array.isArray(dependentSchemas)) {
133
+ for (const [trigger, sub] of Object.entries(dependentSchemas)) {
134
+ const subCoverage = coverageOf(kind, sub, varName, acc, nest(ctx), match);
135
+ if (subCoverage === null)
136
+ return null;
137
+ coverage = merge(coverage, conditioned(subCoverage, `${JSON.stringify(trigger)} in (${acc} as object)`));
138
+ }
139
+ }
140
+ return coverage;
141
+ };
142
+ const unevaluatedPropertiesExpr = (acc, schema, ctx, match) => {
143
+ if (!isSchemaObject(schema))
144
+ return void 0;
145
+ const s = schema;
146
+ if (!("unevaluatedProperties" in s))
147
+ return void 0;
148
+ const unevaluated = s["unevaluatedProperties"];
149
+ if (unevaluated === true)
150
+ return void 0;
151
+ const d = depthOf(ctx);
152
+ const keyVar = `_uk${d}`;
153
+ const coverage = coverageOf("properties", schema, keyVar, acc, ctx, match, true);
154
+ if (coverage === null)
155
+ return null;
156
+ if (coverage.all)
157
+ return void 0;
158
+ const rec = `(${acc} as Record<string, unknown>)`;
159
+ const covered = coverage.terms.length > 0 ? orJoin(coverage.terms) : null;
160
+ if (unevaluated === false) {
161
+ if (covered === null)
162
+ return `Object.keys(${rec}).length === 0`;
163
+ return `Object.keys(${rec}).every((${keyVar}) => ${covered})`;
164
+ }
165
+ const valueMatch = match(`${rec}[${keyVar}]`, unevaluated, nest(ctx));
166
+ if (valueMatch === null)
167
+ return null;
168
+ if (valueMatch === "true")
169
+ return void 0;
170
+ return covered === null ? `Object.keys(${rec}).every((${keyVar}) => ${valueMatch})` : `Object.keys(${rec}).every((${keyVar}) => ${covered} || ${valueMatch})`;
171
+ };
172
+ const unevaluatedItemsExpr = (acc, schema, ctx, match) => {
173
+ if (!isSchemaObject(schema))
174
+ return void 0;
175
+ const s = schema;
176
+ if (!("unevaluatedItems" in s))
177
+ return void 0;
178
+ const unevaluated = s["unevaluatedItems"];
179
+ if (unevaluated === true)
180
+ return void 0;
181
+ const d = depthOf(ctx);
182
+ const indexVar = `_un${d}`;
183
+ const itemVar = `_ue${d}`;
184
+ const coverage = coverageOf("items", schema, indexVar, acc, ctx, match, true);
185
+ if (coverage === null)
186
+ return null;
187
+ if (coverage.all)
188
+ return void 0;
189
+ const elements = `(${acc} as unknown[])`;
190
+ const covered = coverage.terms.length > 0 ? orJoin(coverage.terms) : null;
191
+ if (unevaluated === false) {
192
+ if (covered === null)
193
+ return `${acc}.length === 0`;
194
+ return `${elements}.every((_, ${indexVar}) => ${covered})`;
195
+ }
196
+ const valueMatch = match(itemVar, unevaluated, nest(ctx));
197
+ if (valueMatch === null)
198
+ return null;
199
+ if (valueMatch === "true")
200
+ return void 0;
201
+ return covered === null ? `${elements}.every((${itemVar}) => ${valueMatch})` : `${elements}.every((${itemVar}, ${indexVar}) => ${covered} || ${valueMatch})`;
202
+ };
203
+ export {
204
+ unevaluatedItemsExpr,
205
+ unevaluatedPropertiesExpr
206
+ };
@@ -16,6 +16,14 @@ type CollectImportsOptions = {
16
16
  * the import list, preventing a file from importing itself.
17
17
  */
18
18
  readonly selfRef?: string | undefined;
19
+ /**
20
+ * The filename this file is being written as, without extension. Takes
21
+ * precedence over `selfRef` for self-import detection, and covers the case
22
+ * `selfRef` cannot: the root document has no ref of its own, so a definition
23
+ * whose name collides with the root type name (root `Contact` + `$defs.contact`)
24
+ * used to make the root file import its own type from itself.
25
+ */
26
+ readonly selfFilename?: string | undefined;
19
27
  /**
20
28
  * The root schema document. When provided, URI refs that cannot be resolved
21
29
  * within the root schema's $defs are excluded from the import list, preventing
@@ -21,7 +21,7 @@ const collectImportTypeNames = (schema, options) => {
21
21
  return names;
22
22
  };
23
23
  const collectImportTargets = (schema, options) => {
24
- const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : void 0;
24
+ const selfFilename = options?.selfFilename ?? (options?.selfRef ? refToFilename(options.selfRef) : void 0);
25
25
  const rootSchema = options?.rootSchema;
26
26
  const typeSuffix = options?.typeSuffix;
27
27
  const refs = /* @__PURE__ */ new Set();
@@ -1,11 +1,54 @@
1
1
  import { getMjstPrimitive } from "@amritk/helpers/mjst-extension";
2
2
  import { hasAllOf, hasAnyOf, hasConst, hasDefault, hasEnum, hasExamples, hasOneOf, hasPattern, hasProperties, hasRequired, hasType, isSchemaObject } from "@amritk/helpers/schema-guards";
3
3
  import { generateDefaultFromPattern } from "../generators/generate-default-from-pattern.js";
4
+ const defaultMatchesType = (value, schema) => {
5
+ if (!hasType(schema))
6
+ return true;
7
+ switch (schema.type) {
8
+ case "string":
9
+ return typeof value === "string";
10
+ case "number":
11
+ return typeof value === "number";
12
+ case "integer":
13
+ return typeof value === "number" && Number.isInteger(value);
14
+ case "boolean":
15
+ return typeof value === "boolean";
16
+ case "null":
17
+ return value === null;
18
+ case "array":
19
+ return Array.isArray(value);
20
+ case "object":
21
+ return typeof value === "object" && value !== null && !Array.isArray(value);
22
+ default:
23
+ return true;
24
+ }
25
+ };
26
+ const MAX_FALLBACK_ITEMS = 32;
27
+ const arrayFallback = (schema) => {
28
+ const record = schema;
29
+ const positions = Array.isArray(record["prefixItems"]) ? record["prefixItems"] : Array.isArray(record["items"]) ? record["items"] : [];
30
+ const tail = Array.isArray(record["items"]) ? record["additionalItems"] : record["items"];
31
+ const minItems = typeof record["minItems"] === "number" ? record["minItems"] : 0;
32
+ const wanted = minItems > MAX_FALLBACK_ITEMS ? Math.min(minItems, positions.length) : minItems;
33
+ if (wanted === 0)
34
+ return "[]";
35
+ const elements = [];
36
+ for (let i = 0; i < wanted; i++) {
37
+ const elementSchema = i < positions.length ? positions[i] : tail;
38
+ if (elementSchema === void 0 || typeof elementSchema === "boolean")
39
+ return "[]";
40
+ const element = getDefaultValue(elementSchema);
41
+ if (element === "undefined")
42
+ return "[]";
43
+ elements.push(element);
44
+ }
45
+ return `[${elements.join(", ")}]`;
46
+ };
4
47
  const getDefaultValue = (schema) => {
5
48
  if (!isSchemaObject(schema)) {
6
49
  return "undefined";
7
50
  }
8
- if (hasDefault(schema)) {
51
+ if (hasDefault(schema) && defaultMatchesType(schema.default, schema)) {
9
52
  return JSON.stringify(schema.default);
10
53
  }
11
54
  if (hasConst(schema)) {
@@ -63,7 +106,7 @@ const getDefaultValue = (schema) => {
63
106
  case "null":
64
107
  return "null";
65
108
  case "array":
66
- return "[]";
109
+ return arrayFallback(schema);
67
110
  case "object": {
68
111
  if (hasProperties(schema) && hasRequired(schema)) {
69
112
  const props = schema.properties;
package/package.json CHANGED
@@ -1,9 +1,15 @@
1
1
  {
2
2
  "name": "@amritk/generate-parsers",
3
- "version": "0.17.2",
3
+ "version": "0.19.0",
4
4
  "description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
5
+ "main": "./dist/index.js",
5
6
  "module": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
6
8
  "type": "module",
9
+ "sideEffects": false,
10
+ "engines": {
11
+ "node": ">=20"
12
+ },
7
13
  "license": "MIT",
8
14
  "author": "amritk",
9
15
  "keywords": [
@@ -49,10 +55,10 @@
49
55
  },
50
56
  "dependencies": {
51
57
  "json-schema-typed": "^8.0.1",
52
- "@amritk/helpers": "0.13.5",
53
- "@amritk/generate-markdown": "0.4.4"
58
+ "@amritk/helpers": "^0.15.0"
54
59
  },
55
60
  "devDependencies": {
61
+ "@amritk/generate-markdown": "^0.4.6",
56
62
  "@sinclair/typebox": "^0.34.49",
57
63
  "ajv": "^8.17.1",
58
64
  "zod": "^4.4.3"