Skip to content

Commit 2cc39f0

Browse files
committed
refactor code
1 parent 3e525d8 commit 2cc39f0

7 files changed

Lines changed: 1690 additions & 1667 deletions

File tree

src/codegen/generators/typescript/channels/openapi.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {collectProtocolDependencies} from './utils';
1616
import {
1717
renderHttpFetchClient,
1818
renderHttpCommonTypes
19-
} from './protocols/http/fetch';
19+
} from './protocols/http';
2020
import {getMessageTypeAndModule} from './utils';
2121
import {pascalCase} from '../utils';
2222
import {createMissingInputDocumentError} from '../../../errors';
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
/**
2+
* Generates HTTP client functions for individual API operations.
3+
* Each operation gets a typed function with request/response handling.
4+
*/
5+
import {HttpRenderType} from '../../../../../types';
6+
import {pascalCase} from '../../../utils';
7+
import {ChannelFunctionTypes, RenderHttpParameters} from '../../types';
8+
9+
/**
10+
* Renders an HTTP fetch client function for a specific API operation.
11+
*/
12+
export function renderHttpFetchClient({
13+
requestTopic,
14+
requestMessageType,
15+
requestMessageModule,
16+
replyMessageType,
17+
replyMessageModule,
18+
channelParameters,
19+
method,
20+
servers = [],
21+
subName = pascalCase(requestTopic),
22+
functionName = `${method.toLowerCase()}${subName}`,
23+
includesStatusCodes = false
24+
}: RenderHttpParameters): HttpRenderType {
25+
const messageType = requestMessageModule
26+
? `${requestMessageModule}.${requestMessageType}`
27+
: requestMessageType;
28+
const replyType = replyMessageModule
29+
? `${replyMessageModule}.${replyMessageType}`
30+
: replyMessageType;
31+
32+
// Generate context interface name
33+
const contextInterfaceName = `${pascalCase(functionName)}Context`;
34+
35+
// Determine if operation has path parameters
36+
const hasParameters = channelParameters !== undefined;
37+
38+
// Generate the context interface (extends HttpClientContext)
39+
const contextInterface = generateContextInterface(
40+
contextInterfaceName,
41+
messageType,
42+
hasParameters,
43+
method
44+
);
45+
46+
// Generate the function implementation
47+
const functionCode = generateFunctionImplementation({
48+
functionName,
49+
contextInterfaceName,
50+
replyType,
51+
replyMessageModule,
52+
replyMessageType,
53+
messageType,
54+
requestTopic,
55+
hasParameters,
56+
method,
57+
servers,
58+
includesStatusCodes
59+
});
60+
61+
const code = `${contextInterface}
62+
63+
${functionCode}`;
64+
65+
return {
66+
messageType,
67+
replyType,
68+
code,
69+
functionName,
70+
dependencies: [
71+
`import { URLSearchParams, URL } from 'url';`,
72+
`import * as NodeFetch from 'node-fetch';`
73+
],
74+
functionType: ChannelFunctionTypes.HTTP_CLIENT
75+
};
76+
}
77+
78+
/**
79+
* Generate the context interface for an HTTP operation
80+
*/
81+
function generateContextInterface(
82+
interfaceName: string,
83+
messageType: string | undefined,
84+
hasParameters: boolean,
85+
method: string
86+
): string {
87+
const fields: string[] = [];
88+
89+
// Add payload field for methods that have a body
90+
if (messageType && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
91+
fields.push(` payload: ${messageType};`);
92+
}
93+
94+
// Add parameters field if operation has path parameters
95+
if (hasParameters) {
96+
fields.push(
97+
` parameters: { getChannelWithParameters: (path: string) => string };`
98+
);
99+
}
100+
101+
// Add requestHeaders field (optional) for operations that support typed headers
102+
// This is always optional since headers can also be passed via additionalHeaders
103+
fields.push(` requestHeaders?: { marshal: () => string };`);
104+
105+
const fieldsStr = fields.length > 0 ? `\n${fields.join('\n')}\n` : '';
106+
107+
return `export interface ${interfaceName} extends HttpClientContext {${fieldsStr}}`;
108+
}
109+
110+
/**
111+
* Generate the function implementation
112+
*/
113+
function generateFunctionImplementation(params: {
114+
functionName: string;
115+
contextInterfaceName: string;
116+
replyType: string;
117+
replyMessageModule: string | undefined;
118+
replyMessageType: string;
119+
messageType: string | undefined;
120+
requestTopic: string;
121+
hasParameters: boolean;
122+
method: string;
123+
servers: string[];
124+
includesStatusCodes: boolean;
125+
}): string {
126+
const {
127+
functionName,
128+
contextInterfaceName,
129+
replyType,
130+
replyMessageModule,
131+
replyMessageType,
132+
messageType,
133+
requestTopic,
134+
hasParameters,
135+
method,
136+
servers,
137+
includesStatusCodes
138+
} = params;
139+
140+
const defaultServer = servers[0] ?? "'localhost:3000'";
141+
const hasBody =
142+
messageType && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase());
143+
144+
// Generate URL building code
145+
const urlBuildCode = hasParameters
146+
? `let url = buildUrlWithParameters(config.server, '${requestTopic}', context.parameters);`
147+
: 'let url = `${config.server}${config.path}`;';
148+
149+
// Generate headers initialization
150+
const headersInit = `let headers = context.requestHeaders
151+
? applyTypedHeaders(context.requestHeaders, config.additionalHeaders)
152+
: { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record<string, string | string[]>;`;
153+
154+
// Generate body preparation
155+
const bodyPrep = hasBody
156+
? `const body = context.payload?.marshal();`
157+
: `const body = undefined;`;
158+
159+
// Generate response parsing
160+
// Use unmarshalByStatusCode if the payload is a union type with status code support
161+
let responseParseCode: string;
162+
if (replyMessageModule) {
163+
responseParseCode = includesStatusCodes
164+
? `const responseData = ${replyMessageModule}.unmarshalByStatusCode(rawData, response.status);`
165+
: `const responseData = ${replyMessageModule}.unmarshal(rawData);`;
166+
} else {
167+
responseParseCode = `const responseData = ${replyMessageType}.unmarshal(rawData);`;
168+
}
169+
170+
// Generate default context for optional context parameter
171+
const contextDefault = !hasBody && !hasParameters ? ' = {}' : '';
172+
173+
return `async function ${functionName}(context: ${contextInterfaceName}${contextDefault}): Promise<HttpClientResponse<${replyType}>> {
174+
// Apply defaults
175+
const config = {
176+
path: '${requestTopic}',
177+
server: ${defaultServer},
178+
...context,
179+
};
180+
181+
// Validate OAuth2 config if present
182+
if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) {
183+
validateOAuth2Config(config.auth);
184+
}
185+
186+
// Build headers
187+
${headersInit}
188+
189+
// Build URL
190+
${urlBuildCode}
191+
url = applyQueryParams(config.queryParams, url);
192+
193+
// Apply pagination (can affect URL and/or headers)
194+
const paginationResult = applyPagination(config.pagination, url, headers);
195+
url = paginationResult.url;
196+
headers = paginationResult.headers;
197+
198+
// Apply authentication
199+
const authResult = applyAuth(config.auth, headers, url);
200+
headers = authResult.headers;
201+
url = authResult.url;
202+
203+
// Prepare body
204+
${bodyPrep}
205+
206+
// Determine request function
207+
const makeRequest = config.hooks?.makeRequest ?? defaultMakeRequest;
208+
209+
// Build request params
210+
let requestParams: HttpRequestParams = {
211+
url,
212+
method: '${method}',
213+
headers,
214+
body
215+
};
216+
217+
// Apply beforeRequest hook
218+
if (config.hooks?.beforeRequest) {
219+
requestParams = await config.hooks.beforeRequest(requestParams);
220+
}
221+
222+
try {
223+
// Execute request with retry logic
224+
let response = await executeWithRetry(requestParams, makeRequest, config.retry);
225+
226+
// Apply afterResponse hook
227+
if (config.hooks?.afterResponse) {
228+
response = await config.hooks.afterResponse(response, requestParams);
229+
}
230+
231+
// Handle OAuth2 token flows that require getting a token first
232+
if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) {
233+
const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry);
234+
if (tokenFlowResponse) {
235+
response = tokenFlowResponse;
236+
}
237+
}
238+
239+
// Handle 401 with token refresh
240+
if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) {
241+
try {
242+
const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry);
243+
if (refreshResponse) {
244+
response = refreshResponse;
245+
}
246+
} catch {
247+
throw new Error('Unauthorized');
248+
}
249+
}
250+
251+
// Handle error responses
252+
if (!response.ok) {
253+
handleHttpError(response.status, response.statusText);
254+
}
255+
256+
// Parse response
257+
const rawData = await response.json();
258+
${responseParseCode}
259+
260+
// Extract response metadata
261+
const responseHeaders = extractHeaders(response);
262+
const paginationInfo = extractPaginationInfo(responseHeaders, config.pagination);
263+
264+
// Build response wrapper with pagination helpers
265+
const result: HttpClientResponse<${replyType}> = {
266+
data: responseData,
267+
status: response.status,
268+
statusText: response.statusText,
269+
headers: responseHeaders,
270+
rawData,
271+
pagination: paginationInfo,
272+
...createPaginationHelpers(config, paginationInfo, ${functionName}),
273+
};
274+
275+
return result;
276+
277+
} catch (error) {
278+
// Apply onError hook if present
279+
if (config.hooks?.onError && error instanceof Error) {
280+
throw await config.hooks.onError(error, requestParams);
281+
}
282+
throw error;
283+
}
284+
}`;
285+
}

0 commit comments

Comments
 (0)