From 3c87a47902bb3432a23bc7d85b23a87db9a5511b Mon Sep 17 00:00:00 2001 From: jonaslagoni Date: Sun, 2 Aug 2026 11:49:21 +0200 Subject: [PATCH] fix: align HTTP channel parameter handling with the other protocols The HTTP protocols diverged from every other channel protocol in how they render and document parameters. Four fixes, all inside protocols/http/: Generated client helpers now take object parameters. Ten module-private helpers in the client's common types (applyAuth, buildUrlWithParameters, executeWithRetry, handleHttpError, shouldRetry, ...) used positional signatures, which the rest of the generated code -- including the HTTP server's own common types -- does not. They are not exported from the generated module, so this is invisible to users. The parameter model is referenced through `type` everywhere. The context interface rendered the type from `model.type` while the instanceof guard, the `fromUrl` call and the reported `parameterType` used `model.name`. Real Modelina object models have `type === name`, so nothing was broken, but `parameterType` is a type and every other protocol reads `.type`. HTTP functions document their parameters. Both renderers called renderChannelJSDoc without a `parameters` array, so the generated functions documented nothing while a NATS function documents each argument. The single `context` object is documented as `@param context.`. The `parameters` field is optional when every parameter is optional. An OpenAPI operation declaring only optional query parameters forced callers to write `{parameters: {}}`. The context and the field are now optional in that case, defaulted through a named local so the model constructor never receives `undefined` and the instanceof guard still narrows. AsyncAPI channel parameters are always required, so that path is unchanged. A new `contextOptional` field on the render carries the optionality to the client class wrapper so it matches the function it delegates to. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/generated/http_client.ts | 153 +++++----- .../src/generated/http_client.ts | 153 +++++----- .../src/generated/http_server.ts | 16 ++ .../channels/protocols/http/client.ts | 164 ++++++++--- .../channels/protocols/http/common-types.ts | 75 ++--- .../channels/protocols/http/security.ts | 28 +- .../channels/protocols/http/server.ts | 46 ++- .../generators/typescript/channels/types.ts | 6 + .../generators/typescript/channels/utils.ts | 2 + .../typescript/client/protocols/http.ts | 8 +- src/codegen/types.ts | 7 + .../__snapshots__/channels.spec.ts.snap | 270 ++++++++++-------- .../generators/typescript/channels.spec.ts | 11 +- .../openapi-http-server.spec.ts.snap | 11 + .../openapi-http-client-responses.spec.ts | 59 +++- .../http/__snapshots__/server.spec.ts.snap | 15 + .../generators/typescript/client.spec.ts | 73 +++++ .../channels/http_client.ts | 151 +++++----- .../openapi-primitive/channels/http_client.ts | 131 +++++---- .../openapi-server/channels/http_client.ts | 151 +++++----- .../openapi-server/channels/http_server.ts | 16 ++ .../channels/http_client.ts | 151 +++++----- .../src/openapi/channels/http_client.ts | 151 +++++----- .../src/request-reply/channels/http_client.ts | 259 +++++++++-------- 24 files changed, 1315 insertions(+), 792 deletions(-) diff --git a/examples/openapi-http-client/src/generated/http_client.ts b/examples/openapi-http-client/src/generated/http_client.ts index 6001261b..52abcc8e 100644 --- a/examples/openapi-http-client/src/generated/http_client.ts +++ b/examples/openapi-http-client/src/generated/http_client.ts @@ -300,11 +300,11 @@ const defaultMakeRequest = async (params: HttpRequestParams): Promise, - url: string -): { headers: Record; url: string } { +function applyAuth({auth, headers, url}: { + auth: AuthConfig | undefined; + headers: Record; + url: string; +}): { headers: Record; url: string } { if (!auth) return { headers, url }; switch (auth.type) { @@ -349,7 +349,10 @@ function applyAuth( /** * Apply query parameters to URL */ -function applyQueryParams(queryParams: Record | undefined, url: string): string { +function applyQueryParams({queryParams, url}: { + queryParams: Record | undefined; + url: string; +}): string { if (!queryParams) return url; const params = new URLSearchParams(); @@ -376,10 +379,10 @@ function sleep(ms: number): Promise { /** * Calculate delay for exponential backoff */ -function calculateBackoffDelay( - attempt: number, - config: Required -): number { +function calculateBackoffDelay({attempt, config}: { + attempt: number; + config: Required; +}): number { const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt - 1); return Math.min(delay, config.maxDelayMs); } @@ -387,12 +390,12 @@ function calculateBackoffDelay( /** * Determine if a request should be retried based on error/response */ -function shouldRetry( - error: HttpGlobalError | null, - response: HttpResponse | null, - config: Required, - attempt: number -): boolean { +function shouldRetry({error, response, config, attempt}: { + error: HttpGlobalError | null; + response: HttpResponse | null; + config: Required; + attempt: number; +}): boolean { if (attempt >= config.maxRetries) return false; if (error && config.retryOnNetworkError) return true; @@ -405,11 +408,11 @@ function shouldRetry( /** * Execute request with retry logic */ -async function executeWithRetry( - params: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function executeWithRetry({params, makeRequest, retryConfig}: { + params: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { const config = { ...DEFAULT_RETRY_CONFIG, ...retryConfig }; let lastError: HttpGlobalError | null = null; let lastResponse: HttpResponse | null = null; @@ -417,7 +420,7 @@ async function executeWithRetry( for (let attempt = 0; attempt <= config.maxRetries; attempt++) { try { if (attempt > 0) { - const delay = calculateBackoffDelay(attempt, config); + const delay = calculateBackoffDelay({attempt, config}); config.onRetry(attempt, delay, lastError ?? new HttpGlobalError('Retry attempt')); await sleep(delay); } @@ -425,7 +428,7 @@ async function executeWithRetry( const response = await makeRequest(params); // Check if we should retry this response - if (!shouldRetry(null, response, config, attempt + 1)) { + if (!shouldRetry({error: null, response, config, attempt: attempt + 1})) { return response; } @@ -434,7 +437,7 @@ async function executeWithRetry( } catch (error) { lastError = error instanceof HttpGlobalError ? error : new HttpGlobalError(String(error)); - if (!shouldRetry(lastError, null, config, attempt + 1)) { + if (!shouldRetry({error: lastError, response: null, config, attempt: attempt + 1})) { throw lastError; } } @@ -452,7 +455,11 @@ async function executeWithRetry( * Explicit cases are generated from the error status codes declared by the * input document; undeclared codes fall through to the default handler. */ -function handleHttpError(status: number, statusText: string, body?: unknown): never { +function handleHttpError({status, statusText, body}: { + status: number; + statusText: string; + body?: unknown; +}): never { switch (status) { case 400: throw new HttpError("Bad Request", status, statusText, body); @@ -510,11 +517,11 @@ function extractHeaders(response: HttpResponse): Record { * @param pathTemplate - Path template with {param} placeholders * @param parameters - Parameter object with getChannelWithParameters method */ -function buildUrlWithParameters string }>( - server: string, - pathTemplate: string, - parameters: T -): string { +function buildUrlWithParameters string }>({server, pathTemplate, parameters}: { + server: string; + pathTemplate: string; + parameters: T; +}): string { const path = parameters.getChannelWithParameters(pathTemplate); return `${server}${path}`; } @@ -522,10 +529,10 @@ function buildUrlWithParameters string } | undefined, - additionalHeaders: Record | undefined -): Record { +function applyTypedHeaders({typedHeaders, additionalHeaders}: { + typedHeaders: { marshal: () => string } | undefined; + additionalHeaders: Record | undefined; +}): Record { const headers: Record = { 'Content-Type': 'application/json', ...additionalHeaders @@ -573,12 +580,12 @@ function validateOAuth2Config(auth: OAuth2Auth): void { /** * Handle OAuth2 token flows (client_credentials, password) */ -async function handleOAuth2TokenFlow( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleOAuth2TokenFlow({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.flow || !auth.tokenUrl) return null; const params = new URLSearchParams(); @@ -640,18 +647,18 @@ async function handleOAuth2TokenFlow( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = `Bearer ${tokens.accessToken}`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } /** * Handle OAuth2 token refresh on 401 response */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleTokenRefresh({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; const refreshResponse = await fetch(auth.tokenUrl, { @@ -687,7 +694,7 @@ async function handleTokenRefresh( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = `Bearer ${newTokens.accessToken}`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } // ============================================================================ // Generated HTTP Client Functions @@ -700,6 +707,10 @@ export interface PostV2ConnectContext extends HttpClientContext { /** * Generates a ConnectUrl where the user can be validated and connected. + * + * @param context per-call request configuration + * @param context.payload the request body to send + * @param context.requestHeaders optional headers to send with the request */ async function postV2Connect(context: PostV2ConnectContext): Promise> { // Apply defaults @@ -718,10 +729,10 @@ async function postV2Connect(context: PostV2ConnectContext): Promise undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -812,6 +823,9 @@ export interface GetV2ConnectReferenceIdContext extends HttpClientContext { /** * Translate a ReferenceId into a SafepayAccountId. + * + * @param context per-call request configuration + * @param context.parameters for path and query parameter substitution */ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): Promise> { // Apply defaults @@ -831,11 +845,11 @@ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = buildUrlWithParameters(config.baseUrl, '/v2/connect/{referenceId}', parameters); - url = applyQueryParams(config.additionalQueryParams, url); + let url = buildUrlWithParameters({server: config.baseUrl, pathTemplate: '/v2/connect/{referenceId}', parameters}); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -860,7 +874,7 @@ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -869,7 +883,7 @@ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -878,7 +892,7 @@ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -890,7 +904,7 @@ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -925,6 +939,9 @@ export interface GetV2UsersSafepayAccountIdBankAccountsContext extends HttpClien /** * Returns the bank accounts registered for a Safepay account. + * + * @param context per-call request configuration + * @param context.parameters for path and query parameter substitution */ async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepayAccountIdBankAccountsContext): Promise> { // Apply defaults @@ -944,11 +961,11 @@ async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepay let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = buildUrlWithParameters(config.baseUrl, '/v2/users/{safepayAccountId}/bank-accounts', parameters); - url = applyQueryParams(config.additionalQueryParams, url); + let url = buildUrlWithParameters({server: config.baseUrl, pathTemplate: '/v2/users/{safepayAccountId}/bank-accounts', parameters}); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -973,7 +990,7 @@ async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepay try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -982,7 +999,7 @@ async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepay // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -991,7 +1008,7 @@ async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepay // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -1003,7 +1020,7 @@ async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepay // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response diff --git a/examples/openapi-http-server/src/generated/http_client.ts b/examples/openapi-http-server/src/generated/http_client.ts index 6001261b..52abcc8e 100644 --- a/examples/openapi-http-server/src/generated/http_client.ts +++ b/examples/openapi-http-server/src/generated/http_client.ts @@ -300,11 +300,11 @@ const defaultMakeRequest = async (params: HttpRequestParams): Promise, - url: string -): { headers: Record; url: string } { +function applyAuth({auth, headers, url}: { + auth: AuthConfig | undefined; + headers: Record; + url: string; +}): { headers: Record; url: string } { if (!auth) return { headers, url }; switch (auth.type) { @@ -349,7 +349,10 @@ function applyAuth( /** * Apply query parameters to URL */ -function applyQueryParams(queryParams: Record | undefined, url: string): string { +function applyQueryParams({queryParams, url}: { + queryParams: Record | undefined; + url: string; +}): string { if (!queryParams) return url; const params = new URLSearchParams(); @@ -376,10 +379,10 @@ function sleep(ms: number): Promise { /** * Calculate delay for exponential backoff */ -function calculateBackoffDelay( - attempt: number, - config: Required -): number { +function calculateBackoffDelay({attempt, config}: { + attempt: number; + config: Required; +}): number { const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt - 1); return Math.min(delay, config.maxDelayMs); } @@ -387,12 +390,12 @@ function calculateBackoffDelay( /** * Determine if a request should be retried based on error/response */ -function shouldRetry( - error: HttpGlobalError | null, - response: HttpResponse | null, - config: Required, - attempt: number -): boolean { +function shouldRetry({error, response, config, attempt}: { + error: HttpGlobalError | null; + response: HttpResponse | null; + config: Required; + attempt: number; +}): boolean { if (attempt >= config.maxRetries) return false; if (error && config.retryOnNetworkError) return true; @@ -405,11 +408,11 @@ function shouldRetry( /** * Execute request with retry logic */ -async function executeWithRetry( - params: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function executeWithRetry({params, makeRequest, retryConfig}: { + params: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { const config = { ...DEFAULT_RETRY_CONFIG, ...retryConfig }; let lastError: HttpGlobalError | null = null; let lastResponse: HttpResponse | null = null; @@ -417,7 +420,7 @@ async function executeWithRetry( for (let attempt = 0; attempt <= config.maxRetries; attempt++) { try { if (attempt > 0) { - const delay = calculateBackoffDelay(attempt, config); + const delay = calculateBackoffDelay({attempt, config}); config.onRetry(attempt, delay, lastError ?? new HttpGlobalError('Retry attempt')); await sleep(delay); } @@ -425,7 +428,7 @@ async function executeWithRetry( const response = await makeRequest(params); // Check if we should retry this response - if (!shouldRetry(null, response, config, attempt + 1)) { + if (!shouldRetry({error: null, response, config, attempt: attempt + 1})) { return response; } @@ -434,7 +437,7 @@ async function executeWithRetry( } catch (error) { lastError = error instanceof HttpGlobalError ? error : new HttpGlobalError(String(error)); - if (!shouldRetry(lastError, null, config, attempt + 1)) { + if (!shouldRetry({error: lastError, response: null, config, attempt: attempt + 1})) { throw lastError; } } @@ -452,7 +455,11 @@ async function executeWithRetry( * Explicit cases are generated from the error status codes declared by the * input document; undeclared codes fall through to the default handler. */ -function handleHttpError(status: number, statusText: string, body?: unknown): never { +function handleHttpError({status, statusText, body}: { + status: number; + statusText: string; + body?: unknown; +}): never { switch (status) { case 400: throw new HttpError("Bad Request", status, statusText, body); @@ -510,11 +517,11 @@ function extractHeaders(response: HttpResponse): Record { * @param pathTemplate - Path template with {param} placeholders * @param parameters - Parameter object with getChannelWithParameters method */ -function buildUrlWithParameters string }>( - server: string, - pathTemplate: string, - parameters: T -): string { +function buildUrlWithParameters string }>({server, pathTemplate, parameters}: { + server: string; + pathTemplate: string; + parameters: T; +}): string { const path = parameters.getChannelWithParameters(pathTemplate); return `${server}${path}`; } @@ -522,10 +529,10 @@ function buildUrlWithParameters string } | undefined, - additionalHeaders: Record | undefined -): Record { +function applyTypedHeaders({typedHeaders, additionalHeaders}: { + typedHeaders: { marshal: () => string } | undefined; + additionalHeaders: Record | undefined; +}): Record { const headers: Record = { 'Content-Type': 'application/json', ...additionalHeaders @@ -573,12 +580,12 @@ function validateOAuth2Config(auth: OAuth2Auth): void { /** * Handle OAuth2 token flows (client_credentials, password) */ -async function handleOAuth2TokenFlow( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleOAuth2TokenFlow({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.flow || !auth.tokenUrl) return null; const params = new URLSearchParams(); @@ -640,18 +647,18 @@ async function handleOAuth2TokenFlow( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = `Bearer ${tokens.accessToken}`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } /** * Handle OAuth2 token refresh on 401 response */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleTokenRefresh({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; const refreshResponse = await fetch(auth.tokenUrl, { @@ -687,7 +694,7 @@ async function handleTokenRefresh( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = `Bearer ${newTokens.accessToken}`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } // ============================================================================ // Generated HTTP Client Functions @@ -700,6 +707,10 @@ export interface PostV2ConnectContext extends HttpClientContext { /** * Generates a ConnectUrl where the user can be validated and connected. + * + * @param context per-call request configuration + * @param context.payload the request body to send + * @param context.requestHeaders optional headers to send with the request */ async function postV2Connect(context: PostV2ConnectContext): Promise> { // Apply defaults @@ -718,10 +729,10 @@ async function postV2Connect(context: PostV2ConnectContext): Promise undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -812,6 +823,9 @@ export interface GetV2ConnectReferenceIdContext extends HttpClientContext { /** * Translate a ReferenceId into a SafepayAccountId. + * + * @param context per-call request configuration + * @param context.parameters for path and query parameter substitution */ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): Promise> { // Apply defaults @@ -831,11 +845,11 @@ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = buildUrlWithParameters(config.baseUrl, '/v2/connect/{referenceId}', parameters); - url = applyQueryParams(config.additionalQueryParams, url); + let url = buildUrlWithParameters({server: config.baseUrl, pathTemplate: '/v2/connect/{referenceId}', parameters}); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -860,7 +874,7 @@ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -869,7 +883,7 @@ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -878,7 +892,7 @@ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -890,7 +904,7 @@ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -925,6 +939,9 @@ export interface GetV2UsersSafepayAccountIdBankAccountsContext extends HttpClien /** * Returns the bank accounts registered for a Safepay account. + * + * @param context per-call request configuration + * @param context.parameters for path and query parameter substitution */ async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepayAccountIdBankAccountsContext): Promise> { // Apply defaults @@ -944,11 +961,11 @@ async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepay let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = buildUrlWithParameters(config.baseUrl, '/v2/users/{safepayAccountId}/bank-accounts', parameters); - url = applyQueryParams(config.additionalQueryParams, url); + let url = buildUrlWithParameters({server: config.baseUrl, pathTemplate: '/v2/users/{safepayAccountId}/bank-accounts', parameters}); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -973,7 +990,7 @@ async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepay try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -982,7 +999,7 @@ async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepay // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -991,7 +1008,7 @@ async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepay // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -1003,7 +1020,7 @@ async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepay // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response diff --git a/examples/openapi-http-server/src/generated/http_server.ts b/examples/openapi-http-server/src/generated/http_server.ts index 48a8c5dd..e3e50bdb 100644 --- a/examples/openapi-http-server/src/generated/http_server.ts +++ b/examples/openapi-http-server/src/generated/http_server.ts @@ -228,6 +228,12 @@ export interface RegisterPostV2ConnectContext extends HttpServerContext { /** * Generates a ConnectUrl where the user can be validated and connected. + * + * @param context the handler registration context + * @param context.router the Express router to mount the handler on + * @param context.callback invoked for each request; its return value becomes the response + * @param context.callback.body the deserialized request body + * @param context.callback.requestHeaders deserialized from the request headers */ function registerPostV2Connect(context: RegisterPostV2ConnectContext): void { const validator = PostV2ConnectRequest.createValidator(); @@ -276,6 +282,11 @@ export interface RegisterGetV2ConnectReferenceIdContext extends HttpServerContex /** * Translate a ReferenceId into a SafepayAccountId. + * + * @param context the handler registration context + * @param context.router the Express router to mount the handler on + * @param context.callback invoked for each request; its return value becomes the response + * @param context.callback.parameters extracted from the request path and query */ function registerGetV2ConnectReferenceId(context: RegisterGetV2ConnectReferenceIdContext): void { context.router.get('/v2/connect/:referenceId', async (request: Request, response: Response, next: NextFunction) => { @@ -314,6 +325,11 @@ export interface RegisterGetV2UsersSafepayAccountIdBankAccountsContext extends H /** * Returns the bank accounts registered for a Safepay account. + * + * @param context the handler registration context + * @param context.router the Express router to mount the handler on + * @param context.callback invoked for each request; its return value becomes the response + * @param context.callback.parameters extracted from the request path and query */ function registerGetV2UsersSafepayAccountIdBankAccounts(context: RegisterGetV2UsersSafepayAccountIdBankAccountsContext): void { context.router.get('/v2/users/:safepayAccountId/bank-accounts', async (request: Request, response: Response, next: NextFunction) => { diff --git a/src/codegen/generators/typescript/channels/protocols/http/client.ts b/src/codegen/generators/typescript/channels/protocols/http/client.ts index 430d91f8..7a707961 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/client.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/client.ts @@ -2,6 +2,7 @@ * Generates HTTP client functions for individual API operations. * Each operation gets a typed function with request/response handling. */ +import {ConstrainedObjectModel} from '@asyncapi/modelina'; import {HttpRenderType} from '../../../../../types'; import {pascalCase} from '../../../utils'; import {ChannelFunctionTypes, RenderHttpParameters} from '../../types'; @@ -66,21 +67,58 @@ export function renderHttpFetchClient({ // Determine if operation has path parameters or headers const hasParameters = channelParameters !== undefined; const hasHeaders = channelHeaders !== undefined; + // An OpenAPI operation may declare only optional query parameters, in which + // case forcing the caller to pass `{parameters: {}}` is pure noise. AsyncAPI + // channel parameters are always required, so that path is unaffected. + const parametersOptional = + hasParameters && !hasRequiredProperty(channelParameters); + const headersType = getHeaderTypeAndModule(channelHeaders).headerType; + const hasRequestBody = + payloadInputType !== undefined && + ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()); + // Nothing in the context is required, so the caller can omit it entirely. + const contextOptional = + !hasRequestBody && (!hasParameters || parametersOptional); // Generate the context interface (extends HttpClientContext) - const contextInterface = generateContextInterface( - contextInterfaceName, - payloadInputType, - channelParameters?.type, - getHeaderTypeAndModule(channelHeaders).headerType, + const contextInterface = generateContextInterface({ + interfaceName: contextInterfaceName, + payloadType: payloadInputType, + parametersType: channelParameters?.type, + parametersOptional, + headersType, method - ); + }); - // Generate JSDoc for the function + // Generate JSDoc for the function. The single `context` argument is an object, + // so its fields are documented as `@param context.` — the HTTP shape of + // the per-parameter JSDoc every other protocol emits. const jsDoc = renderChannelJSDoc({ description, deprecated, - fallbackDescription: `HTTP ${method} request to ${requestTopic}` + fallbackDescription: `HTTP ${method} request to ${requestTopic}`, + parameters: [ + {jsDoc: ' * @param context per-call request configuration'}, + ...(hasRequestBody + ? [{jsDoc: ' * @param context.payload the request body to send'}] + : []), + ...(hasParameters + ? [ + { + jsDoc: + ' * @param context.parameters for path and query parameter substitution' + } + ] + : []), + ...(headersType + ? [ + { + jsDoc: + ' * @param context.requestHeaders optional headers to send with the request' + } + ] + : []) + ] }); // Generate the function implementation @@ -95,9 +133,13 @@ export function renderHttpFetchClient({ requestMessageModule, requestTopic, hasParameters, - parameterModelName: channelParameters?.name, + parametersOptional, + contextOptional, + // `type` — not `name` — is how the model is written in generated code, and + // it is what every other protocol renders its `instanceof` guard against. + parameterModelName: channelParameters?.type, hasHeaders, - headersType: getHeaderTypeAndModule(channelHeaders).headerType, + headersType, hasSerializeHeaders, method, servers, @@ -119,10 +161,19 @@ ${functionCode}`; functionName, dependencies: [], functionType: ChannelFunctionTypes.HTTP_CLIENT, - parameterType: channelParameters?.name + parameterType: channelParameters?.type, + contextOptional }; } +/** + * Whether the model declares at least one required property. A parameter model + * without one can be omitted entirely by the caller. + */ +function hasRequiredProperty(model: ConstrainedObjectModel): boolean { + return Object.values(model.properties).some((property) => property.required); +} + /** * Generate the statements that read the response body and unmarshal it. * @@ -200,13 +251,21 @@ function resolveReplyType({ /** * Generate the context interface for an HTTP operation */ -function generateContextInterface( - interfaceName: string, - payloadType: string | undefined, - parametersType: string | undefined, - headersType: string | undefined, - method: string -): string { +function generateContextInterface({ + interfaceName, + payloadType, + parametersType, + parametersOptional, + headersType, + method +}: { + interfaceName: string; + payloadType: string | undefined; + parametersType: string | undefined; + parametersOptional: boolean; + headersType: string | undefined; + method: string; +}): string { const fields: string[] = []; // Add payload field for methods that have a body. For object payloads @@ -216,13 +275,16 @@ function generateContextInterface( fields.push(` payload: ${payloadType};`); } - // Add parameters field if the operation has path parameters. The field - // accepts either a plain object satisfying the parameter interface + // Add parameters field if the operation has path or query parameters. The + // field accepts either a plain object satisfying the parameter interface // (ergonomic) or a concrete parameter class instance (rich behavior); the // function body normalizes it to an instance before use — the normalized // instance still exposes getChannelWithParameters for buildUrlWithParameters. + // It is optional when every parameter the operation declares is optional. if (parametersType) { - fields.push(` parameters: ${parameterUnionType(parametersType)};`); + fields.push( + ` parameters${parametersOptional ? '?' : ''}: ${parameterUnionType(parametersType)};` + ); } // Emit requestHeaders only when the spec defines operation headers so the @@ -255,10 +317,41 @@ function generateHeadersInit(params: { return `let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders, ...(context.requestHeaders ? serialize${headersType}Headers(context.requestHeaders) : {}) } as Record;`; } return `let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) + ? applyTypedHeaders({typedHeaders: context.requestHeaders, additionalHeaders: config.additionalHeaders}) : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record;`; } +/** + * Normalize the user-provided parameters (interface object or class instance) + * to a concrete class instance so the URL builder gets the rich behavior. + * + * When every parameter is optional the caller may omit the field entirely, so + * the input is defaulted to `{}` through a named local first — the model + * constructor reads its fields off the input and would throw on `undefined`, + * and a local is what lets TypeScript narrow the `instanceof` guard. + */ +function renderParameterSetup({ + modelName, + parametersOptional +}: { + modelName: string; + parametersOptional: boolean; +}): string { + if (!parametersOptional) { + return ` ${renderParameterNormalization({ + modelName, + source: 'context.parameters', + target: 'parameters' + })}`; + } + return ` const parameterInput = context.parameters ?? {}; + ${renderParameterNormalization({ + modelName, + source: 'parameterInput', + target: 'parameters' + })}`; +} + /** * Generate the function implementation */ @@ -274,6 +367,8 @@ function generateFunctionImplementation(params: { requestMessageModule: string | undefined; requestTopic: string; hasParameters: boolean; + parametersOptional: boolean; + contextOptional: boolean; parameterModelName: string | undefined; hasHeaders: boolean; headersType: string | undefined; @@ -295,6 +390,8 @@ function generateFunctionImplementation(params: { requestMessageModule, requestTopic, hasParameters, + parametersOptional, + contextOptional, parameterModelName, hasHeaders, headersType, @@ -311,20 +408,17 @@ function generateFunctionImplementation(params: { const hasBody = messageType && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()); - // Normalize the user-provided parameters (interface object or class instance) - // to a concrete class instance so the URL builder gets the rich behavior. const parameterNormalization = hasParameters && parameterModelName - ? ` ${renderParameterNormalization({ + ? `${renderParameterSetup({ modelName: parameterModelName, - source: 'context.parameters', - target: 'parameters' + parametersOptional })}\n\n` : ''; // Generate URL building code const urlBuildCode = hasParameters - ? `let url = buildUrlWithParameters(config.baseUrl, '${requestTopic}', parameters);` + ? `let url = buildUrlWithParameters({server: config.baseUrl, pathTemplate: '${requestTopic}', parameters});` : `let url = \`\${config.baseUrl}${requestTopic}\`;`; // Generate headers initialization @@ -355,7 +449,7 @@ function generateFunctionImplementation(params: { }); // Generate default context for optional context parameter - const contextDefault = !hasBody && !hasParameters ? ' = {}' : ''; + const contextDefault = contextOptional ? ' = {}' : ''; // OAuth2 request handling is only emitted when the API actually defines an // OAuth2 scheme; otherwise these branches reference fields/functions that the @@ -373,7 +467,7 @@ function generateFunctionImplementation(params: { ? ` // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -382,7 +476,7 @@ function generateFunctionImplementation(params: { // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -406,10 +500,10 @@ ${parameterNormalization}${oauth2ValidateBlock} // Build headers // Build URL ${urlBuildCode} - url = applyQueryParams(config.additionalQueryParams, url); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -434,7 +528,7 @@ ${parameterNormalization}${oauth2ValidateBlock} // Build headers try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -444,7 +538,7 @@ ${oauth2TokenBlock} // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response diff --git a/src/codegen/generators/typescript/channels/protocols/http/common-types.ts b/src/codegen/generators/typescript/channels/protocols/http/common-types.ts index e8afe452..200f0a2d 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/common-types.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/common-types.ts @@ -290,11 +290,11 @@ const defaultMakeRequest = async (params: HttpRequestParams): Promise, - url: string -): { headers: Record; url: string } { +function applyAuth({auth, headers, url}: { + auth: AuthConfig | undefined; + headers: Record; + url: string; +}): { headers: Record; url: string } { if (!auth) return { headers, url }; switch (auth.type) { @@ -307,7 +307,10 @@ ${applyAuthCases} /** * Apply query parameters to URL */ -function applyQueryParams(queryParams: Record | undefined, url: string): string { +function applyQueryParams({queryParams, url}: { + queryParams: Record | undefined; + url: string; +}): string { if (!queryParams) return url; const params = new URLSearchParams(); @@ -334,10 +337,10 @@ function sleep(ms: number): Promise { /** * Calculate delay for exponential backoff */ -function calculateBackoffDelay( - attempt: number, - config: Required -): number { +function calculateBackoffDelay({attempt, config}: { + attempt: number; + config: Required; +}): number { const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt - 1); return Math.min(delay, config.maxDelayMs); } @@ -345,12 +348,12 @@ function calculateBackoffDelay( /** * Determine if a request should be retried based on error/response */ -function shouldRetry( - error: HttpGlobalError | null, - response: HttpResponse | null, - config: Required, - attempt: number -): boolean { +function shouldRetry({error, response, config, attempt}: { + error: HttpGlobalError | null; + response: HttpResponse | null; + config: Required; + attempt: number; +}): boolean { if (attempt >= config.maxRetries) return false; if (error && config.retryOnNetworkError) return true; @@ -363,11 +366,11 @@ function shouldRetry( /** * Execute request with retry logic */ -async function executeWithRetry( - params: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function executeWithRetry({params, makeRequest, retryConfig}: { + params: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { const config = { ...DEFAULT_RETRY_CONFIG, ...retryConfig }; let lastError: HttpGlobalError | null = null; let lastResponse: HttpResponse | null = null; @@ -375,7 +378,7 @@ async function executeWithRetry( for (let attempt = 0; attempt <= config.maxRetries; attempt++) { try { if (attempt > 0) { - const delay = calculateBackoffDelay(attempt, config); + const delay = calculateBackoffDelay({attempt, config}); config.onRetry(attempt, delay, lastError ?? new HttpGlobalError('Retry attempt')); await sleep(delay); } @@ -383,7 +386,7 @@ async function executeWithRetry( const response = await makeRequest(params); // Check if we should retry this response - if (!shouldRetry(null, response, config, attempt + 1)) { + if (!shouldRetry({error: null, response, config, attempt: attempt + 1})) { return response; } @@ -392,7 +395,7 @@ async function executeWithRetry( } catch (error) { lastError = error instanceof HttpGlobalError ? error : new HttpGlobalError(String(error)); - if (!shouldRetry(lastError, null, config, attempt + 1)) { + if (!shouldRetry({error: lastError, response: null, config, attempt: attempt + 1})) { throw lastError; } } @@ -410,7 +413,11 @@ async function executeWithRetry( * Explicit cases are generated from the error status codes declared by the * input document; undeclared codes fall through to the default handler. */ -function handleHttpError(status: number, statusText: string, body?: unknown): never { +function handleHttpError({status, statusText, body}: { + status: number; + statusText: string; + body?: unknown; +}): never { ${renderHandleHttpErrorBody(errorStatusCodes)} } @@ -461,11 +468,11 @@ function extractHeaders(response: HttpResponse): Record { * @param pathTemplate - Path template with {param} placeholders * @param parameters - Parameter object with getChannelWithParameters method */ -function buildUrlWithParameters string }>( - server: string, - pathTemplate: string, - parameters: T -): string { +function buildUrlWithParameters string }>({server, pathTemplate, parameters}: { + server: string; + pathTemplate: string; + parameters: T; +}): string { const path = parameters.getChannelWithParameters(pathTemplate); return \`\${server}\${path}\`; } @@ -473,10 +480,10 @@ function buildUrlWithParameters string } | undefined, - additionalHeaders: Record | undefined -): Record { +function applyTypedHeaders({typedHeaders, additionalHeaders}: { + typedHeaders: { marshal: () => string } | undefined; + additionalHeaders: Record | undefined; +}): Record { const headers: Record = { 'Content-Type': 'application/json', ...additionalHeaders diff --git a/src/codegen/generators/typescript/channels/protocols/http/security.ts b/src/codegen/generators/typescript/channels/protocols/http/security.ts index 4334c18b..8ed20da8 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/security.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/security.ts @@ -490,12 +490,12 @@ function validateOAuth2Config(auth: OAuth2Auth): void { /** * Handle OAuth2 token flows (client_credentials, password) */ -async function handleOAuth2TokenFlow( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleOAuth2TokenFlow({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.flow || !auth.tokenUrl) return null; const params = new URLSearchParams(); @@ -557,18 +557,18 @@ async function handleOAuth2TokenFlow( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = \`Bearer \${tokens.accessToken}\`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } /** * Handle OAuth2 token refresh on 401 response */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleTokenRefresh({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; const refreshResponse = await fetch(auth.tokenUrl, { @@ -604,6 +604,6 @@ async function handleTokenRefresh( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = \`Bearer \${newTokens.accessToken}\`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); }`; } diff --git a/src/codegen/generators/typescript/channels/protocols/http/server.ts b/src/codegen/generators/typescript/channels/protocols/http/server.ts index 470ec06c..addf8b3d 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/server.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/server.ts @@ -64,10 +64,48 @@ export function renderHttpServerRegister({ parametersType: channelParameters?.type, headersType: usesHeaders ? headerType : undefined }); + // The single `context` argument is an object, so its fields are documented as + // `@param context.` — the HTTP shape of the per-parameter JSDoc every + // other protocol emits. const jsDoc = renderChannelJSDoc({ description, deprecated, - fallbackDescription: `Registers an HTTP ${method.toUpperCase()} handler for ${requestTopic}` + fallbackDescription: `Registers an HTTP ${method.toUpperCase()} handler for ${requestTopic}`, + parameters: [ + {jsDoc: ' * @param context the handler registration context'}, + { + jsDoc: + ' * @param context.router the Express router to mount the handler on' + }, + { + jsDoc: + ' * @param context.callback invoked for each request; its return value becomes the response' + }, + ...(hasBody + ? [ + { + jsDoc: + ' * @param context.callback.body the deserialized request body' + } + ] + : []), + ...(channelParameters + ? [ + { + jsDoc: + ' * @param context.callback.parameters extracted from the request path and query' + } + ] + : []), + ...(usesHeaders + ? [ + { + jsDoc: + ' * @param context.callback.requestHeaders deserialized from the request headers' + } + ] + : []) + ] }); const implementation = generateRegisterImplementation({ functionName, @@ -79,7 +117,9 @@ export function renderHttpServerRegister({ requestMessageType, requestMessageModule, includeValidation: payloadGenerator.generator.includeValidation === true, - parameterModelName: channelParameters?.name, + // `type` — not `name` — is how the model is written in generated code, and + // it is what every other protocol renders its model references against. + parameterModelName: channelParameters?.type, headersType: usesHeaders ? headerType : undefined, jsDoc }); @@ -97,7 +137,7 @@ ${implementation}`, "import { NextFunction, Request, Response, Router } from 'express';" ], functionType: ChannelFunctionTypes.HTTP_SERVER, - parameterType: channelParameters?.name, + parameterType: channelParameters?.type, headerType: usesHeaders ? headerType : undefined }; } diff --git a/src/codegen/generators/typescript/channels/types.ts b/src/codegen/generators/typescript/channels/types.ts index 83aa3f43..ca367335 100644 --- a/src/codegen/generators/typescript/channels/types.ts +++ b/src/codegen/generators/typescript/channels/types.ts @@ -230,6 +230,12 @@ export type TypeScriptChannelRenderedFunctionType = { headerType?: string; replyType?: string; parameterType?: string; + /** + * Whether the rendered function's `context` argument can be omitted entirely + * (HTTP only). Read by the client generator so its wrapper method declares the + * same optionality as the channel function it delegates to. + */ + contextOptional?: boolean; /** * Grouping metadata consumed only by `finalizeGeneration` when `organization` * is `tag` or `path`. `tags` is the ordered list of tag names for the source diff --git a/src/codegen/generators/typescript/channels/utils.ts b/src/codegen/generators/typescript/channels/utils.ts index bd7c63f5..6df00479 100644 --- a/src/codegen/generators/typescript/channels/utils.ts +++ b/src/codegen/generators/typescript/channels/utils.ts @@ -593,6 +593,7 @@ type RenderForExternal = Pick< headerType?: string; replyType?: string; parameterType?: string; + contextOptional?: boolean; }; /** @@ -635,6 +636,7 @@ export function addRendersToExternal({ headerType: value.headerType, replyType: value.replyType, parameterType: value.parameterType ?? parameter?.type, + contextOptional: value.contextOptional, tags: value.tags, pathSegments: value.pathSegments, method: value.method diff --git a/src/codegen/generators/typescript/client/protocols/http.ts b/src/codegen/generators/typescript/client/protocols/http.ts index 02ec46e3..fbe225b4 100644 --- a/src/codegen/generators/typescript/client/protocols/http.ts +++ b/src/codegen/generators/typescript/client/protocols/http.ts @@ -72,10 +72,10 @@ function renderHttpClientMethod( ): string { const {functionName} = func; const contextType = `http_client.${pascalCase(functionName)}Context`; - // Operations without a request body and without path parameters accept an - // entirely optional context, so the method call can be argument-free. - const needsContext = Boolean(func.messageType) || Boolean(func.parameterType); - const contextDefault = needsContext ? '' : ' = {}'; + // Mirror the channel function's own optionality: an operation with no request + // body and no required parameters accepts an entirely optional context, so the + // wrapper method can be called argument-free too. + const contextDefault = func.contextOptional ? ' = {}' : ''; return ` /** * Invokes the \`${functionName}\` operation using this client's shared configuration. diff --git a/src/codegen/types.ts b/src/codegen/types.ts index 2734325d..c5845b22 100644 --- a/src/codegen/types.ts +++ b/src/codegen/types.ts @@ -253,6 +253,13 @@ export interface HttpRenderType { * type reaches `TypeScriptChannelRenderedFunctionType`. */ headerType?: string; + /** + * Whether the rendered function's `context` argument can be omitted entirely + * (no request body and no required parameters). Read by the client generator + * so its wrapper method declares the same optionality as the channel function + * it delegates to. + */ + contextOptional?: boolean; } const SCHEMA_DESCRIPTION = diff --git a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap index 2f9b1b9a..95945175 100644 --- a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap @@ -286,11 +286,11 @@ const defaultMakeRequest = async (params: HttpRequestParams): Promise, - url: string -): { headers: Record; url: string } { +function applyAuth({auth, headers, url}: { + auth: AuthConfig | undefined; + headers: Record; + url: string; +}): { headers: Record; url: string } { if (!auth) return { headers, url }; switch (auth.type) { @@ -325,7 +325,10 @@ function applyAuth( /** * Apply query parameters to URL */ -function applyQueryParams(queryParams: Record | undefined, url: string): string { +function applyQueryParams({queryParams, url}: { + queryParams: Record | undefined; + url: string; +}): string { if (!queryParams) return url; const params = new URLSearchParams(); @@ -352,10 +355,10 @@ function sleep(ms: number): Promise { /** * Calculate delay for exponential backoff */ -function calculateBackoffDelay( - attempt: number, - config: Required -): number { +function calculateBackoffDelay({attempt, config}: { + attempt: number; + config: Required; +}): number { const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt - 1); return Math.min(delay, config.maxDelayMs); } @@ -363,12 +366,12 @@ function calculateBackoffDelay( /** * Determine if a request should be retried based on error/response */ -function shouldRetry( - error: HttpGlobalError | null, - response: HttpResponse | null, - config: Required, - attempt: number -): boolean { +function shouldRetry({error, response, config, attempt}: { + error: HttpGlobalError | null; + response: HttpResponse | null; + config: Required; + attempt: number; +}): boolean { if (attempt >= config.maxRetries) return false; if (error && config.retryOnNetworkError) return true; @@ -381,11 +384,11 @@ function shouldRetry( /** * Execute request with retry logic */ -async function executeWithRetry( - params: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function executeWithRetry({params, makeRequest, retryConfig}: { + params: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { const config = { ...DEFAULT_RETRY_CONFIG, ...retryConfig }; let lastError: HttpGlobalError | null = null; let lastResponse: HttpResponse | null = null; @@ -393,7 +396,7 @@ async function executeWithRetry( for (let attempt = 0; attempt <= config.maxRetries; attempt++) { try { if (attempt > 0) { - const delay = calculateBackoffDelay(attempt, config); + const delay = calculateBackoffDelay({attempt, config}); config.onRetry(attempt, delay, lastError ?? new HttpGlobalError('Retry attempt')); await sleep(delay); } @@ -401,7 +404,7 @@ async function executeWithRetry( const response = await makeRequest(params); // Check if we should retry this response - if (!shouldRetry(null, response, config, attempt + 1)) { + if (!shouldRetry({error: null, response, config, attempt: attempt + 1})) { return response; } @@ -410,7 +413,7 @@ async function executeWithRetry( } catch (error) { lastError = error instanceof HttpGlobalError ? error : new HttpGlobalError(String(error)); - if (!shouldRetry(lastError, null, config, attempt + 1)) { + if (!shouldRetry({error: lastError, response: null, config, attempt: attempt + 1})) { throw lastError; } } @@ -428,7 +431,11 @@ async function executeWithRetry( * Explicit cases are generated from the error status codes declared by the * input document; undeclared codes fall through to the default handler. */ -function handleHttpError(status: number, statusText: string, body?: unknown): never { +function handleHttpError({status, statusText, body}: { + status: number; + statusText: string; + body?: unknown; +}): never { switch (status) { case 400: throw new HttpError("Bad Request", status, statusText, body); @@ -488,11 +495,11 @@ function extractHeaders(response: HttpResponse): Record { * @param pathTemplate - Path template with {param} placeholders * @param parameters - Parameter object with getChannelWithParameters method */ -function buildUrlWithParameters string }>( - server: string, - pathTemplate: string, - parameters: T -): string { +function buildUrlWithParameters string }>({server, pathTemplate, parameters}: { + server: string; + pathTemplate: string; + parameters: T; +}): string { const path = parameters.getChannelWithParameters(pathTemplate); return \`\${server}\${path}\`; } @@ -500,10 +507,10 @@ function buildUrlWithParameters string } | undefined, - additionalHeaders: Record | undefined -): Record { +function applyTypedHeaders({typedHeaders, additionalHeaders}: { + typedHeaders: { marshal: () => string } | undefined; + additionalHeaders: Record | undefined; +}): Record { const headers: Record = { 'Content-Type': 'application/json', ...additionalHeaders @@ -551,12 +558,12 @@ function validateOAuth2Config(auth: OAuth2Auth): void { /** * Handle OAuth2 token flows (client_credentials, password) */ -async function handleOAuth2TokenFlow( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleOAuth2TokenFlow({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.flow || !auth.tokenUrl) return null; const params = new URLSearchParams(); @@ -618,18 +625,18 @@ async function handleOAuth2TokenFlow( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = \`Bearer \${tokens.accessToken}\`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } /** * Handle OAuth2 token refresh on 401 response */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleTokenRefresh({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; const refreshResponse = await fetch(auth.tokenUrl, { @@ -665,7 +672,7 @@ async function handleTokenRefresh( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = \`Bearer \${newTokens.accessToken}\`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } // ============================================================================ // Generated HTTP Client Functions @@ -677,6 +684,9 @@ export interface AddPetContext extends HttpClientContext { /** * HTTP POST request to /pet + * + * @param context per-call request configuration + * @param context.payload the request body to send */ async function addPet(context: AddPetContext): Promise> { // Apply defaults @@ -695,10 +705,10 @@ async function addPet(context: AddPetContext): Promise> // Build URL let url = \`\${config.baseUrl}/pet\`; - url = applyQueryParams(config.additionalQueryParams, url); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -724,7 +734,7 @@ async function addPet(context: AddPetContext): Promise> try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -733,7 +743,7 @@ async function addPet(context: AddPetContext): Promise> // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -742,7 +752,7 @@ async function addPet(context: AddPetContext): Promise> // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -754,7 +764,7 @@ async function addPet(context: AddPetContext): Promise> // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -789,6 +799,9 @@ export interface UpdatePetContext extends HttpClientContext { /** * HTTP PUT request to /pet + * + * @param context per-call request configuration + * @param context.payload the request body to send */ async function updatePet(context: UpdatePetContext): Promise> { // Apply defaults @@ -807,10 +820,10 @@ async function updatePet(context: UpdatePetContext): Promise undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -902,6 +915,10 @@ export interface FindPetsByStatusAndCategoryContext extends HttpClientContext { /** * Find pets by status and category with additional filtering options + * + * @param context per-call request configuration + * @param context.parameters for path and query parameter substitution + * @param context.requestHeaders optional headers to send with the request */ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryContext): Promise> { // Apply defaults @@ -910,7 +927,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC ...context, }; - const parameters = context.parameters instanceof FindPetsByStatusAndCategoryParameters ? context.parameters : new FindPetsByStatusAndCategoryParameters(context.parameters); + const parameters = context.parameters instanceof Parameter ? context.parameters : new Parameter(context.parameters); // Validate OAuth2 config if present if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { @@ -921,11 +938,11 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders, ...(context.requestHeaders ? serializeFindPetsByStatusAndCategoryHeadersHeaders(context.requestHeaders) : {}) } as Record; // Build URL - let url = buildUrlWithParameters(config.baseUrl, '/pet/findByStatus/{status}/{categoryId}', parameters); - url = applyQueryParams(config.additionalQueryParams, url); + let url = buildUrlWithParameters({server: config.baseUrl, pathTemplate: '/pet/findByStatus/{status}/{categoryId}', parameters}); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -950,7 +967,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -959,7 +976,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -968,7 +985,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -980,7 +997,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -1891,11 +1908,11 @@ const defaultMakeRequest = async (params: HttpRequestParams): Promise, - url: string -): { headers: Record; url: string } { +function applyAuth({auth, headers, url}: { + auth: AuthConfig | undefined; + headers: Record; + url: string; +}): { headers: Record; url: string } { if (!auth) return { headers, url }; switch (auth.type) { @@ -1940,7 +1957,10 @@ function applyAuth( /** * Apply query parameters to URL */ -function applyQueryParams(queryParams: Record | undefined, url: string): string { +function applyQueryParams({queryParams, url}: { + queryParams: Record | undefined; + url: string; +}): string { if (!queryParams) return url; const params = new URLSearchParams(); @@ -1967,10 +1987,10 @@ function sleep(ms: number): Promise { /** * Calculate delay for exponential backoff */ -function calculateBackoffDelay( - attempt: number, - config: Required -): number { +function calculateBackoffDelay({attempt, config}: { + attempt: number; + config: Required; +}): number { const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt - 1); return Math.min(delay, config.maxDelayMs); } @@ -1978,12 +1998,12 @@ function calculateBackoffDelay( /** * Determine if a request should be retried based on error/response */ -function shouldRetry( - error: HttpGlobalError | null, - response: HttpResponse | null, - config: Required, - attempt: number -): boolean { +function shouldRetry({error, response, config, attempt}: { + error: HttpGlobalError | null; + response: HttpResponse | null; + config: Required; + attempt: number; +}): boolean { if (attempt >= config.maxRetries) return false; if (error && config.retryOnNetworkError) return true; @@ -1996,11 +2016,11 @@ function shouldRetry( /** * Execute request with retry logic */ -async function executeWithRetry( - params: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function executeWithRetry({params, makeRequest, retryConfig}: { + params: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { const config = { ...DEFAULT_RETRY_CONFIG, ...retryConfig }; let lastError: HttpGlobalError | null = null; let lastResponse: HttpResponse | null = null; @@ -2008,7 +2028,7 @@ async function executeWithRetry( for (let attempt = 0; attempt <= config.maxRetries; attempt++) { try { if (attempt > 0) { - const delay = calculateBackoffDelay(attempt, config); + const delay = calculateBackoffDelay({attempt, config}); config.onRetry(attempt, delay, lastError ?? new HttpGlobalError('Retry attempt')); await sleep(delay); } @@ -2016,7 +2036,7 @@ async function executeWithRetry( const response = await makeRequest(params); // Check if we should retry this response - if (!shouldRetry(null, response, config, attempt + 1)) { + if (!shouldRetry({error: null, response, config, attempt: attempt + 1})) { return response; } @@ -2025,7 +2045,7 @@ async function executeWithRetry( } catch (error) { lastError = error instanceof HttpGlobalError ? error : new HttpGlobalError(String(error)); - if (!shouldRetry(lastError, null, config, attempt + 1)) { + if (!shouldRetry({error: lastError, response: null, config, attempt: attempt + 1})) { throw lastError; } } @@ -2043,7 +2063,11 @@ async function executeWithRetry( * Explicit cases are generated from the error status codes declared by the * input document; undeclared codes fall through to the default handler. */ -function handleHttpError(status: number, statusText: string, body?: unknown): never { +function handleHttpError({status, statusText, body}: { + status: number; + statusText: string; + body?: unknown; +}): never { throw new HttpError(\`HTTP Error: \${status} \${statusText}\`, status, statusText, body); } @@ -2094,11 +2118,11 @@ function extractHeaders(response: HttpResponse): Record { * @param pathTemplate - Path template with {param} placeholders * @param parameters - Parameter object with getChannelWithParameters method */ -function buildUrlWithParameters string }>( - server: string, - pathTemplate: string, - parameters: T -): string { +function buildUrlWithParameters string }>({server, pathTemplate, parameters}: { + server: string; + pathTemplate: string; + parameters: T; +}): string { const path = parameters.getChannelWithParameters(pathTemplate); return \`\${server}\${path}\`; } @@ -2106,10 +2130,10 @@ function buildUrlWithParameters string } | undefined, - additionalHeaders: Record | undefined -): Record { +function applyTypedHeaders({typedHeaders, additionalHeaders}: { + typedHeaders: { marshal: () => string } | undefined; + additionalHeaders: Record | undefined; +}): Record { const headers: Record = { 'Content-Type': 'application/json', ...additionalHeaders @@ -2157,12 +2181,12 @@ function validateOAuth2Config(auth: OAuth2Auth): void { /** * Handle OAuth2 token flows (client_credentials, password) */ -async function handleOAuth2TokenFlow( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleOAuth2TokenFlow({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.flow || !auth.tokenUrl) return null; const params = new URLSearchParams(); @@ -2224,18 +2248,18 @@ async function handleOAuth2TokenFlow( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = \`Bearer \${tokens.accessToken}\`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } /** * Handle OAuth2 token refresh on 401 response */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleTokenRefresh({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; const refreshResponse = await fetch(auth.tokenUrl, { @@ -2271,7 +2295,7 @@ async function handleTokenRefresh( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = \`Bearer \${newTokens.accessToken}\`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } // ============================================================================ // Generated HTTP Client Functions @@ -2281,6 +2305,8 @@ export interface GetPingRequestContext extends HttpClientContext {} /** * HTTP GET request to /ping + * + * @param context per-call request configuration */ async function getPingRequest(context: GetPingRequestContext = {}): Promise> { // Apply defaults @@ -2299,10 +2325,10 @@ async function getPingRequest(context: GetPingRequestContext = {}): Promise undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response diff --git a/test/codegen/generators/typescript/channels.spec.ts b/test/codegen/generators/typescript/channels.spec.ts index 389ebbe8..99f1fd78 100644 --- a/test/codegen/generators/typescript/channels.spec.ts +++ b/test/codegen/generators/typescript/channels.spec.ts @@ -516,7 +516,7 @@ describe('channels', () => { // `Error` (imported into this same file) cannot shadow it. expect(httpProtocolCode).toContain('export class HttpError extends HttpGlobalError'); expect(httpProtocolCode).toContain( - 'function handleHttpError(status: number, statusText: string, body?: unknown): never {\n throw new HttpError(`HTTP Error: ${status} ${statusText}`, status, statusText, body);\n}' + 'function handleHttpError({status, statusText, body}: {\n status: number;\n statusText: string;\n body?: unknown;\n}): never {\n throw new HttpError(`HTTP Error: ${status} ${statusText}`, status, statusText, body);\n}' ); expect(httpProtocolCode).not.toMatch(/case \d+:/); }); @@ -821,16 +821,21 @@ describe('channels', () => { 'const errorBody = await response.json().catch(() => undefined);' ); - // Operations with path parameters must expose their parameter model name via + // Operations with path parameters must expose their parameter model type via // parameterType so downstream consumers (e.g. README generation) know the // operation requires a `parameters` argument. Operations without parameters // must leave it undefined. Regression guard for the previously hardcoded // `parameterType: undefined`. + // + // The model's `type` is used, matching every other protocol; this fixture + // sets it to `Parameter` while naming the model + // `FindPetsByStatusAndCategoryParameters`, which real Modelina output never + // does (an object model's `type` equals its `name`). const httpFunctions = generatedChannels.renderedFunctions['http_client']; const withParameters = httpFunctions.find( (fn) => fn.functionName === 'findPetsByStatusAndCategory' ); - expect(withParameters?.parameterType).toBe('FindPetsByStatusAndCategoryParameters'); + expect(withParameters?.parameterType).toBe('Parameter'); const withoutParameters = httpFunctions.find( (fn) => fn.functionName !== 'findPetsByStatusAndCategory' ); diff --git a/test/codegen/generators/typescript/channels/__snapshots__/openapi-http-server.spec.ts.snap b/test/codegen/generators/typescript/channels/__snapshots__/openapi-http-server.spec.ts.snap index e6400bff..8a7ed2dd 100644 --- a/test/codegen/generators/typescript/channels/__snapshots__/openapi-http-server.spec.ts.snap +++ b/test/codegen/generators/typescript/channels/__snapshots__/openapi-http-server.spec.ts.snap @@ -223,6 +223,11 @@ export interface RegisterAddPetContext extends HttpServerContext { /** * Add a new pet to the store + * + * @param context the handler registration context + * @param context.router the Express router to mount the handler on + * @param context.callback invoked for each request; its return value becomes the response + * @param context.callback.body the deserialized request body */ function registerAddPet(context: RegisterAddPetContext): void { const validator = AddPetRequest.createValidator(); @@ -271,6 +276,12 @@ export interface RegisterGetPetByIdContext extends HttpServerContext { /** * Registers an HTTP GET handler for /pet/{petId} + * + * @param context the handler registration context + * @param context.router the Express router to mount the handler on + * @param context.callback invoked for each request; its return value becomes the response + * @param context.callback.parameters extracted from the request path and query + * @param context.callback.requestHeaders deserialized from the request headers */ function registerGetPetById(context: RegisterGetPetByIdContext): void { context.router.get('/pet/:petId', async (request: Request, response: Response, next: NextFunction) => { diff --git a/test/codegen/generators/typescript/channels/openapi-http-client-responses.spec.ts b/test/codegen/generators/typescript/channels/openapi-http-client-responses.spec.ts index a0993a26..701c99f6 100644 --- a/test/codegen/generators/typescript/channels/openapi-http-client-responses.spec.ts +++ b/test/codegen/generators/typescript/channels/openapi-http-client-responses.spec.ts @@ -164,7 +164,9 @@ describe('OpenAPI HTTP client response handling', () => { const code = await generateHttpClient(bookstoreSpec); // `bookId` is declared once on the path item, not on the operation. - expect(code).toContain("buildUrlWithParameters(config.baseUrl, '/books/{bookId}'"); + expect(code).toContain( + "buildUrlWithParameters({server: config.baseUrl, pathTemplate: '/books/{bookId}'" + ); expect(code).toContain('DeleteBookParameters'); }); @@ -197,4 +199,59 @@ describe('OpenAPI HTTP client response handling', () => { expect(code).toContain("baseUrl: 'https://api.legacy.example/v1'"); expect(code).not.toContain('http://localhost:3000'); }); + + it('makes the context and its parameters optional when every parameter is optional', async () => { + const optionalParamsSpec = JSON.stringify({ + openapi: '3.0.3', + info: {title: 'Search API', version: '1.0.0'}, + servers: [{url: 'https://api.search.example'}], + paths: { + '/search': { + get: { + operationId: 'search', + parameters: [ + {name: 'q', in: 'query', schema: {type: 'string'}}, + {name: 'limit', in: 'query', schema: {type: 'integer'}} + ], + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: {type: 'object', properties: {id: {type: 'string'}}} + } + } + } + } + } + } + } + }); + + const code = await generateHttpClient(optionalParamsSpec); + + // Every query parameter is optional, so requiring `{parameters: {}}` at the + // call site would be pure noise - the whole context can be omitted. + expect(code).toMatch(/parameters\?: SearchParametersInterface \| SearchParameters;/); + expect(code).toContain('async function search(context: SearchContext = {})'); + // The model constructor reads its fields off the input, so an omitted + // parameters object has to be defaulted before it reaches `new`. + expect(code).toContain('const parameterInput = context.parameters ?? {};'); + expect(code).toContain( + 'const parameters = parameterInput instanceof SearchParameters ? parameterInput : new SearchParameters(parameterInput);' + ); + }); + + it('keeps the parameters field required when a parameter is required', async () => { + const code = await generateHttpClient(bookstoreSpec); + + // `bookId` is a required path parameter, so neither the field nor the + // context may be optional. + expect(code).toContain( + 'parameters: DeleteBookParametersInterface | DeleteBookParameters;' + ); + expect(code).toContain( + 'async function deleteBook(context: DeleteBookContext):' + ); + }); }); diff --git a/test/codegen/generators/typescript/channels/protocols/http/__snapshots__/server.spec.ts.snap b/test/codegen/generators/typescript/channels/protocols/http/__snapshots__/server.spec.ts.snap index 3487cb92..e5cca319 100644 --- a/test/codegen/generators/typescript/channels/protocols/http/__snapshots__/server.spec.ts.snap +++ b/test/codegen/generators/typescript/channels/protocols/http/__snapshots__/server.spec.ts.snap @@ -13,6 +13,10 @@ export interface RegisterHealthContext extends HttpServerContext { /** * Registers an HTTP GET handler for /health + * + * @param context the handler registration context + * @param context.router the Express router to mount the handler on + * @param context.callback invoked for each request; its return value becomes the response */ function registerHealth(context: RegisterHealthContext): void { context.router.get('/health', async (request: Request, response: Response, next: NextFunction) => { @@ -44,6 +48,11 @@ export interface RegisterAddPetContext extends HttpServerContext { /** * Add a new pet to the store + * + * @param context the handler registration context + * @param context.router the Express router to mount the handler on + * @param context.callback invoked for each request; its return value becomes the response + * @param context.callback.body the deserialized request body */ function registerAddPet(context: RegisterAddPetContext): void { context.router.post('/pet', async (request: Request, response: Response, next: NextFunction) => { @@ -88,6 +97,12 @@ export interface RegisterFindPetsByStatusAndCategoryContext extends HttpServerCo /** * Registers an HTTP GET handler for /pet/findByStatus/{status}/{categoryId} + * + * @param context the handler registration context + * @param context.router the Express router to mount the handler on + * @param context.callback invoked for each request; its return value becomes the response + * @param context.callback.parameters extracted from the request path and query + * @param context.callback.requestHeaders deserialized from the request headers */ function registerFindPetsByStatusAndCategory(context: RegisterFindPetsByStatusAndCategoryContext): void { context.router.get('/pet/findByStatus/:status/:categoryId', async (request: Request, response: Response, next: NextFunction) => { diff --git a/test/codegen/generators/typescript/client.spec.ts b/test/codegen/generators/typescript/client.spec.ts index 08f4d941..b3531151 100644 --- a/test/codegen/generators/typescript/client.spec.ts +++ b/test/codegen/generators/typescript/client.spec.ts @@ -155,6 +155,79 @@ describe('client', () => { expect(result.files[0].content).toContain('http_client.getV2Documents({...this.config, ...context})'); }); + it('mirrors the channel function context optionality on the wrapper method', async () => { + const payloadModel = new OutputModel('', new ConstrainedAnyModel('', undefined, {}, 'Payload'), '', {models: {}, originalInput: undefined}, []); + const parametersDependency: TypeScriptParameterRenderType = { + channelModels: {}, + generator: {outputPath: './test'} as any, + files: [] + }; + const payloadsDependency: TypeScriptPayloadRenderType = { + channelModels: {}, + operationModels: { + listThings: { + messageModel: payloadModel, + messageType: 'ListThingsResponse_200', + } + }, + otherModels: [], + generator: {outputPath: './test'} as any, + files: [] + }; + const headersDependency: TypeScriptHeadersRenderType = { + channelModels: {}, + generator: {outputPath: './test'} as any, + headerFunctions: {}, + files: [] + }; + const channelsDependency: TypeScriptChannelRenderType = { + payloadRender: payloadsDependency, + result: '', + parameterRender: parametersDependency, + headerRender: headersDependency, + renderedFunctions: { + http_client: [ + // Nothing required, so the channel function takes `context = {}` and + // the wrapper must be callable with no argument too. + { + functionName: 'listThings', + functionType: ChannelFunctionTypes.HTTP_CLIENT, + messageType: '', + replyType: 'ListThingsResponse_200', + contextOptional: true + }, + // A required parameter makes the context mandatory on both sides; + // defaulting the wrapper to `{}` would hide a missing argument. + { + functionName: 'getThing', + functionType: ChannelFunctionTypes.HTTP_CLIENT, + messageType: '', + replyType: 'GetThingResponse_200', + parameterType: 'GetThingParameters', + contextOptional: false + } + ] + }, + generator: defaultTypeScriptChannelsGenerator, + protocolFiles: {}, + files: [] + }; + const result = await generateTypeScriptClient({ + generator: {...defaultTypeScriptClientGenerator, protocols: ['http']}, + inputType: 'openapi', + openapiDocument: {info: {title: 'Thing API'}} as any, + dependencyOutputs: { + 'parameters-typescript': parametersDependency, + 'payloads-typescript': payloadsDependency, + 'channels-typescript': channelsDependency + } + }); + const content = result.files[0].content; + expect(content).toContain('public async listThings(context: http_client.ListThingsContext = {})'); + expect(content).toContain('public async getThing(context: http_client.GetThingContext)'); + expect(content).not.toContain('GetThingContext = {}'); + }); + it('does not emit a client for a protocol with no rendered functions', async () => { const parametersDependency: TypeScriptParameterRenderType = { channelModels: {}, diff --git a/test/runtime/typescript/src/openapi-path-organization/channels/http_client.ts b/test/runtime/typescript/src/openapi-path-organization/channels/http_client.ts index 38aabc50..c65caf0a 100644 --- a/test/runtime/typescript/src/openapi-path-organization/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi-path-organization/channels/http_client.ts @@ -283,11 +283,11 @@ const defaultMakeRequest = async (params: HttpRequestParams): Promise, - url: string -): { headers: Record; url: string } { +function applyAuth({auth, headers, url}: { + auth: AuthConfig | undefined; + headers: Record; + url: string; +}): { headers: Record; url: string } { if (!auth) return { headers, url }; switch (auth.type) { @@ -322,7 +322,10 @@ function applyAuth( /** * Apply query parameters to URL */ -function applyQueryParams(queryParams: Record | undefined, url: string): string { +function applyQueryParams({queryParams, url}: { + queryParams: Record | undefined; + url: string; +}): string { if (!queryParams) return url; const params = new URLSearchParams(); @@ -349,10 +352,10 @@ function sleep(ms: number): Promise { /** * Calculate delay for exponential backoff */ -function calculateBackoffDelay( - attempt: number, - config: Required -): number { +function calculateBackoffDelay({attempt, config}: { + attempt: number; + config: Required; +}): number { const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt - 1); return Math.min(delay, config.maxDelayMs); } @@ -360,12 +363,12 @@ function calculateBackoffDelay( /** * Determine if a request should be retried based on error/response */ -function shouldRetry( - error: HttpGlobalError | null, - response: HttpResponse | null, - config: Required, - attempt: number -): boolean { +function shouldRetry({error, response, config, attempt}: { + error: HttpGlobalError | null; + response: HttpResponse | null; + config: Required; + attempt: number; +}): boolean { if (attempt >= config.maxRetries) return false; if (error && config.retryOnNetworkError) return true; @@ -378,11 +381,11 @@ function shouldRetry( /** * Execute request with retry logic */ -async function executeWithRetry( - params: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function executeWithRetry({params, makeRequest, retryConfig}: { + params: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { const config = { ...DEFAULT_RETRY_CONFIG, ...retryConfig }; let lastError: HttpGlobalError | null = null; let lastResponse: HttpResponse | null = null; @@ -390,7 +393,7 @@ async function executeWithRetry( for (let attempt = 0; attempt <= config.maxRetries; attempt++) { try { if (attempt > 0) { - const delay = calculateBackoffDelay(attempt, config); + const delay = calculateBackoffDelay({attempt, config}); config.onRetry(attempt, delay, lastError ?? new HttpGlobalError('Retry attempt')); await sleep(delay); } @@ -398,7 +401,7 @@ async function executeWithRetry( const response = await makeRequest(params); // Check if we should retry this response - if (!shouldRetry(null, response, config, attempt + 1)) { + if (!shouldRetry({error: null, response, config, attempt: attempt + 1})) { return response; } @@ -407,7 +410,7 @@ async function executeWithRetry( } catch (error) { lastError = error instanceof HttpGlobalError ? error : new HttpGlobalError(String(error)); - if (!shouldRetry(lastError, null, config, attempt + 1)) { + if (!shouldRetry({error: lastError, response: null, config, attempt: attempt + 1})) { throw lastError; } } @@ -425,7 +428,11 @@ async function executeWithRetry( * Explicit cases are generated from the error status codes declared by the * input document; undeclared codes fall through to the default handler. */ -function handleHttpError(status: number, statusText: string, body?: unknown): never { +function handleHttpError({status, statusText, body}: { + status: number; + statusText: string; + body?: unknown; +}): never { switch (status) { case 400: throw new HttpError("Bad Request", status, statusText, body); @@ -485,11 +492,11 @@ function extractHeaders(response: HttpResponse): Record { * @param pathTemplate - Path template with {param} placeholders * @param parameters - Parameter object with getChannelWithParameters method */ -function buildUrlWithParameters string }>( - server: string, - pathTemplate: string, - parameters: T -): string { +function buildUrlWithParameters string }>({server, pathTemplate, parameters}: { + server: string; + pathTemplate: string; + parameters: T; +}): string { const path = parameters.getChannelWithParameters(pathTemplate); return `${server}${path}`; } @@ -497,10 +504,10 @@ function buildUrlWithParameters string } | undefined, - additionalHeaders: Record | undefined -): Record { +function applyTypedHeaders({typedHeaders, additionalHeaders}: { + typedHeaders: { marshal: () => string } | undefined; + additionalHeaders: Record | undefined; +}): Record { const headers: Record = { 'Content-Type': 'application/json', ...additionalHeaders @@ -548,12 +555,12 @@ function validateOAuth2Config(auth: OAuth2Auth): void { /** * Handle OAuth2 token flows (client_credentials, password) */ -async function handleOAuth2TokenFlow( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleOAuth2TokenFlow({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.flow || !auth.tokenUrl) return null; const params = new URLSearchParams(); @@ -615,18 +622,18 @@ async function handleOAuth2TokenFlow( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = `Bearer ${tokens.accessToken}`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } /** * Handle OAuth2 token refresh on 401 response */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleTokenRefresh({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; const refreshResponse = await fetch(auth.tokenUrl, { @@ -662,7 +669,7 @@ async function handleTokenRefresh( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = `Bearer ${newTokens.accessToken}`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } // ============================================================================ // Generated HTTP Client Functions @@ -674,6 +681,9 @@ export interface AddPetContext extends HttpClientContext { /** * HTTP POST request to /pet + * + * @param context per-call request configuration + * @param context.payload the request body to send */ async function addPet(context: AddPetContext): Promise> { // Apply defaults @@ -692,10 +702,10 @@ async function addPet(context: AddPetContext): Promise> // Build URL let url = `${config.baseUrl}/pet`; - url = applyQueryParams(config.additionalQueryParams, url); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -721,7 +731,7 @@ async function addPet(context: AddPetContext): Promise> try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -730,7 +740,7 @@ async function addPet(context: AddPetContext): Promise> // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -739,7 +749,7 @@ async function addPet(context: AddPetContext): Promise> // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -751,7 +761,7 @@ async function addPet(context: AddPetContext): Promise> // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -786,6 +796,9 @@ export interface UpdatePetContext extends HttpClientContext { /** * HTTP PUT request to /pet + * + * @param context per-call request configuration + * @param context.payload the request body to send */ async function updatePet(context: UpdatePetContext): Promise> { // Apply defaults @@ -804,10 +817,10 @@ async function updatePet(context: UpdatePetContext): Promise undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -899,6 +912,10 @@ export interface FindPetsByStatusAndCategoryContext extends HttpClientContext { /** * Find pets by status and category with additional filtering options + * + * @param context per-call request configuration + * @param context.parameters for path and query parameter substitution + * @param context.requestHeaders optional headers to send with the request */ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryContext): Promise> { // Apply defaults @@ -918,11 +935,11 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders, ...(context.requestHeaders ? serializeFindPetsByStatusAndCategoryHeadersHeaders(context.requestHeaders) : {}) } as Record; // Build URL - let url = buildUrlWithParameters(config.baseUrl, '/pet/findByStatus/{status}/{categoryId}', parameters); - url = applyQueryParams(config.additionalQueryParams, url); + let url = buildUrlWithParameters({server: config.baseUrl, pathTemplate: '/pet/findByStatus/{status}/{categoryId}', parameters}); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -947,7 +964,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -956,7 +973,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -965,7 +982,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -977,7 +994,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response diff --git a/test/runtime/typescript/src/openapi-primitive/channels/http_client.ts b/test/runtime/typescript/src/openapi-primitive/channels/http_client.ts index c97e405e..6918a649 100644 --- a/test/runtime/typescript/src/openapi-primitive/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi-primitive/channels/http_client.ts @@ -290,11 +290,11 @@ const defaultMakeRequest = async (params: HttpRequestParams): Promise, - url: string -): { headers: Record; url: string } { +function applyAuth({auth, headers, url}: { + auth: AuthConfig | undefined; + headers: Record; + url: string; +}): { headers: Record; url: string } { if (!auth) return { headers, url }; switch (auth.type) { @@ -339,7 +339,10 @@ function applyAuth( /** * Apply query parameters to URL */ -function applyQueryParams(queryParams: Record | undefined, url: string): string { +function applyQueryParams({queryParams, url}: { + queryParams: Record | undefined; + url: string; +}): string { if (!queryParams) return url; const params = new URLSearchParams(); @@ -366,10 +369,10 @@ function sleep(ms: number): Promise { /** * Calculate delay for exponential backoff */ -function calculateBackoffDelay( - attempt: number, - config: Required -): number { +function calculateBackoffDelay({attempt, config}: { + attempt: number; + config: Required; +}): number { const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt - 1); return Math.min(delay, config.maxDelayMs); } @@ -377,12 +380,12 @@ function calculateBackoffDelay( /** * Determine if a request should be retried based on error/response */ -function shouldRetry( - error: HttpGlobalError | null, - response: HttpResponse | null, - config: Required, - attempt: number -): boolean { +function shouldRetry({error, response, config, attempt}: { + error: HttpGlobalError | null; + response: HttpResponse | null; + config: Required; + attempt: number; +}): boolean { if (attempt >= config.maxRetries) return false; if (error && config.retryOnNetworkError) return true; @@ -395,11 +398,11 @@ function shouldRetry( /** * Execute request with retry logic */ -async function executeWithRetry( - params: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function executeWithRetry({params, makeRequest, retryConfig}: { + params: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { const config = { ...DEFAULT_RETRY_CONFIG, ...retryConfig }; let lastError: HttpGlobalError | null = null; let lastResponse: HttpResponse | null = null; @@ -407,7 +410,7 @@ async function executeWithRetry( for (let attempt = 0; attempt <= config.maxRetries; attempt++) { try { if (attempt > 0) { - const delay = calculateBackoffDelay(attempt, config); + const delay = calculateBackoffDelay({attempt, config}); config.onRetry(attempt, delay, lastError ?? new HttpGlobalError('Retry attempt')); await sleep(delay); } @@ -415,7 +418,7 @@ async function executeWithRetry( const response = await makeRequest(params); // Check if we should retry this response - if (!shouldRetry(null, response, config, attempt + 1)) { + if (!shouldRetry({error: null, response, config, attempt: attempt + 1})) { return response; } @@ -424,7 +427,7 @@ async function executeWithRetry( } catch (error) { lastError = error instanceof HttpGlobalError ? error : new HttpGlobalError(String(error)); - if (!shouldRetry(lastError, null, config, attempt + 1)) { + if (!shouldRetry({error: lastError, response: null, config, attempt: attempt + 1})) { throw lastError; } } @@ -442,7 +445,11 @@ async function executeWithRetry( * Explicit cases are generated from the error status codes declared by the * input document; undeclared codes fall through to the default handler. */ -function handleHttpError(status: number, statusText: string, body?: unknown): never { +function handleHttpError({status, statusText, body}: { + status: number; + statusText: string; + body?: unknown; +}): never { throw new HttpError(`HTTP Error: ${status} ${statusText}`, status, statusText, body); } @@ -493,11 +500,11 @@ function extractHeaders(response: HttpResponse): Record { * @param pathTemplate - Path template with {param} placeholders * @param parameters - Parameter object with getChannelWithParameters method */ -function buildUrlWithParameters string }>( - server: string, - pathTemplate: string, - parameters: T -): string { +function buildUrlWithParameters string }>({server, pathTemplate, parameters}: { + server: string; + pathTemplate: string; + parameters: T; +}): string { const path = parameters.getChannelWithParameters(pathTemplate); return `${server}${path}`; } @@ -505,10 +512,10 @@ function buildUrlWithParameters string } | undefined, - additionalHeaders: Record | undefined -): Record { +function applyTypedHeaders({typedHeaders, additionalHeaders}: { + typedHeaders: { marshal: () => string } | undefined; + additionalHeaders: Record | undefined; +}): Record { const headers: Record = { 'Content-Type': 'application/json', ...additionalHeaders @@ -556,12 +563,12 @@ function validateOAuth2Config(auth: OAuth2Auth): void { /** * Handle OAuth2 token flows (client_credentials, password) */ -async function handleOAuth2TokenFlow( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleOAuth2TokenFlow({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.flow || !auth.tokenUrl) return null; const params = new URLSearchParams(); @@ -623,18 +630,18 @@ async function handleOAuth2TokenFlow( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = `Bearer ${tokens.accessToken}`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } /** * Handle OAuth2 token refresh on 401 response */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleTokenRefresh({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; const refreshResponse = await fetch(auth.tokenUrl, { @@ -670,7 +677,7 @@ async function handleTokenRefresh( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = `Bearer ${newTokens.accessToken}`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } // ============================================================================ // Generated HTTP Client Functions @@ -680,6 +687,8 @@ export interface GetEchoContext extends HttpClientContext {} /** * Return a plain string body + * + * @param context per-call request configuration */ async function getEcho(context: GetEchoContext = {}): Promise> { // Apply defaults @@ -698,10 +707,10 @@ async function getEcho(context: GetEchoContext = {}): Promise undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -789,6 +798,8 @@ export interface GetCountContext extends HttpClientContext {} /** * Return a plain number body + * + * @param context per-call request configuration */ async function getCount(context: GetCountContext = {}): Promise> { // Apply defaults @@ -807,10 +818,10 @@ async function getCount(context: GetCountContext = {}): Promise undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response diff --git a/test/runtime/typescript/src/openapi-server/channels/http_client.ts b/test/runtime/typescript/src/openapi-server/channels/http_client.ts index 269cd211..a23dca4b 100644 --- a/test/runtime/typescript/src/openapi-server/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi-server/channels/http_client.ts @@ -283,11 +283,11 @@ const defaultMakeRequest = async (params: HttpRequestParams): Promise, - url: string -): { headers: Record; url: string } { +function applyAuth({auth, headers, url}: { + auth: AuthConfig | undefined; + headers: Record; + url: string; +}): { headers: Record; url: string } { if (!auth) return { headers, url }; switch (auth.type) { @@ -322,7 +322,10 @@ function applyAuth( /** * Apply query parameters to URL */ -function applyQueryParams(queryParams: Record | undefined, url: string): string { +function applyQueryParams({queryParams, url}: { + queryParams: Record | undefined; + url: string; +}): string { if (!queryParams) return url; const params = new URLSearchParams(); @@ -349,10 +352,10 @@ function sleep(ms: number): Promise { /** * Calculate delay for exponential backoff */ -function calculateBackoffDelay( - attempt: number, - config: Required -): number { +function calculateBackoffDelay({attempt, config}: { + attempt: number; + config: Required; +}): number { const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt - 1); return Math.min(delay, config.maxDelayMs); } @@ -360,12 +363,12 @@ function calculateBackoffDelay( /** * Determine if a request should be retried based on error/response */ -function shouldRetry( - error: HttpGlobalError | null, - response: HttpResponse | null, - config: Required, - attempt: number -): boolean { +function shouldRetry({error, response, config, attempt}: { + error: HttpGlobalError | null; + response: HttpResponse | null; + config: Required; + attempt: number; +}): boolean { if (attempt >= config.maxRetries) return false; if (error && config.retryOnNetworkError) return true; @@ -378,11 +381,11 @@ function shouldRetry( /** * Execute request with retry logic */ -async function executeWithRetry( - params: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function executeWithRetry({params, makeRequest, retryConfig}: { + params: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { const config = { ...DEFAULT_RETRY_CONFIG, ...retryConfig }; let lastError: HttpGlobalError | null = null; let lastResponse: HttpResponse | null = null; @@ -390,7 +393,7 @@ async function executeWithRetry( for (let attempt = 0; attempt <= config.maxRetries; attempt++) { try { if (attempt > 0) { - const delay = calculateBackoffDelay(attempt, config); + const delay = calculateBackoffDelay({attempt, config}); config.onRetry(attempt, delay, lastError ?? new HttpGlobalError('Retry attempt')); await sleep(delay); } @@ -398,7 +401,7 @@ async function executeWithRetry( const response = await makeRequest(params); // Check if we should retry this response - if (!shouldRetry(null, response, config, attempt + 1)) { + if (!shouldRetry({error: null, response, config, attempt: attempt + 1})) { return response; } @@ -407,7 +410,7 @@ async function executeWithRetry( } catch (error) { lastError = error instanceof HttpGlobalError ? error : new HttpGlobalError(String(error)); - if (!shouldRetry(lastError, null, config, attempt + 1)) { + if (!shouldRetry({error: lastError, response: null, config, attempt: attempt + 1})) { throw lastError; } } @@ -425,7 +428,11 @@ async function executeWithRetry( * Explicit cases are generated from the error status codes declared by the * input document; undeclared codes fall through to the default handler. */ -function handleHttpError(status: number, statusText: string, body?: unknown): never { +function handleHttpError({status, statusText, body}: { + status: number; + statusText: string; + body?: unknown; +}): never { switch (status) { case 400: throw new HttpError("Bad Request", status, statusText, body); @@ -485,11 +492,11 @@ function extractHeaders(response: HttpResponse): Record { * @param pathTemplate - Path template with {param} placeholders * @param parameters - Parameter object with getChannelWithParameters method */ -function buildUrlWithParameters string }>( - server: string, - pathTemplate: string, - parameters: T -): string { +function buildUrlWithParameters string }>({server, pathTemplate, parameters}: { + server: string; + pathTemplate: string; + parameters: T; +}): string { const path = parameters.getChannelWithParameters(pathTemplate); return `${server}${path}`; } @@ -497,10 +504,10 @@ function buildUrlWithParameters string } | undefined, - additionalHeaders: Record | undefined -): Record { +function applyTypedHeaders({typedHeaders, additionalHeaders}: { + typedHeaders: { marshal: () => string } | undefined; + additionalHeaders: Record | undefined; +}): Record { const headers: Record = { 'Content-Type': 'application/json', ...additionalHeaders @@ -548,12 +555,12 @@ function validateOAuth2Config(auth: OAuth2Auth): void { /** * Handle OAuth2 token flows (client_credentials, password) */ -async function handleOAuth2TokenFlow( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleOAuth2TokenFlow({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.flow || !auth.tokenUrl) return null; const params = new URLSearchParams(); @@ -615,18 +622,18 @@ async function handleOAuth2TokenFlow( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = `Bearer ${tokens.accessToken}`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } /** * Handle OAuth2 token refresh on 401 response */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleTokenRefresh({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; const refreshResponse = await fetch(auth.tokenUrl, { @@ -662,7 +669,7 @@ async function handleTokenRefresh( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = `Bearer ${newTokens.accessToken}`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } // ============================================================================ // Generated HTTP Client Functions @@ -674,6 +681,9 @@ export interface AddPetContext extends HttpClientContext { /** * HTTP POST request to /pet + * + * @param context per-call request configuration + * @param context.payload the request body to send */ async function addPet(context: AddPetContext): Promise> { // Apply defaults @@ -692,10 +702,10 @@ async function addPet(context: AddPetContext): Promise> // Build URL let url = `${config.baseUrl}/pet`; - url = applyQueryParams(config.additionalQueryParams, url); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -721,7 +731,7 @@ async function addPet(context: AddPetContext): Promise> try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -730,7 +740,7 @@ async function addPet(context: AddPetContext): Promise> // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -739,7 +749,7 @@ async function addPet(context: AddPetContext): Promise> // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -751,7 +761,7 @@ async function addPet(context: AddPetContext): Promise> // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -786,6 +796,9 @@ export interface UpdatePetContext extends HttpClientContext { /** * HTTP PUT request to /pet + * + * @param context per-call request configuration + * @param context.payload the request body to send */ async function updatePet(context: UpdatePetContext): Promise> { // Apply defaults @@ -804,10 +817,10 @@ async function updatePet(context: UpdatePetContext): Promise undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -899,6 +912,10 @@ export interface FindPetsByStatusAndCategoryContext extends HttpClientContext { /** * Find pets by status and category with additional filtering options + * + * @param context per-call request configuration + * @param context.parameters for path and query parameter substitution + * @param context.requestHeaders optional headers to send with the request */ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryContext): Promise> { // Apply defaults @@ -918,11 +935,11 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders, ...(context.requestHeaders ? serializeFindPetsByStatusAndCategoryHeadersHeaders(context.requestHeaders) : {}) } as Record; // Build URL - let url = buildUrlWithParameters(config.baseUrl, '/pet/findByStatus/{status}/{categoryId}', parameters); - url = applyQueryParams(config.additionalQueryParams, url); + let url = buildUrlWithParameters({server: config.baseUrl, pathTemplate: '/pet/findByStatus/{status}/{categoryId}', parameters}); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -947,7 +964,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -956,7 +973,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -965,7 +982,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -977,7 +994,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response diff --git a/test/runtime/typescript/src/openapi-server/channels/http_server.ts b/test/runtime/typescript/src/openapi-server/channels/http_server.ts index 15f67e37..04bfe1a7 100644 --- a/test/runtime/typescript/src/openapi-server/channels/http_server.ts +++ b/test/runtime/typescript/src/openapi-server/channels/http_server.ts @@ -226,6 +226,11 @@ export interface RegisterAddPetContext extends HttpServerContext { /** * Registers an HTTP POST handler for /pet + * + * @param context the handler registration context + * @param context.router the Express router to mount the handler on + * @param context.callback invoked for each request; its return value becomes the response + * @param context.callback.body the deserialized request body */ function registerAddPet(context: RegisterAddPetContext): void { const validator = APet.createValidator(); @@ -275,6 +280,11 @@ export interface RegisterUpdatePetContext extends HttpServerContext { /** * Registers an HTTP PUT handler for /pet + * + * @param context the handler registration context + * @param context.router the Express router to mount the handler on + * @param context.callback invoked for each request; its return value becomes the response + * @param context.callback.body the deserialized request body */ function registerUpdatePet(context: RegisterUpdatePetContext): void { const validator = APet.createValidator(); @@ -324,6 +334,12 @@ export interface RegisterFindPetsByStatusAndCategoryContext extends HttpServerCo /** * Find pets by status and category with additional filtering options + * + * @param context the handler registration context + * @param context.router the Express router to mount the handler on + * @param context.callback invoked for each request; its return value becomes the response + * @param context.callback.parameters extracted from the request path and query + * @param context.callback.requestHeaders deserialized from the request headers */ function registerFindPetsByStatusAndCategory(context: RegisterFindPetsByStatusAndCategoryContext): void { context.router.get('/pet/findByStatus/:status/:categoryId', async (request: Request, response: Response, next: NextFunction) => { diff --git a/test/runtime/typescript/src/openapi-tag-organization/channels/http_client.ts b/test/runtime/typescript/src/openapi-tag-organization/channels/http_client.ts index 38aabc50..c65caf0a 100644 --- a/test/runtime/typescript/src/openapi-tag-organization/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi-tag-organization/channels/http_client.ts @@ -283,11 +283,11 @@ const defaultMakeRequest = async (params: HttpRequestParams): Promise, - url: string -): { headers: Record; url: string } { +function applyAuth({auth, headers, url}: { + auth: AuthConfig | undefined; + headers: Record; + url: string; +}): { headers: Record; url: string } { if (!auth) return { headers, url }; switch (auth.type) { @@ -322,7 +322,10 @@ function applyAuth( /** * Apply query parameters to URL */ -function applyQueryParams(queryParams: Record | undefined, url: string): string { +function applyQueryParams({queryParams, url}: { + queryParams: Record | undefined; + url: string; +}): string { if (!queryParams) return url; const params = new URLSearchParams(); @@ -349,10 +352,10 @@ function sleep(ms: number): Promise { /** * Calculate delay for exponential backoff */ -function calculateBackoffDelay( - attempt: number, - config: Required -): number { +function calculateBackoffDelay({attempt, config}: { + attempt: number; + config: Required; +}): number { const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt - 1); return Math.min(delay, config.maxDelayMs); } @@ -360,12 +363,12 @@ function calculateBackoffDelay( /** * Determine if a request should be retried based on error/response */ -function shouldRetry( - error: HttpGlobalError | null, - response: HttpResponse | null, - config: Required, - attempt: number -): boolean { +function shouldRetry({error, response, config, attempt}: { + error: HttpGlobalError | null; + response: HttpResponse | null; + config: Required; + attempt: number; +}): boolean { if (attempt >= config.maxRetries) return false; if (error && config.retryOnNetworkError) return true; @@ -378,11 +381,11 @@ function shouldRetry( /** * Execute request with retry logic */ -async function executeWithRetry( - params: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function executeWithRetry({params, makeRequest, retryConfig}: { + params: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { const config = { ...DEFAULT_RETRY_CONFIG, ...retryConfig }; let lastError: HttpGlobalError | null = null; let lastResponse: HttpResponse | null = null; @@ -390,7 +393,7 @@ async function executeWithRetry( for (let attempt = 0; attempt <= config.maxRetries; attempt++) { try { if (attempt > 0) { - const delay = calculateBackoffDelay(attempt, config); + const delay = calculateBackoffDelay({attempt, config}); config.onRetry(attempt, delay, lastError ?? new HttpGlobalError('Retry attempt')); await sleep(delay); } @@ -398,7 +401,7 @@ async function executeWithRetry( const response = await makeRequest(params); // Check if we should retry this response - if (!shouldRetry(null, response, config, attempt + 1)) { + if (!shouldRetry({error: null, response, config, attempt: attempt + 1})) { return response; } @@ -407,7 +410,7 @@ async function executeWithRetry( } catch (error) { lastError = error instanceof HttpGlobalError ? error : new HttpGlobalError(String(error)); - if (!shouldRetry(lastError, null, config, attempt + 1)) { + if (!shouldRetry({error: lastError, response: null, config, attempt: attempt + 1})) { throw lastError; } } @@ -425,7 +428,11 @@ async function executeWithRetry( * Explicit cases are generated from the error status codes declared by the * input document; undeclared codes fall through to the default handler. */ -function handleHttpError(status: number, statusText: string, body?: unknown): never { +function handleHttpError({status, statusText, body}: { + status: number; + statusText: string; + body?: unknown; +}): never { switch (status) { case 400: throw new HttpError("Bad Request", status, statusText, body); @@ -485,11 +492,11 @@ function extractHeaders(response: HttpResponse): Record { * @param pathTemplate - Path template with {param} placeholders * @param parameters - Parameter object with getChannelWithParameters method */ -function buildUrlWithParameters string }>( - server: string, - pathTemplate: string, - parameters: T -): string { +function buildUrlWithParameters string }>({server, pathTemplate, parameters}: { + server: string; + pathTemplate: string; + parameters: T; +}): string { const path = parameters.getChannelWithParameters(pathTemplate); return `${server}${path}`; } @@ -497,10 +504,10 @@ function buildUrlWithParameters string } | undefined, - additionalHeaders: Record | undefined -): Record { +function applyTypedHeaders({typedHeaders, additionalHeaders}: { + typedHeaders: { marshal: () => string } | undefined; + additionalHeaders: Record | undefined; +}): Record { const headers: Record = { 'Content-Type': 'application/json', ...additionalHeaders @@ -548,12 +555,12 @@ function validateOAuth2Config(auth: OAuth2Auth): void { /** * Handle OAuth2 token flows (client_credentials, password) */ -async function handleOAuth2TokenFlow( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleOAuth2TokenFlow({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.flow || !auth.tokenUrl) return null; const params = new URLSearchParams(); @@ -615,18 +622,18 @@ async function handleOAuth2TokenFlow( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = `Bearer ${tokens.accessToken}`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } /** * Handle OAuth2 token refresh on 401 response */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleTokenRefresh({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; const refreshResponse = await fetch(auth.tokenUrl, { @@ -662,7 +669,7 @@ async function handleTokenRefresh( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = `Bearer ${newTokens.accessToken}`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } // ============================================================================ // Generated HTTP Client Functions @@ -674,6 +681,9 @@ export interface AddPetContext extends HttpClientContext { /** * HTTP POST request to /pet + * + * @param context per-call request configuration + * @param context.payload the request body to send */ async function addPet(context: AddPetContext): Promise> { // Apply defaults @@ -692,10 +702,10 @@ async function addPet(context: AddPetContext): Promise> // Build URL let url = `${config.baseUrl}/pet`; - url = applyQueryParams(config.additionalQueryParams, url); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -721,7 +731,7 @@ async function addPet(context: AddPetContext): Promise> try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -730,7 +740,7 @@ async function addPet(context: AddPetContext): Promise> // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -739,7 +749,7 @@ async function addPet(context: AddPetContext): Promise> // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -751,7 +761,7 @@ async function addPet(context: AddPetContext): Promise> // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -786,6 +796,9 @@ export interface UpdatePetContext extends HttpClientContext { /** * HTTP PUT request to /pet + * + * @param context per-call request configuration + * @param context.payload the request body to send */ async function updatePet(context: UpdatePetContext): Promise> { // Apply defaults @@ -804,10 +817,10 @@ async function updatePet(context: UpdatePetContext): Promise undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -899,6 +912,10 @@ export interface FindPetsByStatusAndCategoryContext extends HttpClientContext { /** * Find pets by status and category with additional filtering options + * + * @param context per-call request configuration + * @param context.parameters for path and query parameter substitution + * @param context.requestHeaders optional headers to send with the request */ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryContext): Promise> { // Apply defaults @@ -918,11 +935,11 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders, ...(context.requestHeaders ? serializeFindPetsByStatusAndCategoryHeadersHeaders(context.requestHeaders) : {}) } as Record; // Build URL - let url = buildUrlWithParameters(config.baseUrl, '/pet/findByStatus/{status}/{categoryId}', parameters); - url = applyQueryParams(config.additionalQueryParams, url); + let url = buildUrlWithParameters({server: config.baseUrl, pathTemplate: '/pet/findByStatus/{status}/{categoryId}', parameters}); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -947,7 +964,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -956,7 +973,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -965,7 +982,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -977,7 +994,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response diff --git a/test/runtime/typescript/src/openapi/channels/http_client.ts b/test/runtime/typescript/src/openapi/channels/http_client.ts index 269cd211..a23dca4b 100644 --- a/test/runtime/typescript/src/openapi/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi/channels/http_client.ts @@ -283,11 +283,11 @@ const defaultMakeRequest = async (params: HttpRequestParams): Promise, - url: string -): { headers: Record; url: string } { +function applyAuth({auth, headers, url}: { + auth: AuthConfig | undefined; + headers: Record; + url: string; +}): { headers: Record; url: string } { if (!auth) return { headers, url }; switch (auth.type) { @@ -322,7 +322,10 @@ function applyAuth( /** * Apply query parameters to URL */ -function applyQueryParams(queryParams: Record | undefined, url: string): string { +function applyQueryParams({queryParams, url}: { + queryParams: Record | undefined; + url: string; +}): string { if (!queryParams) return url; const params = new URLSearchParams(); @@ -349,10 +352,10 @@ function sleep(ms: number): Promise { /** * Calculate delay for exponential backoff */ -function calculateBackoffDelay( - attempt: number, - config: Required -): number { +function calculateBackoffDelay({attempt, config}: { + attempt: number; + config: Required; +}): number { const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt - 1); return Math.min(delay, config.maxDelayMs); } @@ -360,12 +363,12 @@ function calculateBackoffDelay( /** * Determine if a request should be retried based on error/response */ -function shouldRetry( - error: HttpGlobalError | null, - response: HttpResponse | null, - config: Required, - attempt: number -): boolean { +function shouldRetry({error, response, config, attempt}: { + error: HttpGlobalError | null; + response: HttpResponse | null; + config: Required; + attempt: number; +}): boolean { if (attempt >= config.maxRetries) return false; if (error && config.retryOnNetworkError) return true; @@ -378,11 +381,11 @@ function shouldRetry( /** * Execute request with retry logic */ -async function executeWithRetry( - params: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function executeWithRetry({params, makeRequest, retryConfig}: { + params: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { const config = { ...DEFAULT_RETRY_CONFIG, ...retryConfig }; let lastError: HttpGlobalError | null = null; let lastResponse: HttpResponse | null = null; @@ -390,7 +393,7 @@ async function executeWithRetry( for (let attempt = 0; attempt <= config.maxRetries; attempt++) { try { if (attempt > 0) { - const delay = calculateBackoffDelay(attempt, config); + const delay = calculateBackoffDelay({attempt, config}); config.onRetry(attempt, delay, lastError ?? new HttpGlobalError('Retry attempt')); await sleep(delay); } @@ -398,7 +401,7 @@ async function executeWithRetry( const response = await makeRequest(params); // Check if we should retry this response - if (!shouldRetry(null, response, config, attempt + 1)) { + if (!shouldRetry({error: null, response, config, attempt: attempt + 1})) { return response; } @@ -407,7 +410,7 @@ async function executeWithRetry( } catch (error) { lastError = error instanceof HttpGlobalError ? error : new HttpGlobalError(String(error)); - if (!shouldRetry(lastError, null, config, attempt + 1)) { + if (!shouldRetry({error: lastError, response: null, config, attempt: attempt + 1})) { throw lastError; } } @@ -425,7 +428,11 @@ async function executeWithRetry( * Explicit cases are generated from the error status codes declared by the * input document; undeclared codes fall through to the default handler. */ -function handleHttpError(status: number, statusText: string, body?: unknown): never { +function handleHttpError({status, statusText, body}: { + status: number; + statusText: string; + body?: unknown; +}): never { switch (status) { case 400: throw new HttpError("Bad Request", status, statusText, body); @@ -485,11 +492,11 @@ function extractHeaders(response: HttpResponse): Record { * @param pathTemplate - Path template with {param} placeholders * @param parameters - Parameter object with getChannelWithParameters method */ -function buildUrlWithParameters string }>( - server: string, - pathTemplate: string, - parameters: T -): string { +function buildUrlWithParameters string }>({server, pathTemplate, parameters}: { + server: string; + pathTemplate: string; + parameters: T; +}): string { const path = parameters.getChannelWithParameters(pathTemplate); return `${server}${path}`; } @@ -497,10 +504,10 @@ function buildUrlWithParameters string } | undefined, - additionalHeaders: Record | undefined -): Record { +function applyTypedHeaders({typedHeaders, additionalHeaders}: { + typedHeaders: { marshal: () => string } | undefined; + additionalHeaders: Record | undefined; +}): Record { const headers: Record = { 'Content-Type': 'application/json', ...additionalHeaders @@ -548,12 +555,12 @@ function validateOAuth2Config(auth: OAuth2Auth): void { /** * Handle OAuth2 token flows (client_credentials, password) */ -async function handleOAuth2TokenFlow( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleOAuth2TokenFlow({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.flow || !auth.tokenUrl) return null; const params = new URLSearchParams(); @@ -615,18 +622,18 @@ async function handleOAuth2TokenFlow( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = `Bearer ${tokens.accessToken}`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } /** * Handle OAuth2 token refresh on 401 response */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleTokenRefresh({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; const refreshResponse = await fetch(auth.tokenUrl, { @@ -662,7 +669,7 @@ async function handleTokenRefresh( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = `Bearer ${newTokens.accessToken}`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } // ============================================================================ // Generated HTTP Client Functions @@ -674,6 +681,9 @@ export interface AddPetContext extends HttpClientContext { /** * HTTP POST request to /pet + * + * @param context per-call request configuration + * @param context.payload the request body to send */ async function addPet(context: AddPetContext): Promise> { // Apply defaults @@ -692,10 +702,10 @@ async function addPet(context: AddPetContext): Promise> // Build URL let url = `${config.baseUrl}/pet`; - url = applyQueryParams(config.additionalQueryParams, url); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -721,7 +731,7 @@ async function addPet(context: AddPetContext): Promise> try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -730,7 +740,7 @@ async function addPet(context: AddPetContext): Promise> // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -739,7 +749,7 @@ async function addPet(context: AddPetContext): Promise> // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -751,7 +761,7 @@ async function addPet(context: AddPetContext): Promise> // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -786,6 +796,9 @@ export interface UpdatePetContext extends HttpClientContext { /** * HTTP PUT request to /pet + * + * @param context per-call request configuration + * @param context.payload the request body to send */ async function updatePet(context: UpdatePetContext): Promise> { // Apply defaults @@ -804,10 +817,10 @@ async function updatePet(context: UpdatePetContext): Promise undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -899,6 +912,10 @@ export interface FindPetsByStatusAndCategoryContext extends HttpClientContext { /** * Find pets by status and category with additional filtering options + * + * @param context per-call request configuration + * @param context.parameters for path and query parameter substitution + * @param context.requestHeaders optional headers to send with the request */ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryContext): Promise> { // Apply defaults @@ -918,11 +935,11 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders, ...(context.requestHeaders ? serializeFindPetsByStatusAndCategoryHeadersHeaders(context.requestHeaders) : {}) } as Record; // Build URL - let url = buildUrlWithParameters(config.baseUrl, '/pet/findByStatus/{status}/{categoryId}', parameters); - url = applyQueryParams(config.additionalQueryParams, url); + let url = buildUrlWithParameters({server: config.baseUrl, pathTemplate: '/pet/findByStatus/{status}/{categoryId}', parameters}); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -947,7 +964,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -956,7 +973,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -965,7 +982,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -977,7 +994,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response diff --git a/test/runtime/typescript/src/request-reply/channels/http_client.ts b/test/runtime/typescript/src/request-reply/channels/http_client.ts index bf08444e..d4a819c3 100644 --- a/test/runtime/typescript/src/request-reply/channels/http_client.ts +++ b/test/runtime/typescript/src/request-reply/channels/http_client.ts @@ -300,11 +300,11 @@ const defaultMakeRequest = async (params: HttpRequestParams): Promise, - url: string -): { headers: Record; url: string } { +function applyAuth({auth, headers, url}: { + auth: AuthConfig | undefined; + headers: Record; + url: string; +}): { headers: Record; url: string } { if (!auth) return { headers, url }; switch (auth.type) { @@ -349,7 +349,10 @@ function applyAuth( /** * Apply query parameters to URL */ -function applyQueryParams(queryParams: Record | undefined, url: string): string { +function applyQueryParams({queryParams, url}: { + queryParams: Record | undefined; + url: string; +}): string { if (!queryParams) return url; const params = new URLSearchParams(); @@ -376,10 +379,10 @@ function sleep(ms: number): Promise { /** * Calculate delay for exponential backoff */ -function calculateBackoffDelay( - attempt: number, - config: Required -): number { +function calculateBackoffDelay({attempt, config}: { + attempt: number; + config: Required; +}): number { const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt - 1); return Math.min(delay, config.maxDelayMs); } @@ -387,12 +390,12 @@ function calculateBackoffDelay( /** * Determine if a request should be retried based on error/response */ -function shouldRetry( - error: HttpGlobalError | null, - response: HttpResponse | null, - config: Required, - attempt: number -): boolean { +function shouldRetry({error, response, config, attempt}: { + error: HttpGlobalError | null; + response: HttpResponse | null; + config: Required; + attempt: number; +}): boolean { if (attempt >= config.maxRetries) return false; if (error && config.retryOnNetworkError) return true; @@ -405,11 +408,11 @@ function shouldRetry( /** * Execute request with retry logic */ -async function executeWithRetry( - params: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function executeWithRetry({params, makeRequest, retryConfig}: { + params: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { const config = { ...DEFAULT_RETRY_CONFIG, ...retryConfig }; let lastError: HttpGlobalError | null = null; let lastResponse: HttpResponse | null = null; @@ -417,7 +420,7 @@ async function executeWithRetry( for (let attempt = 0; attempt <= config.maxRetries; attempt++) { try { if (attempt > 0) { - const delay = calculateBackoffDelay(attempt, config); + const delay = calculateBackoffDelay({attempt, config}); config.onRetry(attempt, delay, lastError ?? new HttpGlobalError('Retry attempt')); await sleep(delay); } @@ -425,7 +428,7 @@ async function executeWithRetry( const response = await makeRequest(params); // Check if we should retry this response - if (!shouldRetry(null, response, config, attempt + 1)) { + if (!shouldRetry({error: null, response, config, attempt: attempt + 1})) { return response; } @@ -434,7 +437,7 @@ async function executeWithRetry( } catch (error) { lastError = error instanceof HttpGlobalError ? error : new HttpGlobalError(String(error)); - if (!shouldRetry(lastError, null, config, attempt + 1)) { + if (!shouldRetry({error: lastError, response: null, config, attempt: attempt + 1})) { throw lastError; } } @@ -452,7 +455,11 @@ async function executeWithRetry( * Explicit cases are generated from the error status codes declared by the * input document; undeclared codes fall through to the default handler. */ -function handleHttpError(status: number, statusText: string, body?: unknown): never { +function handleHttpError({status, statusText, body}: { + status: number; + statusText: string; + body?: unknown; +}): never { throw new HttpError(`HTTP Error: ${status} ${statusText}`, status, statusText, body); } @@ -503,11 +510,11 @@ function extractHeaders(response: HttpResponse): Record { * @param pathTemplate - Path template with {param} placeholders * @param parameters - Parameter object with getChannelWithParameters method */ -function buildUrlWithParameters string }>( - server: string, - pathTemplate: string, - parameters: T -): string { +function buildUrlWithParameters string }>({server, pathTemplate, parameters}: { + server: string; + pathTemplate: string; + parameters: T; +}): string { const path = parameters.getChannelWithParameters(pathTemplate); return `${server}${path}`; } @@ -515,10 +522,10 @@ function buildUrlWithParameters string } | undefined, - additionalHeaders: Record | undefined -): Record { +function applyTypedHeaders({typedHeaders, additionalHeaders}: { + typedHeaders: { marshal: () => string } | undefined; + additionalHeaders: Record | undefined; +}): Record { const headers: Record = { 'Content-Type': 'application/json', ...additionalHeaders @@ -566,12 +573,12 @@ function validateOAuth2Config(auth: OAuth2Auth): void { /** * Handle OAuth2 token flows (client_credentials, password) */ -async function handleOAuth2TokenFlow( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleOAuth2TokenFlow({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.flow || !auth.tokenUrl) return null; const params = new URLSearchParams(); @@ -633,18 +640,18 @@ async function handleOAuth2TokenFlow( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = `Bearer ${tokens.accessToken}`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } /** * Handle OAuth2 token refresh on 401 response */ -async function handleTokenRefresh( - auth: OAuth2Auth, - originalParams: HttpRequestParams, - makeRequest: (params: HttpRequestParams) => Promise, - retryConfig?: RetryConfig -): Promise { +async function handleTokenRefresh({auth, originalParams, makeRequest, retryConfig}: { + auth: OAuth2Auth; + originalParams: HttpRequestParams; + makeRequest: (params: HttpRequestParams) => Promise; + retryConfig?: RetryConfig; +}): Promise { if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; const refreshResponse = await fetch(auth.tokenUrl, { @@ -680,7 +687,7 @@ async function handleTokenRefresh( const updatedHeaders = { ...originalParams.headers }; updatedHeaders['Authorization'] = `Bearer ${newTokens.accessToken}`; - return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); + return executeWithRetry({params: { ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig}); } // ============================================================================ // Generated HTTP Client Functions @@ -692,6 +699,9 @@ export interface PostPingPostRequestContext extends HttpClientContext { /** * HTTP POST request to /ping + * + * @param context per-call request configuration + * @param context.payload the request body to send */ async function postPingPostRequest(context: PostPingPostRequestContext): Promise> { // Apply defaults @@ -710,10 +720,10 @@ async function postPingPostRequest(context: PostPingPostRequestContext): Promise // Build URL let url = `${config.baseUrl}/ping`; - url = applyQueryParams(config.additionalQueryParams, url); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -739,7 +749,7 @@ async function postPingPostRequest(context: PostPingPostRequestContext): Promise try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -748,7 +758,7 @@ async function postPingPostRequest(context: PostPingPostRequestContext): Promise // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -757,7 +767,7 @@ async function postPingPostRequest(context: PostPingPostRequestContext): Promise // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -769,7 +779,7 @@ async function postPingPostRequest(context: PostPingPostRequestContext): Promise // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -802,6 +812,8 @@ export interface GetPingGetRequestContext extends HttpClientContext {} /** * HTTP GET request to /ping + * + * @param context per-call request configuration */ async function getPingGetRequest(context: GetPingGetRequestContext = {}): Promise> { // Apply defaults @@ -820,10 +832,10 @@ async function getPingGetRequest(context: GetPingGetRequestContext = {}): Promis // Build URL let url = `${config.baseUrl}/ping`; - url = applyQueryParams(config.additionalQueryParams, url); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -848,7 +860,7 @@ async function getPingGetRequest(context: GetPingGetRequestContext = {}): Promis try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -857,7 +869,7 @@ async function getPingGetRequest(context: GetPingGetRequestContext = {}): Promis // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -866,7 +878,7 @@ async function getPingGetRequest(context: GetPingGetRequestContext = {}): Promis // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -878,7 +890,7 @@ async function getPingGetRequest(context: GetPingGetRequestContext = {}): Promis // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -913,6 +925,9 @@ export interface PutPingPutRequestContext extends HttpClientContext { /** * HTTP PUT request to /ping + * + * @param context per-call request configuration + * @param context.payload the request body to send */ async function putPingPutRequest(context: PutPingPutRequestContext): Promise> { // Apply defaults @@ -931,10 +946,10 @@ async function putPingPutRequest(context: PutPingPutRequestContext): Promise undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -1023,6 +1038,8 @@ export interface DeletePingDeleteRequestContext extends HttpClientContext {} /** * HTTP DELETE request to /ping + * + * @param context per-call request configuration */ async function deletePingDeleteRequest(context: DeletePingDeleteRequestContext = {}): Promise> { // Apply defaults @@ -1041,10 +1058,10 @@ async function deletePingDeleteRequest(context: DeletePingDeleteRequestContext = // Build URL let url = `${config.baseUrl}/ping`; - url = applyQueryParams(config.additionalQueryParams, url); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -1069,7 +1086,7 @@ async function deletePingDeleteRequest(context: DeletePingDeleteRequestContext = try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -1078,7 +1095,7 @@ async function deletePingDeleteRequest(context: DeletePingDeleteRequestContext = // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -1087,7 +1104,7 @@ async function deletePingDeleteRequest(context: DeletePingDeleteRequestContext = // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -1099,7 +1116,7 @@ async function deletePingDeleteRequest(context: DeletePingDeleteRequestContext = // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -1134,6 +1151,9 @@ export interface PatchPingPatchRequestContext extends HttpClientContext { /** * HTTP PATCH request to /ping + * + * @param context per-call request configuration + * @param context.payload the request body to send */ async function patchPingPatchRequest(context: PatchPingPatchRequestContext): Promise> { // Apply defaults @@ -1152,10 +1172,10 @@ async function patchPingPatchRequest(context: PatchPingPatchRequestContext): Pro // Build URL let url = `${config.baseUrl}/ping`; - url = applyQueryParams(config.additionalQueryParams, url); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -1181,7 +1201,7 @@ async function patchPingPatchRequest(context: PatchPingPatchRequestContext): Pro try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -1190,7 +1210,7 @@ async function patchPingPatchRequest(context: PatchPingPatchRequestContext): Pro // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -1199,7 +1219,7 @@ async function patchPingPatchRequest(context: PatchPingPatchRequestContext): Pro // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -1211,7 +1231,7 @@ async function patchPingPatchRequest(context: PatchPingPatchRequestContext): Pro // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -1244,6 +1264,8 @@ export interface HeadPingHeadRequestContext extends HttpClientContext {} /** * HTTP HEAD request to /ping + * + * @param context per-call request configuration */ async function headPingHeadRequest(context: HeadPingHeadRequestContext = {}): Promise> { // Apply defaults @@ -1262,10 +1284,10 @@ async function headPingHeadRequest(context: HeadPingHeadRequestContext = {}): Pr // Build URL let url = `${config.baseUrl}/ping`; - url = applyQueryParams(config.additionalQueryParams, url); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -1290,7 +1312,7 @@ async function headPingHeadRequest(context: HeadPingHeadRequestContext = {}): Pr try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -1299,7 +1321,7 @@ async function headPingHeadRequest(context: HeadPingHeadRequestContext = {}): Pr // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -1308,7 +1330,7 @@ async function headPingHeadRequest(context: HeadPingHeadRequestContext = {}): Pr // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -1320,7 +1342,7 @@ async function headPingHeadRequest(context: HeadPingHeadRequestContext = {}): Pr // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -1353,6 +1375,8 @@ export interface OptionsPingOptionsRequestContext extends HttpClientContext {} /** * HTTP OPTIONS request to /ping + * + * @param context per-call request configuration */ async function optionsPingOptionsRequest(context: OptionsPingOptionsRequestContext = {}): Promise> { // Apply defaults @@ -1371,10 +1395,10 @@ async function optionsPingOptionsRequest(context: OptionsPingOptionsRequestConte // Build URL let url = `${config.baseUrl}/ping`; - url = applyQueryParams(config.additionalQueryParams, url); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -1399,7 +1423,7 @@ async function optionsPingOptionsRequest(context: OptionsPingOptionsRequestConte try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -1408,7 +1432,7 @@ async function optionsPingOptionsRequest(context: OptionsPingOptionsRequestConte // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -1417,7 +1441,7 @@ async function optionsPingOptionsRequest(context: OptionsPingOptionsRequestConte // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -1429,7 +1453,7 @@ async function optionsPingOptionsRequest(context: OptionsPingOptionsRequestConte // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -1462,6 +1486,8 @@ export interface GetMultiStatusResponseContext extends HttpClientContext {} /** * HTTP GET request to /ping + * + * @param context per-call request configuration */ async function getMultiStatusResponse(context: GetMultiStatusResponseContext = {}): Promise> { // Apply defaults @@ -1480,10 +1506,10 @@ async function getMultiStatusResponse(context: GetMultiStatusResponseContext = { // Build URL let url = `${config.baseUrl}/ping`; - url = applyQueryParams(config.additionalQueryParams, url); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -1508,7 +1534,7 @@ async function getMultiStatusResponse(context: GetMultiStatusResponseContext = { try { // Execute request with retry logic - let response = await executeWithRetry(requestParams, makeRequest, config.retry); + let response = await executeWithRetry({params: requestParams, makeRequest, retryConfig: config.retry}); // Apply afterResponse hook if (config.hooks?.afterResponse) { @@ -1517,7 +1543,7 @@ async function getMultiStatusResponse(context: GetMultiStatusResponseContext = { // Handle OAuth2 token flows that require getting a token first if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { - const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + const tokenFlowResponse = await handleOAuth2TokenFlow({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (tokenFlowResponse) { response = tokenFlowResponse; } @@ -1526,7 +1552,7 @@ async function getMultiStatusResponse(context: GetMultiStatusResponseContext = { // Handle 401 with token refresh if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { try { - const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + const refreshResponse = await handleTokenRefresh({auth: config.auth, originalParams: requestParams, makeRequest, retryConfig: config.retry}); if (refreshResponse) { response = refreshResponse; } @@ -1538,7 +1564,7 @@ async function getMultiStatusResponse(context: GetMultiStatusResponseContext = { // Handle error responses if (!response.ok) { const errorBody = await response.json().catch(() => undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -1574,6 +1600,10 @@ export interface GetGetUserItemContext extends HttpClientContext { /** * HTTP GET request to /users/{userId}/items/{itemId} + * + * @param context per-call request configuration + * @param context.parameters for path and query parameter substitution + * @param context.requestHeaders optional headers to send with the request */ async function getGetUserItem(context: GetGetUserItemContext): Promise> { // Apply defaults @@ -1591,15 +1621,15 @@ async function getGetUserItem(context: GetGetUserItemContext): Promise; // Build URL - let url = buildUrlWithParameters(config.baseUrl, '/users/{userId}/items/{itemId}', parameters); - url = applyQueryParams(config.additionalQueryParams, url); + let url = buildUrlWithParameters({server: config.baseUrl, pathTemplate: '/users/{userId}/items/{itemId}', parameters}); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -1624,7 +1654,7 @@ async function getGetUserItem(context: GetGetUserItemContext): Promise undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response @@ -1691,6 +1721,11 @@ export interface PutUpdateUserItemContext extends HttpClientContext { /** * HTTP PUT request to /users/{userId}/items/{itemId} + * + * @param context per-call request configuration + * @param context.payload the request body to send + * @param context.parameters for path and query parameter substitution + * @param context.requestHeaders optional headers to send with the request */ async function putUpdateUserItem(context: PutUpdateUserItemContext): Promise> { // Apply defaults @@ -1708,15 +1743,15 @@ async function putUpdateUserItem(context: PutUpdateUserItemContext): Promise; // Build URL - let url = buildUrlWithParameters(config.baseUrl, '/users/{userId}/items/{itemId}', parameters); - url = applyQueryParams(config.additionalQueryParams, url); + let url = buildUrlWithParameters({server: config.baseUrl, pathTemplate: '/users/{userId}/items/{itemId}', parameters}); + url = applyQueryParams({queryParams: config.additionalQueryParams, url}); // Apply authentication - const authResult = applyAuth(config.auth, headers, url); + const authResult = applyAuth({auth: config.auth, headers, url}); headers = authResult.headers; url = authResult.url; @@ -1742,7 +1777,7 @@ async function putUpdateUserItem(context: PutUpdateUserItemContext): Promise undefined); - handleHttpError(response.status, response.statusText, errorBody); + handleHttpError({status: response.status, statusText: response.statusText, body: errorBody}); } // Parse response