Skip to content

Commit 9907427

Browse files
committed
feat: enhance multipart/form-data support in API client generation
1 parent 8ba4dfd commit 9907427

File tree

3 files changed

+44
-33
lines changed

3 files changed

+44
-33
lines changed

src/generator/clientGenerator.ts

Lines changed: 40 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export interface OperationInfo {
1111
responses: OpenAPIV3.ResponsesObject;
1212
}
1313

14-
function generateAxiosMethod(operation: OperationInfo): string {
14+
function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document): string {
1515
const { method, path, operationId, summary, description, parameters, requestBody, responses } = operation;
1616

1717
// Generate JSDoc
@@ -50,59 +50,67 @@ function generateAxiosMethod(operation: OperationInfo): string {
5050

5151
jsDocLines.push(' */');
5252

53-
// Generate method parameters
5453
const urlParams = parameters?.filter(p => p.in === 'path') || [];
5554
const queryParams = parameters?.filter(p => p.in === 'query') || [];
5655

56+
const isFormData = requestBody &&
57+
'content' in requestBody &&
58+
requestBody.content?.['multipart/form-data'];
59+
60+
const formDataSchema = isFormData && requestBody.content['multipart/form-data'].schema ? (
61+
'$ref' in requestBody.content['multipart/form-data'].schema
62+
? spec.components?.schemas?.[requestBody.content['multipart/form-data'].schema.$ref.split('/').pop()!]
63+
: requestBody.content['multipart/form-data'].schema
64+
) as OpenAPIV3.SchemaObject : undefined;
65+
5766
// Build data type parts
58-
const typeComponents: string[] = [];
67+
const dataProps: string[] = [];
5968

60-
// Add request body type if it exists
61-
if (requestBody) {
62-
typeComponents.push(operationId + 'Request');
63-
}
64-
65-
// Add path and query parameters if any
66-
const additionalProps: string[] = [];
69+
// Add path and query parameters
6770
urlParams.forEach(p => {
68-
additionalProps.push(`${p.name}: ${getTypeFromParam(p)}`);
71+
dataProps.push(`${p.name}: ${getTypeFromParam(p)}`);
6972
});
7073
queryParams.forEach(p => {
71-
additionalProps.push(`${p.name}${p.required ? '' : '?'}: ${getTypeFromParam(p)}`);
74+
dataProps.push(`${p.name}${p.required ? '' : '?'}: ${getTypeFromParam(p)}`);
7275
});
7376

74-
if (additionalProps.length > 0) {
75-
typeComponents.push(`{ ${additionalProps.join('; ')} }`);
76-
}
77-
78-
const hasData = typeComponents.length > 0;
79-
const dataType = typeComponents.length > 1
80-
? typeComponents.join(' & ')
81-
: typeComponents[0] || 'undefined';
77+
// Add request body type if it exists
78+
const hasData = (parameters && parameters.length > 0) || operation.requestBody;
79+
const dataType = hasData
80+
? requestBody
81+
? `${operationId}Request & { ${dataProps.join('; ')} }`
82+
: `{ ${dataProps.join('; ')} }`
83+
: 'undefined';
8284

8385
// Get response type from 2xx response
8486
const successResponse = Object.entries(responses).find(([code]) => code.startsWith('2'));
8587
const responseType = successResponse ? `${operationId}Response${successResponse[0]}` : 'any';
8688

87-
// Generate method
8889
const urlWithParams = urlParams.length > 0
8990
? path.replace(/{(\w+)}/g, '${data.$1}')
9091
: path;
9192

9293
const methodBody = [
9394
`const url = \`${urlWithParams}\`;`,
94-
// Combine query and path params to filter from body
95-
(queryParams.length > 0 || urlParams.length > 0) ?
96-
`const paramsToFilter = ${JSON.stringify([...queryParams, ...urlParams].map(p => p.name))};` : '',
9795
queryParams.length > 0 ?
9896
`const queryData = Object.fromEntries(Object.entries(data).filter(([key]) => ${JSON.stringify(queryParams.map(p => p.name))}.includes(key)));` : '',
99-
(requestBody && (queryParams.length > 0 || urlParams.length > 0)) ?
100-
`const bodyData = Object.fromEntries(Object.entries(data).filter(([key]) => !paramsToFilter.includes(key)));` : '',
101-
queryParams.length > 0 ?
102-
'const queryString = `?${new URLSearchParams(queryData)}`;' : '',
103-
`return this.axios.${method.toLowerCase()}<${responseType}>(url${queryParams.length > 0 ? ' + queryString' : ''}, {
104-
${requestBody ? `data: ${(queryParams.length > 0 || urlParams.length > 0) ? 'bodyData' : 'data'},` : ''}
105-
headers
97+
requestBody && queryParams.length > 0 ?
98+
`const bodyData = Object.fromEntries(Object.entries(data).filter(([key]) => !${JSON.stringify(queryParams.map(p => p.name))}.includes(key)));` : '',
99+
isFormData ?
100+
`const formData = new FormData();
101+
${Object.entries((formDataSchema?.properties || {})
102+
).map(([key, prop]: [string, any]) => {
103+
const isBinary = prop.format === 'binary';
104+
return formDataSchema?.required?.includes(key)
105+
? `formData.append("${key}", ${isBinary ? '' : 'String('}${queryParams.length > 0 ? 'bodyData' : 'data'}.${key}${isBinary ? '' : ')'});`
106+
: `if (${queryParams.length > 0 ? 'bodyData' : 'data'}.${key} != null) {
107+
formData.append("${key}", ${isBinary ? '' : 'String('}${queryParams.length > 0 ? 'bodyData' : 'data'}.${key}${isBinary ? '' : ')'});
108+
}`
109+
}).join('\n ')}` : '',
110+
`return this.axios.${method.toLowerCase()}<${responseType}>(url, {
111+
${queryParams.length > 0 ? `params: queryData,` : ''}
112+
${requestBody ? `data: ${isFormData ? 'formData' : queryParams.length > 0 ? 'bodyData' : 'data'},` : ''}
113+
${isFormData ? `headers: { 'Content-Type': 'multipart/form-data', ...headers },` : 'headers'}
106114
});`
107115
].filter(Boolean).join('\n ');
108116

@@ -184,7 +192,7 @@ export class ApiClient {
184192
}
185193
});
186194
}
187-
${operations.map(op => generateAxiosMethod(op)).join('\n\n')}
195+
${operations.map(op => generateAxiosMethod(op, spec)).join('\n\n')}
188196
}
189197
`;
190198
}

src/generator/reactQueryGenerator.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ function generateQueryOptions(operation: OperationInfo, spec: OpenAPIV3.Document
2121
...(parameters?.filter(p => p.required).map(p => `'${p.name}'`) || []),
2222
...(requestBody && 'content' in requestBody && requestBody.content?.['application/json']?.schema
2323
? getRequiredFields(requestBody.content['application/json'].schema, { schemas: spec.components?.schemas as { [key: string]: OpenAPIV3.SchemaObject } || {} })
24+
: []),
25+
...(requestBody && 'content' in requestBody && requestBody.content?.['multipart/form-data']?.schema
26+
? getRequiredFields(requestBody.content['multipart/form-data'].schema, { schemas: spec.components?.schemas as { [key: string]: OpenAPIV3.SchemaObject } || {} })
2427
: [])
2528
];
2629

src/generator/schemaGenerator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ export function generateTypeDefinitions(spec: OpenAPIV3.Document): string {
101101
// Generate request body type
102102
if (operationObject.requestBody) {
103103
const content = (operationObject.requestBody as OpenAPIV3.RequestBodyObject).content;
104-
const jsonContent = content['application/json'];
104+
const jsonContent = content['application/json'] || content['multipart/form-data']
105105
if (jsonContent?.schema) {
106106
const typeName = `${operationObject.operationId}Request`;
107107
output += generateTypeDefinition(typeName, jsonContent.schema as OpenAPIV3.SchemaObject, context);

0 commit comments

Comments
 (0)