@kubb/plugin-mcp 5.0.0-alpha.9 → 5.0.0-beta.3
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/LICENSE +17 -10
- package/README.md +1 -4
- package/dist/index.cjs +957 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +207 -4
- package/dist/index.js +923 -90
- package/dist/index.js.map +1 -1
- package/package.json +42 -64
- package/src/components/McpHandler.tsx +173 -0
- package/src/components/Server.tsx +89 -109
- package/src/generators/mcpGenerator.tsx +65 -84
- package/src/generators/serverGenerator.tsx +95 -58
- package/src/index.ts +11 -2
- package/src/plugin.ts +87 -135
- package/src/resolvers/resolverMcp.ts +25 -0
- package/src/types.ts +46 -28
- package/src/utils.ts +113 -0
- package/dist/Server-DV9zFrUP.cjs +0 -221
- package/dist/Server-DV9zFrUP.cjs.map +0 -1
- package/dist/Server-KWLMg0Lm.js +0 -173
- package/dist/Server-KWLMg0Lm.js.map +0 -1
- package/dist/components.cjs +0 -3
- package/dist/components.d.ts +0 -41
- package/dist/components.js +0 -2
- package/dist/generators-CWAFnA94.cjs +0 -285
- package/dist/generators-CWAFnA94.cjs.map +0 -1
- package/dist/generators-TtEOkDB1.js +0 -274
- package/dist/generators-TtEOkDB1.js.map +0 -1
- package/dist/generators.cjs +0 -4
- package/dist/generators.d.ts +0 -508
- package/dist/generators.js +0 -2
- package/dist/types-DXZDZ3vf.d.ts +0 -64
- package/src/components/index.ts +0 -1
- package/src/generators/index.ts +0 -2
package/src/plugin.ts
CHANGED
|
@@ -1,170 +1,122 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import { camelCase } from '@internals/utils'
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
import { ast, definePlugin, type Group } from '@kubb/core'
|
|
4
5
|
import { pluginClientName } from '@kubb/plugin-client'
|
|
5
6
|
import { source as axiosClientSource } from '@kubb/plugin-client/templates/clients/axios.source'
|
|
6
7
|
import { source as fetchClientSource } from '@kubb/plugin-client/templates/clients/fetch.source'
|
|
7
8
|
import { source as configSource } from '@kubb/plugin-client/templates/config.source'
|
|
8
|
-
import { OperationGenerator, pluginOasName } from '@kubb/plugin-oas'
|
|
9
9
|
import { pluginTsName } from '@kubb/plugin-ts'
|
|
10
10
|
import { pluginZodName } from '@kubb/plugin-zod'
|
|
11
|
-
import { mcpGenerator
|
|
11
|
+
import { mcpGenerator } from './generators/mcpGenerator.tsx'
|
|
12
|
+
import { serverGenerator } from './generators/serverGenerator.tsx'
|
|
13
|
+
import { resolverMcp } from './resolvers/resolverMcp.ts'
|
|
12
14
|
import type { PluginMcp } from './types.ts'
|
|
13
15
|
|
|
14
16
|
export const pluginMcpName = 'plugin-mcp' satisfies PluginMcp['name']
|
|
15
17
|
|
|
16
|
-
export const pluginMcp =
|
|
18
|
+
export const pluginMcp = definePlugin<PluginMcp>((options) => {
|
|
17
19
|
const {
|
|
18
20
|
output = { path: 'mcp', barrelType: 'named' },
|
|
19
21
|
group,
|
|
20
22
|
exclude = [],
|
|
21
23
|
include,
|
|
22
24
|
override = [],
|
|
23
|
-
transformers = {},
|
|
24
|
-
generators = [mcpGenerator, serverGenerator].filter(Boolean),
|
|
25
|
-
contentType,
|
|
26
25
|
paramsCasing,
|
|
27
26
|
client,
|
|
27
|
+
resolver: userResolver,
|
|
28
|
+
transformer: userTransformer,
|
|
29
|
+
generators: userGenerators = [],
|
|
28
30
|
} = options
|
|
29
31
|
|
|
30
32
|
const clientName = client?.client ?? 'axios'
|
|
31
33
|
const clientImportPath = client?.importPath ?? (!client?.bundle ? `@kubb/plugin-client/clients/${clientName}` : undefined)
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
group,
|
|
38
|
-
paramsCasing,
|
|
39
|
-
client: {
|
|
40
|
-
client: clientName,
|
|
41
|
-
clientType: client?.clientType ?? 'function',
|
|
42
|
-
importPath: clientImportPath,
|
|
43
|
-
dataReturnType: client?.dataReturnType ?? 'data',
|
|
44
|
-
bundle: client?.bundle,
|
|
45
|
-
baseURL: client?.baseURL,
|
|
46
|
-
paramsCasing: client?.paramsCasing,
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
pre: [pluginOasName, pluginTsName, pluginZodName].filter(Boolean),
|
|
50
|
-
resolvePath(baseName, pathMode, options) {
|
|
51
|
-
const root = path.resolve(this.config.root, this.config.output.path)
|
|
52
|
-
const mode = pathMode ?? getMode(path.resolve(root, output.path))
|
|
53
|
-
|
|
54
|
-
if (mode === 'single') {
|
|
55
|
-
/**
|
|
56
|
-
* when output is a file then we will always append to the same file(output file), see fileManager.addOrAppend
|
|
57
|
-
* Other plugins then need to call addOrAppend instead of just add from the fileManager class
|
|
58
|
-
*/
|
|
59
|
-
return path.resolve(root, output.path)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (group && (options?.group?.path || options?.group?.tag)) {
|
|
63
|
-
const groupName: Group['name'] = group?.name
|
|
35
|
+
const groupConfig = group
|
|
36
|
+
? ({
|
|
37
|
+
...group,
|
|
38
|
+
name: group.name
|
|
64
39
|
? group.name
|
|
65
|
-
: (ctx) => {
|
|
66
|
-
if (group
|
|
40
|
+
: (ctx: { group: string }) => {
|
|
41
|
+
if (group.type === 'path') {
|
|
67
42
|
return `${ctx.group.split('/')[1]}`
|
|
68
43
|
}
|
|
69
44
|
return `${camelCase(ctx.group)}Requests`
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return path.resolve(
|
|
73
|
-
root,
|
|
74
|
-
output.path,
|
|
75
|
-
groupName({
|
|
76
|
-
group: group.type === 'path' ? options.group.path! : options.group.tag!,
|
|
77
|
-
}),
|
|
78
|
-
baseName,
|
|
79
|
-
)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return path.resolve(root, output.path, baseName)
|
|
83
|
-
},
|
|
84
|
-
resolveName(name, type) {
|
|
85
|
-
const resolvedName = camelCase(name, {
|
|
86
|
-
isFile: type === 'file',
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
if (type) {
|
|
90
|
-
return transformers?.name?.(resolvedName, type) || resolvedName
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return resolvedName
|
|
94
|
-
},
|
|
95
|
-
async install() {
|
|
96
|
-
const root = path.resolve(this.config.root, this.config.output.path)
|
|
97
|
-
const mode = getMode(path.resolve(root, output.path))
|
|
98
|
-
const oas = await this.getOas()
|
|
99
|
-
const baseURL = await this.getBaseURL()
|
|
100
|
-
|
|
101
|
-
if (baseURL) {
|
|
102
|
-
this.plugin.options.client.baseURL = baseURL
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const hasClientPlugin = !!this.driver.getPluginByName(pluginClientName)
|
|
106
|
-
|
|
107
|
-
if (this.plugin.options.client.bundle && !hasClientPlugin && !this.plugin.options.client.importPath) {
|
|
108
|
-
// pre add bundled fetch
|
|
109
|
-
await this.addFile({
|
|
110
|
-
baseName: 'fetch.ts',
|
|
111
|
-
path: path.resolve(root, '.kubb/fetch.ts'),
|
|
112
|
-
sources: [
|
|
113
|
-
{
|
|
114
|
-
name: 'fetch',
|
|
115
|
-
value: this.plugin.options.client.client === 'fetch' ? fetchClientSource : axiosClientSource,
|
|
116
|
-
isExportable: true,
|
|
117
|
-
isIndexable: true,
|
|
118
45
|
},
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
exports: [],
|
|
122
|
-
})
|
|
123
|
-
}
|
|
46
|
+
} satisfies Group)
|
|
47
|
+
: undefined
|
|
124
48
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
49
|
+
return {
|
|
50
|
+
name: pluginMcpName,
|
|
51
|
+
options,
|
|
52
|
+
dependencies: [pluginTsName, pluginZodName],
|
|
53
|
+
hooks: {
|
|
54
|
+
'kubb:plugin:setup'(ctx) {
|
|
55
|
+
const resolver = userResolver ? { ...resolverMcp, ...userResolver } : resolverMcp
|
|
56
|
+
|
|
57
|
+
ctx.setOptions({
|
|
58
|
+
output,
|
|
59
|
+
exclude,
|
|
60
|
+
include,
|
|
61
|
+
override,
|
|
62
|
+
group: groupConfig,
|
|
63
|
+
paramsCasing,
|
|
64
|
+
client: {
|
|
65
|
+
client: clientName,
|
|
66
|
+
clientType: client?.clientType ?? 'function',
|
|
67
|
+
importPath: clientImportPath,
|
|
68
|
+
dataReturnType: client?.dataReturnType ?? 'data',
|
|
69
|
+
bundle: client?.bundle,
|
|
70
|
+
baseURL: client?.baseURL,
|
|
71
|
+
paramsCasing: client?.paramsCasing,
|
|
72
|
+
},
|
|
73
|
+
resolver,
|
|
139
74
|
})
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
75
|
+
ctx.setResolver(resolver)
|
|
76
|
+
if (userTransformer) {
|
|
77
|
+
ctx.setTransformer(userTransformer)
|
|
78
|
+
}
|
|
79
|
+
ctx.addGenerator(mcpGenerator)
|
|
80
|
+
ctx.addGenerator(serverGenerator)
|
|
81
|
+
for (const gen of userGenerators) {
|
|
82
|
+
ctx.addGenerator(gen)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const root = path.resolve(ctx.config.root, ctx.config.output.path)
|
|
86
|
+
const hasClientPlugin = ctx.config.plugins?.some((p) => p.name === pluginClientName)
|
|
87
|
+
|
|
88
|
+
if (client?.bundle && !hasClientPlugin && !clientImportPath) {
|
|
89
|
+
ctx.injectFile({
|
|
90
|
+
baseName: 'fetch.ts',
|
|
91
|
+
path: path.resolve(root, '.kubb/fetch.ts'),
|
|
92
|
+
sources: [
|
|
93
|
+
ast.createSource({
|
|
94
|
+
name: 'fetch',
|
|
95
|
+
nodes: [ast.createText(clientName === 'fetch' ? fetchClientSource : axiosClientSource)],
|
|
96
|
+
isExportable: true,
|
|
97
|
+
isIndexable: true,
|
|
98
|
+
}),
|
|
99
|
+
],
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!hasClientPlugin) {
|
|
104
|
+
ctx.injectFile({
|
|
105
|
+
baseName: 'config.ts',
|
|
106
|
+
path: path.resolve(root, '.kubb/config.ts'),
|
|
107
|
+
sources: [
|
|
108
|
+
ast.createSource({
|
|
109
|
+
name: 'config',
|
|
110
|
+
nodes: [ast.createText(configSource)],
|
|
111
|
+
isExportable: false,
|
|
112
|
+
isIndexable: false,
|
|
113
|
+
}),
|
|
114
|
+
],
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
},
|
|
168
118
|
},
|
|
169
119
|
}
|
|
170
120
|
})
|
|
121
|
+
|
|
122
|
+
export default pluginMcp
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { camelCase } from '@internals/utils'
|
|
2
|
+
import { defineResolver } from '@kubb/core'
|
|
3
|
+
import type { PluginMcp } from '../types.ts'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Naming convention resolver for MCP plugin.
|
|
7
|
+
*
|
|
8
|
+
* Provides default naming helpers using camelCase with a `handler` suffix for functions.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* `resolverMcp.default('addPet', 'function') // → 'addPetHandler'`
|
|
12
|
+
*/
|
|
13
|
+
export const resolverMcp = defineResolver<PluginMcp>((ctx) => ({
|
|
14
|
+
name: 'default',
|
|
15
|
+
pluginName: 'plugin-mcp',
|
|
16
|
+
default(name, type) {
|
|
17
|
+
if (type === 'file') {
|
|
18
|
+
return camelCase(name, { isFile: true })
|
|
19
|
+
}
|
|
20
|
+
return camelCase(name, { suffix: 'handler' })
|
|
21
|
+
},
|
|
22
|
+
resolveName(name) {
|
|
23
|
+
return ctx.default(name, 'function')
|
|
24
|
+
},
|
|
25
|
+
}))
|
package/src/types.ts
CHANGED
|
@@ -1,62 +1,80 @@
|
|
|
1
|
-
import type { Group, Output, PluginFactoryOptions,
|
|
2
|
-
|
|
3
|
-
import type { contentType, Oas } from '@kubb/oas'
|
|
1
|
+
import type { ast, Exclude, Generator, Group, Include, Output, Override, PluginFactoryOptions, Resolver } from '@kubb/core'
|
|
4
2
|
import type { ClientImportPath, PluginClient } from '@kubb/plugin-client'
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolver for MCP that provides naming methods for handler functions.
|
|
6
|
+
*/
|
|
7
|
+
export type ResolverMcp = Resolver & {
|
|
8
|
+
/**
|
|
9
|
+
* Resolves the handler function name for an operation.
|
|
10
|
+
*
|
|
11
|
+
* @example Resolving handler function names
|
|
12
|
+
* `resolver.resolveName('show pet by id') // -> 'showPetByIdHandler'`
|
|
13
|
+
*/
|
|
14
|
+
resolveName(this: ResolverMcp, name: string): string
|
|
15
|
+
}
|
|
7
16
|
|
|
8
17
|
export type Options = {
|
|
9
18
|
/**
|
|
10
|
-
* Specify the export location for the files and define the behavior of the output
|
|
19
|
+
* Specify the export location for the files and define the behavior of the output.
|
|
11
20
|
* @default { path: 'mcp', barrelType: 'named' }
|
|
12
21
|
*/
|
|
13
|
-
output?: Output
|
|
22
|
+
output?: Output
|
|
14
23
|
/**
|
|
15
|
-
*
|
|
16
|
-
* By default, the first JSON valid mediaType is used
|
|
24
|
+
* Client configuration for HTTP request generation.
|
|
17
25
|
*/
|
|
18
|
-
contentType?: contentType
|
|
19
26
|
client?: ClientImportPath & Pick<PluginClient['options'], 'clientType' | 'dataReturnType' | 'baseURL' | 'bundle' | 'paramsCasing'>
|
|
20
27
|
/**
|
|
21
|
-
*
|
|
22
|
-
* When set to 'camelcase', parameter names in path, query, and header params will be transformed to camelCase.
|
|
23
|
-
* This should match the paramsCasing setting used in @kubb/plugin-ts.
|
|
24
|
-
* @default undefined
|
|
28
|
+
* Apply casing to parameter names to match your configuration.
|
|
25
29
|
*/
|
|
26
30
|
paramsCasing?: 'camelcase'
|
|
27
31
|
/**
|
|
28
|
-
* Group the
|
|
32
|
+
* Group the MCP requests based on the provided name.
|
|
29
33
|
*/
|
|
30
34
|
group?: Group
|
|
31
35
|
/**
|
|
32
|
-
*
|
|
36
|
+
* Tags, operations, or paths to exclude from generation.
|
|
33
37
|
*/
|
|
34
38
|
exclude?: Array<Exclude>
|
|
35
39
|
/**
|
|
36
|
-
*
|
|
40
|
+
* Tags, operations, or paths to include in generation.
|
|
37
41
|
*/
|
|
38
42
|
include?: Array<Include>
|
|
39
43
|
/**
|
|
40
|
-
*
|
|
44
|
+
* Override options for specific tags, operations, or paths.
|
|
41
45
|
*/
|
|
42
46
|
override?: Array<Override<ResolvedOptions>>
|
|
43
|
-
transformers?: {
|
|
44
|
-
/**
|
|
45
|
-
* Customize the names based on the type that is provided by the plugin.
|
|
46
|
-
*/
|
|
47
|
-
name?: (name: ResolveNameParams['name'], type?: ResolveNameParams['type']) => string
|
|
48
|
-
}
|
|
49
47
|
/**
|
|
50
|
-
*
|
|
48
|
+
* Override naming conventions for function names and types.
|
|
49
|
+
*/
|
|
50
|
+
resolver?: Partial<ResolverMcp> & ThisType<ResolverMcp>
|
|
51
|
+
/**
|
|
52
|
+
* AST visitor to transform generated nodes.
|
|
53
|
+
*/
|
|
54
|
+
transformer?: ast.Visitor
|
|
55
|
+
/**
|
|
56
|
+
* Additional generators alongside the default generators.
|
|
51
57
|
*/
|
|
52
58
|
generators?: Array<Generator<PluginMcp>>
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
type ResolvedOptions = {
|
|
56
|
-
output: Output
|
|
57
|
-
|
|
62
|
+
output: Output
|
|
63
|
+
exclude: Array<Exclude>
|
|
64
|
+
include: Array<Include> | undefined
|
|
65
|
+
override: Array<Override<ResolvedOptions>>
|
|
66
|
+
group: Group | undefined
|
|
58
67
|
client: Pick<PluginClient['options'], 'client' | 'clientType' | 'dataReturnType' | 'importPath' | 'baseURL' | 'bundle' | 'paramsCasing'>
|
|
59
68
|
paramsCasing: Options['paramsCasing']
|
|
69
|
+
resolver: ResolverMcp
|
|
60
70
|
}
|
|
61
71
|
|
|
62
|
-
export type PluginMcp = PluginFactoryOptions<'plugin-mcp', Options, ResolvedOptions,
|
|
72
|
+
export type PluginMcp = PluginFactoryOptions<'plugin-mcp', Options, ResolvedOptions, ResolverMcp>
|
|
73
|
+
|
|
74
|
+
declare global {
|
|
75
|
+
namespace Kubb {
|
|
76
|
+
interface PluginRegistry {
|
|
77
|
+
'plugin-mcp': PluginMcp
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { camelCase } from '@internals/utils'
|
|
2
|
+
import type { ast } from '@kubb/core'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Find the first 2xx response status code from an operation's responses.
|
|
6
|
+
*/
|
|
7
|
+
export function findSuccessStatusCode(responses: Array<{ statusCode: number | string }>): ast.StatusCode | undefined {
|
|
8
|
+
for (const res of responses) {
|
|
9
|
+
const code = Number(res.statusCode)
|
|
10
|
+
if (code >= 200 && code < 300) {
|
|
11
|
+
return res.statusCode as ast.StatusCode
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return undefined
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ZodParam = {
|
|
18
|
+
name: string
|
|
19
|
+
schemaName: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Render a group param value — compose individual schemas into `z.object({ ... })`,
|
|
24
|
+
* or use a schema name string directly.
|
|
25
|
+
*/
|
|
26
|
+
export function zodGroupExpr(entry: string | Array<ZodParam>): string {
|
|
27
|
+
if (typeof entry === 'string') {
|
|
28
|
+
return entry
|
|
29
|
+
}
|
|
30
|
+
const entries = entry.map((p) => `${JSON.stringify(p.name)}: ${p.schemaName}`)
|
|
31
|
+
return `z.object({ ${entries.join(', ')} })`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Build JSDoc comment lines from an OperationNode.
|
|
36
|
+
*/
|
|
37
|
+
export function getComments(node: ast.OperationNode): Array<string> {
|
|
38
|
+
return [
|
|
39
|
+
node.description && `@description ${node.description}`,
|
|
40
|
+
node.summary && `@summary ${node.summary}`,
|
|
41
|
+
node.deprecated && '@deprecated',
|
|
42
|
+
`{@link ${node.path.replaceAll('{', ':').replaceAll('}', '')}}`,
|
|
43
|
+
].filter((x): x is string => Boolean(x))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build a mapping of original param names → camelCase names.
|
|
48
|
+
* Returns `undefined` when no names actually change (no remapping needed).
|
|
49
|
+
*/
|
|
50
|
+
export function getParamsMapping(params: Array<{ name: string }>): Record<string, string> | undefined {
|
|
51
|
+
if (!params.length) {
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const mapping: Record<string, string> = {}
|
|
56
|
+
let hasDifference = false
|
|
57
|
+
|
|
58
|
+
for (const p of params) {
|
|
59
|
+
const camelName = camelCase(p.name)
|
|
60
|
+
mapping[p.name] = camelName
|
|
61
|
+
if (p.name !== camelName) {
|
|
62
|
+
hasDifference = true
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return hasDifference ? mapping : undefined
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Convert a SchemaNode type to an inline Zod expression string.
|
|
71
|
+
* Used as fallback when no named zod schema is available for a path parameter.
|
|
72
|
+
*/
|
|
73
|
+
export function zodExprFromSchemaNode(schema: ast.SchemaNode): string {
|
|
74
|
+
let expr: string
|
|
75
|
+
switch (schema.type) {
|
|
76
|
+
case 'enum': {
|
|
77
|
+
// namedEnumValues takes priority over enumValues
|
|
78
|
+
const rawValues: Array<string | number | boolean> = schema.namedEnumValues?.length
|
|
79
|
+
? schema.namedEnumValues.map((v) => v.value)
|
|
80
|
+
: (schema.enumValues ?? []).filter((v): v is string | number | boolean => v !== null)
|
|
81
|
+
|
|
82
|
+
if (rawValues.length > 0 && rawValues.every((v) => typeof v === 'string')) {
|
|
83
|
+
expr = `z.enum([${rawValues.map((v) => JSON.stringify(v)).join(', ')}])`
|
|
84
|
+
} else if (rawValues.length > 0) {
|
|
85
|
+
const literals = rawValues.map((v) => `z.literal(${JSON.stringify(v)})`)
|
|
86
|
+
expr = literals.length === 1 ? literals[0]! : `z.union([${literals.join(', ')}])`
|
|
87
|
+
} else {
|
|
88
|
+
expr = 'z.string()'
|
|
89
|
+
}
|
|
90
|
+
break
|
|
91
|
+
}
|
|
92
|
+
case 'integer':
|
|
93
|
+
expr = 'z.coerce.number()'
|
|
94
|
+
break
|
|
95
|
+
case 'number':
|
|
96
|
+
expr = 'z.number()'
|
|
97
|
+
break
|
|
98
|
+
case 'boolean':
|
|
99
|
+
expr = 'z.boolean()'
|
|
100
|
+
break
|
|
101
|
+
case 'array':
|
|
102
|
+
expr = 'z.array(z.unknown())'
|
|
103
|
+
break
|
|
104
|
+
default:
|
|
105
|
+
expr = 'z.string()'
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (schema.nullable) {
|
|
109
|
+
expr = `${expr}.nullable()`
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return expr
|
|
113
|
+
}
|