@amritk/generate-validators 0.11.8 → 0.11.10

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/AI.md ADDED
@@ -0,0 +1,38 @@
1
+ # @amritk/generate-validators — notes for AI coding agents
2
+
3
+ Programmatic API: generate lightweight predicate validators (`validateFoo(input)`)
4
+ plus types from a JSON Schema. Full reference is [README.md](./README.md).
5
+
6
+ > Pre-alpha: APIs and generated output change pre-1.0.
7
+
8
+ ## Minimal example
9
+
10
+ ```ts
11
+ import { buildValidatorSchema } from '@amritk/generate-validators'
12
+ import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
13
+
14
+ const schema: JSONSchema = {
15
+ type: 'object',
16
+ properties: { title: { type: 'string' } },
17
+ required: ['title'],
18
+ }
19
+
20
+ const files = await buildValidatorSchema(schema, 'Document')
21
+ // → document.ts, validation-result.ts, index.ts
22
+ ```
23
+
24
+ ## Gotchas — where agents fail
25
+
26
+ 1. **Success is the literal `true`, not `{ valid: true }`.** A generated
27
+ `validateFoo` returns `true | { valid: false; errors: ValidationError[] }`.
28
+ Check `if (result !== true)` for the failure path — `if (result.valid)` is
29
+ wrong.
30
+ 2. **Small signature:** `buildValidatorSchema(rootSchema, rootTypeName, typeSuffix?)`
31
+ — async, no `strict`/`typesOnly`/options. Returns `GeneratedFile[]` in memory
32
+ (you write them).
33
+ 3. **Output includes a shared `validation-result.ts`** (`ValidationError`,
34
+ `ValidationResult`, helpers) plus the `index.ts` barrel.
35
+ 4. **`NaN` satisfies numeric bounds** (`minimum`/`maximum`/`multipleOf`) — differs
36
+ from Ajv. Draft-07 schemas are auto-upgraded to 2020-12.
37
+
38
+ Only the `.` entry. Install: `bun add @amritk/generate-validators`.
@@ -1,6 +1,6 @@
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';
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 unlike \`JSON.stringify\`, which is key-order
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
- * Builds all TypeScript validator files from a JSON Schema by traversing all
80
- * `$ref` / `$dynamicRef` references recursively (via the shared
81
- * `@amritk/helpers/walk-ref-graph` walker).
82
- *
83
- * Each generated file exports:
84
- * - A TypeScript type definition
85
- * - A `validateFoo(input: unknown, _path?: string): ValidationResult` function
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
- // Emit the runtime contract for validators. ValidationResult is mjst-defined
115
- // (not derived from the input schema), so its content is fixed.
116
- files.push({ filename: 'validation-result.ts', content: VALIDATION_RESULT_CONTENT });
117
- files.push({ filename: 'index.ts', content: generateIndexBarrel(files) });
118
- return files;
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 '@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';
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
- const filename = refToFilename(ref);
11
- const typeName = refToName(ref, suffix);
12
- const validatorName = `validate${typeName}`;
13
- // `.js` extension so the emitted import resolves under Node ESM (not just Bun);
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
- const base = ref.endsWith('-or-reference') ? ref.replace('-or-reference', '') : ref;
23
- return refToFilename(base);
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
- if (typeof value !== 'object' || value === null)
37
- return refs;
38
- if (Array.isArray(value)) {
39
- for (const item of value)
40
- collectDirectRefs(item, refs);
41
- return refs;
42
- }
43
- const schema = value;
44
- // A `$ref` is a leaf: the emitter delegates the whole value to the referenced
45
- // validator, so record the ref and do not descend past it.
46
- if (hasRef(schema)) {
47
- refs.push(schema.$ref);
48
- return refs;
49
- }
50
- // Every keyword whose subschema(s) the emitter recurses into. `properties` and
51
- // `patternProperties` hold subschemas as object *values*; the combinator/tuple
52
- // keywords hold them in arrays; the rest are single subschemas. We deliberately
53
- // do NOT descend into `$defs`/`definitions` — those are split into their own
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
- const arraySubSchemas = ['oneOf', 'anyOf', 'allOf', 'prefixItems'];
75
- for (const key of arraySubSchemas) {
76
- const arr = schema[key];
77
- if (Array.isArray(arr)) {
78
- for (const sub of arr)
79
- collectDirectRefs(sub, refs);
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
- return refs;
48
+ }
49
+ return refs;
83
50
  };
84
- /**
85
- * Collects import statements for all $ref dependencies of a schema.
86
- * Each import brings in both the generated TypeScript type and validator function.
87
- *
88
- * @example
89
- * ```typescript
90
- * const schema = { properties: { contact: { $ref: '#/$defs/contact' } } }
91
- * collectValidatorImports(schema)
92
- * // ["import { type Contact, validateContact } from './contact'"]
93
- * ```
94
- */
95
- export const collectValidatorImports = (schema, options) => {
96
- const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null;
97
- const rootSchema = options?.rootSchema;
98
- const typeSuffix = options?.typeSuffix ?? '';
99
- const refs = collectDirectRefs(schema);
100
- const seen = new Set();
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
- return imports;
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 '@amritk/helpers/generate-type-definition';
2
- import { collectValidatorImports } from './collect-validator-imports.js';
3
- import { generateBooleanGuard, generateValidatorFunction } from './generate-validator-function.js';
4
- /**
5
- * Generates a complete TypeScript validator file from a JSON Schema.
6
- *
7
- * The file contains:
8
- * - Imports for the ValidationResult/ValidationError types
9
- * - Imports for any $ref types and their validator functions
10
- * - The exported TypeScript type definition
11
- * - The exported validator function (`validateX`, rich `ValidationResult`)
12
- * - The exported boolean type-guard (`isX`, a flat `input is X` predicate)
13
- *
14
- * @example
15
- * ```typescript
16
- * const schema = {
17
- * type: 'object',
18
- * properties: { title: { type: 'string' } },
19
- * required: ['title'],
20
- * }
21
- * generateValidatorFile(schema, 'Info')
22
- * // import type { ValidationResult, ValidationError } from './validation-result'
23
- * // export type Info = { title: string }
24
- * // export const validateInfo = (input: unknown, _path = ''): ValidationResult => { ... }
25
- * // export const isInfo = (input: unknown): input is Info => { ... }
26
- * ```
27
- */
28
- export const generateValidatorFile = (schema, typeName, options) => {
29
- const typeSuffix = options?.typeSuffix ?? '';
30
- const refImports = collectValidatorImports(schema, {
31
- selfRef: options?.selfRef,
32
- rootSchema: options?.rootSchema,
33
- typeSuffix,
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
  };