Skip to content

Commit 80b6fb0

Browse files
committed
feat: implement API client code generation
1 parent e47f84d commit 80b6fb0

File tree

2 files changed

+189
-0
lines changed

2 files changed

+189
-0
lines changed

src/generator/apiGenerator.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import axios from 'axios';
55
import * as yaml from 'yaml';
66
import { OpenAPIV3 } from 'openapi-types';
77
import { generateTypeDefinitions } from './schemaGenerator';
8+
import { generateApiClient as generateApiClientCode } from './clientGenerator';
89

910
/**
1011
* Loads the OpenAPI specification from either a URL or local file
@@ -48,7 +49,16 @@ export async function generateApiClient(config: OpenAPIConfig): Promise<void> {
4849
'utf-8'
4950
);
5051

52+
// Generate and write API client
53+
const clientCode = generateApiClientCode(spec);
54+
await writeFile(
55+
resolve(config.exportDir, 'client.ts'),
56+
clientCode,
57+
'utf-8'
58+
);
59+
5160
console.log('Generated TypeScript interfaces successfully');
61+
console.log('Generated API client successfully');
5262

5363
// TODO: Implement remaining steps
5464
// 1. Generate Axios client methods for each path

src/generator/clientGenerator.ts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { OpenAPIV3 } from 'openapi-types';
2+
3+
interface OperationInfo {
4+
method: string;
5+
path: string;
6+
operationId: string;
7+
summary?: string;
8+
description?: string;
9+
parameters?: OpenAPIV3.ParameterObject[];
10+
requestBody?: OpenAPIV3.RequestBodyObject;
11+
responses: OpenAPIV3.ResponsesObject;
12+
}
13+
14+
function generateAxiosMethod(operation: OperationInfo): string {
15+
const { method, path, operationId, summary, description, parameters, requestBody, responses } = operation;
16+
17+
// Generate JSDoc
18+
const jsDocLines = ['/**'];
19+
if (summary) jsDocLines.push(` * ${summary}`);
20+
if (description) jsDocLines.push(` * ${description}`);
21+
22+
// Add parameter descriptions
23+
parameters?.forEach(param => {
24+
const desc = param.description ? ` - ${param.description}` : '';
25+
jsDocLines.push(` * @param ${param.in === 'path' ? 'params.' : param.in === 'query' ? 'query.' : ''}${param.name}${desc}`);
26+
});
27+
28+
if (requestBody && 'description' in requestBody) {
29+
jsDocLines.push(` * @param data - ${requestBody.description}`);
30+
}
31+
32+
// Add return type description
33+
const responseDetails = Object.entries(responses).find(([code]) => code.startsWith('2'));
34+
if (responseDetails) {
35+
const [code, response] = responseDetails;
36+
const responseObj = response as OpenAPIV3.ResponseObject;
37+
const desc = 'description' in responseObj ? responseObj.description : '';
38+
const contentType = responseObj.content?.['application/json']?.schema;
39+
const typeName = `${operationId}Response${code}`;
40+
41+
if (contentType) {
42+
if (desc) {
43+
jsDocLines.push(` * @returns ${desc}`);
44+
}
45+
jsDocLines.push(` * @see ${typeName}`);
46+
} else if (desc) {
47+
jsDocLines.push(` * @returns ${desc}`);
48+
}
49+
}
50+
51+
jsDocLines.push(' */');
52+
53+
// Generate method parameters
54+
const params: string[] = [];
55+
const urlParams = parameters?.filter(p => p.in === 'path') || [];
56+
const queryParams = parameters?.filter(p => p.in === 'query') || [];
57+
58+
// Add request body parameter if it exists
59+
if (requestBody) {
60+
params.push(`data: ${operationId}Request`);
61+
}
62+
63+
// Add path parameters
64+
if (urlParams.length > 0) {
65+
params.push(`params: { ${
66+
urlParams.map(p => `${p.name}: ${getTypeFromParam(p)}`).join(', ')
67+
} }`);
68+
}
69+
70+
// Add query parameters
71+
if (queryParams.length > 0) {
72+
params.push(`query?: { ${
73+
queryParams.map(p => `${p.name}${p.required ? '' : '?'}: ${getTypeFromParam(p)}`).join(', ')
74+
} }`);
75+
}
76+
77+
// Add headers parameter
78+
params.push('headers?: Record<string, string>');
79+
80+
// Get response type from 2xx response
81+
const successResponse = Object.entries(responses).find(([code]) => code.startsWith('2'));
82+
const responseType = successResponse ? `${operationId}Response${successResponse[0]}` : 'any';
83+
84+
// Generate method
85+
const urlWithParams = urlParams.length > 0
86+
? path.replace(/{(\w+)}/g, '${params.$1}')
87+
: path;
88+
89+
const methodBody = [
90+
`const url = \`${urlWithParams}\`;`,
91+
queryParams.length > 0 ? 'const queryString = query ? `?${new URLSearchParams(query)}` : \'\';' : '',
92+
`return this.axios.${method.toLowerCase()}<${responseType}>(url${queryParams.length > 0 ? ' + queryString' : ''}, {
93+
${requestBody ? 'data,' : ''}
94+
headers
95+
});`
96+
].filter(Boolean).join('\n ');
97+
98+
return `
99+
${jsDocLines.join('\n ')}
100+
async ${operationId}(${params.join(', ')}): Promise<AxiosResponse<${responseType}>> {
101+
${methodBody}
102+
}`;
103+
}
104+
105+
function getTypeFromParam(param: OpenAPIV3.ParameterObject): string {
106+
if ('schema' in param) {
107+
const schema = param.schema as OpenAPIV3.SchemaObject;
108+
switch (schema.type) {
109+
case 'string': return 'string';
110+
case 'integer':
111+
case 'number': return 'number';
112+
case 'boolean': return 'boolean';
113+
case 'array': return 'Array<any>'; // You might want to make this more specific
114+
default: return 'any';
115+
}
116+
}
117+
return 'any';
118+
}
119+
120+
export function generateApiClient(spec: OpenAPIV3.Document): string {
121+
let operations: OperationInfo[] = [];
122+
123+
// Collect all operations
124+
Object.entries(spec.paths || {}).forEach(([path, pathItem]) => {
125+
if (!pathItem) return;
126+
127+
['get', 'post', 'put', 'delete', 'patch'].forEach(method => {
128+
const operation = pathItem[method as keyof OpenAPIV3.PathItemObject] as OpenAPIV3.OperationObject;
129+
if (!operation) return;
130+
131+
operations.push({
132+
method: method.toUpperCase(),
133+
path,
134+
operationId: operation.operationId || `${method}${path.replace(/\W+/g, '_')}`,
135+
summary: operation.summary,
136+
description: operation.description,
137+
parameters: [...(pathItem.parameters || []), ...(operation.parameters || [])] as OpenAPIV3.ParameterObject[],
138+
requestBody: operation.requestBody as OpenAPIV3.RequestBodyObject,
139+
responses: operation.responses
140+
});
141+
});
142+
});
143+
144+
// Collect actually used types
145+
const usedTypes = new Set<string>();
146+
operations.forEach(op => {
147+
// Add request type if method has request body
148+
if (op.requestBody) {
149+
usedTypes.add(`${op.operationId}Request`);
150+
}
151+
// Add only the 2xx response type that's used
152+
const successResponse = Object.entries(op.responses).find(([code]) => code.startsWith('2'));
153+
if (successResponse) {
154+
usedTypes.add(`${op.operationId}Response${successResponse[0]}`);
155+
}
156+
});
157+
158+
// Generate the client class
159+
return `import axios, { AxiosInstance, AxiosResponse } from 'axios';
160+
import {
161+
${Array.from(usedTypes).join(',\n ')}
162+
} from './types';
163+
164+
export class ApiClient {
165+
private axios: AxiosInstance;
166+
167+
constructor(baseURL: string, headers?: Record<string, string>) {
168+
this.axios = axios.create({
169+
baseURL,
170+
headers: {
171+
'Content-Type': 'application/json',
172+
...headers
173+
}
174+
});
175+
}
176+
${operations.map(op => generateAxiosMethod(op)).join('\n\n')}
177+
}
178+
`;
179+
}

0 commit comments

Comments
 (0)