hono-takibi 0.9.60 → 0.9.61

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/cli/index.js CHANGED
@@ -5,6 +5,7 @@ import { route } from '../core/route.js';
5
5
  import { rpc } from '../core/rpc.js';
6
6
  import { schema } from '../core/schema.js';
7
7
  import { takibi } from '../core/takibi.js';
8
+ import { type } from '../core/type.js';
8
9
  // import { honoRpcWithSWR } from '../generator/swr/index.js'
9
10
  import { parseCli } from '../utils/index.js';
10
11
  /**
@@ -104,13 +105,17 @@ export async function honoTakibi() {
104
105
  : undefined;
105
106
  if (routeResult && !routeResult.ok)
106
107
  return { ok: false, error: routeResult.error };
108
+ /** type */
109
+ const typeResult = c.type ? await type(c.input, c.type.output) : undefined;
110
+ if (typeResult && !typeResult.ok)
111
+ return { ok: false, error: typeResult.error };
107
112
  /** rpc */
108
113
  const rpcResult = c.rpc
109
114
  ? await rpc(c.input, c.rpc.output, c.rpc.import, c.rpc.split ?? false)
110
115
  : undefined;
111
116
  if (rpcResult && !rpcResult.ok)
112
117
  return { ok: false, error: rpcResult.error };
113
- const results = [takibiResult?.value, rpcResult?.value].filter((v) => Boolean(v));
118
+ const results = [takibiResult?.value, typeResult?.value, rpcResult?.value].filter((v) => v !== undefined);
114
119
  return {
115
120
  ok: true,
116
121
  value: results.join('\n'),
@@ -15,6 +15,9 @@ type Config = {
15
15
  readonly split?: boolean;
16
16
  };
17
17
  };
