@amritk/generate-validators
Advanced tools
@@ -1,4 +0,4 @@ | ||
| import { generateIndexBarrel } from '@amritk/helpers/generate-index-barrel'; | ||
| import { walkRefGraph } from '@amritk/helpers/walk-ref-graph'; | ||
| import { generateValidatorFile } from './generate-files.js'; | ||
| import { generateIndexBarrel } from "@amritk/helpers/generate-index-barrel"; | ||
| import { walkRefGraph } from "@amritk/helpers/walk-ref-graph"; | ||
| import { generateValidatorFile } from "./generate-files.js"; | ||
| const VALIDATION_RESULT_CONTENT = `/** | ||
@@ -23,3 +23,3 @@ * A single validation error with a human-readable message and a JSON Pointer | ||
| * their key sets rather than serialization, so \`{ a: 1, b: 2 }\` and | ||
| * \`{ b: 2, a: 1 }\` are equal — unlike \`JSON.stringify\`, which is key-order | ||
| * \`{ b: 2, a: 1 }\` are equal \u2014 unlike \`JSON.stringify\`, which is key-order | ||
| * sensitive and would reject a reordered-but-equal value. | ||
@@ -79,43 +79,20 @@ */ | ||
| `; | ||
| /** | ||
| * Builds all TypeScript validator files from a JSON Schema by traversing all | ||
| * `$ref` / `$dynamicRef` references recursively (via the shared | ||
| * `@amritk/helpers/walk-ref-graph` walker). | ||
| * | ||
| * Each generated file exports: | ||
| * - A TypeScript type definition | ||
| * - A `validateFoo(input: unknown, _path?: string): ValidationResult` function | ||
| * | ||
| * A `validation-result.ts` file containing the `ValidationResult` and `ValidationError` | ||
| * runtime contract is always emitted. An `index.ts` re-exports everything. | ||
| * | ||
| * @param rootSchema - The root JSON Schema to build from | ||
| * @param rootTypeName - The name for the root type (e.g. "Document") | ||
| * @returns An array of generated TypeScript files | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const files = await buildValidatorSchema(schema, 'Document') | ||
| * // files → [{ filename: 'document.ts', content: '...' }, { filename: 'info.ts', ... }, ...] | ||
| * ``` | ||
| */ | ||
| export const buildValidatorSchema = async (rootSchema, rootTypeName, typeSuffix = '') => { | ||
| const files = []; | ||
| walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => { | ||
| // `validation-result` and `index` are reserved output filenames, so never | ||
| // let a definition of either name overwrite them. | ||
| if (node.filename === 'validation-result' || node.filename === 'index') | ||
| return; | ||
| const content = generateValidatorFile(node.schema, node.typeName, { | ||
| rootSchema: node.rootSchema, | ||
| typeSuffix, | ||
| ...(node.ref !== undefined ? { selfRef: node.ref } : {}), | ||
| }); | ||
| files.push({ filename: `${node.filename}.ts`, content }); | ||
| const buildValidatorSchema = async (rootSchema, rootTypeName, typeSuffix = "") => { | ||
| const files = []; | ||
| walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => { | ||
| if (node.filename === "validation-result" || node.filename === "index") | ||
| return; | ||
| const content = generateValidatorFile(node.schema, node.typeName, { | ||
| rootSchema: node.rootSchema, | ||
| typeSuffix, | ||
| ...node.ref !== void 0 ? { selfRef: node.ref } : {} | ||
| }); | ||
| // Emit the runtime contract for validators. ValidationResult is mjst-defined | ||
| // (not derived from the input schema), so its content is fixed. | ||
| files.push({ filename: 'validation-result.ts', content: VALIDATION_RESULT_CONTENT }); | ||
| files.push({ filename: 'index.ts', content: generateIndexBarrel(files) }); | ||
| return files; | ||
| files.push({ filename: `${node.filename}.ts`, content }); | ||
| }); | ||
| files.push({ filename: "validation-result.ts", content: VALIDATION_RESULT_CONTENT }); | ||
| files.push({ filename: "index.ts", content: generateIndexBarrel(files) }); | ||
| return files; | ||
| }; | ||
| export { | ||
| buildValidatorSchema | ||
| }; |
@@ -1,120 +0,77 @@ | ||
| import { refToFilename } from '@amritk/helpers/ref-to-filename'; | ||
| import { refToName } from '@amritk/helpers/ref-to-name'; | ||
| import { resolveRef } from '@amritk/helpers/resolve-ref'; | ||
| import { hasRef } from '@amritk/helpers/schema-guards'; | ||
| /** | ||
| * Generates an import statement for a single $ref, importing both the type | ||
| * and the validator function from the ref's generated file. | ||
| */ | ||
| import { refToFilename } from "@amritk/helpers/ref-to-filename"; | ||
| import { refToName } from "@amritk/helpers/ref-to-name"; | ||
| import { resolveRef } from "@amritk/helpers/resolve-ref"; | ||
| import { hasRef } from "@amritk/helpers/schema-guards"; | ||
| const buildImport = (ref, suffix) => { | ||
| const filename = refToFilename(ref); | ||
| const typeName = refToName(ref, suffix); | ||
| const validatorName = `validate${typeName}`; | ||
| // `.js` extension so the emitted import resolves under Node ESM (not just Bun); | ||
| // `./x.js` → sibling `x.ts` is the standard NodeNext form. | ||
| return `import { type ${typeName}, ${validatorName} } from './${filename}.js'`; | ||
| const filename = refToFilename(ref); | ||
| const typeName = refToName(ref, suffix); | ||
| const validatorName = `validate${typeName}`; | ||
| return `import { type ${typeName}, ${validatorName} } from './${filename}.js'`; | ||
| }; | ||
| /** | ||
| * Resolves the canonical filename for a ref, stripping `-or-reference` suffixes | ||
| * so that `#/$defs/parameter-or-reference` maps to `parameter`. | ||
| */ | ||
| const canonicalFilename = (ref) => { | ||
| const base = ref.endsWith('-or-reference') ? ref.replace('-or-reference', '') : ref; | ||
| return refToFilename(base); | ||
| const base = ref.endsWith("-or-reference") ? ref.replace("-or-reference", "") : ref; | ||
| return refToFilename(base); | ||
| }; | ||
| /** | ||
| * Recursively walks a schema and yields every `$ref` the validator emitter can | ||
| * turn into a `validateX(...)` call, in traversal order. The emitter recurses | ||
| * into far more than properties/items/additionalProperties/top-level | ||
| * combinators: it also delegates for `patternProperties`, `propertyNames`, | ||
| * `if`/`then`/`else`, `contains`, `prefixItems`, `dependentSchemas`, `not`, and | ||
| * objects nested inside any combinator branch. A `$ref` reached by *any* of those | ||
| * paths must become an import, or the generated file references an undefined | ||
| * `validateX`. (Mirrors the parsers package's `collect-imports` traversal.) | ||
| */ | ||
| const collectDirectRefs = (value, refs = []) => { | ||
| if (typeof value !== 'object' || value === null) | ||
| return refs; | ||
| if (Array.isArray(value)) { | ||
| for (const item of value) | ||
| collectDirectRefs(item, refs); | ||
| return refs; | ||
| if (typeof value !== "object" || value === null) | ||
| return refs; | ||
| if (Array.isArray(value)) { | ||
| for (const item of value) | ||
| collectDirectRefs(item, refs); | ||
| return refs; | ||
| } | ||
| const schema = value; | ||
| if (hasRef(schema)) { | ||
| refs.push(schema.$ref); | ||
| return refs; | ||
| } | ||
| const subSchemaMaps = ["properties", "patternProperties", "dependentSchemas", "dependencies"]; | ||
| for (const mapKey of subSchemaMaps) { | ||
| const map = schema[mapKey]; | ||
| if (typeof map === "object" && map !== null && !Array.isArray(map)) { | ||
| for (const sub of Object.values(map)) | ||
| collectDirectRefs(sub, refs); | ||
| } | ||
| const schema = value; | ||
| // A `$ref` is a leaf: the emitter delegates the whole value to the referenced | ||
| // validator, so record the ref and do not descend past it. | ||
| if (hasRef(schema)) { | ||
| refs.push(schema.$ref); | ||
| return refs; | ||
| } | ||
| const singleSubSchemas = ["items", "additionalProperties", "propertyNames", "contains", "if", "then", "else", "not"]; | ||
| for (const key of singleSubSchemas) { | ||
| if (key in schema) | ||
| collectDirectRefs(schema[key], refs); | ||
| } | ||
| const arraySubSchemas = ["oneOf", "anyOf", "allOf", "prefixItems"]; | ||
| for (const key of arraySubSchemas) { | ||
| const arr = schema[key]; | ||
| if (Array.isArray(arr)) { | ||
| for (const sub of arr) | ||
| collectDirectRefs(sub, refs); | ||
| } | ||
| // Every keyword whose subschema(s) the emitter recurses into. `properties` and | ||
| // `patternProperties` hold subschemas as object *values*; the combinator/tuple | ||
| // keywords hold them in arrays; the rest are single subschemas. We deliberately | ||
| // do NOT descend into `$defs`/`definitions` — those are split into their own | ||
| // generated files, not inlined by this validator. `collectDirectRefs` | ||
| // self-guards on non-objects, so a keyword that is a boolean or missing is a | ||
| // harmless no-op. | ||
| // `dependencies` (draft-07) is dual-form: a string array (dependentRequired) or | ||
| // a subschema (dependentSchemas). The emitter delegates the schema form via | ||
| // `validateX`, so a `$ref` inside it must be imported; the string-array form is | ||
| // a harmless no-op here (its values are strings, not schemas). | ||
| const subSchemaMaps = ['properties', 'patternProperties', 'dependentSchemas', 'dependencies']; | ||
| for (const mapKey of subSchemaMaps) { | ||
| const map = schema[mapKey]; | ||
| if (typeof map === 'object' && map !== null && !Array.isArray(map)) { | ||
| for (const sub of Object.values(map)) | ||
| collectDirectRefs(sub, refs); | ||
| } | ||
| } | ||
| const singleSubSchemas = ['items', 'additionalProperties', 'propertyNames', 'contains', 'if', 'then', 'else', 'not']; | ||
| for (const key of singleSubSchemas) { | ||
| if (key in schema) | ||
| collectDirectRefs(schema[key], refs); | ||
| } | ||
| const arraySubSchemas = ['oneOf', 'anyOf', 'allOf', 'prefixItems']; | ||
| for (const key of arraySubSchemas) { | ||
| const arr = schema[key]; | ||
| if (Array.isArray(arr)) { | ||
| for (const sub of arr) | ||
| collectDirectRefs(sub, refs); | ||
| } | ||
| } | ||
| return refs; | ||
| } | ||
| return refs; | ||
| }; | ||
| /** | ||
| * Collects import statements for all $ref dependencies of a schema. | ||
| * Each import brings in both the generated TypeScript type and validator function. | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const schema = { properties: { contact: { $ref: '#/$defs/contact' } } } | ||
| * collectValidatorImports(schema) | ||
| * // ["import { type Contact, validateContact } from './contact'"] | ||
| * ``` | ||
| */ | ||
| export const collectValidatorImports = (schema, options) => { | ||
| const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null; | ||
| const rootSchema = options?.rootSchema; | ||
| const typeSuffix = options?.typeSuffix ?? ''; | ||
| const refs = collectDirectRefs(schema); | ||
| const seen = new Set(); | ||
| const imports = []; | ||
| for (const ref of refs) { | ||
| const filename = canonicalFilename(ref); | ||
| if (seen.has(filename)) | ||
| continue; | ||
| if (selfFilename && filename === selfFilename) | ||
| continue; | ||
| // Skip refs that don't resolve in this schema (external / never generated) | ||
| if (rootSchema) { | ||
| const resolved = resolveRef(ref, rootSchema); | ||
| if (!resolved) | ||
| continue; | ||
| } | ||
| seen.add(filename); | ||
| // -or-reference unions import the base type's validator | ||
| const importRef = ref.endsWith('-or-reference') ? ref.replace('-or-reference', '') : ref; | ||
| imports.push(buildImport(importRef, typeSuffix)); | ||
| const collectValidatorImports = (schema, options) => { | ||
| const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null; | ||
| const rootSchema = options?.rootSchema; | ||
| const typeSuffix = options?.typeSuffix ?? ""; | ||
| const refs = collectDirectRefs(schema); | ||
| const seen = /* @__PURE__ */ new Set(); | ||
| const imports = []; | ||
| for (const ref of refs) { | ||
| const filename = canonicalFilename(ref); | ||
| if (seen.has(filename)) | ||
| continue; | ||
| if (selfFilename && filename === selfFilename) | ||
| continue; | ||
| if (rootSchema) { | ||
| const resolved = resolveRef(ref, rootSchema); | ||
| if (!resolved) | ||
| continue; | ||
| } | ||
| return imports; | ||
| seen.add(filename); | ||
| const importRef = ref.endsWith("-or-reference") ? ref.replace("-or-reference", "") : ref; | ||
| imports.push(buildImport(importRef, typeSuffix)); | ||
| } | ||
| return imports; | ||
| }; | ||
| export { | ||
| collectValidatorImports | ||
| }; |
@@ -1,60 +0,35 @@ | ||
| import { generateTypeDefinition } from '@amritk/helpers/generate-type-definition'; | ||
| import { collectValidatorImports } from './collect-validator-imports.js'; | ||
| import { generateBooleanGuard, generateValidatorFunction } from './generate-validator-function.js'; | ||
| /** | ||
| * Generates a complete TypeScript validator file from a JSON Schema. | ||
| * | ||
| * The file contains: | ||
| * - Imports for the ValidationResult/ValidationError types | ||
| * - Imports for any $ref types and their validator functions | ||
| * - The exported TypeScript type definition | ||
| * - The exported validator function (`validateX`, rich `ValidationResult`) | ||
| * - The exported boolean type-guard (`isX`, a flat `input is X` predicate) | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const schema = { | ||
| * type: 'object', | ||
| * properties: { title: { type: 'string' } }, | ||
| * required: ['title'], | ||
| * } | ||
| * generateValidatorFile(schema, 'Info') | ||
| * // import type { ValidationResult, ValidationError } from './validation-result' | ||
| * // export type Info = { title: string } | ||
| * // export const validateInfo = (input: unknown, _path = ''): ValidationResult => { ... } | ||
| * // export const isInfo = (input: unknown): input is Info => { ... } | ||
| * ``` | ||
| */ | ||
| export const generateValidatorFile = (schema, typeName, options) => { | ||
| const typeSuffix = options?.typeSuffix ?? ''; | ||
| const refImports = collectValidatorImports(schema, { | ||
| selfRef: options?.selfRef, | ||
| rootSchema: options?.rootSchema, | ||
| typeSuffix, | ||
| }); | ||
| const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix }); | ||
| const validatorFunction = generateValidatorFunction(schema, typeName, typeSuffix); | ||
| const booleanGuard = generateBooleanGuard(schema, typeName, typeSuffix); | ||
| // `.js` extension so the relative import resolves under Node ESM, not only Bun. | ||
| let result = `import type { ValidationResult, ValidationError } from './validation-result.js'\n`; | ||
| // Structural `const` checks call the runtime `valuesEqual` helper; structural | ||
| // `uniqueItems` checks call `allUnique`. Both live in `validation-result.js`; | ||
| // import each only when the generated body (validator or boolean guard) uses | ||
| // it, so files that need neither carry no unused import. | ||
| const body = validatorFunction + booleanGuard; | ||
| const runtimeHelpers = ['valuesEqual', 'allUnique'].filter((name) => body.includes(`${name}(`)); | ||
| if (runtimeHelpers.length > 0) { | ||
| result += `import { ${runtimeHelpers.join(', ')} } from './validation-result.js'\n`; | ||
| } | ||
| for (const imp of refImports) { | ||
| result += imp + '\n'; | ||
| } | ||
| if (refImports.length > 0) { | ||
| result += '\n'; | ||
| } | ||
| else { | ||
| result += '\n'; | ||
| } | ||
| result += typeDefinition + '\n\n' + validatorFunction + '\n\n' + booleanGuard; | ||
| return result; | ||
| import { generateTypeDefinition } from "@amritk/helpers/generate-type-definition"; | ||
| import { collectValidatorImports } from "./collect-validator-imports.js"; | ||
| import { generateBooleanGuard, generateValidatorFunction } from "./generate-validator-function.js"; | ||
| const generateValidatorFile = (schema, typeName, options) => { | ||
| const typeSuffix = options?.typeSuffix ?? ""; | ||
| const refImports = collectValidatorImports(schema, { | ||
| selfRef: options?.selfRef, | ||
| rootSchema: options?.rootSchema, | ||
| typeSuffix | ||
| }); | ||
| const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix }); | ||
| const validatorFunction = generateValidatorFunction(schema, typeName, typeSuffix); | ||
| const booleanGuard = generateBooleanGuard(schema, typeName, typeSuffix); | ||
| let result = `import type { ValidationResult, ValidationError } from './validation-result.js' | ||
| `; | ||
| const body = validatorFunction + booleanGuard; | ||
| const runtimeHelpers = ["valuesEqual", "allUnique"].filter((name) => body.includes(`${name}(`)); | ||
| if (runtimeHelpers.length > 0) { | ||
| result += `import { ${runtimeHelpers.join(", ")} } from './validation-result.js' | ||
| `; | ||
| } | ||
| for (const imp of refImports) { | ||
| result += imp + "\n"; | ||
| } | ||
| if (refImports.length > 0) { | ||
| result += "\n"; | ||
| } else { | ||
| result += "\n"; | ||
| } | ||
| result += typeDefinition + "\n\n" + validatorFunction + "\n\n" + booleanGuard; | ||
| return result; | ||
| }; | ||
| export { | ||
| generateValidatorFile | ||
| }; |
+4
-1
@@ -1,1 +0,4 @@ | ||
| export { buildValidatorSchema } from './generators/build-schema.js'; | ||
| import { buildValidatorSchema } from "./generators/build-schema.js"; | ||
| export { | ||
| buildValidatorSchema | ||
| }; |
+4
-4
| { | ||
| "name": "@amritk/generate-validators", | ||
| "version": "0.11.8", | ||
| "version": "0.11.9", | ||
| "description": "Generate TypeScript validation functions from JSON Schemas.", | ||
@@ -33,3 +33,3 @@ "module": "./dist/index.js", | ||
| "scripts": { | ||
| "build": "tsgo -p tsconfig.build.json && tsc-alias -p tsconfig.build.json -f", | ||
| "build": "tsgo -p tsconfig.build.json && tsc-alias -p tsconfig.build.json -f && node ../../scripts/strip-comments.mjs", | ||
| "types:check": "tsgo -p . --noEmit", | ||
@@ -50,6 +50,6 @@ "test": "NODE_ENV=production vitest run --root ../.. generate-validators", | ||
| "json-schema-typed": "^8.0.1", | ||
| "@amritk/helpers": "0.13.2" | ||
| "@amritk/helpers": "0.13.3" | ||
| }, | ||
| "devDependencies": { | ||
| "@amritk/runtime-validators": "0.7.2", | ||
| "@amritk/runtime-validators": "0.7.3", | ||
| "@ryoppippi/unplugin-typia": "^2.6.5", | ||
@@ -56,0 +56,0 @@ "@scalar/openapi-parser": "^0.26.1", |
Sorry, the diff of this file is too big to display
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
79451
-35.59%1643
-29.55%+ Added
- Removed
Updated