@@ -54,12 +54,58 @@ function extractPathBlock(content: string, startIndex: number): string | null {
5454 return content . slice ( startIndex , pos ) ;
5555}
5656
57+ /**
58+ * Extract the `{...}` block for a named member of an interface, e.g. an entry of `operations`.
59+ * Handles both bare identifier keys (`updatePet: {`) and quoted keys (`"activity/star-repo": {`) —
60+ * openapi-typescript quotes operationIds that contain non-identifier characters (`/`, `-`, `.`).
61+ */
62+ function extractNamedBlock ( content : string , name : string ) : string | null {
63+ const escaped = name . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) ;
64+ const re = new RegExp ( `(?:^|[\\s{;,])["']?${ escaped } ["']?\\s*:\\s*\\{` , 'm' ) ;
65+ const m = re . exec ( content ) ;
66+ if ( ! m ) return null ;
67+ const braceIdx = content . indexOf ( '{' , m . index ) ;
68+ let depth = 1 ;
69+ let pos = braceIdx + 1 ;
70+ while ( pos < content . length && depth > 0 ) {
71+ if ( content [ pos ] === '{' ) depth ++ ;
72+ else if ( content [ pos ] === '}' ) depth -- ;
73+ pos ++ ;
74+ }
75+ return content . slice ( braceIdx , pos ) ;
76+ }
77+
78+ /** A block has a request body if `requestBody` maps to an object; `requestBody?: never`/absent => no body. */
79+ function blockHasRequestBody ( block : string ) : boolean {
80+ return / r e q u e s t B o d y \s * \? ? \s * : \s * \{ / . test ( block ) ;
81+ }
82+
83+ /**
84+ * Whether an operation actually declares a request body. Resolves a `<method>: operations["Name"]`
85+ * reference against the `operations` interface, or reads an inline `<method>: { ... }` block.
86+ * Defaults to `true` when the operation can't be resolved, so a real body is never dropped.
87+ */
88+ function operationHasBody ( pathBlock : string , method : string , operationsContent : string ) : boolean {
89+ const ref = new RegExp ( `\\b${ method } \\s*:\\s*operations\\[["']([^"']+)["']\\]` ) . exec ( pathBlock ) ;
90+ if ( ref ) {
91+ const opBlock = extractNamedBlock ( operationsContent , ref [ 1 ] ) ;
92+ return opBlock ? blockHasRequestBody ( opBlock ) : true ;
93+ }
94+ const inline = new RegExp ( `\\b${ method } \\s*:\\s*\\{` ) . exec ( pathBlock ) ;
95+ if ( inline ) {
96+ const inlineBlock = extractPathBlock ( pathBlock , inline . index ) ;
97+ return inlineBlock ? blockHasRequestBody ( inlineBlock ) : true ;
98+ }
99+ return true ;
100+ }
101+
57102export function extractPaths ( apiTypesContent : string ) : PathInfo [ ] {
58103 const pathsContent = extractInterfaceBody ( apiTypesContent , 'paths' ) ;
59104 if ( ! pathsContent ) {
60105 throw new Error ( 'Could not find paths interface in api.d.ts' ) ;
61106 }
62107
108+ const operationsContent = extractInterfaceBody ( apiTypesContent , 'operations' ) ?? '' ;
63109 const pathKeyRegex = / " ( \/ [ ^ " ] * ) " : \s * \{ / g;
64110 const paths : PathInfo [ ] = [ ] ;
65111
@@ -84,7 +130,10 @@ export function extractPaths(apiTypesContent: string): PathInfo[] {
84130 for ( const { method, regex } of methodChecks ) {
85131 if ( regex . test ( pathBlock ) ) {
86132 const hasParams = pathStr . includes ( '{' ) ;
87- const hasBody = method !== 'get' && method !== 'delete' ;
133+ const hasBody =
134+ method !== 'get' &&
135+ method !== 'delete' &&
136+ operationHasBody ( pathBlock , method , operationsContent ) ;
88137 const hasQuery = method === 'get' ;
89138 methods . push ( { method, hasParams, hasBody, hasQuery } ) ;
90139 }
@@ -177,7 +226,7 @@ export function generateFileContent(paths: PathInfo[], clientImport: string): st
177226 allHandlers . add ( `handle${ Method } Command` ) ;
178227 allHandlers . add ( `handle${ Method } Form` ) ;
179228 if ( methodInfo . hasParams ) allTypes . add ( 'GetParameters' ) ;
180- allTypes . add ( 'GetRequestBody' ) ;
229+ if ( methodInfo . hasBody ) allTypes . add ( 'GetRequestBody' ) ;
181230 }
182231 }
183232 }
@@ -226,22 +275,30 @@ function generateFunctionCode(
226275 const formName = pathToFunctionName ( pathStr , method , 'Form' ) ;
227276 codes . push ( `export const ${ commandName } = command(\n\tz.custom<GetParameters<paths, '${ pathStr } ', 'delete'>>(),\n\tasync (params) => handleDeleteCommand('${ pathStr } ', params)\n);` ) ;
228277 codes . push ( `export const ${ formName } = form(\n\tz.record(z.string(), z.any()).pipe(z.custom<GetParameters<paths, '${ pathStr } ', 'delete'>>()),\n\tasync (params) => handleDeleteForm('${ pathStr } ', params)\n);` ) ;
229- } else if ( info . hasParams ) {
230- const commandName = pathToFunctionName ( pathStr , method , 'Command' ) ;
231- const formName = pathToFunctionName ( pathStr , method , 'Form' ) ;
232- const Method = method . charAt ( 0 ) . toUpperCase ( ) + method . slice ( 1 ) ;
233- const commandHandler = `handle${ Method } Command` ;
234- const formHandler = `handle${ Method } Form` ;
235- codes . push ( `export const ${ commandName } = command(\n\tz.object({\n\t\tpath: z.custom<GetParameters<paths, '${ pathStr } ', '${ method } '>['path']>(),\n\t\tbody: z.custom<GetRequestBody<paths, '${ pathStr } ', '${ method } '>>()\n\t}),\n\tasync (input) => ${ commandHandler } ('${ pathStr } ', input)\n);` ) ;
236- codes . push ( `export const ${ formName } = form(\n\tz.record(z.string(), z.any()).pipe(z.custom<{ path: GetParameters<paths, '${ pathStr } ', '${ method } '>['path']; body: GetRequestBody<paths, '${ pathStr } ', '${ method } '> }>()),\n\tasync (input) => ${ formHandler } ('${ pathStr } ', input)\n);` ) ;
237278 } else {
238279 const commandName = pathToFunctionName ( pathStr , method , 'Command' ) ;
239280 const formName = pathToFunctionName ( pathStr , method , 'Form' ) ;
240281 const Method = method . charAt ( 0 ) . toUpperCase ( ) + method . slice ( 1 ) ;
241282 const commandHandler = `handle${ Method } Command` ;
242283 const formHandler = `handle${ Method } Form` ;
243- codes . push ( `export const ${ commandName } = command(\n\tz.custom<GetRequestBody<paths, '${ pathStr } ', '${ method } '>>(),\n\tasync (body) => ${ commandHandler } ('${ pathStr } ', body)\n);` ) ;
244- codes . push ( `export const ${ formName } = form(\n\tz.record(z.string(), z.any()).pipe(z.custom<GetRequestBody<paths, '${ pathStr } ', '${ method } '>>()),\n\tasync (body) => ${ formHandler } ('${ pathStr } ', body)\n);` ) ;
284+ const pathType = `GetParameters<paths, '${ pathStr } ', '${ method } '>['path']` ;
285+ const bodyType = `GetRequestBody<paths, '${ pathStr } ', '${ method } '>` ;
286+
287+ if ( info . hasParams && info . hasBody ) {
288+ codes . push ( `export const ${ commandName } = command(\n\tz.object({\n\t\tpath: z.custom<${ pathType } >(),\n\t\tbody: z.custom<${ bodyType } >()\n\t}),\n\tasync (input) => ${ commandHandler } ('${ pathStr } ', input)\n);` ) ;
289+ codes . push ( `export const ${ formName } = form(\n\tz.record(z.string(), z.any()).pipe(z.custom<{ path: ${ pathType } ; body: ${ bodyType } }>()),\n\tasync (input) => ${ formHandler } ('${ pathStr } ', input)\n);` ) ;
290+ } else if ( info . hasParams && ! info . hasBody ) {
291+ // Path params but no request body: only accept `{ path }` so callers don't need `body: {}`.
292+ codes . push ( `export const ${ commandName } = command(\n\tz.object({\n\t\tpath: z.custom<${ pathType } >()\n\t}),\n\tasync (input) => ${ commandHandler } ('${ pathStr } ', input)\n);` ) ;
293+ codes . push ( `export const ${ formName } = form(\n\tz.record(z.string(), z.any()).pipe(z.custom<{ path: ${ pathType } }>()),\n\tasync (input) => ${ formHandler } ('${ pathStr } ', input)\n);` ) ;
294+ } else if ( ! info . hasParams && info . hasBody ) {
295+ codes . push ( `export const ${ commandName } = command(\n\tz.custom<${ bodyType } >(),\n\tasync (body) => ${ commandHandler } ('${ pathStr } ', body)\n);` ) ;
296+ codes . push ( `export const ${ formName } = form(\n\tz.record(z.string(), z.any()).pipe(z.custom<${ bodyType } >()),\n\tasync (body) => ${ formHandler } ('${ pathStr } ', body)\n);` ) ;
297+ } else {
298+ // No path params and no request body: a no-argument action command (e.g. billing reactivate).
299+ codes . push ( `export const ${ commandName } = command(\n\tz.void(),\n\tasync () => ${ commandHandler } ('${ pathStr } ')\n);` ) ;
300+ codes . push ( `export const ${ formName } = form(\n\tz.record(z.string(), z.any()),\n\tasync () => ${ formHandler } ('${ pathStr } ')\n);` ) ;
301+ }
245302 }
246303
247304 return codes ;
0 commit comments