🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@amritk/generate-validators

Package Overview
Dependencies
Maintainers
1
Versions
27
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@amritk/generate-validators - npm Package Compare versions

Comparing version
0.11.8
to
0.11.9
+21
-44
dist/generators/build-schema.js

@@ -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
};

@@ -1,1 +0,4 @@

export { buildValidatorSchema } from './generators/build-schema.js';
import { buildValidatorSchema } from "./generators/build-schema.js";
export {
buildValidatorSchema
};
{
"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