@amritk/generate-parsers 0.22.0 → 0.24.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/README.md
CHANGED
|
@@ -331,7 +331,7 @@ each schema in the official
|
|
|
331
331
|
(the required Draft 2020-12 tests — 1281 cases), links the emitted files in
|
|
332
332
|
memory, and runs the suite's instances through the real generated code:
|
|
333
333
|
|
|
334
|
-
**
|
|
334
|
+
**1242 / 1281 cases pass (97.0%).**
|
|
335
335
|
|
|
336
336
|
A case passes only if the parser throws exactly when the spec says invalid *and*
|
|
337
337
|
returns an accepted document unchanged — strict mode does not coerce, so a parser
|
|
@@ -343,14 +343,14 @@ no I/O to answer the retrieval step. Everything else — applying the base URIs,
|
|
|
343
343
|
walking anchors across documents, emitting a parser per definition and an import
|
|
344
344
|
graph that links — the generator still has to do.
|
|
345
345
|
|
|
346
|
-
Of the
|
|
346
|
+
Of the 39 that do not, **18** follow from the third departure above — the
|
|
347
347
|
`ignores a non-object` cases plus three `ref.json` cases where a recursive `$ref`
|
|
348
348
|
lands on a root that declares `properties` — and **12** are a keyword strict mode will not
|
|
349
349
|
approximate (a cyclic `$ref` with siblings, a cyclic `unevaluatedProperties`) —
|
|
350
350
|
those cost a build error naming the cause, never a wrong verdict. The rest: **8**
|
|
351
351
|
`$dynamicRef`s whose binding depends on the evaluation path (a generator emits one
|
|
352
|
-
function per definition, shared by every path that reaches it),
|
|
353
|
-
|
|
352
|
+
function per definition, shared by every path that reaches it), and one
|
|
353
|
+
`$vocabulary` case.
|
|
354
354
|
|
|
355
355
|
Every one is listed in
|
|
356
356
|
`src/generators/json-schema-conformance-expected-failures.test-utils.ts` with the
|
|
@@ -22,7 +22,8 @@ const generateFile = (schema, typeName, options) => {
|
|
|
22
22
|
selfFilename: options?.selfFilename,
|
|
23
23
|
rootSchema,
|
|
24
24
|
typeSuffix,
|
|
25
|
-
importExt
|
|
25
|
+
importExt,
|
|
26
|
+
usedIn: typeDefinition
|
|
26
27
|
});
|
|
27
28
|
let result2 = "";
|
|
28
29
|
if (imports2.length > 0) {
|
|
@@ -61,7 +62,16 @@ const generateFile = (schema, typeName, options) => {
|
|
|
61
62
|
${parserFunction}`;
|
|
62
63
|
const helpers = collectHelpers(combinedFunctions, helpersMode, options?.helpersImportPrefix, importExt);
|
|
63
64
|
const imports = [
|
|
64
|
-
...collectImports(schema, {
|
|
65
|
+
...collectImports(schema, {
|
|
66
|
+
selfRef,
|
|
67
|
+
selfFilename: options?.selfFilename,
|
|
68
|
+
rootSchema,
|
|
69
|
+
typeSuffix,
|
|
70
|
+
importExt,
|
|
71
|
+
// Both halves of the file: the type names the type definition spells and
|
|
72
|
+
// the functions the emitted code calls are one question about one text.
|
|
73
|
+
usedIn: typeDefinition + "\n\n" + combinedFunctions
|
|
74
|
+
}),
|
|
65
75
|
...helpers.imports
|
|
66
76
|
];
|
|
67
77
|
let result = "";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { regexLiteral } from "@amritk/helpers/escape-regex-pattern";
|
|
2
2
|
import { generateTypeDefinition } from "@amritk/helpers/generate-type-definition";
|
|
3
3
|
import { quoteJsString } from "@amritk/helpers/quote-js-string";
|
|
4
|
+
import { declaresKey, readKey } from "@amritk/helpers/read-key";
|
|
4
5
|
import { refToName } from "@amritk/helpers/ref-to-name";
|
|
5
6
|
import { resolveRef } from "@amritk/helpers/resolve-ref";
|
|
6
7
|
import { hasOwnCheck, missingCheck, safeAccessor, safeKey } from "@amritk/helpers/safe-accessor";
|
|
@@ -208,7 +209,10 @@ const generateFallbackObject = (schema, useRefImports, typeName, suffix, subType
|
|
|
208
209
|
}
|
|
209
210
|
}
|
|
210
211
|
result += "\n }";
|
|
211
|
-
|
|
212
|
+
if (declaresPrototypeMemberProperty(schema))
|
|
213
|
+
return `${result} as unknown as ${typeName}`;
|
|
214
|
+
const composesUnion = isSchemaObject(schema) && (hasOneOf(schema) || hasAnyOf(schema));
|
|
215
|
+
return composesUnion ? `${result} as ${typeName}` : result;
|
|
212
216
|
};
|
|
213
217
|
const SCALAR_ROOT_CONSTRAINTS = [
|
|
214
218
|
"pattern",
|
|
@@ -1699,14 +1703,28 @@ const getConditionalObjectSchema = (schema) => {
|
|
|
1699
1703
|
...required.size > 0 ? { required: Array.from(required) } : {}
|
|
1700
1704
|
};
|
|
1701
1705
|
};
|
|
1702
|
-
const
|
|
1706
|
+
const OBJECT_SHAPE_KEYWORDS = ["properties", "patternProperties", "additionalProperties", "required"];
|
|
1707
|
+
const DELEGATING_KEYWORDS = ["$ref", "$dynamicRef", "allOf", "anyOf", "oneOf", "if", "not"];
|
|
1708
|
+
const admitsBooleanSchema = (schema) => {
|
|
1709
|
+
if (typeof schema === "boolean")
|
|
1710
|
+
return true;
|
|
1711
|
+
if (!isSchemaObject(schema))
|
|
1712
|
+
return false;
|
|
1713
|
+
const record = schema;
|
|
1714
|
+
const type = readKey(record, "type");
|
|
1715
|
+
if (type !== void 0)
|
|
1716
|
+
return Array.isArray(type) ? type.includes("boolean") : type === "boolean";
|
|
1717
|
+
return ![...OBJECT_SHAPE_KEYWORDS, ...DELEGATING_KEYWORDS].some((keyword) => declaresKey(record, keyword));
|
|
1718
|
+
};
|
|
1719
|
+
const generateSchemaObjectParser = (typeName, schema) => {
|
|
1703
1720
|
const functionName = generateParserName(typeName);
|
|
1704
|
-
|
|
1705
|
-
if (typeof input === 'boolean') {
|
|
1721
|
+
const booleanBranch = admitsBooleanSchema(schema) ? ` if (typeof input === 'boolean') {
|
|
1706
1722
|
return input as ${typeName};
|
|
1707
1723
|
}
|
|
1708
1724
|
|
|
1709
|
-
|
|
1725
|
+
` : "";
|
|
1726
|
+
return `export const ${functionName} = (input: unknown): ${typeName} => {
|
|
1727
|
+
${booleanBranch} if (!isObject(input)) {
|
|
1710
1728
|
return {} as ${typeName};
|
|
1711
1729
|
}
|
|
1712
1730
|
|
|
@@ -1730,7 +1748,7 @@ const selectParserStrategy = (schema, typeName, options) => {
|
|
|
1730
1748
|
};
|
|
1731
1749
|
const copyLoopContext = { ...context, subParsers: false };
|
|
1732
1750
|
if (!options?.isRoot && typeName === `Schema${suffix}`) {
|
|
1733
|
-
return generateSchemaObjectParser(typeName);
|
|
1751
|
+
return generateSchemaObjectParser(typeName, schema);
|
|
1734
1752
|
}
|
|
1735
1753
|
const isObjectLikeSchema = isObjectSchema(schema) || isSchemaObject(schema) && ("patternProperties" in schema || "additionalProperties" in schema);
|
|
1736
1754
|
if (!isObjectLikeSchema && !isSchemaObject(schema)) {
|
|
@@ -42,6 +42,17 @@ type CollectImportsOptions = {
|
|
|
42
42
|
* type stripping.
|
|
43
43
|
*/
|
|
44
44
|
readonly importExt?: ImportExtension;
|
|
45
|
+
/**
|
|
46
|
+
* The file body this import list is being written above. When given, only the
|
|
47
|
+
* `$ref` imports whose names the body actually spells are emitted, and one
|
|
48
|
+
* whose parser is never called is narrowed to a type-only import.
|
|
49
|
+
*
|
|
50
|
+
* The walk below can only approximate what the emitters wrote: it stops at a
|
|
51
|
+
* `$ref` the type emitter inlines *through*, and it cannot see a branch that
|
|
52
|
+
* folded away. Asking the emitted text settles both directions, the way
|
|
53
|
+
* `collectHelpers` already settles which runtime helpers a file needs.
|
|
54
|
+
*/
|
|
55
|
+
readonly usedIn?: string | undefined;
|
|
45
56
|
};
|
|
46
57
|
/**
|
|
47
58
|
* Collects all import statements needed for a schema by finding $ref references
|
|
@@ -1,16 +1,28 @@
|
|
|
1
|
+
import { identifierMentions } from "@amritk/helpers/identifier-mentions";
|
|
2
|
+
import { readKey } from "@amritk/helpers/read-key";
|
|
1
3
|
import { refToFilename } from "@amritk/helpers/ref-to-filename";
|
|
2
4
|
import { refToName } from "@amritk/helpers/ref-to-name";
|
|
5
|
+
import { referencedConditional } from "@amritk/helpers/referenced-conditional";
|
|
3
6
|
import { resolveRef } from "@amritk/helpers/resolve-ref";
|
|
4
|
-
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasItems, hasOneOf, hasRef } from "@amritk/helpers/schema-guards";
|
|
7
|
+
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasItems, hasOneOf, hasRef, isSchemaObject } from "@amritk/helpers/schema-guards";
|
|
5
8
|
const getImportPathForFilename = (filename, ext) => `./${filename}.${ext}`;
|
|
6
9
|
const collectImports = (schema, options) => {
|
|
7
10
|
const typesOnly = options?.typesOnly === true;
|
|
8
11
|
const importExt = options?.importExt ?? "js";
|
|
9
12
|
const importMap = /* @__PURE__ */ new Map();
|
|
13
|
+
const mentions = options?.usedIn === void 0 ? () => true : identifierMentions(options.usedIn);
|
|
10
14
|
for (const [filename, { typeName, typeOnly }] of collectImportTargets(schema, options)) {
|
|
11
15
|
const importPath = getImportPathForFilename(filename, importExt);
|
|
12
|
-
const
|
|
13
|
-
|
|
16
|
+
const values = typesOnly || typeOnly ? [] : [`parse${typeName}`, `validate${typeName}Shape`].filter((name) => mentions(name));
|
|
17
|
+
const named = mentions(typeName);
|
|
18
|
+
if (values.length === 0) {
|
|
19
|
+
if (!named)
|
|
20
|
+
continue;
|
|
21
|
+
importMap.set(filename, `import type { ${typeName} } from '${importPath}';`);
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
const specifiers = named ? [`type ${typeName}`, ...values] : values;
|
|
25
|
+
importMap.set(filename, `import { ${specifiers.join(", ")} } from '${importPath}';`);
|
|
14
26
|
}
|
|
15
27
|
return Array.from(importMap.values()).sort();
|
|
16
28
|
};
|
|
@@ -27,6 +39,24 @@ const collectImportTargets = (schema, options) => {
|
|
|
27
39
|
const refs = /* @__PURE__ */ new Set();
|
|
28
40
|
const valueRefs = /* @__PURE__ */ new Set();
|
|
29
41
|
let typeOnlyDepth = 0;
|
|
42
|
+
const inlinedRefs = /* @__PURE__ */ new Set();
|
|
43
|
+
const collectRefsFromAllOfMember = (entry) => {
|
|
44
|
+
collectRefsFromValue(entry);
|
|
45
|
+
const ref = isSchemaObject(entry) ? readKey(entry, "$ref") : void 0;
|
|
46
|
+
if (typeof ref === "string" && inlinedRefs.has(ref))
|
|
47
|
+
return;
|
|
48
|
+
const inlined = referencedConditional(entry, rootSchema);
|
|
49
|
+
if (inlined === void 0)
|
|
50
|
+
return;
|
|
51
|
+
if (typeof ref === "string")
|
|
52
|
+
inlinedRefs.add(ref);
|
|
53
|
+
typeOnlyDepth++;
|
|
54
|
+
for (const arm of ["if", "then", "else"]) {
|
|
55
|
+
if (Object.hasOwn(inlined, arm))
|
|
56
|
+
collectRefsFromValue(readKey(inlined, arm));
|
|
57
|
+
}
|
|
58
|
+
typeOnlyDepth--;
|
|
59
|
+
};
|
|
30
60
|
const collectRefsFromValue = (value) => {
|
|
31
61
|
if (typeof value !== "object" || value === null) {
|
|
32
62
|
return;
|
|
@@ -87,7 +117,7 @@ const collectImportTargets = (schema, options) => {
|
|
|
87
117
|
}
|
|
88
118
|
if (hasAllOf(record)) {
|
|
89
119
|
for (const item of record.allOf) {
|
|
90
|
-
|
|
120
|
+
collectRefsFromAllOfMember(item);
|
|
91
121
|
}
|
|
92
122
|
}
|
|
93
123
|
if (hasItems(record) && !Array.isArray(record.items)) {
|
|
@@ -163,7 +193,7 @@ const collectImportTargets = (schema, options) => {
|
|
|
163
193
|
}
|
|
164
194
|
if (typeof schema === "object" && schema !== null && hasAllOf(schema)) {
|
|
165
195
|
for (const item of schema.allOf) {
|
|
166
|
-
|
|
196
|
+
collectRefsFromAllOfMember(item);
|
|
167
197
|
}
|
|
168
198
|
}
|
|
169
199
|
if (typeof schema === "object" && schema !== null && Object.hasOwn(schema, "if")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/generate-parsers",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Generate TypeScript parsers and type definitions from JSON Schemas.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -60,10 +60,10 @@
|
|
|
60
60
|
},
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"json-schema-typed": "^8.0.1",
|
|
63
|
-
"@amritk/helpers": "^0.
|
|
63
|
+
"@amritk/helpers": "^0.22.0"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@amritk/generate-markdown": "^0.
|
|
66
|
+
"@amritk/generate-markdown": "^0.8.0",
|
|
67
67
|
"@sinclair/typebox": "^0.34.49",
|
|
68
68
|
"ajv": "^8.17.1",
|
|
69
69
|
"benny": "^3.7.1",
|