@amritk/generate-validators 0.4.1 → 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.
- package/dist/generators/build-schema.d.ts +4 -2
- package/dist/generators/build-schema.d.ts.map +1 -0
- package/dist/generators/build-schema.js +61 -0
- package/dist/generators/collect-validator-imports.d.ts +1 -0
- package/dist/generators/collect-validator-imports.d.ts.map +1 -0
- package/dist/generators/collect-validator-imports.js +99 -0
- package/dist/generators/generate-files.d.ts +1 -0
- package/dist/generators/generate-files.d.ts.map +1 -0
- package/dist/generators/generate-files.js +47 -0
- package/dist/generators/generate-validator-function.d.ts +1 -0
- package/dist/generators/generate-validator-function.d.ts.map +1 -0
- package/dist/generators/generate-validator-function.js +405 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -1468
- package/package.json +6 -6
- package/src/generators/build-schema.ts +15 -80
|
@@ -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
|
-
*
|
|
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
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -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
|
+
};
|
|
@@ -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
|