@amritk/generate-validators 0.11.7 → 0.11.9
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/LICENSE +21 -0
- package/dist/generators/build-schema.js +21 -44
- package/dist/generators/collect-validator-imports.js +67 -110
- package/dist/generators/generate-files.js +34 -59
- package/dist/generators/generate-validator-function.js +1201 -1800
- package/dist/index.js +4 -1
- package/package.json +6 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 amritk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { generateIndexBarrel } from
|
|
2
|
-
import { walkRefGraph } from
|
|
3
|
-
import { generateValidatorFile } from
|
|
1
|
+
import { generateIndexBarrel } from "@amritk/helpers/generate-index-barrel";
|
|
2
|
+
import { walkRefGraph } from "@amritk/helpers/walk-ref-graph";
|
|
3
|
+
import { generateValidatorFile } from "./generate-files.js";
|
|
4
4
|
const VALIDATION_RESULT_CONTENT = `/**
|
|
5
5
|
* A single validation error with a human-readable message and a JSON Pointer
|
|
6
6
|
* path indicating where in the document the error occurred.
|
|
@@ -20,7 +20,7 @@ export type ValidationResult = true | { valid: false; errors: ValidationError[]
|
|
|
20
20
|
/**
|
|
21
21
|
* Structural deep equality used by generated \`const\` checks. Objects compare by
|
|
22
22
|
* their key sets rather than serialization, so \`{ a: 1, b: 2 }\` and
|
|
23
|
-
* \`{ b: 2, a: 1 }\` are equal
|
|
23
|
+
* \`{ b: 2, a: 1 }\` are equal \u2014 unlike \`JSON.stringify\`, which is key-order
|
|
24
24
|
* sensitive and would reject a reordered-but-equal value.
|
|
25
25
|
*/
|
|
26
26
|
export const valuesEqual = (a: unknown, b: unknown): boolean => {
|
|
@@ -75,45 +75,22 @@ export const allUnique = (arr: readonly unknown[]): boolean => {
|
|
|
75
75
|
return true
|
|
76
76
|
}
|
|
77
77
|
`;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
* A `validation-result.ts` file containing the `ValidationResult` and `ValidationError`
|
|
88
|
-
* runtime contract is always emitted. An `index.ts` re-exports everything.
|
|
89
|
-
*
|
|
90
|
-
* @param rootSchema - The root JSON Schema to build from
|
|
91
|
-
* @param rootTypeName - The name for the root type (e.g. "Document")
|
|
92
|
-
* @returns An array of generated TypeScript files
|
|
93
|
-
*
|
|
94
|
-
* @example
|
|
95
|
-
* ```typescript
|
|
96
|
-
* const files = await buildValidatorSchema(schema, 'Document')
|
|
97
|
-
* // files → [{ filename: 'document.ts', content: '...' }, { filename: 'info.ts', ... }, ...]
|
|
98
|
-
* ```
|
|
99
|
-
*/
|
|
100
|
-
export const buildValidatorSchema = async (rootSchema, rootTypeName, typeSuffix = '') => {
|
|
101
|
-
const files = [];
|
|
102
|
-
walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => {
|
|
103
|
-
// `validation-result` and `index` are reserved output filenames, so never
|
|
104
|
-
// let a definition of either name overwrite them.
|
|
105
|
-
if (node.filename === 'validation-result' || node.filename === 'index')
|
|
106
|
-
return;
|
|
107
|
-
const content = generateValidatorFile(node.schema, node.typeName, {
|
|
108
|
-
rootSchema: node.rootSchema,
|
|
109
|
-
typeSuffix,
|
|
110
|
-
...(node.ref !== undefined ? { selfRef: node.ref } : {}),
|
|
111
|
-
});
|
|
112
|
-
files.push({ filename: `${node.filename}.ts`, content });
|
|
78
|
+
const buildValidatorSchema = async (rootSchema, rootTypeName, typeSuffix = "") => {
|
|
79
|
+
const files = [];
|
|
80
|
+
walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => {
|
|
81
|
+
if (node.filename === "validation-result" || node.filename === "index")
|
|
82
|
+
return;
|
|
83
|
+
const content = generateValidatorFile(node.schema, node.typeName, {
|
|
84
|
+
rootSchema: node.rootSchema,
|
|
85
|
+
typeSuffix,
|
|
86
|
+
...node.ref !== void 0 ? { selfRef: node.ref } : {}
|
|
113
87
|
});
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
88
|
+
files.push({ filename: `${node.filename}.ts`, content });
|
|
89
|
+
});
|
|
90
|
+
files.push({ filename: "validation-result.ts", content: VALIDATION_RESULT_CONTENT });
|
|
91
|
+
files.push({ filename: "index.ts", content: generateIndexBarrel(files) });
|
|
92
|
+
return files;
|
|
93
|
+
};
|
|
94
|
+
export {
|
|
95
|
+
buildValidatorSchema
|
|
119
96
|
};
|
|
@@ -1,120 +1,77 @@
|
|
|
1
|
-
import { refToFilename } from
|
|
2
|
-
import { refToName } from
|
|
3
|
-
import { resolveRef } from
|
|
4
|
-
import { hasRef } from
|
|
5
|
-
/**
|
|
6
|
-
* Generates an import statement for a single $ref, importing both the type
|
|
7
|
-
* and the validator function from the ref's generated file.
|
|
8
|
-
*/
|
|
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 { hasRef } from "@amritk/helpers/schema-guards";
|
|
9
5
|
const buildImport = (ref, suffix) => {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
// `./x.js` → sibling `x.ts` is the standard NodeNext form.
|
|
15
|
-
return `import { type ${typeName}, ${validatorName} } from './${filename}.js'`;
|
|
6
|
+
const filename = refToFilename(ref);
|
|
7
|
+
const typeName = refToName(ref, suffix);
|
|
8
|
+
const validatorName = `validate${typeName}`;
|
|
9
|
+
return `import { type ${typeName}, ${validatorName} } from './${filename}.js'`;
|
|
16
10
|
};
|
|
17
|
-
/**
|
|
18
|
-
* Resolves the canonical filename for a ref, stripping `-or-reference` suffixes
|
|
19
|
-
* so that `#/$defs/parameter-or-reference` maps to `parameter`.
|
|
20
|
-
*/
|
|
21
11
|
const canonicalFilename = (ref) => {
|
|
22
|
-
|
|
23
|
-
|
|
12
|
+
const base = ref.endsWith("-or-reference") ? ref.replace("-or-reference", "") : ref;
|
|
13
|
+
return refToFilename(base);
|
|
24
14
|
};
|
|
25
|
-
/**
|
|
26
|
-
* Recursively walks a schema and yields every `$ref` the validator emitter can
|
|
27
|
-
* turn into a `validateX(...)` call, in traversal order. The emitter recurses
|
|
28
|
-
* into far more than properties/items/additionalProperties/top-level
|
|
29
|
-
* combinators: it also delegates for `patternProperties`, `propertyNames`,
|
|
30
|
-
* `if`/`then`/`else`, `contains`, `prefixItems`, `dependentSchemas`, `not`, and
|
|
31
|
-
* objects nested inside any combinator branch. A `$ref` reached by *any* of those
|
|
32
|
-
* paths must become an import, or the generated file references an undefined
|
|
33
|
-
* `validateX`. (Mirrors the parsers package's `collect-imports` traversal.)
|
|
34
|
-
*/
|
|
35
15
|
const collectDirectRefs = (value, refs = []) => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// generated files, not inlined by this validator. `collectDirectRefs`
|
|
55
|
-
// self-guards on non-objects, so a keyword that is a boolean or missing is a
|
|
56
|
-
// harmless no-op.
|
|
57
|
-
// `dependencies` (draft-07) is dual-form: a string array (dependentRequired) or
|
|
58
|
-
// a subschema (dependentSchemas). The emitter delegates the schema form via
|
|
59
|
-
// `validateX`, so a `$ref` inside it must be imported; the string-array form is
|
|
60
|
-
// a harmless no-op here (its values are strings, not schemas).
|
|
61
|
-
const subSchemaMaps = ['properties', 'patternProperties', 'dependentSchemas', 'dependencies'];
|
|
62
|
-
for (const mapKey of subSchemaMaps) {
|
|
63
|
-
const map = schema[mapKey];
|
|
64
|
-
if (typeof map === 'object' && map !== null && !Array.isArray(map)) {
|
|
65
|
-
for (const sub of Object.values(map))
|
|
66
|
-
collectDirectRefs(sub, refs);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
const singleSubSchemas = ['items', 'additionalProperties', 'propertyNames', 'contains', 'if', 'then', 'else', 'not'];
|
|
70
|
-
for (const key of singleSubSchemas) {
|
|
71
|
-
if (key in schema)
|
|
72
|
-
collectDirectRefs(schema[key], refs);
|
|
16
|
+
if (typeof value !== "object" || value === null)
|
|
17
|
+
return refs;
|
|
18
|
+
if (Array.isArray(value)) {
|
|
19
|
+
for (const item of value)
|
|
20
|
+
collectDirectRefs(item, refs);
|
|
21
|
+
return refs;
|
|
22
|
+
}
|
|
23
|
+
const schema = value;
|
|
24
|
+
if (hasRef(schema)) {
|
|
25
|
+
refs.push(schema.$ref);
|
|
26
|
+
return refs;
|
|
27
|
+
}
|
|
28
|
+
const subSchemaMaps = ["properties", "patternProperties", "dependentSchemas", "dependencies"];
|
|
29
|
+
for (const mapKey of subSchemaMaps) {
|
|
30
|
+
const map = schema[mapKey];
|
|
31
|
+
if (typeof map === "object" && map !== null && !Array.isArray(map)) {
|
|
32
|
+
for (const sub of Object.values(map))
|
|
33
|
+
collectDirectRefs(sub, refs);
|
|
73
34
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
35
|
+
}
|
|
36
|
+
const singleSubSchemas = ["items", "additionalProperties", "propertyNames", "contains", "if", "then", "else", "not"];
|
|
37
|
+
for (const key of singleSubSchemas) {
|
|
38
|
+
if (key in schema)
|
|
39
|
+
collectDirectRefs(schema[key], refs);
|
|
40
|
+
}
|
|
41
|
+
const arraySubSchemas = ["oneOf", "anyOf", "allOf", "prefixItems"];
|
|
42
|
+
for (const key of arraySubSchemas) {
|
|
43
|
+
const arr = schema[key];
|
|
44
|
+
if (Array.isArray(arr)) {
|
|
45
|
+
for (const sub of arr)
|
|
46
|
+
collectDirectRefs(sub, refs);
|
|
81
47
|
}
|
|
82
|
-
|
|
48
|
+
}
|
|
49
|
+
return refs;
|
|
83
50
|
};
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const imports = [];
|
|
102
|
-
for (const ref of refs) {
|
|
103
|
-
const filename = canonicalFilename(ref);
|
|
104
|
-
if (seen.has(filename))
|
|
105
|
-
continue;
|
|
106
|
-
if (selfFilename && filename === selfFilename)
|
|
107
|
-
continue;
|
|
108
|
-
// Skip refs that don't resolve in this schema (external / never generated)
|
|
109
|
-
if (rootSchema) {
|
|
110
|
-
const resolved = resolveRef(ref, rootSchema);
|
|
111
|
-
if (!resolved)
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
seen.add(filename);
|
|
115
|
-
// -or-reference unions import the base type's validator
|
|
116
|
-
const importRef = ref.endsWith('-or-reference') ? ref.replace('-or-reference', '') : ref;
|
|
117
|
-
imports.push(buildImport(importRef, typeSuffix));
|
|
51
|
+
const collectValidatorImports = (schema, options) => {
|
|
52
|
+
const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null;
|
|
53
|
+
const rootSchema = options?.rootSchema;
|
|
54
|
+
const typeSuffix = options?.typeSuffix ?? "";
|
|
55
|
+
const refs = collectDirectRefs(schema);
|
|
56
|
+
const seen = /* @__PURE__ */ new Set();
|
|
57
|
+
const imports = [];
|
|
58
|
+
for (const ref of refs) {
|
|
59
|
+
const filename = canonicalFilename(ref);
|
|
60
|
+
if (seen.has(filename))
|
|
61
|
+
continue;
|
|
62
|
+
if (selfFilename && filename === selfFilename)
|
|
63
|
+
continue;
|
|
64
|
+
if (rootSchema) {
|
|
65
|
+
const resolved = resolveRef(ref, rootSchema);
|
|
66
|
+
if (!resolved)
|
|
67
|
+
continue;
|
|
118
68
|
}
|
|
119
|
-
|
|
69
|
+
seen.add(filename);
|
|
70
|
+
const importRef = ref.endsWith("-or-reference") ? ref.replace("-or-reference", "") : ref;
|
|
71
|
+
imports.push(buildImport(importRef, typeSuffix));
|
|
72
|
+
}
|
|
73
|
+
return imports;
|
|
74
|
+
};
|
|
75
|
+
export {
|
|
76
|
+
collectValidatorImports
|
|
120
77
|
};
|
|
@@ -1,60 +1,35 @@
|
|
|
1
|
-
import { generateTypeDefinition } from
|
|
2
|
-
import { collectValidatorImports } from
|
|
3
|
-
import { generateBooleanGuard, generateValidatorFunction } from
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix });
|
|
36
|
-
const validatorFunction = generateValidatorFunction(schema, typeName, typeSuffix);
|
|
37
|
-
const booleanGuard = generateBooleanGuard(schema, typeName, typeSuffix);
|
|
38
|
-
// `.js` extension so the relative import resolves under Node ESM, not only Bun.
|
|
39
|
-
let result = `import type { ValidationResult, ValidationError } from './validation-result.js'\n`;
|
|
40
|
-
// Structural `const` checks call the runtime `valuesEqual` helper; structural
|
|
41
|
-
// `uniqueItems` checks call `allUnique`. Both live in `validation-result.js`;
|
|
42
|
-
// import each only when the generated body (validator or boolean guard) uses
|
|
43
|
-
// it, so files that need neither carry no unused import.
|
|
44
|
-
const body = validatorFunction + booleanGuard;
|
|
45
|
-
const runtimeHelpers = ['valuesEqual', 'allUnique'].filter((name) => body.includes(`${name}(`));
|
|
46
|
-
if (runtimeHelpers.length > 0) {
|
|
47
|
-
result += `import { ${runtimeHelpers.join(', ')} } from './validation-result.js'\n`;
|
|
48
|
-
}
|
|
49
|
-
for (const imp of refImports) {
|
|
50
|
-
result += imp + '\n';
|
|
51
|
-
}
|
|
52
|
-
if (refImports.length > 0) {
|
|
53
|
-
result += '\n';
|
|
54
|
-
}
|
|
55
|
-
else {
|
|
56
|
-
result += '\n';
|
|
57
|
-
}
|
|
58
|
-
result += typeDefinition + '\n\n' + validatorFunction + '\n\n' + booleanGuard;
|
|
59
|
-
return result;
|
|
1
|
+
import { generateTypeDefinition } from "@amritk/helpers/generate-type-definition";
|
|
2
|
+
import { collectValidatorImports } from "./collect-validator-imports.js";
|
|
3
|
+
import { generateBooleanGuard, generateValidatorFunction } from "./generate-validator-function.js";
|
|
4
|
+
const generateValidatorFile = (schema, typeName, options) => {
|
|
5
|
+
const typeSuffix = options?.typeSuffix ?? "";
|
|
6
|
+
const refImports = collectValidatorImports(schema, {
|
|
7
|
+
selfRef: options?.selfRef,
|
|
8
|
+
rootSchema: options?.rootSchema,
|
|
9
|
+
typeSuffix
|
|
10
|
+
});
|
|
11
|
+
const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix });
|
|
12
|
+
const validatorFunction = generateValidatorFunction(schema, typeName, typeSuffix);
|
|
13
|
+
const booleanGuard = generateBooleanGuard(schema, typeName, typeSuffix);
|
|
14
|
+
let result = `import type { ValidationResult, ValidationError } from './validation-result.js'
|
|
15
|
+
`;
|
|
16
|
+
const body = validatorFunction + booleanGuard;
|
|
17
|
+
const runtimeHelpers = ["valuesEqual", "allUnique"].filter((name) => body.includes(`${name}(`));
|
|
18
|
+
if (runtimeHelpers.length > 0) {
|
|
19
|
+
result += `import { ${runtimeHelpers.join(", ")} } from './validation-result.js'
|
|
20
|
+
`;
|
|
21
|
+
}
|
|
22
|
+
for (const imp of refImports) {
|
|
23
|
+
result += imp + "\n";
|
|
24
|
+
}
|
|
25
|
+
if (refImports.length > 0) {
|
|
26
|
+
result += "\n";
|
|
27
|
+
} else {
|
|
28
|
+
result += "\n";
|
|
29
|
+
}
|
|
30
|
+
result += typeDefinition + "\n\n" + validatorFunction + "\n\n" + booleanGuard;
|
|
31
|
+
return result;
|
|
32
|
+
};
|
|
33
|
+
export {
|
|
34
|
+
generateValidatorFile
|
|
60
35
|
};
|