@amritk/generate-validators 0.6.0 → 0.8.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/README.md +37 -0
- package/dist/generators/build-schema.d.ts +0 -1
- package/dist/generators/build-schema.js +29 -0
- package/dist/generators/collect-validator-imports.d.ts +0 -1
- package/dist/generators/collect-validator-imports.js +5 -1
- package/dist/generators/generate-files.d.ts +0 -1
- package/dist/generators/generate-files.js +6 -0
- package/dist/generators/generate-validator-function.d.ts +0 -1
- package/dist/generators/generate-validator-function.js +309 -40
- package/dist/index.d.ts +0 -1
- package/package.json +10 -7
- package/dist/generators/build-schema.d.ts.map +0 -1
- package/dist/generators/collect-validator-imports.d.ts.map +0 -1
- package/dist/generators/generate-files.d.ts.map +0 -1
- package/dist/generators/generate-validator-function.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/src/generators/build-schema.test.ts +0 -163
- package/src/generators/build-schema.ts +0 -81
- package/src/generators/collect-validator-imports.ts +0 -134
- package/src/generators/generate-files.ts +0 -79
- package/src/generators/generate-validator-function.test.ts +0 -311
- package/src/generators/generate-validator-function.ts +0 -550
- package/src/index.ts +0 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@amritk/generate-validators",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Generate TypeScript validation functions from JSON Schemas.",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -24,8 +24,7 @@
|
|
|
24
24
|
"url": "https://github.com/amritk/mjst/issues"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
|
-
"dist"
|
|
28
|
-
"src"
|
|
27
|
+
"dist"
|
|
29
28
|
],
|
|
30
29
|
"publishConfig": {
|
|
31
30
|
"access": "public"
|
|
@@ -33,23 +32,27 @@
|
|
|
33
32
|
"scripts": {
|
|
34
33
|
"build": "tsgo -p tsconfig.build.json && tsc-alias -p tsconfig.build.json -f",
|
|
35
34
|
"types:check": "tsgo -p . --noEmit",
|
|
36
|
-
"test": "NODE_ENV=production vitest run --root ../.. generate-validators"
|
|
35
|
+
"test": "NODE_ENV=production vitest run --root ../.. generate-validators",
|
|
36
|
+
"bench": "bun run ./bench/run.ts"
|
|
37
37
|
},
|
|
38
38
|
"imports": {
|
|
39
39
|
"#generators/*": "./src/generators/*.ts"
|
|
40
40
|
},
|
|
41
41
|
"exports": {
|
|
42
42
|
".": {
|
|
43
|
-
"development": "./src/index.ts",
|
|
44
43
|
"default": "./dist/index.js",
|
|
45
44
|
"types": "./dist/index.d.ts"
|
|
46
45
|
}
|
|
47
46
|
},
|
|
48
47
|
"dependencies": {
|
|
49
48
|
"json-schema-typed": "^8.0.1",
|
|
50
|
-
"@amritk/helpers": "0.
|
|
49
|
+
"@amritk/helpers": "0.10.0"
|
|
51
50
|
},
|
|
52
51
|
"devDependencies": {
|
|
53
|
-
"@scalar/openapi-parser": "^0.26.1"
|
|
52
|
+
"@scalar/openapi-parser": "^0.26.1",
|
|
53
|
+
"@sinclair/typebox": "^0.34.49",
|
|
54
|
+
"ajv": "^8.17.1",
|
|
55
|
+
"ajv-formats": "^3.0.1",
|
|
56
|
+
"zod": "^4.4.3"
|
|
54
57
|
}
|
|
55
58
|
}
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"generate-validator-function.d.ts","sourceRoot":"","sources":["../../src/generators/generate-validator-function.ts"],"names":[],"mappings":"AA0BA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iCAAiC,CAAA;AA+ejE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,yBAAyB,WAAY,UAAU,YAAY,MAAM,sBAAgB,MAM7F,CAAA"}
|
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AAC9D,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA"}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { validate } from '@scalar/openapi-parser'
|
|
2
|
-
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
3
|
-
import { describe, expect, it } from 'vitest'
|
|
4
|
-
|
|
5
|
-
import { buildValidatorSchema } from './build-schema'
|
|
6
|
-
|
|
7
|
-
describe('build-schema', () => {
|
|
8
|
-
it('generates a validator file for the root schema', async () => {
|
|
9
|
-
const schema: JSONSchema = {
|
|
10
|
-
type: 'object',
|
|
11
|
-
properties: { title: { type: 'string' } },
|
|
12
|
-
required: ['title'],
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const files = await buildValidatorSchema(schema, 'Document')
|
|
16
|
-
const filenames = files.map((f) => f.filename)
|
|
17
|
-
|
|
18
|
-
expect(filenames).toContain('document.ts')
|
|
19
|
-
expect(filenames).toContain('validation-result.ts')
|
|
20
|
-
expect(filenames).toContain('index.ts')
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
it('generates a file per $ref definition', async () => {
|
|
24
|
-
const schema: JSONSchema = {
|
|
25
|
-
type: 'object',
|
|
26
|
-
properties: {
|
|
27
|
-
info: { $ref: '#/$defs/info' },
|
|
28
|
-
},
|
|
29
|
-
$defs: {
|
|
30
|
-
info: {
|
|
31
|
-
type: 'object',
|
|
32
|
-
properties: { title: { type: 'string' } },
|
|
33
|
-
required: ['title'],
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const files = await buildValidatorSchema(schema, 'Document')
|
|
39
|
-
const filenames = files.map((f) => f.filename)
|
|
40
|
-
|
|
41
|
-
expect(filenames).toContain('document.ts')
|
|
42
|
-
expect(filenames).toContain('info.ts')
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('generated document.ts exports a validateDocument function', async () => {
|
|
46
|
-
const schema: JSONSchema = {
|
|
47
|
-
type: 'object',
|
|
48
|
-
properties: { title: { type: 'string' } },
|
|
49
|
-
required: ['title'],
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const files = await buildValidatorSchema(schema, 'Document')
|
|
53
|
-
const documentFile = files.find((f) => f.filename === 'document.ts')
|
|
54
|
-
|
|
55
|
-
expect(documentFile?.content).toContain('export const validateDocument')
|
|
56
|
-
expect(documentFile?.content).toContain('ValidationResult')
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('generated file imports the ref validator for $ref properties', async () => {
|
|
60
|
-
const schema: JSONSchema = {
|
|
61
|
-
type: 'object',
|
|
62
|
-
properties: {
|
|
63
|
-
info: { $ref: '#/$defs/info' },
|
|
64
|
-
},
|
|
65
|
-
$defs: {
|
|
66
|
-
info: {
|
|
67
|
-
type: 'object',
|
|
68
|
-
properties: { title: { type: 'string' } },
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const files = await buildValidatorSchema(schema, 'Document')
|
|
74
|
-
const documentFile = files.find((f) => f.filename === 'document.ts')
|
|
75
|
-
|
|
76
|
-
expect(documentFile?.content).toContain("from './info'")
|
|
77
|
-
expect(documentFile?.content).toContain('validateInfo')
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('generates a valid index.ts with re-exports', async () => {
|
|
81
|
-
const schema: JSONSchema = {
|
|
82
|
-
type: 'object',
|
|
83
|
-
properties: { title: { type: 'string' } },
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const files = await buildValidatorSchema(schema, 'Document')
|
|
87
|
-
const indexFile = files.find((f) => f.filename === 'index.ts')
|
|
88
|
-
|
|
89
|
-
expect(indexFile?.content).toContain("from './document'")
|
|
90
|
-
expect(indexFile?.content).toContain('validateDocument')
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('does not generate a file named validation-result for a schema ref', async () => {
|
|
94
|
-
const schema: JSONSchema = {
|
|
95
|
-
type: 'object',
|
|
96
|
-
properties: {
|
|
97
|
-
result: { $ref: '#/$defs/validation-result' },
|
|
98
|
-
},
|
|
99
|
-
$defs: {
|
|
100
|
-
'validation-result': { type: 'object' },
|
|
101
|
-
},
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const files = await buildValidatorSchema(schema, 'Document')
|
|
105
|
-
// Should still have exactly one validation-result.ts (the runtime contract)
|
|
106
|
-
const vrFiles = files.filter((f) => f.filename === 'validation-result.ts')
|
|
107
|
-
expect(vrFiles).toHaveLength(1)
|
|
108
|
-
expect(vrFiles[0]?.content).toContain('export type ValidationResult')
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('produces generated validators that agree with @scalar/openapi-parser on a valid document', async () => {
|
|
112
|
-
// Build validators for a minimal OpenAPI-like schema
|
|
113
|
-
const schema: JSONSchema = {
|
|
114
|
-
type: 'object',
|
|
115
|
-
properties: {
|
|
116
|
-
openapi: { type: 'string' },
|
|
117
|
-
info: { $ref: '#/$defs/info' },
|
|
118
|
-
},
|
|
119
|
-
required: ['openapi', 'info'],
|
|
120
|
-
$defs: {
|
|
121
|
-
info: {
|
|
122
|
-
type: 'object',
|
|
123
|
-
properties: {
|
|
124
|
-
title: { type: 'string' },
|
|
125
|
-
version: { type: 'string' },
|
|
126
|
-
},
|
|
127
|
-
required: ['title', 'version'],
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const files = await buildValidatorSchema(schema, 'Document')
|
|
133
|
-
|
|
134
|
-
// Sanity-check generated code shape
|
|
135
|
-
const documentFile = files.find((f) => f.filename === 'document.ts')
|
|
136
|
-
const infoFile = files.find((f) => f.filename === 'info.ts')
|
|
137
|
-
|
|
138
|
-
expect(documentFile?.content).toContain('validateDocument')
|
|
139
|
-
expect(infoFile?.content).toContain('validateInfo')
|
|
140
|
-
|
|
141
|
-
// Cross-check: @scalar/openapi-parser says a complete document is valid
|
|
142
|
-
const validDoc = { openapi: '3.1.0', info: { title: 'API', version: '1.0' }, paths: {} }
|
|
143
|
-
const refResult = await validate(validDoc)
|
|
144
|
-
expect(refResult.valid).toBe(true)
|
|
145
|
-
|
|
146
|
-
// Cross-check: @scalar/openapi-parser says a document missing info.title is invalid
|
|
147
|
-
const invalidDoc = { openapi: '3.1.0', info: { version: '1.0' }, paths: {} }
|
|
148
|
-
const refInvalid = await validate(invalidDoc)
|
|
149
|
-
expect(refInvalid.valid).toBe(false)
|
|
150
|
-
expect(refInvalid.errors.some((e) => e.message.includes('title'))).toBe(true)
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
it('emits a validation-result.ts with the runtime ValidationResult/ValidationError types', async () => {
|
|
154
|
-
const schema: JSONSchema = { type: 'object' }
|
|
155
|
-
const files = await buildValidatorSchema(schema, 'Doc')
|
|
156
|
-
const vrFile = files.find((f) => f.filename === 'validation-result.ts')
|
|
157
|
-
|
|
158
|
-
expect(vrFile?.content).toContain('export type ValidationError')
|
|
159
|
-
expect(vrFile?.content).toContain('export type ValidationResult')
|
|
160
|
-
expect(vrFile?.content).toContain('message: string')
|
|
161
|
-
expect(vrFile?.content).toContain('path: string')
|
|
162
|
-
})
|
|
163
|
-
})
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { generateIndexBarrel } from '@amritk/helpers/generate-index-barrel'
|
|
2
|
-
import { walkRefGraph } from '@amritk/helpers/walk-ref-graph'
|
|
3
|
-
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
4
|
-
|
|
5
|
-
import { generateValidatorFile } from './generate-files'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Represents a generated TypeScript file with its filename and content.
|
|
9
|
-
*/
|
|
10
|
-
export type GeneratedFile = {
|
|
11
|
-
filename: string
|
|
12
|
-
content: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const VALIDATION_RESULT_CONTENT = `/**
|
|
16
|
-
* A single validation error with a human-readable message and a JSON Pointer
|
|
17
|
-
* path indicating where in the document the error occurred.
|
|
18
|
-
*/
|
|
19
|
-
export type ValidationError = {
|
|
20
|
-
message: string
|
|
21
|
-
path: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* The result of a generated validator function.
|
|
26
|
-
* Returns \`true\` when the input is valid, or an object with \`valid: false\`
|
|
27
|
-
* and a list of errors when it is not.
|
|
28
|
-
*/
|
|
29
|
-
export type ValidationResult = true | { valid: false; errors: ValidationError[] }
|
|
30
|
-
`
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Builds all TypeScript validator files from a JSON Schema by traversing all
|
|
34
|
-
* `$ref` / `$dynamicRef` references recursively (via the shared
|
|
35
|
-
* `@amritk/helpers/walk-ref-graph` walker).
|
|
36
|
-
*
|
|
37
|
-
* Each generated file exports:
|
|
38
|
-
* - A TypeScript type definition
|
|
39
|
-
* - A `validateFoo(input: unknown, _path?: string): ValidationResult` function
|
|
40
|
-
*
|
|
41
|
-
* A `validation-result.ts` file containing the `ValidationResult` and `ValidationError`
|
|
42
|
-
* runtime contract is always emitted. An `index.ts` re-exports everything.
|
|
43
|
-
*
|
|
44
|
-
* @param rootSchema - The root JSON Schema to build from
|
|
45
|
-
* @param rootTypeName - The name for the root type (e.g. "Document")
|
|
46
|
-
* @returns An array of generated TypeScript files
|
|
47
|
-
*
|
|
48
|
-
* @example
|
|
49
|
-
* ```typescript
|
|
50
|
-
* const files = await buildValidatorSchema(schema, 'Document')
|
|
51
|
-
* // files → [{ filename: 'document.ts', content: '...' }, { filename: 'info.ts', ... }, ...]
|
|
52
|
-
* ```
|
|
53
|
-
*/
|
|
54
|
-
export const buildValidatorSchema = async (
|
|
55
|
-
rootSchema: JSONSchema,
|
|
56
|
-
rootTypeName: string,
|
|
57
|
-
typeSuffix = '',
|
|
58
|
-
): Promise<GeneratedFile[]> => {
|
|
59
|
-
const files: GeneratedFile[] = []
|
|
60
|
-
|
|
61
|
-
walkRefGraph(rootSchema, rootTypeName, { typeSuffix }, (node) => {
|
|
62
|
-
// `validation-result` and `index` are reserved output filenames, so never
|
|
63
|
-
// let a definition of either name overwrite them.
|
|
64
|
-
if (node.filename === 'validation-result' || node.filename === 'index') return
|
|
65
|
-
|
|
66
|
-
const content = generateValidatorFile(node.schema, node.typeName, {
|
|
67
|
-
rootSchema: node.rootSchema,
|
|
68
|
-
typeSuffix,
|
|
69
|
-
...(node.ref !== undefined ? { selfRef: node.ref } : {}),
|
|
70
|
-
})
|
|
71
|
-
files.push({ filename: `${node.filename}.ts`, content })
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
// Emit the runtime contract for validators. ValidationResult is mjst-defined
|
|
75
|
-
// (not derived from the input schema), so its content is fixed.
|
|
76
|
-
files.push({ filename: 'validation-result.ts', content: VALIDATION_RESULT_CONTENT })
|
|
77
|
-
|
|
78
|
-
files.push({ filename: 'index.ts', content: generateIndexBarrel(files) })
|
|
79
|
-
|
|
80
|
-
return files
|
|
81
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
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
|
-
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Options for controlling how validator imports are collected.
|
|
9
|
-
*/
|
|
10
|
-
type CollectValidatorImportsOptions = {
|
|
11
|
-
/**
|
|
12
|
-
* The $ref path of the schema being generated (e.g. `#/$defs/encoding`).
|
|
13
|
-
* Prevents a file from importing itself.
|
|
14
|
-
*/
|
|
15
|
-
readonly selfRef?: string | undefined
|
|
16
|
-
/**
|
|
17
|
-
* The root schema document. URI refs that cannot be resolved within it
|
|
18
|
-
* are excluded from the import list (they were never generated as files).
|
|
19
|
-
*/
|
|
20
|
-
readonly rootSchema?: Record<string, unknown> | undefined
|
|
21
|
-
/**
|
|
22
|
-
* Suffix appended to every type/validator name derived from a `$ref`. Must
|
|
23
|
-
* match the suffix used when generating the referenced files. Defaults to `''`.
|
|
24
|
-
*/
|
|
25
|
-
readonly typeSuffix?: string
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Generates an import statement for a single $ref, importing both the type
|
|
30
|
-
* and the validator function from the ref's generated file.
|
|
31
|
-
*/
|
|
32
|
-
const buildImport = (ref: string, suffix: string): string => {
|
|
33
|
-
const filename = refToFilename(ref)
|
|
34
|
-
const typeName = refToName(ref, suffix)
|
|
35
|
-
const validatorName = `validate${typeName}`
|
|
36
|
-
return `import { type ${typeName}, ${validatorName} } from './${filename}'`
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Resolves the canonical filename for a ref, stripping `-or-reference` suffixes
|
|
41
|
-
* so that `#/$defs/parameter-or-reference` maps to `parameter`.
|
|
42
|
-
*/
|
|
43
|
-
const canonicalFilename = (ref: string): string => {
|
|
44
|
-
const base = ref.endsWith('-or-reference') ? ref.replace('-or-reference', '') : ref
|
|
45
|
-
return refToFilename(base)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Walks one level of the schema and yields all direct $ref strings that should
|
|
50
|
-
* become imports: properties, additionalProperties, items, and union branches.
|
|
51
|
-
*/
|
|
52
|
-
const collectDirectRefs = (schema: JSONSchema): string[] => {
|
|
53
|
-
if (typeof schema === 'boolean' || schema === null) return []
|
|
54
|
-
|
|
55
|
-
const refs: string[] = []
|
|
56
|
-
|
|
57
|
-
if (hasRef(schema)) {
|
|
58
|
-
refs.push(schema.$ref)
|
|
59
|
-
return refs
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const propSchemas =
|
|
63
|
-
'properties' in schema && typeof schema.properties === 'object' && schema.properties !== null
|
|
64
|
-
? Object.values(schema.properties as Record<string, JSONSchema>)
|
|
65
|
-
: []
|
|
66
|
-
|
|
67
|
-
for (const prop of propSchemas) {
|
|
68
|
-
if (hasRef(prop)) refs.push((prop as { $ref: string }).$ref)
|
|
69
|
-
if (hasItems(prop) && hasRef(prop.items)) refs.push((prop.items as { $ref: string }).$ref)
|
|
70
|
-
if (hasAdditionalProperties(prop) && hasRef(prop.additionalProperties as JSONSchema)) {
|
|
71
|
-
refs.push((prop.additionalProperties as { $ref: string }).$ref)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (hasItems(schema) && hasRef(schema.items)) {
|
|
76
|
-
refs.push((schema.items as { $ref: string }).$ref)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (hasAdditionalProperties(schema) && hasRef(schema.additionalProperties as JSONSchema)) {
|
|
80
|
-
refs.push((schema.additionalProperties as { $ref: string }).$ref)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
for (const branch of [
|
|
84
|
-
...(hasOneOf(schema) ? schema.oneOf : []),
|
|
85
|
-
...(hasAnyOf(schema) ? schema.anyOf : []),
|
|
86
|
-
...(hasAllOf(schema) ? schema.allOf : []),
|
|
87
|
-
]) {
|
|
88
|
-
if (hasRef(branch)) refs.push((branch as { $ref: string }).$ref)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return refs
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Collects import statements for all $ref dependencies of a schema.
|
|
96
|
-
* Each import brings in both the generated TypeScript type and validator function.
|
|
97
|
-
*
|
|
98
|
-
* @example
|
|
99
|
-
* ```typescript
|
|
100
|
-
* const schema = { properties: { contact: { $ref: '#/$defs/contact' } } }
|
|
101
|
-
* collectValidatorImports(schema)
|
|
102
|
-
* // ["import { type Contact, validateContact } from './contact'"]
|
|
103
|
-
* ```
|
|
104
|
-
*/
|
|
105
|
-
export const collectValidatorImports = (schema: JSONSchema, options?: CollectValidatorImportsOptions): string[] => {
|
|
106
|
-
const selfFilename = options?.selfRef ? refToFilename(options.selfRef) : null
|
|
107
|
-
const rootSchema = options?.rootSchema
|
|
108
|
-
const typeSuffix = options?.typeSuffix ?? ''
|
|
109
|
-
|
|
110
|
-
const refs = collectDirectRefs(schema)
|
|
111
|
-
const seen = new Set<string>()
|
|
112
|
-
const imports: string[] = []
|
|
113
|
-
|
|
114
|
-
for (const ref of refs) {
|
|
115
|
-
const filename = canonicalFilename(ref)
|
|
116
|
-
|
|
117
|
-
if (seen.has(filename)) continue
|
|
118
|
-
if (selfFilename && filename === selfFilename) continue
|
|
119
|
-
|
|
120
|
-
// Skip refs that don't resolve in this schema (external / never generated)
|
|
121
|
-
if (rootSchema) {
|
|
122
|
-
const resolved = resolveRef(ref, rootSchema)
|
|
123
|
-
if (!resolved) continue
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
seen.add(filename)
|
|
127
|
-
|
|
128
|
-
// -or-reference unions import the base type's validator
|
|
129
|
-
const importRef = ref.endsWith('-or-reference') ? ref.replace('-or-reference', '') : ref
|
|
130
|
-
imports.push(buildImport(importRef, typeSuffix))
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return imports
|
|
134
|
-
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { generateTypeDefinition } from '@amritk/helpers/generate-type-definition'
|
|
2
|
-
import type { JSONSchema } from 'json-schema-typed/draft-2020-12'
|
|
3
|
-
|
|
4
|
-
import { collectValidatorImports } from './collect-validator-imports'
|
|
5
|
-
import { generateValidatorFunction } from './generate-validator-function'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Options for controlling what gets generated in a validator file.
|
|
9
|
-
*/
|
|
10
|
-
type GenerateValidatorFileOptions = {
|
|
11
|
-
/**
|
|
12
|
-
* The $ref path of the schema being generated (e.g. `#/$defs/info`).
|
|
13
|
-
* Prevents the file from importing itself.
|
|
14
|
-
*/
|
|
15
|
-
readonly selfRef?: string
|
|
16
|
-
/**
|
|
17
|
-
* The root schema document. Used to filter out unresolvable refs.
|
|
18
|
-
*/
|
|
19
|
-
readonly rootSchema?: Record<string, unknown>
|
|
20
|
-
/**
|
|
21
|
-
* Suffix appended to every type/validator name derived from a `$ref`.
|
|
22
|
-
* Defaults to `''` (no suffix).
|
|
23
|
-
*/
|
|
24
|
-
readonly typeSuffix?: string
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Generates a complete TypeScript validator file from a JSON Schema.
|
|
29
|
-
*
|
|
30
|
-
* The file contains:
|
|
31
|
-
* - Imports for the ValidationResult/ValidationError types
|
|
32
|
-
* - Imports for any $ref types and their validator functions
|
|
33
|
-
* - The exported TypeScript type definition
|
|
34
|
-
* - The exported validator function
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* ```typescript
|
|
38
|
-
* const schema = {
|
|
39
|
-
* type: 'object',
|
|
40
|
-
* properties: { title: { type: 'string' } },
|
|
41
|
-
* required: ['title'],
|
|
42
|
-
* }
|
|
43
|
-
* generateValidatorFile(schema, 'Info')
|
|
44
|
-
* // import type { ValidationResult, ValidationError } from './validation-result'
|
|
45
|
-
* // export type Info = { title: string }
|
|
46
|
-
* // export const validateInfo = (input: unknown, _path = ''): ValidationResult => { ... }
|
|
47
|
-
* ```
|
|
48
|
-
*/
|
|
49
|
-
export const generateValidatorFile = (
|
|
50
|
-
schema: JSONSchema,
|
|
51
|
-
typeName: string,
|
|
52
|
-
options?: GenerateValidatorFileOptions,
|
|
53
|
-
): string => {
|
|
54
|
-
const typeSuffix = options?.typeSuffix ?? ''
|
|
55
|
-
const refImports = collectValidatorImports(schema, {
|
|
56
|
-
selfRef: options?.selfRef,
|
|
57
|
-
rootSchema: options?.rootSchema,
|
|
58
|
-
typeSuffix,
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix })
|
|
62
|
-
const validatorFunction = generateValidatorFunction(schema, typeName, typeSuffix)
|
|
63
|
-
|
|
64
|
-
let result = `import type { ValidationResult, ValidationError } from './validation-result'\n`
|
|
65
|
-
|
|
66
|
-
for (const imp of refImports) {
|
|
67
|
-
result += imp + '\n'
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (refImports.length > 0) {
|
|
71
|
-
result += '\n'
|
|
72
|
-
} else {
|
|
73
|
-
result += '\n'
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
result += typeDefinition + '\n\n' + validatorFunction
|
|
77
|
-
|
|
78
|
-
return result
|
|
79
|
-
}
|