@amritk/generate-parsers 0.23.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.
|
@@ -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",
|