18
+ readonly type?: {
19
+ readonly output: `${string}.ts`;
20
+ };
18
21
  readonly rpc?: {
19
22
  readonly output: string | `${string}.ts`;
20
23
  readonly import: string;
package/dist/core/rpc.js CHANGED
@@ -139,14 +139,7 @@ const createResolveParameter = (componentsParameters) => (p) => {
139
139
  const cand = name ? componentsParameters[name] : undefined;
140
140
  return isParameterObject(cand) ? cand : undefined;
141
141
  };
142
- const createToParameterLikes = (resolveParam) => (arr) => Array.isArray(arr)
143
- ? arr.reduce((acc, x) => {
144
- const r = resolveParam(x);
145
- if (r)
146
- acc.push(r);
147
- return acc;
148
- }, [])
149
- : [];
142
+ const createToParameterLikes = (resolveParam) => (arr) => Array.isArray(arr) ? arr.map((x) => resolveParam(x)).filter((param) => param !== undefined) : [];
150
143
  const isOperationLike = (v) => isRecord(v) && 'responses' in v;
151
144
  const HTTP_METHODS = [
152
145
  'get',
@@ -183,16 +176,14 @@ const pickBodySchema = (op) => {
183
176
  };
184
177
  /* ─────────────────────────────── Args builders ─────────────────────────────── */
185
178
  const createBuildParamsType = (tsTypeFromSchema) => (pathParams, queryParams) => {
186
- const parts = [];
187
- if (pathParams.length) {
188
- const inner = pathParams.map((p) => `${p.name}:${tsTypeFromSchema(p.schema)}`).join(',');
189
- parts.push(`path:{${inner}}`);
190
- }
191
- if (queryParams.length) {
192
- const inner = queryParams.map((p) => `${p.name}:${tsTypeFromSchema(p.schema)}`).join(',');
193
- parts.push(`query:{${inner}}`);
194
- }
195
- return parts.length ? `{${parts.join(',')}}` : '';
179
+ const pathPart = pathParams.length === 0
180
+ ? []
181
+ : [`path:{${pathParams.map((p) => `${p.name}:${tsTypeFromSchema(p.schema)}`).join(',')}}`];
182
+ const queryPart = queryParams.length === 0
183
+ ? []
184
+ : [`query:{${queryParams.map((p) => `${p.name}:${tsTypeFromSchema(p.schema)}`).join(',')}}`];
185
+ const parts = [...pathPart, ...queryPart];
186
+ return parts.length === 0 ? '' : `{${parts.join(',')}}`;
196
187
  };
197
188
  const buildArgSignature = (paramsType, bodyType) => paramsType && bodyType
198
189
  ? `params:${paramsType},body:${bodyType}`
@@ -203,14 +194,11 @@ const buildArgSignature = (paramsType, bodyType) => paramsType && bodyType
203
194
  : '';
204
195
  /** pass path/query as-is (keep numbers/arrays) */
205
196
  const buildClientArgs = (pathParams, queryParams, hasBody) => {
206
- const pieces = [];
207
- if (pathParams.length)
208
- pieces.push('param:params.path');
209
- if (queryParams.length)
210
- pieces.push('query:params.query');
211
- if (hasBody)
212
- pieces.push('json:body');
213
- return pieces.length ? `{${pieces.join(',')}}` : '';
197
+ const paramPiece = pathParams.length ? ['param:params.path'] : [];
198
+ const queryPiece = queryParams.length ? ['query:params.query'] : [];
199
+ const bodyPiece = hasBody ? ['json:body'] : [];
200
+ const pieces = [...paramPiece, ...queryPiece, ...bodyPiece];
201
+ return pieces.length === 0 ? '' : `{${pieces.join(',')}}`;
214
202
  };
215
203
  /* ─────────────────────────────── Single-operation generator ─────────────────────────────── */
216
204
  const generateOperationCode = (pathStr, method, item, deps) => {
@@ -246,6 +234,25 @@ const generateOperationCode = (pathStr, method, item, deps) => {
246
234
  const func = `export async function ${funcName}(${argSig}){return await ${call}}`;
247
235
  return `${docs}\n${func}`;
248
236
  };
237
+ const buildOperationCodes = (paths, deps) => Object.entries(paths)
238
+ .filter((entry) => isRecord(entry[1]))
239
+ .flatMap(([p, rawItem]) => {
240
+ const pathItem = {
241
+ parameters: rawItem.parameters,
242
+ get: isOperationLike(rawItem.get) ? rawItem.get : undefined,
243
+ put: isOperationLike(rawItem.put) ? rawItem.put : undefined,
244
+ post: isOperationLike(rawItem.post) ? rawItem.post : undefined,
245
+ delete: isOperationLike(rawItem.delete) ? rawItem.delete : undefined,
246
+ options: isOperationLike(rawItem.options) ? rawItem.options : undefined,
247
+ head: isOperationLike(rawItem.head) ? rawItem.head : undefined,
248
+ patch: isOperationLike(rawItem.patch) ? rawItem.patch : undefined,
249
+ trace: isOperationLike(rawItem.trace) ? rawItem.trace : undefined,
250
+ };
251
+ return HTTP_METHODS.map((method) => {
252
+ const code = generateOperationCode(p, method, pathItem, deps);
253
+ return code ? { funcName: methodPath(method, p), code } : null;
254
+ }).filter((item) => item !== null);
255
+ });
249
256
  /* ─────────────────────────────── Split ─────────────────────────────── */
250
257
  const resolveSplitOutDir = (output) => {
251
258
  const looksLikeFile = output.endsWith('.ts');
@@ -268,7 +275,6 @@ export async function rpc(input, output, importPath, split) {
268
275
  const client = 'client';
269
276
  const s = `import { client } from '${importPath}'`;
270
277
  const header = s.length ? `${s}\n\n` : '';
271
- const combinedOut = [];
272
278
  const pathsMaybe = openAPI.paths;
273
279
  if (!isOpenAPIPaths(pathsMaybe)) {
274
280
  return { ok: false, error: 'Invalid OpenAPI paths' };
@@ -279,60 +285,19 @@ export async function rpc(input, output, importPath, split) {
279
285
  const componentsParameters = openAPI.components?.parameters ?? {};
280
286
  const resolveParameter = createResolveParameter(componentsParameters);
281
287
  const toParameterLikes = createToParameterLikes(resolveParameter);
282
- // Split-mode: prepare an index file exports collector.
283
- const splitExports = new Set();
284
- // Iterate paths and generate per-operation code
285
- for (const p in pathsMaybe) {
286
- const rawItem = pathsMaybe[p];
287
- if (!isRecord(rawItem))
288
- continue;
289
- const pathItem = {
290
- parameters: rawItem.parameters,
291
- get: isOperationLike(rawItem.get) ? rawItem.get : undefined,
292
- put: isOperationLike(rawItem.put) ? rawItem.put : undefined,
293
- post: isOperationLike(rawItem.post) ? rawItem.post : undefined,
294
- delete: isOperationLike(rawItem.delete) ? rawItem.delete : undefined,
295
- options: isOperationLike(rawItem.options) ? rawItem.options : undefined,
296
- head: isOperationLike(rawItem.head) ? rawItem.head : undefined,
297
- patch: isOperationLike(rawItem.patch) ? rawItem.patch : undefined,
298
- trace: isOperationLike(rawItem.trace) ? rawItem.trace : undefined,
299
- };
300
- for (const method of HTTP_METHODS) {
301
- const code = generateOperationCode(p, method, pathItem, {
302
- client,
303
- tsTypeFromSchema,
304
- toParameterLikes,
305
- });
306
- if (!code)
307
- continue;
308
- if (split) {
309
- // One file per RPC function: <funcName>.ts
310
- const funcName = methodPath(method, p);
311
- const fileSrc = `${header}${code}\n`;
312
- const fmtResult = await fmt(fileSrc);
313
- if (!fmtResult.ok)
314
- return { ok: false, error: fmtResult.error };
315
- const { outDir } = resolveSplitOutDir(output);
316
- const filePath = path.join(outDir, `${funcName}.ts`);
317
- const mkdirResult = await mkdir(path.dirname(filePath));
318
- if (!mkdirResult.ok)
319
- return { ok: false, error: mkdirResult.error };
320
- const writeResult = await writeFile(filePath, fmtResult.value);
321
- if (!writeResult.ok)
322
- return { ok: false, error: writeResult.error };
323
- splitExports.add(`export * from './${funcName}'`);
324
- }
325
- else {
326
- combinedOut.push(code);
327
- }
328
- }
329
- }
288
+ const deps = {
289
+ client,
290
+ tsTypeFromSchema,
291
+ toParameterLikes,
292
+ };
293
+ const operationCodes = buildOperationCodes(pathsMaybe, deps);
330
294
  // Non-split: write single file
331
295
  if (!split) {
332
296
  const mkdirResult = await mkdir(path.dirname(output));
333
297
  if (!mkdirResult.ok)
334
298
  return { ok: false, error: mkdirResult.error };
335
- const code = `${header}${combinedOut.join('\n\n')}${combinedOut.length ? '\n' : ''}`;
299
+ const body = operationCodes.map(({ code }) => code).join('\n\n');
300
+ const code = `${header}${body}${operationCodes.length ? '\n' : ''}`;
336
301
  const fmtResult = await fmt(code);
337
302
  if (!fmtResult.ok)
338
303
  return { ok: false, error: fmtResult.error };
@@ -341,9 +306,23 @@ export async function rpc(input, output, importPath, split) {
341
306
  return { ok: false, error: writeResult.error };
342
307
  return { ok: true, value: `Generated rpc code written to ${output}` };
343
308
  }
344
- // Split: write index.ts (barrel)
309
+ // Split: write each file + index.ts (barrel)
345
310
  const { outDir, indexPath } = resolveSplitOutDir(output);
346
- const index = `${Array.from(splitExports).sort().join('\n')}\n`;
311
+ for (const { funcName, code } of operationCodes) {
312
+ const fileSrc = `${header}${code}\n`;
313
+ const fmtResult = await fmt(fileSrc);
314
+ if (!fmtResult.ok)
315
+ return { ok: false, error: fmtResult.error };
316
+ const filePath = path.join(outDir, `${funcName}.ts`);
317
+ const mkdirResult = await mkdir(path.dirname(filePath));
318
+ if (!mkdirResult.ok)
319
+ return { ok: false, error: mkdirResult.error };
320
+ const writeResult = await writeFile(filePath, fmtResult.value);
321
+ if (!writeResult.ok)
322
+ return { ok: false, error: writeResult.error };
323
+ }
324
+ const exportLines = Array.from(new Set(operationCodes.map(({ funcName }) => `export * from './${funcName}'`))).sort();
325
+ const index = `${exportLines.join('\n')}\n`;
347
326
  const fmtResult = await fmt(index);
348
327
  if (!fmtResult.ok)
349
328
  return { ok: false, error: fmtResult.error };
@@ -353,5 +332,8 @@ export async function rpc(input, output, importPath, split) {
353
332
  const writeResult = await writeFile(indexPath, fmtResult.value);
354
333
  if (!writeResult.ok)
355
334
  return { ok: false, error: writeResult.error };
356
- return { ok: true, value: `Generated rpc code written to ${outDir}/*.ts (index.ts included)` };
335
+ return {
336
+ ok: true,
337
+ value: `Generated rpc code written to ${outDir}/*.ts (index.ts included)`,
338
+ };
357
339
  }
@@ -30,9 +30,9 @@
30
30
  * Y --> Z["writeResult2 = writeFile(target, appResult.value)"]
31
31
  * Z --> ZA{"writeResult2.ok ?"}
32
32
  * ZA -->|No| ZB["return { ok:false, error: writeResult2.error }"]
33
- * ZA -->|Yes| ZC["handlerResult = zodOpenapiHonoHandler(openAPI, output, test)"]
34
- * ZC --> ZD{"handlerResult.ok ?"}
35
- * ZD -->|No| ZE["return { ok:false, error: handlerResult.error }"]
33
+ * ZA -->|Yes| ZC["zodOpenAPIHonoHandlerResult = zodOpenAPIHonoHandler(openAPI, output, test)"]
34
+ * ZC --> ZD{"zodOpenAPIHonoHandlerResult.ok ?"}
35
+ * ZD -->|No| ZE["return { ok:false, error: zodOpenAPIHonoHandlerResult.error }"]
36
36
  * ZD -->|Yes| ZF["return { ok:true, value: 'Generated code and template files written' }"]
37
37
  * ```
38
38
  *
@@ -37,9 +37,9 @@ import { isHttpMethod, methodPath } from '../utils/index.js';
37
37
  * Y --> Z["writeResult2 = writeFile(target, appResult.value)"]
38
38
  * Z --> ZA{"writeResult2.ok ?"}
39
39
  * ZA -->|No| ZB["return { ok:false, error: writeResult2.error }"]
40
- * ZA -->|Yes| ZC["handlerResult = zodOpenapiHonoHandler(openAPI, output, test)"]
41
- * ZC --> ZD{"handlerResult.ok ?"}
42
- * ZD -->|No| ZE["return { ok:false, error: handlerResult.error }"]
40
+ * ZA -->|Yes| ZC["zodOpenAPIHonoHandlerResult = zodOpenAPIHonoHandler(openAPI, output, test)"]
41
+ * ZC --> ZD{"zodOpenAPIHonoHandlerResult.ok ?"}
42
+ * ZD -->|No| ZE["return { ok:false, error: zodOpenAPIHonoHandlerResult.error }"]
43
43
  * ZD -->|Yes| ZF["return { ok:true, value: 'Generated code and template files written' }"]
44
44
  * ```
45
45
  *
@@ -80,9 +80,9 @@ export async function takibi(input, output, exportSchema, exportType, template,
80
80
  const writeResult = await writeFile(target, appResult.value);
81
81
  if (!writeResult.ok)
82
82
  return { ok: false, error: writeResult.error };
83
- const zodOpenapiHonoHandlerResult = await zodOpenapiHonoHandler(openAPI, output, test);
84
- if (!zodOpenapiHonoHandlerResult.ok)
85
- return { ok: false, error: zodOpenapiHonoHandlerResult.error };
83
+ const zodOpenAPIHonoHandlerResult = await zodOpenAPIHonoHandler(openAPI, output, test);
84
+ if (!zodOpenAPIHonoHandlerResult.ok)
85
+ return { ok: false, error: zodOpenAPIHonoHandlerResult.error };
86
86
  return { ok: true, value: 'Generated code and template files written' };
87
87
  }
88
88
  return {
@@ -102,7 +102,7 @@ export async function takibi(input, output, exportSchema, exportType, template,
102
102
  * @param test - Whether to generate corresponding empty test files.
103
103
  * @returns A `Result` indicating success or error with message.
104
104
  */
105
- async function zodOpenapiHonoHandler(openapi, output, test) {
105
+ async function zodOpenAPIHonoHandler(openapi, output, test) {
106
106
  const paths = openapi.paths;
107
107
  const handlers = Object.entries(paths).flatMap(([p, pathItem]) => Object.entries(pathItem)
108
108
  .filter(([m]) => isHttpMethod(m))
@@ -0,0 +1,7 @@
1
+ export declare function type(input: `${string}.yaml` | `${string}.json` | `${string}.tsp`, output: `${string}.ts`): Promise<{
2
+ ok: true;
3
+ value: string;
4
+ } | {
5
+ ok: false;
6
+ error: string;
7
+ }>;
@@ -0,0 +1,96 @@
1
+ import path from 'node:path';
2
+ import ts from 'typescript';
3
+ import { fmt } from '../format/index.js';
4
+ import { mkdir, writeFile } from '../fsp/index.js';
5
+ import { zodOpenAPIHono } from '../generator/zod-openapi-hono/openapi/index.js';
6
+ import { parseOpenAPI } from '../openapi/index.js';
7
+ import { isHttpMethod, methodPath } from '../utils/index.js';
8
+ export async function type(input, output) {
9
+ try {
10
+ const openAPIResult = await parseOpenAPI(input);
11
+ if (!openAPIResult.ok)
12
+ return { ok: false, error: openAPIResult.error };
13
+ const openAPI = openAPIResult.value;
14
+ const hono = zodOpenAPIHono(openAPI, false, false);
15
+ const paths = openAPI.paths;
16
+ const routes = Object.entries(paths).flatMap(([p, pathItem]) => Object.entries(pathItem)
17
+ .filter(([m]) => isHttpMethod(m))
18
+ .map(([method]) => {
19
+ const routeId = methodPath(method, p);
20
+ return `export const ${routeId}RouteHandler:RouteHandler<typeof ${routeId}Route>=async(c)=>{}`;
21
+ }));
22
+ const getRouteMaps = (openapi) => {
23
+ const openapiPaths = openapi.paths;
24
+ const routeMappings = Object.entries(openapiPaths).flatMap(([path, pathItem]) => {
25
+ return Object.entries(pathItem).flatMap(([method]) => {
26
+ if (!isHttpMethod(method)) {
27
+ return [];
28
+ }
29
+ const base = methodPath(method, path);
30
+ return {
31
+ routeName: `${base}Route`,
32
+ handlerName: `${base}RouteHandler`,
33
+ path,
34
+ };
35
+ });
36
+ });
37
+ return routeMappings;
38
+ };
39
+ const routeMappings = getRouteMaps(openAPI);
40
+ const appInit = 'export const api = app' +
41
+ routeMappings
42
+ .sort((a, b) => (a.routeName < b.routeName ? -1 : a.routeName > b.routeName ? 1 : 0))
43
+ .map(({ routeName, handlerName }) => `.openapi(${routeName},${handlerName})`)
44
+ .join('');
45
+ const code = `import { OpenAPIHono, type RouteHandler } from '@hono/zod-openapi'\n${hono}\nconst app = new OpenAPIHono()\n${routes.join('\n')}\n${appInit}\nexport type AddType = typeof api`;
46
+ const honoType = apiType(code);
47
+ if (honoType === undefined)
48
+ return { ok: false, error: 'not generated type' };
49
+ const mkdirResult = await mkdir(path.dirname(output));
50
+ if (!mkdirResult.ok)
51
+ return { ok: false, error: mkdirResult.error };
52
+ const type = `declare const routes:\n${honoType}\nexport default routes`;
53
+ const fmtResult = await fmt(type);
54
+ if (!fmtResult.ok)
55
+ return { ok: false, error: fmtResult.error };
56
+ const writeResult = await writeFile(output, fmtResult.value);
57
+ if (!writeResult.ok)
58
+ return { ok: false, error: writeResult.error };
59
+ return {
60
+ ok: true,
61
+ value: `Generated type code written to ${output}`,
62
+ };
63
+ }
64
+ catch (e) {
65
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
66
+ }
67
+ }
68
+ function apiType(code) {
69
+ const VIRTUAL_FILE_NAME = 'virtual.ts';
70
+ const compilerOptions = {
71
+ target: ts.ScriptTarget.ESNext,
72
+ module: ts.ModuleKind.NodeNext,
73
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
74
+ strict: true,
75
+ };
76
+ const sourceFile = ts.createSourceFile(VIRTUAL_FILE_NAME, code, compilerOptions.target ?? ts.ScriptTarget.ESNext);
77
+ const host = ts.createCompilerHost(compilerOptions);
78
+ const originalGetSourceFile = host.getSourceFile.bind(host);
79
+ host.getSourceFile = (fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile) => {
80
+ if (fileName === VIRTUAL_FILE_NAME)
81
+ return sourceFile;
82
+ return originalGetSourceFile(fileName, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
83
+ };
84
+ const program = ts.createProgram([VIRTUAL_FILE_NAME], compilerOptions, host);
85
+ const checker = program.getTypeChecker();
86
+ const moduleSymbol = checker.getSymbolAtLocation(sourceFile);
87
+ if (!moduleSymbol)
88
+ return undefined;
89
+ const apiSymbol = checker.getExportsOfModule(moduleSymbol).find((s) => s.getName() === 'api');
90
+ if (!apiSymbol)
91
+ return undefined;
92
+ const type = checker.getTypeOfSymbolAtLocation(apiSymbol, sourceFile);
93
+ return checker.typeToString(type, undefined, ts.TypeFormatFlags.NoTruncation |
94
+ ts.TypeFormatFlags.UseFullyQualifiedType |
95
+ ts.TypeFormatFlags.WriteTypeArgumentsOfSignature);
96
+ }
@@ -16,18 +16,16 @@ import { paramsObject } from './index.js';
16
16
  */
17
17
  export function requestParameter(parameters, body) {
18
18
  // Early return if no parameters or content
19
- if (!(parameters || body?.content)) {
19
+ if (!(parameters || body?.content))
20
20
  return '';
21
- }
22
21
  const requestBodyContentTypes = Object.keys(body?.content || {});
23
22
  const params = parameters
24
23
  ? (() => {
25
24
  const paramsObj = paramsObject(parameters);
26
25
  const requestParamsArr = requestParamsArray(paramsObj);
27
- if (requestParamsArr.length) {
28
- // params
26
+ // params
27
+ if (requestParamsArr.length)
29
28
  return `request:{${requestParamsArr.join(',')}},`;
30
- }
31
29
  return '';
32
30
  })()
33
31
  : '';
@@ -42,9 +40,8 @@ export function requestParameter(parameters, body) {
42
40
  const required = body.required ?? false;
43
41
  const [firstSchema] = uniqueSchemas.values();
44
42
  const requestBodyCode = requestBody(required, body.content, firstSchema);
45
- if (params) {
43
+ if (params)
46
44
  return params.replace('request:{', `request:{${requestBodyCode}`);
47
- }
48
45
  return `request:{${requestBodyCode}},`;
49
46
  }
50
47
  return params;
@@ -11,21 +11,12 @@ export function requestBody(required, content, schema) {
11
11
  const contentTypes = Object.keys(content);
12
12
  if (contentTypes.length === 0)
13
13
  return '';
14
- // check duplication
15
14
  const isUniqueSchema = isUniqueContentSchema(contentTypes, content);
16
- // all duplication same schema
17
- if (isUniqueSchema) {
18
- const contentParts = [];
19
- for (const contentType of contentTypes) {
20
- // z.date() z.coerce.date()
21
- if (schema.includes('z.date()')) {
22
- contentParts.push(`'${contentType}':{schema:z.coerce.${schema.replace('z.', '')}}`);
23
- }
24
- else {
25
- contentParts.push(`'${contentType}':{schema:${schema}}`);
26
- }
27
- }
28
- return `body:{required:${required},content:{${contentParts.join(',')}}},`;
29
- }
30
- return '';
15
+ if (!isUniqueSchema)
16
+ return '';
17
+ // if z.date() → z.coerce.date()
18
+ const contentParts = contentTypes
19
+ .map((contentType) => `'${contentType}':{schema:${schema.includes('z.date()') ? `z.coerce.${schema.replace('z.', '')}` : schema}}`)
20
+ .join(',');
21
+ return `body:{required:${required},content:{${contentParts}}},`;
31
22
  }
@@ -12,53 +12,14 @@ import { zodToOpenAPI } from '../../../../zod-to-openapi/index.js';
12
12
  * - Deduplicates content types if all share the same schema.
13
13
  * - Escapes all descriptions safely for inline code.
14
14
  */
15
- // export function response(responses: Responses): string {
16
- // // 1. get response codes (200, 404, etc.)
17
- // const responseCodes = Object.keys(responses)
18
- // // 2. processing for each response code
19
- // const responseEntries = responseCodes.map((code) => {
20
- // const response = responses[code]
21
- // // 2.1 no content (description only response)
22
- // if (!response.content)
23
- // return `${code}:{description:'${escapeStringLiteral(response.description ?? '')}',},`
24
- // // check duplication
25
- // const contentTypes = Object.keys(response.content)
26
- // const isUniqueSchema = isUniqueContentSchema(contentTypes, response.content)
27
- // // all duplication same schema
28
- // if (isUniqueSchema) {
29
- // const contentParts: string[] = []
30
- // for (const contentType of contentTypes) {
31
- // const content = response.content[contentType]
32
- // const z = zodToOpenAPI(content.schema)
33
- // const examples = content.examples
34
- // const exampleString =
35
- // examples && Object.keys(examples).length > 0
36
- // ? `,examples:{${Object.entries(examples)
37
- // .map(([key, example]) => {
38
- // const parts = []
39
- // if (example.summary) parts.push(`summary:${JSON.stringify(example.summary)}`)
40
- // if (example.value !== undefined)
41
- // parts.push(`value:${JSON.stringify(example.value)}`)
42
- // return `${JSON.stringify(key)}:{${parts.join(',')}}`
43
- // })
44
- // .join(',')}}`
45
- // : ''
46
- // contentParts.push(`'${contentType}':{schema:${z}${exampleString}}`)
47
- // }
48
- // return `${code}:{description:'${escapeStringLiteral(response.description ?? '')}',content:{${contentParts.join(',')}}},`
49
- // }
50
- // })
51
- // // 3.combine all response definitions
52
- // return responseEntries.join('')
53
- // }
54
15
  export function response(responses) {
55
16
  // 1. get response codes (200, 404, etc.)
56
- const responseEntries = Object.keys(responses).map((code) => {
17
+ return Object.keys(responses)
18
+ .map((code) => {
57
19
  const res = responses[code];
58
20
  const content = res.content;
59
- if (!content) {
21
+ if (!content)
60
22
  return `${code}:{description:'${escapeStringLiteral(res.description ?? '')}',},`;
61
- }
62
23
  const contentTypes = Object.keys(content);
63
24
  const isUnique = isUniqueContentSchema(contentTypes, content);
64
25
  const sharedZ = isUnique ? zodToOpenAPI(content[contentTypes[0]].schema) : undefined;
@@ -68,19 +29,22 @@ export function response(responses) {
68
29
  const examples = media.examples;
69
30
  const exampleString = examples && Object.keys(examples).length > 0
70
31
  ? `,examples:{${Object.entries(examples)
71
- .map(([k, v]) => {
72
- const parts = [];
73
- if (v.summary)
74
- parts.push(`summary:${JSON.stringify(v.summary)}`);
75
- if (v.value !== undefined)
76
- parts.push(`value:${JSON.stringify(v.value)}`);
77
- return `${JSON.stringify(k)}:{${parts.join(',')}}`;
32
+ .map(([exampleKey, example]) => {
33
+ const fields = [
34
+ example.summary !== undefined
35
+ ? `summary:${JSON.stringify(example.summary)}`
36
+ : undefined,
37
+ example.value !== undefined
38
+ ? `value:${JSON.stringify(example.value)}`
39
+ : undefined,
40
+ ].filter((field) => field !== undefined);
41
+ return `${JSON.stringify(exampleKey)}:{${fields.join(',')}}`;
78
42
  })
79
43
  .join(',')}}`
80
44
  : '';
81
45
  return `'${ct}':{schema:${z}${exampleString}}`;
82
46
  });
83
47
  return `${code}:{description:'${escapeStringLiteral(res.description ?? '')}',content:{${contentParts.join(',')}}},`;
84
- });
85
- return responseEntries.join('');
48
+ })
49
+ .join('');
86
50
  }
@@ -1,27 +1,27 @@
1
1
  export function _enum(schema) {
2
- /* hasType */
3
- const hasType = (t) => schema.type === t || (Array.isArray(schema.type) && schema.type.some((x) => x === t));
2
+ /* ht */
3
+ const ht = (t) => schema.type === t || (Array.isArray(schema.type) && schema.type.some((v) => v === t));
4
4
  /* lit */
5
5
  const lit = (v) => v === null ? 'null' : typeof v === 'string' ? `'${v}'` : String(v);
6
6
  /* tuple */
7
- const tuple = (arr) => `z.tuple([${arr.map((i) => `z.literal(${lit(i)})`).join(',')}])`;
7
+ const tuple = (arr) => `z.tuple([${arr.map((v) => `z.literal(${lit(v)})`).join(',')}])`;
8
8
  /* guard */
9
9
  if (!schema.enum || schema.enum.length === 0)
10
10
  return 'z.any()';
11
11
  /* number / integer enum */
12
- if (hasType('number') || hasType('integer')) {
12
+ if (ht('number') || ht('integer')) {
13
13
  return schema.enum.length > 1
14
14
  ? `z.union([${schema.enum.map((v) => `z.literal(${v})`).join(',')}])`
15
15
  : `z.literal(${schema.enum[0]})`;
16
16
  }
17
17
  /* boolean enum */
18
- if (hasType('boolean')) {
18
+ if (ht('boolean')) {
19
19
  return schema.enum.length > 1
20
20
  ? `z.union([${schema.enum.map((v) => `z.literal(${v})`).join(',')}])`
21
21
  : `z.literal(${schema.enum[0]})`;
22
22
  }
23
23
  /* array enum */
24
- if (hasType('array')) {
24
+ if (ht('array')) {
25
25
  if (schema.enum.length === 1 && Array.isArray(schema.enum[0])) {
26
26
  return tuple(schema.enum[0]);
27
27
  }
@@ -6,86 +6,64 @@
6
6
  * @returns The Zod schema string
7
7
  */
8
8
  export function integer(schema) {
9
- const isInt32 = schema.format === 'int32';
10
- const isInt64 = schema.format === 'int64';
11
- const isBigInt = schema.format === 'bigint';
12
- const o = [
13
- isInt32 ? 'z.int32()' : isInt64 ? 'z.int64()' : isBigInt ? 'z.bigint()' : 'z.int()',
14
- ];
9
+ const base = schema.format === 'int32'
10
+ ? 'z.int32()'
11
+ : schema.format === 'int64'
12
+ ? 'z.int64()'
13
+ : schema.format === 'bigint'
14
+ ? 'z.bigint()'
15
+ : 'z.int()';
15
16
  const lit = (n) => {
16
- if (isBigInt)
17
+ if (schema.format === 'bigint')
17
18
  return `BigInt(${n})`;
18
- if (isInt64)
19
+ if (schema.format === 'int64')
19
20
  return `${n}n`;
20
21
  return `${n}`;
21
22
  };
22
- // minimum
23
- if (schema.minimum !== undefined || schema.exclusiveMinimum !== undefined) {
24
- // > 0
25
- // z.int().positive().safeParse(1) // { success: true }
26
- // z.int().positive().safeParse(0) // { success: false }
27
- if ((schema.minimum ?? schema.exclusiveMinimum) === 0 && schema.exclusiveMinimum === true) {
28
- o.push('.positive()');
23
+ const minimum = (() => {
24
+ if (schema.minimum === undefined && schema.exclusiveMinimum === undefined) {
25
+ return undefined;
29
26
  }
30
- // >= 0
31
- // z.int().nonnegative().safeParse(0) // { success: true }
32
- // z.int().nonnegative().safeParse(-1) // { success: false }
33
- else if ((schema.minimum ?? schema.exclusiveMinimum) === 0 &&
34
- schema.exclusiveMinimum === false) {
35
- o.push('.nonnegative()');
27
+ const value = schema.minimum ?? schema.exclusiveMinimum;
28
+ if (value === 0 && schema.exclusiveMinimum === true) {
29
+ return '.positive()';
36
30
  }
37
- // > value
38
- // z.int().gt(100) // value > 100
39
- // z.int().gt(100).safeParse(101) // { success: true }
40
- // z.int().gt(100).safeParse(100) // { success: false }
41
- else if ((schema.exclusiveMinimum === true || schema.minimum === undefined) &&
42
- typeof (schema.minimum ?? schema.exclusiveMinimum) === 'number') {
43
- o.push(`.gt(${lit((schema.minimum ?? schema.exclusiveMinimum))})`);
31
+ if (value === 0 && schema.exclusiveMinimum === false) {
32
+ return '.nonnegative()';
44
33
  }
45
- // >= value
46
- // z.int().min(100) // value >= 100
47
- // z.int().min(100).safeParse(100) // { success: true }
48
- // z.int().min(100).safeParse(99) // { success: false }
49
- else if (typeof schema.minimum === 'number') {
50
- o.push(`.min(${lit(schema.minimum)})`);
34
+ // > value → .gt(...)
35
+ if ((schema.exclusiveMinimum === true || schema.minimum === undefined) &&
36
+ typeof value === 'number') {
37
+ return `.gt(${lit(value)})`;
51
38
  }
52
- }
53
- // maximum
54
- if (schema.maximum !== undefined || schema.exclusiveMaximum !== undefined) {
55
- // < 0
56
- // z.int().negative().safeParse(-1) // { success: true }
57
- // z.int().negative().safeParse(0) // { success: false }
58
- if ((schema.maximum ?? schema.exclusiveMaximum) === 0 && schema.exclusiveMaximum === true) {
59
- o.push('.negative()');
39
+ // >= value → .min(...)
40
+ if (typeof schema.minimum === 'number') {
41
+ return `.min(${lit(schema.minimum)})`;
60
42
  }
61
- // <= 0
62
- // z.int().nonpositive().safeParse(0) // { success: true }
63
- // z.int().nonpositive().safeParse(1) // { success: false }
64
- else if ((schema.maximum ?? schema.exclusiveMaximum) === 0 &&
65
- schema.exclusiveMaximum === false) {
66
- o.push('.nonpositive()');
43
+ return undefined;
44
+ })();
45
+ const maximum = (() => {
46
+ if (schema.maximum === undefined && schema.exclusiveMaximum === undefined) {
47
+ return undefined;
67
48
  }
68
- // < value
69
- // z.int().lt(100) // value < 100
70
- // z.int().lt(100).safeParse(99) -> { success: true }
71
- // z.int().lt(100).safeParse(100) -> { success: false }
72
- else if ((schema.exclusiveMaximum === true || schema.maximum === undefined) &&
73
- typeof (schema.maximum ?? schema.exclusiveMaximum) === 'number') {
74
- o.push(`.lt(${lit((schema.maximum ?? schema.exclusiveMaximum))})`);
49
+ const value = schema.maximum ?? schema.exclusiveMaximum;
50
+ if (value === 0 && schema.exclusiveMaximum === true) {
51
+ return '.negative()';
75
52
  }
76
- // <= value
77
- // z.int().max(100) // value <= 100
78
- // z.int().max(100).safeParse(100) -> { success: true }
79
- // z.int().max(100).safeParse(101) -> { success: false }
80
- else if (typeof schema.maximum === 'number') {
81
- o.push(`.max(${lit(schema.maximum)})`);
53
+ if (value === 0 && schema.exclusiveMaximum === false) {
54
+ return '.nonpositive()';
82
55
  }
83
- }
84
- // multipleOf
85
- // z.int().multipleOf(2).safeParse(2) // { success: true }
86
- // z.int().multipleOf(2).safeParse(1) // { success: false }
87
- if (schema.multipleOf !== undefined && typeof schema.multipleOf === 'number') {
88
- o.push(`.multipleOf(${lit(schema.multipleOf)})`);
89
- }
90
- return o.join('');
56
+ if ((schema.exclusiveMaximum === true || schema.maximum === undefined) &&
57
+ typeof value === 'number') {
58
+ return `.lt(${lit(value)})`;
59
+ }
60
+ if (typeof schema.maximum === 'number') {
61
+ return `.max(${lit(schema.maximum)})`;
62
+ }
63
+ return undefined;
64
+ })();
65
+ const multipleOf = schema.multipleOf !== undefined && typeof schema.multipleOf === 'number'
66
+ ? `.multipleOf(${lit(schema.multipleOf)})`
67
+ : undefined;
68
+ return [base, minimum, maximum, multipleOf].filter((v) => v !== undefined).join('');
91
69
  }
@@ -6,76 +6,47 @@
6
6
  * @returns The Zod schema string
7
7
  */
8
8
  export function number(schema) {
9
- const o = [
10
- schema.format === 'float' || schema.format === 'float32'
11
- ? 'z.float32()'
12
- : schema.format === 'float64'
13
- ? 'z.float64()'
14
- : 'z.number()',
15
- ];
16
- // minimum
17
- if (schema.minimum !== undefined) {
18
- // > 0
19
- // z.number().positive().safeParse(1) // { success: true }
20
- // z.number().positive().safeParse(0) // { success: false }
21
- if (schema.minimum === 0 && schema.exclusiveMinimum === true) {
22
- o.push('.positive()');
23
- }
24
- // >= 0
25
- // z.number().nonnegative().safeParse(0) // { success: true }
26
- // z.number().nonnegative().safeParse(-1) // { success: false }
27
- else if (schema.minimum === 0 && schema.exclusiveMinimum === false) {
28
- o.push('.nonnegative()');
29
- }
30
- // > value
31
- // z.number().gt(100) // value > 100
32
- else if (schema.exclusiveMinimum === true) {
33
- o.push(`.gt(${schema.minimum})`);
34
- }
35
- // >= value
36
- // z.number().min(100) // value >= 100
37
- else {
38
- o.push(`.min(${schema.minimum})`);
39
- }
40
- }
41
- else if (typeof schema.exclusiveMinimum === 'number') {
42
- // > value (no minimum)
43
- o.push(`.gt(${schema.exclusiveMinimum})`);
44
- }
45
- // maximum
46
- if (schema.maximum !== undefined) {
47
- // < 0
48
- // z.number().negative().safeParse(-1) // { success: true }
49
- // z.number().negative().safeParse(0) // { success: false }
50
- if (schema.maximum === 0 && schema.exclusiveMaximum === true) {
51
- o.push('.negative()');
52
- }
53
- // <= 0
54
- // z.number().nonpositive().safeParse(0) // { success: true }
55
- // z.number().nonpositive().safeParse(1) // { success: false }
56
- else if (schema.maximum === 0 && schema.exclusiveMaximum === false) {
57
- o.push('.nonpositive()');
58
- }
59
- // < value
60
- // z.number().lt(100) // value < 100
61
- else if (schema.exclusiveMaximum === true) {
62
- o.push(`.lt(${schema.maximum})`);
63
- }
64
- // <= value
65
- // z.number().max(100) // value <= 100
66
- else {
67
- o.push(`.max(${schema.maximum})`);
68
- }
69
- }
70
- else if (typeof schema.exclusiveMaximum === 'number') {
71
- // < value (no maximum)
72
- o.push(`.lt(${schema.exclusiveMaximum})`);
73
- }
74
- // multipleOf
75
- // z.number().multipleOf(2).safeParse(2) // { success: true }
76
- // z.number().multipleOf(2).safeParse(1) // { success: false }
77
- if (schema.multipleOf !== undefined) {
78
- o.push(`.multipleOf(${schema.multipleOf})`);
79
- }
80
- return o.join('');
9
+ const base = schema.format === 'float' || schema.format === 'float32'
10
+ ? 'z.float32()'
11
+ : schema.format === 'float64'
12
+ ? 'z.float64()'
13
+ : 'z.number()';
14
+ const minimum = (() => {
15
+ if (schema.minimum !== undefined) {
16
+ if (schema.minimum === 0 && schema.exclusiveMinimum === true) {
17
+ return '.positive()';
18
+ }
19
+ if (schema.minimum === 0 && schema.exclusiveMinimum === false) {
20
+ return '.nonnegative()';
21
+ }
22
+ if (schema.exclusiveMinimum === true) {
23
+ return `.gt(${schema.minimum})`;
24
+ }
25
+ return `.min(${schema.minimum})`;
26
+ }
27
+ if (typeof schema.exclusiveMinimum === 'number') {
28
+ return `.gt(${schema.exclusiveMinimum})`;
29
+ }
30
+ return undefined;
31
+ })();
32
+ const maximum = (() => {
33
+ if (schema.maximum !== undefined) {
34
+ if (schema.maximum === 0 && schema.exclusiveMaximum === true) {
35
+ return '.negative()';
36
+ }
37
+ if (schema.maximum === 0 && schema.exclusiveMaximum === false) {
38
+ return '.nonpositive()';
39
+ }
40
+ if (schema.exclusiveMaximum === true) {
41
+ return `.lt(${schema.maximum})`;
42
+ }
43
+ return `.max(${schema.maximum})`;
44
+ }
45
+ if (typeof schema.exclusiveMaximum === 'number') {
46
+ return `.lt(${schema.exclusiveMaximum})`;
47
+ }
48
+ return undefined;
49
+ })();
50
+ const multipleOf = schema.multipleOf !== undefined ? `.multipleOf(${schema.multipleOf})` : undefined;
51
+ return [base, minimum, maximum, multipleOf].filter((v) => v !== undefined).join('');
81
52
  }
@@ -7,7 +7,7 @@ import { zodToOpenAPI } from '../index.js';
7
7
  * @returns The Zod object schema string.
8
8
  */
9
9
  export function object(schema) {
10
- // // allOf, oneOf, anyOf, not
10
+ // allOf, oneOf, anyOf, not
11
11
  if (schema.oneOf)
12
12
  return zodToOpenAPI(schema);
13
13
  if (schema.anyOf)
@@ -8,26 +8,6 @@ import type { Schema } from '../../../openapi/index.js';
8
8
  * - Otherwise, appends `.min(n)` and/or `.max(n)` individually
9
9
  * - Returns the concatenated Zod schema string
10
10
  *
11
- * ```mermaid
12
- * flowchart TD
13
- * A["string(schema)"] --> B["format = schema.format && FORMAT_STRING[format]"]
14
- * B --> C{"Format exists?"}
15
- * C -- "Yes" --> D["o.push(z.${format})"]
16
- * C -- "No" --> E["o.push(z.string())"]
17
- * D --> F{"Pattern exists?"}
18
- * E --> F
19
- * F -- "Yes" --> G["o.push(regex(schema.pattern))"]
20
- * F -- "No" --> I
21
- * G --> I{"minLength and maxLength are equal?"}
22
- * I -- "Yes" --> J["o.push(length(minLength))"]
23
- * I -- "No" --> K{"minLength or maxLength?"}
24
- * K -- "min" --> L["o.push(min(minLength))"]
25
- * K -- "max" --> M["o.push(max(maxLength))"]
26
- * L --> N["return o.join('')"]
27
- * M --> N
28
- * J --> N
29
- * ```
30
- *
31
11
  * @example
32
12
  * ```ts
33
13
  * // With format, pattern, and minLength
@@ -36,26 +36,6 @@ const FORMAT_STRING = {
36
36
  * - Otherwise, appends `.min(n)` and/or `.max(n)` individually
37
37
  * - Returns the concatenated Zod schema string
38
38
  *
39
- * ```mermaid
40
- * flowchart TD
41
- * A["string(schema)"] --> B["format = schema.format && FORMAT_STRING[format]"]
42
- * B --> C{"Format exists?"}
43
- * C -- "Yes" --> D["o.push(z.${format})"]
44
- * C -- "No" --> E["o.push(z.string())"]
45
- * D --> F{"Pattern exists?"}
46
- * E --> F
47
- * F -- "Yes" --> G["o.push(regex(schema.pattern))"]
48
- * F -- "No" --> I
49
- * G --> I{"minLength and maxLength are equal?"}
50
- * I -- "Yes" --> J["o.push(length(minLength))"]
51
- * I -- "No" --> K{"minLength or maxLength?"}
52
- * K -- "min" --> L["o.push(min(minLength))"]
53
- * K -- "max" --> M["o.push(max(maxLength))"]
54
- * L --> N["return o.join('')"]
55
- * M --> N
56
- * J --> N
57
- * ```
58
- *
59
39
  * @example
60
40
  * ```ts
61
41
  * // With format, pattern, and minLength
@@ -67,26 +47,20 @@ const FORMAT_STRING = {
67
47
  * @returns Concatenated Zod string schema
68
48
  */
69
49
  export function string(schema) {
70
- const o = [];
71
50
  const format = schema.format && FORMAT_STRING[schema.format];
72
- o.push(format ? `z.${format}` : 'z.string()');
73
- // pattern
74
- if (schema.pattern) {
75
- o.push(regex(schema.pattern));
76
- }
77
- // length
78
- if (schema.minLength !== undefined &&
51
+ const base = format ? `z.${format}` : 'z.string()';
52
+ const pattern = schema.pattern ? regex(schema.pattern) : undefined;
53
+ const isFixedLength = schema.minLength !== undefined &&
79
54
  schema.maxLength !== undefined &&
80
- schema.minLength === schema.maxLength) {
81
- o.push(`.length(${schema.minLength})`);
82
- }
83
- else {
84
- if (schema.minLength !== undefined) {
85
- o.push(`.min(${schema.minLength})`);
86
- }
87
- if (schema.maxLength !== undefined) {
88
- o.push(`.max(${schema.maxLength})`);
89
- }
90
- }
91
- return o.join('');
55
+ schema.minLength === schema.maxLength;
56
+ return [
57
+ base,
58
+ pattern,
59
+ // minLength === maxLength → .length(n)
60
+ isFixedLength ? `.length(${schema.minLength})` : undefined,
61
+ !isFixedLength && schema.minLength !== undefined ? `.min(${schema.minLength})` : undefined,
62
+ !isFixedLength && schema.maxLength !== undefined ? `.max(${schema.maxLength})` : undefined,
63
+ ]
64
+ .filter((v) => v !== undefined)
65
+ .join('');
92
66
  }
@@ -11,5 +11,5 @@ export function docs(openapi) {
11
11
  servers: openapi.servers,
12
12
  externalDocs: openapi.externalDocs,
13
13
  tags: openapi.tags,
14
- }).filter(([, value]) => value !== undefined));
14
+ }).filter(([, v]) => v !== undefined));
15
15
  }
@@ -51,10 +51,10 @@ export function propertiesSchema(properties, required) {
51
51
  return `${safeKey}:${zodToOpenAPI(schema)}${isRequired ? '' : '.optional()'}`;
52
52
  });
53
53
  // Check if all properties are optional
54
- const allOptional = objectProperties.every((prop) => prop.includes('.optional()'));
54
+ const allOptional = objectProperties.every((v) => v.includes('.optional()'));
55
55
  // If all properties are optional and no required properties, return partial schema
56
56
  if (required.length === 0 && allOptional) {
57
- const cleanProperties = objectProperties.map((prop) => prop.replace('.optional()', ''));
57
+ const cleanProperties = objectProperties.map((v) => v.replace('.optional()', ''));
58
58
  return `z.object({${cleanProperties}}).partial()`;
59
59
  }
60
60
  return `z.object({${objectProperties}})`;
@@ -30,22 +30,26 @@ export function wrap(zod, schema, paramName, paramIn) {
30
30
  const isNullable = schema.nullable === true ||
31
31
  (Array.isArray(schema.type) ? schema.type.includes('null') : schema.type === 'null');
32
32
  const z = isNullable ? `${s}.nullable()` : s;
33
- const openapiProps = [];
34
- if (paramIn && paramName) {
35
- const required = paramIn === 'path' ? true : !!schema.required;
36
- openapiProps.push(`param:{in:"${paramIn}",name:${JSON.stringify(paramName)},required:${required}}`);
37
- }
38
- /* 'example' if defined */
39
- if ('example' in schema && schema.example !== undefined) {
40
- openapiProps.push(`example:${JSON.stringify(schema.example)}`);
41
- }
42
- /* 'examples' if defined */
43
- if ('examples' in schema && Array.isArray(schema.examples) && schema.examples.length > 0) {
44
- openapiProps.push(`examples:${JSON.stringify(schema.examples)}`);
45
- }
46
- /* 'description' if defined */
47
- if ('description' in schema && schema.description !== undefined) {
48
- openapiProps.push(`description:${JSON.stringify(schema.description)}`);
49
- }
33
+ const openapiProps = [
34
+ // param
35
+ paramIn && paramName
36
+ ? (() => {
37
+ const required = paramIn === 'path' ? true : !!schema.required;
38
+ return `param:{in:"${paramIn}",name:${JSON.stringify(paramName)},required:${required}}`;
39
+ })()
40
+ : undefined,
41
+ // example
42
+ 'example' in schema && schema.example !== undefined
43
+ ? `example:${JSON.stringify(schema.example)}`
44
+ : undefined,
45
+ // examples
46
+ 'examples' in schema && Array.isArray(schema.examples) && schema.examples.length > 0
47
+ ? `examples:${JSON.stringify(schema.examples)}`
48
+ : undefined,
49
+ // description
50
+ 'description' in schema && schema.description !== undefined
51
+ ? `description:${JSON.stringify(schema.description)}`
52
+ : undefined,
53
+ ].filter((v) => v !== undefined);
50
54
  return openapiProps.length === 0 ? z : `${z}.openapi({${openapiProps.join(',')}})`;
51
55
  }
@@ -128,7 +128,7 @@ export type Schema = {
128
128
  name?: string;
129
129
  description?: string;
130
130
  type?: Type | [Type, ...Type[]];
131
- format?: Format | FormatString | FormatNumber;
131
+ format?: Format;
132
132
  pattern?: string;
133
133
  minLength?: number;
134
134
  maxLength?: number;
@@ -143,7 +143,7 @@ export type Schema = {
143
143
  example?: unknown;
144
144
  examples?: unknown;
145
145
  properties?: Record<string, Schema>;
146
- required?: string[];
146
+ required?: string[] | boolean;
147
147
  items?: Schema;
148
148
  enum?: (string | number | boolean | null | (string | number | boolean | null)[])[];
149
149
  nullable?: boolean;
@@ -1,3 +1,8 @@
1
+ /**
2
+ * Parse the config object.
3
+ *
4
+ * @param config - The config object to parse.
5
+ */
1
6
  export declare function parseConfig(config: {
2
7
  readonly input: `${string}.yaml` | `${string}.json` | `${string}.tsp`;
3
8
  readonly 'zod-openapi'?: {
@@ -15,6 +20,9 @@ export declare function parseConfig(config: {
15
20
  readonly split?: boolean;
16
21
  };
17
22
  };
23
+ readonly type?: {
24
+ readonly output: `${string}.ts`;
25
+ };
18
26
  readonly rpc?: {
19
27
  readonly output: string | `${string}.ts`;
20
28
  readonly import: string;
@@ -39,6 +47,9 @@ export declare function parseConfig(config: {
39
47
  readonly split?: boolean;
40
48
  };
41
49
  };
50
+ readonly type?: {
51
+ readonly output: `${string}.ts`;
52
+ };
42
53
  readonly rpc?: {
43
54
  readonly output: string | `${string}.ts`;
44
55
  readonly import: string;
@@ -1,5 +1,11 @@
1
+ /**
2
+ * Parse the config object.
3
+ *
4
+ * @param config - The config object to parse.
5
+ */
1
6
  export function parseConfig(config) {
2
7
  const isYamlOrJsonOrTsp = (i) => typeof i === 'string' && (i.endsWith('.yaml') || i.endsWith('.json') || i.endsWith('.tsp'));
8
+ // ts
3
9
  const isTs = (o) => typeof o === 'string' && o.endsWith('.ts');
4
10
  const c = config;
5
11
  if (!isYamlOrJsonOrTsp(c.input)) {
@@ -118,6 +124,16 @@ export function parseConfig(config) {
118
124
  }
119
125
  }
120
126
  }
127
+ // type
128
+ const t = c.type;
129
+ if (t !== undefined) {
130
+ if (!isTs(t.output)) {
131
+ return {
132
+ ok: false,
133
+ error: `Invalid type output format: ${String(t.output)} (must be .ts file)`,
134
+ };
135
+ }
136
+ }
121
137
  // rpc
122
138
  const rpc = c.rpc;
123
139
  if (rpc !== undefined) {
@@ -44,7 +44,7 @@ const loadConfigHot = async (server) => {
44
44
  const def = isRecord(mod) ? Reflect.get(mod, 'default') : undefined;
45
45
  if (def === undefined)
46
46
  return { ok: false, error: 'Config must export default object' };
47
- // as unknown as Conf
47
+ // as Conf
48
48
  const parsed = parseConfig(def);
49
49
  return parsed.ok ? { ok: true, value: parsed.value } : { ok: false, error: parsed.error };
50
50
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hono-takibi",
3
3
  "description": "Hono Takibi is a CLI tool that generates Hono routes from OpenAPI specifications.",
4
- "version": "0.9.60",
4
+ "version": "0.9.61",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "keywords": [
@@ -47,22 +47,22 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@apidevtools/swagger-parser": "^12.1.0",
50
- "@typespec/compiler": "^1.5.0",
51
- "@typespec/openapi3": "^1.5.0",
52
- "prettier": "^3.6.2",
53
- "tsx": "^4.20.6"
50
+ "@typespec/compiler": "^1.7.0",
51
+ "@typespec/openapi3": "^1.7.0",
52
+ "prettier": "^3.7.4",
53
+ "tsx": "^4.21.0"
54
54
  },
55
55
  "devDependencies": {
56
- "@hono/zod-openapi": "1.1.4",
57
- "@types/node": "^24.9.1",
58
- "@typespec/http": "^1.5.0",
59
- "@typespec/rest": "^0.75.0",
60
- "@typespec/versioning": "^0.75.0",
61
- "@vitest/coverage-v8": "^4.0.3",
56
+ "@hono/zod-openapi": "1.1.5",
57
+ "@types/node": "^25.0.0",
58
+ "@typespec/http": "^1.7.0",
59
+ "@typespec/rest": "^0.77.0",
60
+ "@typespec/versioning": "^0.77.0",
61
+ "@vitest/coverage-v8": "^4.0.15",
62
62
  "typescript": "^5.9.3",
63
- "vite": "^7.1.12",
64
- "vitest": "^4.0.3",
65
- "zod": "^4.1.12"
63
+ "vite": "^7.2.7",
64
+ "vitest": "^4.0.15",
65
+ "zod": "^4.1.13"
66
66
  },
67
67
  "scripts": {
68
68
  "dev": "vite --host",