@amritk/generate-validators 0.11.8 → 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.
|
@@ -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
|
};
|