@amritk/generate-validators 0.4.2 → 0.5.0

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.
@@ -7,8 +7,9 @@ export type GeneratedFile = {
7
7
  content: string;
8
8
  };
9
9
  /**
10
- * Builds all TypeScript validator files from a JSON Schema by traversing
11
- * all $ref references recursively, mirroring the generate-parsers pipeline.
10
+ * Builds all TypeScript validator files from a JSON Schema by traversing all
11
+ * `$ref` / `$dynamicRef` references recursively (via the shared
12
+ * `@amritk/helpers/walk-ref-graph` walker).
12
13
  *
13
14
  * Each generated file exports:
14
15
  * - A TypeScript type definition
@@ -28,3 +29,4 @@ export type GeneratedFile = {
28
29
  * ```
29
30
  */
30
31
  export declare const buildValidatorSchema: (rootSchema: JSONSchema, rootTypeName: string, typeSuffix?: string) => Promise<GeneratedFile[]>;
32
+ //# sourceMappingURL=build-schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-schema.d.ts","sourceRoot":"","sources":["../../src/generators/build-schema.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAIjE;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,EAAE,MAAM,CAAA;IAChB,OAAO,EAAE,MAAM,CAAA;CAChB,CAAA;AAmBD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,oBAAoB,eACnB,UAAU,gBACR,MAAM,0BAEnB,OAAO,CAAC,aAAa,EAAE,CAuBzB,CAAA"}
@@ -0,0 +1,61 @@
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
+ const VALIDATION_RESULT_CONTENT = `/**
5
+ * A single validation error with a human-readable message and a JSON Pointer
6
+ * path indicating where in the document the error occurred.
7
+ */
8
+ export type ValidationError = {
9
+ message: string
10
+ path: string
11
+ }
12
+
13
+ /**
14
+ * The result of a generated validator function.
15
+ * Returns \`true\` when the input is valid, or an object with \`valid: false\`
16
+ * and a list of errors when it is not.
17
+ */
18
+ export type ValidationResult = true | { valid: false; errors: ValidationError[] }
19
+ `;
20
+ /**
21
+ * Builds all TypeScript validator files from a JSON Schema by traversing all
22
+ * `$ref` / `$dynamicRef` references recursively (via the shared
23
+ * `@amritk/helpers/walk-ref-graph` walker).
24
+ *
25
+ * Each generated file exports:
26
+ * - A TypeScript type definition
27
+ * - A `validateFoo(input: unknown, _path?: string): ValidationResult` function
28
+ *
29
+ * A `validation-result.ts` file containing the `ValidationResult` and `ValidationError`
30
+ * runtime contract is always emitted. An `index.ts` re-exports everything.
31
+ *
32
+ * @param rootSchema - The root JSON Schema to build from
33
+ * @param rootTypeName - The name for the root type (e.g. "Document")
34
+ * @returns An array of generated TypeScript files
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * const files = await buildValidatorSchema(schema, 'Document')
39
+ * // files → [{ filename: 'document.ts', content: '...' }, { filename: 'info.ts', ... }, ...]
40
+ * ```
41
+ */
42
+ export const buildValidatorSchema = async (rootSchema, rootTypeName, typeSuffix = '') => {
43
+ const files = [];
44
+ walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => {
45
+ // `validation-result` and `index` are reserved output filenames, so never
46
+ // let a definition of either name overwrite them.
47
+ if (node.filename === 'validation-result' || node.filename === 'index')
48
+ return;
49
+ const content = generateValidatorFile(node.schema, node.typeName, {
50
+ rootSchema: node.rootSchema,
51
+ typeSuffix,
52
+ ...(node.ref !== undefined ? { selfRef: node.ref } : {}),
53
+ });
54
+ files.push({ filename: `${node.filename}.ts`, content });
55
+ });
56
+ // Emit the runtime contract for validators. ValidationResult is mjst-defined
57
+ // (not derived from the input schema), so its content is fixed.
58
+ files.push({ filename: 'validation-result.ts', content: VALIDATION_RESULT_CONTENT });
59
+ files.push({ filename: 'index.ts', content: generateIndexBarrel(files) });
60
+ return files;
61
+ };
@@ -32,3 +32,4 @@ type CollectValidatorImportsOptions = {
32
32
  */
33
33
  export declare const collectValidatorImports: (schema: JSONSchema, options?: CollectValidatorImportsOptions) => string[];
34
34
  export {};
35
+ //# sourceMappingURL=collect-validator-imports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect-validator-imports.d.ts","sourceRoot":"","sources":["../../src/generators/collect-validator-imports.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAEjE;;GAEG;AACH,KAAK,8BAA8B,GAAG;IACpC;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACrC;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;IACzD;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AAoED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,uBAAuB,WAAY,UAAU,YAAY,8BAA8B,KAAG,MAAM,EA6B5G,CAAA"}
@@ -0,0 +1,99 @@
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 { hasAdditionalProperties, hasAllOf, hasAnyOf, hasItems, hasOneOf, 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
+ */
9
+ const buildImport = (ref, suffix) => {
10
+ const filename = refToFilename(ref);
11
+ const typeName = refToName(ref, suffix);
12
+ const validatorName = `validate${typeName}`;
13
+ return `import { type ${typeName}, ${validatorName} } from './${filename}'`;
14
+ };
15
+ /**
16
+ * Resolves the canonical filename for a ref, stripping `-or-reference` suffixes
17
+ * so that `#/$defs/parameter-or-reference` maps to `parameter`.
18
+ */
19
+ const canonicalFilename = (ref) => {
20
+ const base = ref.endsWith('-or-reference') ? ref.replace('-or-reference', '') : ref;
21
+ return refToFilename(base);
22
+ };
23
+ /**
24
+ * Walks one level of the schema and yields all direct $ref strings that should
25
+ * become imports: properties, additionalProperties, items, and union branches.
26
+ */
27
+ const collectDirectRefs = (schema) => {
28
+ if (typeof schema === 'boolean' || schema === null)
29
+ return [];
30
+ const refs = [];
31
+ if (hasRef(schema)) {
32
+ refs.push(schema.$ref);
33
+ return refs;
34
+ }
35
+ const propSchemas = 'properties' in schema && typeof schema.properties === 'object' && schema.properties !== null
36
+ ? Object.values(schema.properties)
37
+ : [];
38
+ for (const prop of propSchemas) {
39
+ if (hasRef(prop))
40
+ refs.push(prop.$ref);
41
+ if (hasItems(prop) && hasRef(prop.items))
42
+ refs.push(prop.items.$ref);
43
+ if (hasAdditionalProperties(prop) && hasRef(prop.additionalProperties)) {
44
+ refs.push(prop.additionalProperties.$ref);
45
+ }
46
+ }
47
+ if (hasItems(schema) && hasRef(schema.items)) {
48
+ refs.push(schema.items.$ref);
49
+ }
50
+ if (hasAdditionalProperties(schema) && hasRef(schema.additionalProperties)) {
51
+ refs.push(schema.additionalProperties.$ref);
52
+ }
53
+ for (const branch of [
54
+ ...(hasOneOf(schema) ? schema.oneOf : []),
55
+ ...(hasAnyOf(schema) ? schema.anyOf : []),
56
+ ...(hasAllOf(schema) ? schema.allOf : []),
57
+ ]) {
58
+ if (hasRef(branch))
59
+ refs.push(branch.$ref);
60
+ }
61
+ return refs;
62
+ };
63
+ /**
64
+ * Collects import statements for all $ref dependencies of a schema.
65
+ * Each import brings in both the generated TypeScript type and validator function.
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * const schema = { properties: { contact: { $ref: '#/$defs/contact' } } }
70
+ * collectValidatorImports(schema)
71
+ * // ["import { type Contact, validateContact } from './contact'"]
72
+ * ```
73
+ */
74
+ export const collectValidatorImports = (schema, options) => {
75
+ const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null;
76
+ const rootSchema = options?.rootSchema;
77
+ const typeSuffix = options?.typeSuffix ?? '';
78
+ const refs = collectDirectRefs(schema);
79
+ const seen = new Set();
80
+ const imports = [];
81
+ for (const ref of refs) {
82
+ const filename = canonicalFilename(ref);
83
+ if (seen.has(filename))
84
+ continue;
85
+ if (selfFilename && filename === selfFilename)
86
+ continue;
87
+ // Skip refs that don't resolve in this schema (external / never generated)
88
+ if (rootSchema) {
89
+ const resolved = resolveRef(ref, rootSchema);
90
+ if (!resolved)
91
+ continue;
92
+ }
93
+ seen.add(filename);
94
+ // -or-reference unions import the base type's validator
95
+ const importRef = ref.endsWith('-or-reference') ? ref.replace('-or-reference', '') : ref;
96
+ imports.push(buildImport(importRef, typeSuffix));
97
+ }
98
+ return imports;
99
+ };
@@ -42,3 +42,4 @@ type GenerateValidatorFileOptions = {
42
42
  */
43
43
  export declare const generateValidatorFile: (schema: JSONSchema, typeName: string, options?: GenerateValidatorFileOptions) => string;
44
44
  export {};
45
+ //# sourceMappingURL=generate-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-files.d.ts","sourceRoot":"","sources":["../../src/generators/generate-files.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAKjE;;GAEG;AACH,KAAK,4BAA4B,GAAG;IAClC;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB;;OAEG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC7C;;;OAGG;IACH,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,qBAAqB,WACxB,UAAU,YACR,MAAM,YACN,4BAA4B,KACrC,MA0BF,CAAA"}
@@ -0,0 +1,47 @@
1
+ import { generateTypeDefinition } from '@amritk/helpers/generate-type-definition';
2
+ import { collectValidatorImports } from './collect-validator-imports.js';
3
+ import { 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
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const schema = {
16
+ * type: 'object',
17
+ * properties: { title: { type: 'string' } },
18
+ * required: ['title'],
19
+ * }
20
+ * generateValidatorFile(schema, 'Info')
21
+ * // import type { ValidationResult, ValidationError } from './validation-result'
22
+ * // export type Info = { title: string }
23
+ * // export const validateInfo = (input: unknown, _path = ''): ValidationResult => { ... }
24
+ * ```
25
+ */
26
+ export const generateValidatorFile = (schema, typeName, options) => {
27
+ const typeSuffix = options?.typeSuffix ?? '';
28
+ const refImports = collectValidatorImports(schema, {
29
+ selfRef: options?.selfRef,
30
+ rootSchema: options?.rootSchema,
31
+ typeSuffix,
32
+ });
33
+ const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix });
34
+ const validatorFunction = generateValidatorFunction(schema, typeName, typeSuffix);
35
+ let result = `import type { ValidationResult, ValidationError } from './validation-result'\n`;
36
+ for (const imp of refImports) {
37
+ result += imp + '\n';
38
+ }
39
+ if (refImports.length > 0) {
40
+ result += '\n';
41
+ }
42
+ else {
43
+ result += '\n';
44
+ }
45
+ result += typeDefinition + '\n\n' + validatorFunction;
46
+ return result;
47
+ };
@@ -22,3 +22,4 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
22
22
  * ```
23
23
  */
24
24
  export declare const generateValidatorFunction: (schema: JSONSchema, typeName: string, suffix?: string) => string;
25
+ //# sourceMappingURL=generate-validator-function.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-validator-function.d.ts","sourceRoot":"","sources":["../../src/generators/generate-validator-function.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AAqajE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,yBAAyB,WAAY,UAAU,YAAY,MAAM,sBAAgB,MAM7F,CAAA"}
@@ -0,0 +1,405 @@
1
+ import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';
2
+ import { refToName } from '@amritk/helpers/ref-to-name';
3
+ import { hasAdditionalProperties, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaximum, hasMaxLength, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasRef, hasRequired, hasType, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
4
+ /**
5
+ * Derives the validator function name from a type name.
6
+ * e.g. "InfoObject" → "validateInfoObject"
7
+ */
8
+ const validatorName = (typeName) => `validate${typeName}`;
9
+ /**
10
+ * Returns the TypeScript typeof string for a JSON Schema primitive type.
11
+ */
12
+ const typeofString = (type) => {
13
+ if (type === 'integer')
14
+ return 'number';
15
+ return type;
16
+ };
17
+ /**
18
+ * Generates the inline condition that is TRUE when a value is the wrong type.
19
+ */
20
+ const wrongTypeCondition = (accessor, type) => {
21
+ switch (type) {
22
+ case 'string':
23
+ return `typeof ${accessor} !== 'string'`;
24
+ case 'number':
25
+ case 'integer':
26
+ return `typeof ${accessor} !== 'number'`;
27
+ case 'boolean':
28
+ return `typeof ${accessor} !== 'boolean'`;
29
+ case 'array':
30
+ return `!Array.isArray(${accessor})`;
31
+ case 'object':
32
+ return `typeof ${accessor} !== 'object' || ${accessor} === null || Array.isArray(${accessor})`;
33
+ default:
34
+ return '';
35
+ }
36
+ };
37
+ /**
38
+ * Generates validation lines for a single property in an object schema.
39
+ * Handles $ref delegation, enum checks, type checks, and string/number constraints.
40
+ */
41
+ const generatePropertyChecks = (key, propSchema, isRequired, suffix) => {
42
+ if (!isSchemaObject(propSchema))
43
+ return [];
44
+ const raw = `obj[${JSON.stringify(key)}]`;
45
+ const path = `\`\${_path}/${key}\``;
46
+ const lines = [];
47
+ // $ref — delegate to the imported validator
48
+ if (hasRef(propSchema)) {
49
+ const ref = propSchema.$ref;
50
+ const vName = validatorName(refToName(ref, suffix));
51
+ if (isRequired) {
52
+ lines.push(` if (!(${JSON.stringify(key)} in obj)) {`);
53
+ lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`);
54
+ lines.push(` } else {`);
55
+ lines.push(` const _r = ${vName}(${raw}, ${path})`);
56
+ lines.push(` if (_r !== true) errors.push(..._r.errors)`);
57
+ lines.push(` }`);
58
+ }
59
+ else {
60
+ lines.push(` if (${raw} !== undefined) {`);
61
+ lines.push(` const _r = ${vName}(${raw}, ${path})`);
62
+ lines.push(` if (_r !== true) errors.push(..._r.errors)`);
63
+ lines.push(` }`);
64
+ }
65
+ return lines;
66
+ }
67
+ // x-mjst instanceOf (e.g. Date) — value must be an instance of the class
68
+ const instanceOf = getMjstInstanceOf(propSchema);
69
+ if (instanceOf) {
70
+ if (isRequired) {
71
+ lines.push(` if (!(${JSON.stringify(key)} in obj)) {`);
72
+ lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`);
73
+ lines.push(` } else if (!(${raw} instanceof ${instanceOf})) {`);
74
+ lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
75
+ lines.push(` }`);
76
+ }
77
+ else {
78
+ lines.push(` if (${raw} !== undefined && !(${raw} instanceof ${instanceOf})) {`);
79
+ lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
80
+ lines.push(` }`);
81
+ }
82
+ return lines;
83
+ }
84
+ // x-mjst primitive (e.g. bigint) — value must satisfy a typeof check
85
+ const primitive = getMjstPrimitive(propSchema);
86
+ if (primitive) {
87
+ if (isRequired) {
88
+ lines.push(` if (!(${JSON.stringify(key)} in obj)) {`);
89
+ lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`);
90
+ lines.push(` } else if (typeof ${raw} !== "${primitive}") {`);
91
+ lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
92
+ lines.push(` }`);
93
+ }
94
+ else {
95
+ lines.push(` if (${raw} !== undefined && typeof ${raw} !== "${primitive}") {`);
96
+ lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
97
+ lines.push(` }`);
98
+ }
99
+ return lines;
100
+ }
101
+ // enum
102
+ if (hasEnum(propSchema)) {
103
+ const allowed = JSON.stringify(propSchema.enum);
104
+ const label = propSchema.enum.map((v) => JSON.stringify(v)).join(', ');
105
+ if (isRequired) {
106
+ lines.push(` if (!(${JSON.stringify(key)} in obj)) {`);
107
+ lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`);
108
+ lines.push(` } else if (!(${allowed} as unknown[]).includes(${raw})) {`);
109
+ lines.push(` errors.push({ message: \`must be one of: ${label}\`, path: ${path} })`);
110
+ lines.push(` }`);
111
+ }
112
+ else {
113
+ lines.push(` if (${raw} !== undefined && !(${allowed} as unknown[]).includes(${raw})) {`);
114
+ lines.push(` errors.push({ message: \`must be one of: ${label}\`, path: ${path} })`);
115
+ lines.push(` }`);
116
+ }
117
+ return lines;
118
+ }
119
+ // typed property
120
+ if (hasType(propSchema)) {
121
+ const t = propSchema.type;
122
+ const wrongType = wrongTypeCondition(raw, t);
123
+ const typLabel = typeofString(t);
124
+ if (isRequired) {
125
+ lines.push(` if (!(${JSON.stringify(key)} in obj)) {`);
126
+ lines.push(` errors.push({ message: "must have required property '${key}'", path: _path })`);
127
+ if (wrongType) {
128
+ lines.push(` } else if (${wrongType}) {`);
129
+ lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
130
+ }
131
+ lines.push(` }`);
132
+ }
133
+ else if (wrongType) {
134
+ lines.push(` if (${raw} !== undefined && (${wrongType})) {`);
135
+ lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
136
+ lines.push(` }`);
137
+ }
138
+ // String constraints
139
+ if (t === 'string') {
140
+ if (hasPattern(propSchema)) {
141
+ lines.push(` if (typeof ${raw} === 'string' && !/${propSchema.pattern}/.test(${raw})) {`);
142
+ lines.push(` errors.push({ message: 'must match pattern ${propSchema.pattern}', path: ${path} })`);
143
+ lines.push(` }`);
144
+ }
145
+ if (hasMinLength(propSchema)) {
146
+ lines.push(` if (typeof ${raw} === 'string' && ${raw}.length < ${propSchema.minLength}) {`);
147
+ lines.push(` errors.push({ message: 'must have at least ${propSchema.minLength} characters', path: ${path} })`);
148
+ lines.push(` }`);
149
+ }
150
+ if (hasMaxLength(propSchema)) {
151
+ lines.push(` if (typeof ${raw} === 'string' && ${raw}.length > ${propSchema.maxLength}) {`);
152
+ lines.push(` errors.push({ message: 'must have at most ${propSchema.maxLength} characters', path: ${path} })`);
153
+ lines.push(` }`);
154
+ }
155
+ }
156
+ // Number constraints
157
+ if (t === 'number' || t === 'integer') {
158
+ if (hasMinimum(propSchema)) {
159
+ lines.push(` if (typeof ${raw} === 'number' && ${raw} < ${propSchema.minimum}) {`);
160
+ lines.push(` errors.push({ message: 'must be >= ${propSchema.minimum}', path: ${path} })`);
161
+ lines.push(` }`);
162
+ }
163
+ if (hasMaximum(propSchema)) {
164
+ lines.push(` if (typeof ${raw} === 'number' && ${raw} > ${propSchema.maximum}) {`);
165
+ lines.push(` errors.push({ message: 'must be <= ${propSchema.maximum}', path: ${path} })`);
166
+ lines.push(` }`);
167
+ }
168
+ if (hasExclusiveMinimum(propSchema)) {
169
+ lines.push(` if (typeof ${raw} === 'number' && ${raw} <= ${propSchema.exclusiveMinimum}) {`);
170
+ lines.push(` errors.push({ message: 'must be > ${propSchema.exclusiveMinimum}', path: ${path} })`);
171
+ lines.push(` }`);
172
+ }
173
+ if (hasExclusiveMaximum(propSchema)) {
174
+ lines.push(` if (typeof ${raw} === 'number' && ${raw} >= ${propSchema.exclusiveMaximum}) {`);
175
+ lines.push(` errors.push({ message: 'must be < ${propSchema.exclusiveMaximum}', path: ${path} })`);
176
+ lines.push(` }`);
177
+ }
178
+ if (hasMultipleOf(propSchema)) {
179
+ lines.push(` if (typeof ${raw} === 'number' && ${raw} % ${propSchema.multipleOf} !== 0) {`);
180
+ lines.push(` errors.push({ message: 'must be a multiple of ${propSchema.multipleOf}', path: ${path} })`);
181
+ lines.push(` }`);
182
+ }
183
+ }
184
+ // Array with typed items
185
+ if (t === 'array' && hasItems(propSchema)) {
186
+ const itemSchema = propSchema.items;
187
+ if (hasRef(itemSchema)) {
188
+ const vName = validatorName(refToName(itemSchema.$ref, suffix));
189
+ lines.push(` if (Array.isArray(${raw})) {`);
190
+ lines.push(` for (let _i = 0; _i < ${raw}.length; _i++) {`);
191
+ lines.push(` const _ir = ${vName}(${raw}[_i], \`${path.slice(1, -1)}/${key}/\${_i}\`)`);
192
+ lines.push(` if (_ir !== true) errors.push(..._ir.errors)`);
193
+ lines.push(` }`);
194
+ lines.push(` }`);
195
+ }
196
+ else if (hasType(itemSchema)) {
197
+ const itemType = itemSchema.type;
198
+ const itemWrong = wrongTypeCondition('_item', itemType);
199
+ const itemLabel = typeofString(itemType);
200
+ if (itemWrong) {
201
+ lines.push(` if (Array.isArray(${raw})) {`);
202
+ lines.push(` for (let _i = 0; _i < ${raw}.length; _i++) {`);
203
+ lines.push(` const _item = ${raw}[_i]`);
204
+ lines.push(` if (${itemWrong}) errors.push({ message: 'items must be ${itemLabel}', path: \`${path.slice(1, -1)}/${key}/\${_i}\` })`);
205
+ lines.push(` }`);
206
+ lines.push(` }`);
207
+ }
208
+ }
209
+ }
210
+ }
211
+ return lines;
212
+ };
213
+ /**
214
+ * Generates a validator function body for an object schema, checking each
215
+ * property's presence and type and collecting all errors.
216
+ */
217
+ const generateObjectValidator = (schema, typeName, suffix) => {
218
+ const vName = validatorName(typeName);
219
+ const required = new Set(hasRequired(schema) ? schema.required : []);
220
+ const properties = hasProperties(schema) ? schema.properties : {};
221
+ const propertyLines = [];
222
+ for (const [key, propSchema] of Object.entries(properties)) {
223
+ const checks = generatePropertyChecks(key, propSchema, required.has(key), suffix);
224
+ if (checks.length > 0) {
225
+ propertyLines.push(...checks);
226
+ }
227
+ }
228
+ // additionalProperties with a $ref schema validates all extra keys
229
+ if (hasAdditionalProperties(schema) &&
230
+ isSchemaObject(schema.additionalProperties) &&
231
+ hasRef(schema.additionalProperties)) {
232
+ const vRefName = validatorName(refToName(schema.additionalProperties.$ref, suffix));
233
+ propertyLines.push(` for (const _key of Object.keys(obj)) {`);
234
+ propertyLines.push(` if (${JSON.stringify(Object.keys(properties))}.includes(_key)) continue`);
235
+ propertyLines.push(` const _r = ${vRefName}(obj[_key as keyof typeof obj], \`\${_path}/\${_key}\`)`);
236
+ propertyLines.push(` if (_r !== true) errors.push(..._r.errors)`);
237
+ propertyLines.push(` }`);
238
+ }
239
+ const body = propertyLines.length > 0 ? '\n' + propertyLines.join('\n') + '\n' : '';
240
+ return [
241
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
242
+ ` if (typeof input !== 'object' || input === null || Array.isArray(input)) {`,
243
+ ` return { valid: false, errors: [{ message: 'must be object', path: _path }] }`,
244
+ ` }`,
245
+ ``,
246
+ ` const errors: ValidationError[] = []`,
247
+ ` const obj = input as Record<string, unknown>`,
248
+ body,
249
+ ` return errors.length > 0 ? { valid: false, errors } : true`,
250
+ `}`,
251
+ ].join('\n');
252
+ };
253
+ /**
254
+ * Generates a validator function for a non-object schema (primitive, array, enum, $ref).
255
+ */
256
+ const generateScalarValidator = (schema, typeName, suffix) => {
257
+ const vName = validatorName(typeName);
258
+ if (!isSchemaObject(schema)) {
259
+ return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join('\n');
260
+ }
261
+ // Top-level $ref — delegate entirely
262
+ if (hasRef(schema)) {
263
+ const delegateName = validatorName(refToName(schema.$ref, suffix));
264
+ return [
265
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
266
+ ` return ${delegateName}(input, _path)`,
267
+ `}`,
268
+ ].join('\n');
269
+ }
270
+ // Top-level x-mjst instanceOf (e.g. a schema that is itself a Date)
271
+ const instanceOf = getMjstInstanceOf(schema);
272
+ if (instanceOf) {
273
+ return [
274
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
275
+ ` if (!(input instanceof ${instanceOf})) {`,
276
+ ` return { valid: false, errors: [{ message: 'must be ${instanceOf}', path: _path }] }`,
277
+ ` }`,
278
+ ` return true`,
279
+ `}`,
280
+ ].join('\n');
281
+ }
282
+ // Top-level x-mjst primitive (e.g. a schema that is itself a bigint)
283
+ const primitive = getMjstPrimitive(schema);
284
+ if (primitive) {
285
+ return [
286
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
287
+ ` if (typeof input !== "${primitive}") {`,
288
+ ` return { valid: false, errors: [{ message: 'must be ${primitive}', path: _path }] }`,
289
+ ` }`,
290
+ ` return true`,
291
+ `}`,
292
+ ].join('\n');
293
+ }
294
+ // Top-level enum
295
+ if (hasEnum(schema)) {
296
+ const allowed = JSON.stringify(schema.enum);
297
+ const label = schema.enum.map((v) => JSON.stringify(v)).join(', ');
298
+ return [
299
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
300
+ ` if (!(${allowed} as unknown[]).includes(input)) {`,
301
+ ` return { valid: false, errors: [{ message: \`must be one of: ${label}\`, path: _path }] }`,
302
+ ` }`,
303
+ ` return true`,
304
+ `}`,
305
+ ].join('\n');
306
+ }
307
+ // oneOf — try each branch, return errors from all if none match
308
+ if (hasOneOf(schema)) {
309
+ const branches = schema.oneOf
310
+ .map((branch, i) => {
311
+ if (!hasRef(branch))
312
+ return null;
313
+ const bName = validatorName(refToName(branch.$ref, suffix));
314
+ return ` const _r${i} = ${bName}(input, _path)\n if (_r${i} === true) return true`;
315
+ })
316
+ .filter(Boolean)
317
+ .join('\n');
318
+ return [
319
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
320
+ branches,
321
+ ` return { valid: false, errors: [{ message: 'must match one of the expected schemas', path: _path }] }`,
322
+ `}`,
323
+ ].join('\n');
324
+ }
325
+ // Top-level typed schema (string, number, boolean, array)
326
+ if (hasType(schema)) {
327
+ const t = schema.type;
328
+ const wrongType = wrongTypeCondition('input', t);
329
+ const typLabel = typeofString(t);
330
+ const constraintLines = [];
331
+ if (t === 'string') {
332
+ if (hasPattern(schema)) {
333
+ constraintLines.push(` if (typeof input === 'string' && !/${schema.pattern}/.test(input)) {`);
334
+ constraintLines.push(` errors.push({ message: 'must match pattern ${schema.pattern}', path: _path })`);
335
+ constraintLines.push(` }`);
336
+ }
337
+ if (hasMinLength(schema)) {
338
+ constraintLines.push(` if (typeof input === 'string' && input.length < ${schema.minLength}) {`);
339
+ constraintLines.push(` errors.push({ message: 'must have at least ${schema.minLength} characters', path: _path })`);
340
+ constraintLines.push(` }`);
341
+ }
342
+ if (hasMaxLength(schema)) {
343
+ constraintLines.push(` if (typeof input === 'string' && input.length > ${schema.maxLength}) {`);
344
+ constraintLines.push(` errors.push({ message: 'must have at most ${schema.maxLength} characters', path: _path })`);
345
+ constraintLines.push(` }`);
346
+ }
347
+ }
348
+ if (!wrongType) {
349
+ return [
350
+ `export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
351
+ ` return true`,
352
+ `}`,
353
+ ].join('\n');
354
+ }
355
+ if (constraintLines.length === 0) {
356
+ return [
357
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
358
+ ` if (${wrongType}) {`,
359
+ ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
360
+ ` }`,
361
+ ` return true`,
362
+ `}`,
363
+ ].join('\n');
364
+ }
365
+ return [
366
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
367
+ ` if (${wrongType}) {`,
368
+ ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
369
+ ` }`,
370
+ ` const errors: ValidationError[] = []`,
371
+ constraintLines.join('\n'),
372
+ ` return errors.length > 0 ? { valid: false, errors } : true`,
373
+ `}`,
374
+ ].join('\n');
375
+ }
376
+ return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join('\n');
377
+ };
378
+ /**
379
+ * Generates a TypeScript validator function from a JSON Schema.
380
+ *
381
+ * The generated function accepts `unknown` input and returns `true` if valid,
382
+ * or `{ valid: false, errors }` with a list of errors if not.
383
+ *
384
+ * Object schemas check that required properties are present and that all
385
+ * provided properties match their declared types. Non-object schemas (strings,
386
+ * numbers, enums, $refs) emit an inline type check.
387
+ *
388
+ * @example
389
+ * ```typescript
390
+ * generateValidatorFunction({ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, 'Info')
391
+ * // export const validateInfo = (input: unknown, _path = ''): ValidationResult => {
392
+ * // if (typeof input !== 'object' || ...) return { valid: false, ... }
393
+ * // const errors: ValidationError[] = []
394
+ * // const obj = input as Record<string, unknown>
395
+ * // if (!('name' in obj)) { errors.push(...) } else if (typeof obj['name'] !== 'string') { errors.push(...) }
396
+ * // return errors.length > 0 ? { valid: false, errors } : true
397
+ * // }
398
+ * ```
399
+ */
400
+ export const generateValidatorFunction = (schema, typeName, suffix = '') => {
401
+ if (isObjectSchema(schema)) {
402
+ return generateObjectValidator(schema, typeName, suffix);
403
+ }
404
+ return generateScalarValidator(schema, typeName, suffix);
405
+ };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,3 @@
1
- export type { GeneratedFile } from './generators/build-schema';
2
- export { buildValidatorSchema } from './generators/build-schema';
1
+ export type { GeneratedFile } from './generators/build-schema.js';
2
+ export { buildValidatorSchema } from './generators/build-schema.js';
3
+ //# sourceMappingURL=index.d.ts.map