diff --git a/docs/generators/client.md b/docs/generators/client.md index 2401cc57..48bb78c0 100644 --- a/docs/generators/client.md +++ b/docs/generators/client.md @@ -116,7 +116,7 @@ export class NatsClient { ### HTTP -For `http` a single API client class is generated that wraps the standalone [`http_client`](../protocols/http_client.md) channel functions. You construct it once with the shared request configuration (`server`, `auth`, `hooks`, retry, pagination, ...) and every operation becomes a method that reuses that configuration; any field can still be overridden per call. +For `http` a single API client class is generated that wraps the standalone [`http_client`](../protocols/http_client.md) channel functions. You construct it once with the shared request configuration (`baseUrl`, `auth`, `hooks`, retry, ...) and every operation becomes a method that reuses that configuration; any field can still be overridden per call. ```js export default { @@ -140,7 +140,7 @@ Example generated usage: import {SafepayNordicClient} from './__gen__/client/SafepayNordicClient'; const safepay = new SafepayNordicClient({ - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', auth: {type: 'bearer', token: process.env.API_TOKEN ?? ''} }); @@ -149,4 +149,4 @@ const response = await safepay.getV2Documents(); console.log(response.status, response.data); ``` -Each method returns the same `HttpClientResponse` as the underlying channel function, so response metadata (status, headers, pagination helpers) is preserved. \ No newline at end of file +Each method returns the same `HttpClientResponse` as the underlying channel function, so response metadata (status, headers) is preserved. \ No newline at end of file diff --git a/docs/protocols/http_client.md b/docs/protocols/http_client.md index 4b942efc..236c67e3 100644 --- a/docs/protocols/http_client.md +++ b/docs/protocols/http_client.md @@ -4,7 +4,7 @@ sidebar_position: 99 # HTTP(S) -HTTP client generator creates type-safe functions for making HTTP requests based on your API specification. It supports various authentication methods, pagination, retry logic, and extensibility hooks. +HTTP client generator creates type-safe functions for making HTTP requests based on your API specification. It supports various authentication methods, retry logic, and extensibility hooks. It is currently available through the generators ([channels](../generators/channels.md)): @@ -16,10 +16,6 @@ This is available through [AsyncAPI](../inputs/asyncapi.md) ([requires the HTTP |---|---| | Download | ❌ | | Upload | ❌ | -| Offset based Pagination | ✅ | -| Cursor based Pagination | ✅ | -| Page based Pagination | ✅ | -| Range based Pagination | ✅ | | Retry with backoff | ✅ | | OAuth2 Authorization code | ❌ (browser-only) | | OAuth2 Implicit | ❌ (browser-only) | @@ -118,7 +114,7 @@ const pingMessage = new Ping({ message: 'Hello!' }); // Make a simple request const response = await postPingPostRequest({ payload: pingMessage, - server: 'https://api.example.com' + baseUrl: 'https://api.example.com' }); // Access the response @@ -147,14 +143,14 @@ import { GetV2ConnectReferenceIdParameters } from './__gen__/channels/parameter/ // Request with a body: build the model, pass it as `payload`. const created = await http_client.postV2Connect({ - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', payload: new PostV2ConnectRequest({ returnUrl: 'https://shop.example/return' }) }); console.log(created.data.connectUrl); // typed response model // Request with a path parameter: supply it through the parameter model. const connect = await http_client.getV2ConnectReferenceId({ - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', parameters: new GetV2ConnectReferenceIdParameters({ referenceId: 'ref_123' }) }); console.log(connect.data.safepayAccountId); @@ -171,7 +167,7 @@ The HTTP client uses a discriminated union for authentication, providing excelle ```typescript const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', auth: { type: 'bearer', token: 'your-jwt-token' @@ -184,7 +180,7 @@ const response = await postPingPostRequest({ ```typescript const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', auth: { type: 'basic', username: 'user', @@ -199,7 +195,7 @@ const response = await postPingPostRequest({ // API Key in header (default) const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', auth: { type: 'apiKey', key: 'your-api-key', @@ -211,7 +207,7 @@ const response = await postPingPostRequest({ // API Key in query parameter const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', auth: { type: 'apiKey', key: 'your-api-key', @@ -228,7 +224,7 @@ For server-to-server authentication: ```typescript const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', auth: { type: 'oauth2', flow: 'client_credentials', @@ -251,7 +247,7 @@ For legacy applications requiring username/password: ```typescript const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', auth: { type: 'oauth2', flow: 'password', @@ -277,7 +273,7 @@ const accessToken = getTokenFromBrowserFlow(); const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', auth: { type: 'oauth2', accessToken: accessToken, @@ -292,81 +288,6 @@ const response = await postPingPostRequest({ }); ``` -## Pagination - -The HTTP client supports multiple pagination strategies. Pagination parameters can be placed in query parameters or headers. - -### Offset-based Pagination - -```typescript -const response = await getItemsRequest({ - server: 'https://api.example.com', - pagination: { - type: 'offset', - offset: 0, - limit: 25, - in: 'query', // 'query' or 'header' - offsetParam: 'offset', // Query param name (default: 'offset') - limitParam: 'limit' // Query param name (default: 'limit') - } -}); - -// Navigate pages -if (response.hasNextPage?.()) { - const nextPage = await response.getNextPage?.(); -} -``` - -### Cursor-based Pagination - -```typescript -const response = await getItemsRequest({ - server: 'https://api.example.com', - pagination: { - type: 'cursor', - cursor: undefined, // First page - limit: 25, - cursorParam: 'cursor' - } -}); - -// Get next page using cursor from response -if (response.pagination?.nextCursor) { - const nextPage = await response.getNextPage?.(); -} -``` - -### Page-based Pagination - -```typescript -const response = await getItemsRequest({ - server: 'https://api.example.com', - pagination: { - type: 'page', - page: 1, - pageSize: 25, - pageParam: 'page', - pageSizeParam: 'per_page' - } -}); -``` - -### Range-based Pagination (RFC 7233) - -```typescript -const response = await getItemsRequest({ - server: 'https://api.example.com', - pagination: { - type: 'range', - start: 0, - end: 24, - unit: 'items', // Range unit (default: 'items') - rangeHeader: 'Range' // Header name (default: 'Range') - } -}); -// Sends: Range: items=0-24 -``` - ## Retry with Exponential Backoff Configure automatic retry for failed requests: @@ -374,7 +295,7 @@ Configure automatic retry for failed requests: ```typescript const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', retry: { maxRetries: 3, // Maximum retry attempts (default: 3) initialDelayMs: 1000, // Initial delay before first retry (default: 1000) @@ -396,7 +317,7 @@ Customize request behavior with hooks: ```typescript const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', hooks: { // Modify request before sending beforeRequest: async (params) => { @@ -459,7 +380,7 @@ const params = new UserItemsParameters({ }); const response = await getGetUserItem({ - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', parameters: params // Replaces {userId} and {itemId} in path }); ``` @@ -477,7 +398,7 @@ const headers = new ItemRequestHeaders({ }); const response = await putUpdateUserItem({ - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', parameters: params, payload: itemData, requestHeaders: headers // Type-safe headers @@ -491,12 +412,12 @@ Add custom headers or query parameters to any request: ```typescript const response = await postPingPostRequest({ payload: message, - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', additionalHeaders: { 'X-Custom-Header': 'value', 'Accept-Language': 'en-US' }, - queryParams: { + additionalQueryParams: { include: 'metadata', format: 'detailed' } @@ -519,7 +440,7 @@ operations: ```typescript const response = await getItemRequest({ - server: 'https://api.example.com', + baseUrl: 'https://api.example.com', parameters: params }); diff --git a/src/codegen/generators/typescript/channels/openapi.ts b/src/codegen/generators/typescript/channels/openapi.ts index 9fb4045a..485cac73 100644 --- a/src/codegen/generators/typescript/channels/openapi.ts +++ b/src/codegen/generators/typescript/channels/openapi.ts @@ -106,6 +106,7 @@ export async function generateTypeScriptChannelsForOpenAPI( openapiDocument, payloads, parameters, + headers, oauth2Enabled ); @@ -136,6 +137,7 @@ function processOpenAPIOperations( openapiDocument: OpenAPIDocument, payloads: TypeScriptPayloadRenderType, parameters: TypeScriptParameterRenderType, + headers: TypeScriptHeadersRenderType, oauth2Enabled: boolean ): ReturnType[] { const renders: ReturnType[] = []; @@ -152,6 +154,7 @@ function processOpenAPIOperations( path, payloads, parameters, + headers, oauth2Enabled ); if (render) { @@ -172,6 +175,7 @@ function processOperation( path: string, payloads: TypeScriptPayloadRenderType, parameters: TypeScriptParameterRenderType, + headers: TypeScriptHeadersRenderType, oauth2Enabled: boolean ): ReturnType | undefined { // eslint-disable-next-line security/detect-object-injection @@ -202,6 +206,10 @@ function processOperation( // eslint-disable-next-line security/detect-object-injection const parameterModel = parameters.channelModels[operationId]; + // Look up headers + // eslint-disable-next-line security/detect-object-injection + const headersModel = headers.channelModels[operationId]; + // Get message types - handle undefined payloads const requestMessageInfo = requestPayload ? getMessageTypeAndModule(requestPayload) @@ -257,10 +265,13 @@ function processOperation( channelParameters: parameterModel?.model as | ConstrainedObjectModel | undefined, + channelHeaders: headersModel?.model as ConstrainedObjectModel | undefined, includesStatusCodes: replyIncludesStatusCodes, description, deprecated, - oauth2Enabled + oauth2Enabled, + hasSerializeUrl: parameterModel !== undefined, + hasSerializeHeaders: headersModel !== undefined }); // Grouping metadata for the `organization` option (consumed in diff --git a/src/codegen/generators/typescript/channels/protocols/http/client.ts b/src/codegen/generators/typescript/channels/protocols/http/client.ts index 7b83cbc5..96cd80f4 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/client.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/client.ts @@ -25,7 +25,9 @@ export function renderHttpFetchClient({ includesStatusCodes = false, description, deprecated, - oauth2Enabled = true + oauth2Enabled = true, + hasSerializeUrl = false, + hasSerializeHeaders = false }: RenderHttpParameters): HttpRenderType { const messageType = requestMessageModule ? `${requestMessageModule}.${requestMessageType}` @@ -37,8 +39,9 @@ export function renderHttpFetchClient({ // Generate context interface name const contextInterfaceName = `${pascalCase(functionName)}Context`; - // Determine if operation has path parameters + // Determine if operation has path parameters or headers const hasParameters = channelParameters !== undefined; + const hasHeaders = channelHeaders !== undefined; // Generate the context interface (extends HttpClientContext) const contextInterface = generateContextInterface( @@ -66,11 +69,16 @@ export function renderHttpFetchClient({ messageType, requestTopic, hasParameters, + hasHeaders, + headersType: channelHeaders?.type, + hasSerializeHeaders, + parametersType: channelParameters?.type, method, servers, includesStatusCodes, jsDoc, - oauth2Enabled + oauth2Enabled, + hasSerializeUrl }); const code = `${contextInterface} @@ -105,25 +113,38 @@ function generateContextInterface( fields.push(` payload: ${messageType};`); } - // Reference the concrete generated parameter class so consumers get typed - // query/path parameters. It still exposes getChannelWithParameters, so the - // buildUrlWithParameters call in the function body keeps working. + // Reference the concrete generated parameter interface so consumers get typed + // query/path parameters. The serialize function is called by buildUrlWithParameters. if (parametersType) { fields.push(` parameters: ${parametersType};`); } - // Reference the concrete generated headers model when available; fall back to - // the structural marshal contract otherwise. Always optional since headers can - // also be passed via additionalHeaders. - fields.push( - ` requestHeaders?: ${headersType ?? '{ marshal: () => string }'};` - ); + // Emit requestHeaders only when the spec defines operation headers so the + // context stays minimal for operations that don't have them. + if (headersType) { + fields.push(` requestHeaders?: ${headersType};`); + } const fieldsStr = fields.length > 0 ? `\n${fields.join('\n')}\n` : ''; return `export interface ${interfaceName} extends HttpClientContext {${fieldsStr}}`; } +function buildUrlCode( + requestTopic: string, + hasParameters: boolean, + parametersType: string | undefined, + hasSerializeUrl: boolean +): string { + if (!hasParameters || !parametersType) { + return `let url = \`\${config.baseUrl}${requestTopic}\`;`; + } + const serializeFn = hasSerializeUrl + ? `serialize${parametersType}Url(context.parameters, path)` + : `context.parameters.getChannelWithParameters(path)`; + return `let url = buildUrlWithParameters(config.baseUrl, '${requestTopic}', (path) => ${serializeFn});`; +} + /** * Generate the function implementation */ @@ -136,11 +157,16 @@ function generateFunctionImplementation(params: { messageType: string | undefined; requestTopic: string; hasParameters: boolean; + hasHeaders: boolean; + headersType: string | undefined; + hasSerializeHeaders: boolean; + parametersType: string | undefined; method: string; servers: string[]; includesStatusCodes: boolean; jsDoc: string; oauth2Enabled: boolean; + hasSerializeUrl: boolean; }): string { const { functionName, @@ -151,26 +177,35 @@ function generateFunctionImplementation(params: { messageType, requestTopic, hasParameters, + hasHeaders, + headersType, + hasSerializeHeaders, + parametersType, method, servers, includesStatusCodes, jsDoc, - oauth2Enabled + oauth2Enabled, + hasSerializeUrl } = params; - const defaultServer = servers[0] ?? "'localhost:3000'"; + const defaultServer = servers[0] ?? "'http://localhost:3000'"; const hasBody = messageType && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()); - // Generate URL building code - const urlBuildCode = hasParameters - ? `let url = buildUrlWithParameters(config.server, '${requestTopic}', context.parameters);` - : 'let url = `${config.server}${config.path}`;'; + const urlBuildCode = buildUrlCode(requestTopic, hasParameters, parametersType, hasSerializeUrl); // Generate headers initialization - const headersInit = `let headers = context.requestHeaders + let headersInit: string; + if (!hasHeaders) { + headersInit = `let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record;`; + } else if (hasSerializeHeaders) { + headersInit = `let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders, ...(context.requestHeaders ? serialize${headersType}Headers(context.requestHeaders) : {}) } as Record;`; + } else { + headersInit = `let headers = context.requestHeaders ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record;`; + } // Generate body preparation const bodyPrep = hasBody @@ -236,8 +271,7 @@ function generateFunctionImplementation(params: { async function ${functionName}(context: ${contextInterfaceName}${contextDefault}): Promise> { // Apply defaults const config = { - path: '${requestTopic}', - server: ${defaultServer}, + baseUrl: ${defaultServer}, ...context, }; @@ -246,12 +280,7 @@ ${oauth2ValidateBlock} // Build headers // Build URL ${urlBuildCode} - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -297,17 +326,13 @@ ${oauth2TokenBlock} // Extract response metadata const responseHeaders = extractHeaders(response); - const paginationInfo = extractPaginationInfo(responseHeaders, config.pagination); - // Build response wrapper with pagination helpers const result: HttpClientResponse<${replyType}> = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, ${functionName}), }; return result; 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 e1f0a65b..2fcb6371 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/common-types.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/common-types.ts @@ -70,26 +70,6 @@ export interface HttpResponse { json: () => Record | Promise>; } -/** - * Pagination info extracted from response - */ -export interface PaginationInfo { - /** Total number of items (if available from headers like X-Total-Count) */ - totalCount?: number; - /** Total number of pages (if available) */ - totalPages?: number; - /** Current page/offset */ - currentOffset?: number; - /** Items per page */ - limit?: number; - /** Next cursor (for cursor-based pagination) */ - nextCursor?: string; - /** Previous cursor */ - prevCursor?: string; - /** Whether there are more items */ - hasMore?: boolean; -} - /** * Rich response wrapper returned by HTTP client functions */ @@ -104,16 +84,6 @@ export interface HttpClientResponse { headers: Record; /** Raw JSON response before deserialization */ rawData: Record; - /** Pagination info extracted from response (if applicable) */ - pagination?: PaginationInfo; - /** Fetch the next page (if pagination is configured and more data exists) */ - getNextPage?: () => Promise>; - /** Fetch the previous page (if pagination is configured) */ - getPrevPage?: () => Promise>; - /** Check if there's a next page */ - hasNextPage?: () => boolean; - /** Check if there's a previous page */ - hasPrevPage?: () => boolean; } /** @@ -138,69 +108,6 @@ export interface TokenResponse { ${securityTypes} ${authFeaturesBlock}${apiKeyDefaultsBlock} -// ============================================================================ -// Pagination Types -// ============================================================================ - -/** - * Where to place pagination parameters - */ -export type PaginationLocation = 'query' | 'header'; - -/** - * Offset-based pagination configuration - */ -export interface OffsetPagination { - type: 'offset'; - in?: PaginationLocation; // Where to place params (default: 'query') - offset: number; - limit: number; - offsetParam?: string; // Param name for offset (default: 'offset' for query, 'X-Offset' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Cursor-based pagination configuration - */ -export interface CursorPagination { - type: 'cursor'; - in?: PaginationLocation; // Where to place params (default: 'query') - cursor?: string; - limit?: number; - cursorParam?: string; // Param name for cursor (default: 'cursor' for query, 'X-Cursor' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Page-based pagination configuration - */ -export interface PagePagination { - type: 'page'; - in?: PaginationLocation; // Where to place params (default: 'query') - page: number; - pageSize: number; - pageParam?: string; // Param name for page (default: 'page' for query, 'X-Page' for header) - pageSizeParam?: string; // Param name for page size (default: 'pageSize' for query, 'X-Page-Size' for header) -} - -/** - * Range-based pagination (typically used with headers) - * Follows RFC 7233 style: Range: items=0-24 - */ -export interface RangePagination { - type: 'range'; - in?: 'header'; // Range pagination is typically header-only - start: number; - end: number; - unit?: string; // Range unit (default: 'items') - rangeHeader?: string; // Header name (default: 'Range') -} - -/** - * Union type for all pagination methods - */ -export type PaginationConfig = OffsetPagination | CursorPagination | PagePagination | RangePagination; - // ============================================================================ // Retry Configuration // ============================================================================ @@ -258,15 +165,11 @@ export interface HttpHooks { * Base context shared by all HTTP client functions */ export interface HttpClientContext { - server?: string; - path?: string; + baseUrl?: string; // Authentication - grouped for better autocomplete auth?: AuthConfig; - // Pagination configuration - pagination?: PaginationConfig; - // Retry configuration retry?: RetryConfig; @@ -276,8 +179,8 @@ export interface HttpClientContext { // Additional options additionalHeaders?: Record; - // Query parameters - queryParams?: Record; + // Extra query parameters not covered by the typed parameters interface + additionalQueryParams?: Record; } // ============================================================================ @@ -337,94 +240,6 @@ ${applyAuthCases} return { headers, url }; } -/** - * Apply pagination parameters to URL and/or headers based on configuration - */ -function applyPagination( - pagination: PaginationConfig | undefined, - url: string, - headers: Record -): { url: string; headers: Record } { - if (!pagination) return { url, headers }; - - const location = pagination.in ?? 'query'; - const isHeader = location === 'header'; - - // Helper to get default param names based on location - const getDefaultName = (queryName: string, headerName: string) => - isHeader ? headerName : queryName; - - const queryParams = new URLSearchParams(); - const headerParams: Record = {}; - - const addParam = (name: string, value: string) => { - if (isHeader) { - headerParams[name] = value; - } else { - queryParams.append(name, value); - } - }; - - switch (pagination.type) { - case 'offset': - addParam( - pagination.offsetParam ?? getDefaultName('offset', 'X-Offset'), - String(pagination.offset) - ); - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - break; - - case 'cursor': - if (pagination.cursor) { - addParam( - pagination.cursorParam ?? getDefaultName('cursor', 'X-Cursor'), - pagination.cursor - ); - } - if (pagination.limit !== undefined) { - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - } - break; - - case 'page': - addParam( - pagination.pageParam ?? getDefaultName('page', 'X-Page'), - String(pagination.page) - ); - addParam( - pagination.pageSizeParam ?? getDefaultName('pageSize', 'X-Page-Size'), - String(pagination.pageSize) - ); - break; - - case 'range': { - // Range pagination is always header-based (RFC 7233 style) - const unit = pagination.unit ?? 'items'; - const headerName = pagination.rangeHeader ?? 'Range'; - headerParams[headerName] = \`\${unit}=\${pagination.start}-\${pagination.end}\`; - break; - } - } - - // Apply query params to URL - const queryString = queryParams.toString(); - if (queryString) { - const separator = url.includes('?') ? '&' : '?'; - url = \`\${url}\${separator}\${queryString}\`; - } - - // Merge header params - const updatedHeaders = { ...headers, ...headerParams }; - - return { url, headers: updatedHeaders }; -} - /** * Apply query parameters to URL */ @@ -568,210 +383,17 @@ function extractHeaders(response: HttpResponse): Record { } /** - * Extract pagination info from response headers - */ -function extractPaginationInfo( - headers: Record, - currentPagination?: PaginationConfig -): PaginationInfo | undefined { - const info: PaginationInfo = {}; - let hasPaginationInfo = false; - - // Common total count headers - const totalCount = headers['x-total-count'] || headers['x-total'] || headers['total-count']; - if (totalCount) { - info.totalCount = parseInt(totalCount, 10); - hasPaginationInfo = true; - } - - // Total pages - const totalPages = headers['x-total-pages'] || headers['x-page-count']; - if (totalPages) { - info.totalPages = parseInt(totalPages, 10); - hasPaginationInfo = true; - } - - // Next cursor - const nextCursor = headers['x-next-cursor'] || headers['x-cursor-next']; - if (nextCursor) { - info.nextCursor = nextCursor; - info.hasMore = true; - hasPaginationInfo = true; - } - - // Previous cursor - const prevCursor = headers['x-prev-cursor'] || headers['x-cursor-prev']; - if (prevCursor) { - info.prevCursor = prevCursor; - hasPaginationInfo = true; - } - - // Has more indicator - const hasMore = headers['x-has-more'] || headers['x-has-next']; - if (hasMore) { - info.hasMore = hasMore.toLowerCase() === 'true' || hasMore === '1'; - hasPaginationInfo = true; - } - - // Parse Link header (RFC 5988) - const linkHeader = headers['link']; - if (linkHeader) { - const links = parseLinkHeader(linkHeader); - if (links.next) { - info.hasMore = true; - hasPaginationInfo = true; - } - } - - // Include current pagination state - if (currentPagination) { - switch (currentPagination.type) { - case 'offset': - info.currentOffset = currentPagination.offset; - info.limit = currentPagination.limit; - break; - case 'cursor': - info.limit = currentPagination.limit; - break; - case 'page': - info.currentOffset = (currentPagination.page - 1) * currentPagination.pageSize; - info.limit = currentPagination.pageSize; - break; - case 'range': - info.currentOffset = currentPagination.start; - info.limit = currentPagination.end - currentPagination.start + 1; - break; - } - hasPaginationInfo = true; - } - - // Calculate hasMore based on total count - if (info.hasMore === undefined && info.totalCount !== undefined && - info.currentOffset !== undefined && info.limit !== undefined) { - info.hasMore = info.currentOffset + info.limit < info.totalCount; - } - - return hasPaginationInfo ? info : undefined; -} - -/** - * Parse RFC 5988 Link header - */ -function parseLinkHeader(header: string): Record { - const links: Record = {}; - const parts = header.split(','); - - for (const part of parts) { - const match = part.match(/<([^>]+)>;\\s*rel="?([^";\\s]+)"?/); - if (match) { - links[match[2]] = match[1]; - } - } - - return links; -} - -/** - * Create pagination helper functions for the response - */ -function createPaginationHelpers( - currentConfig: TContext, - paginationInfo: PaginationInfo | undefined, - requestFn: (config: TContext) => Promise> -): Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> { - const helpers: Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> = {}; - - if (!currentConfig.pagination) { - return helpers; - } - - const pagination = currentConfig.pagination; - - helpers.hasNextPage = () => { - if (paginationInfo?.hasMore !== undefined) return paginationInfo.hasMore; - if (paginationInfo?.nextCursor) return true; - if (paginationInfo?.totalCount !== undefined && - paginationInfo.currentOffset !== undefined && - paginationInfo.limit !== undefined) { - return paginationInfo.currentOffset + paginationInfo.limit < paginationInfo.totalCount; - } - return false; - }; - - helpers.hasPrevPage = () => { - if (paginationInfo?.prevCursor) return true; - if (paginationInfo?.currentOffset !== undefined) { - return paginationInfo.currentOffset > 0; - } - return false; - }; - - helpers.getNextPage = async () => { - let nextPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - nextPagination = { ...pagination, offset: pagination.offset + pagination.limit }; - break; - case 'cursor': - if (!paginationInfo?.nextCursor) throw new Error('No next cursor available'); - nextPagination = { ...pagination, cursor: paginationInfo.nextCursor }; - break; - case 'page': - nextPagination = { ...pagination, page: pagination.page + 1 }; - break; - case 'range': - const rangeSize = pagination.end - pagination.start + 1; - nextPagination = { ...pagination, start: pagination.end + 1, end: pagination.end + rangeSize }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: nextPagination }); - }; - - helpers.getPrevPage = async () => { - let prevPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - prevPagination = { ...pagination, offset: Math.max(0, pagination.offset - pagination.limit) }; - break; - case 'cursor': - if (!paginationInfo?.prevCursor) throw new Error('No previous cursor available'); - prevPagination = { ...pagination, cursor: paginationInfo.prevCursor }; - break; - case 'page': - prevPagination = { ...pagination, page: Math.max(1, pagination.page - 1) }; - break; - case 'range': - const size = pagination.end - pagination.start + 1; - const newStart = Math.max(0, pagination.start - size); - prevPagination = { ...pagination, start: newStart, end: newStart + size - 1 }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: prevPagination }); - }; - - return helpers; -} - -/** - * Builds a URL with path parameters replaced + * Builds a URL with path parameters replaced using a serializer function * @param server - Base server URL * @param pathTemplate - Path template with {param} placeholders - * @param parameters - Parameter object with getChannelWithParameters method + * @param serializeFn - Function that takes the path template and returns the serialized path */ -function buildUrlWithParameters string }>( +function buildUrlWithParameters( server: string, pathTemplate: string, - parameters: T + serializeFn: (path: string) => string ): string { - const path = parameters.getChannelWithParameters(pathTemplate); + const path = serializeFn(pathTemplate); return \`\${server}\${path}\`; } diff --git a/src/codegen/generators/typescript/channels/types.ts b/src/codegen/generators/typescript/channels/types.ts index d3c329cf..0eb4bc3b 100644 --- a/src/codegen/generators/typescript/channels/types.ts +++ b/src/codegen/generators/typescript/channels/types.ts @@ -304,6 +304,18 @@ export interface RenderHttpParameters { * the AsyncAPI path (which generates all auth types) unchanged. */ oauth2Enabled?: boolean; + /** + * Whether the parameter model exports a standalone `serializeXUrl` function + * (OpenAPI plain-object style). When false, falls back to the class-based + * `getChannelWithParameters` method (AsyncAPI style). Defaults to false. + */ + hasSerializeUrl?: boolean; + /** + * Whether the header model exports a standalone `serializeXHeaders` function + * (OpenAPI plain-object style). When false, falls back to the class-based + * `.marshal()` method (AsyncAPI style). Defaults to false. + */ + hasSerializeHeaders?: boolean; } export type SupportedProtocols = diff --git a/src/codegen/generators/typescript/channels/utils.ts b/src/codegen/generators/typescript/channels/utils.ts index c45587eb..e44c3d1a 100644 --- a/src/codegen/generators/typescript/channels/utils.ts +++ b/src/codegen/generators/typescript/channels/utils.ts @@ -76,7 +76,8 @@ export function addParametersToDependencies( parameterGenerator: {outputPath: string}, currentGenerator: {outputPath: string}, dependencies: string[], - importExtension: ImportExtension = 'none' + importExtension: ImportExtension = 'none', + parameterFunctions?: Record ) { Object.values(parameters) .filter((model) => model !== undefined) @@ -93,9 +94,12 @@ export function addParametersToDependencies( importExtension ); - dependencies.push( - `import {${parameter.modelName}} from '${importPath}';` - ); + const fns = parameterFunctions?.[parameter.modelName] ?? []; + const importNames = + fns.length > 0 + ? `${parameter.modelName}, ${fns.join(', ')}` + : parameter.modelName; + dependencies.push(`import {${importNames}} from '${importPath}';`); }); } export function addParametersToExports( @@ -117,7 +121,8 @@ export function addHeadersToDependencies( headerGenerator: {outputPath: string}, currentGenerator: {outputPath: string}, dependencies: string[], - importExtension: ImportExtension = 'none' + importExtension: ImportExtension = 'none', + headerFunctions?: Record ) { Object.values(headers) .filter((model) => model !== undefined) @@ -134,7 +139,12 @@ export function addHeadersToDependencies( importExtension ); - dependencies.push(`import {${header.modelName}} from '${importPath}';`); + const fns = headerFunctions?.[header.modelName] ?? []; + const importNames = + fns.length > 0 + ? `${header.modelName}, ${fns.join(', ')}` + : header.modelName; + dependencies.push(`import {${importNames}} from '${importPath}';`); }); } export function getMessageTypeAndModule(payload: ChannelPayload) { @@ -231,7 +241,8 @@ export function collectProtocolDependencies( parameters.generator, context.generator, protocolDeps, - importExtension + importExtension, + parameters.parameterFunctions ); // Add header imports @@ -241,7 +252,8 @@ export function collectProtocolDependencies( headers.generator, context.generator, protocolDeps, - importExtension + importExtension, + headers.headerFunctions ); } } diff --git a/src/codegen/generators/typescript/client/protocols/http.ts b/src/codegen/generators/typescript/client/protocols/http.ts index 0eedb80c..02ec46e3 100644 --- a/src/codegen/generators/typescript/client/protocols/http.ts +++ b/src/codegen/generators/typescript/client/protocols/http.ts @@ -178,7 +178,7 @@ import * as http_client from '${channelImportPath}'; * @class ${className} * * ${classDescription} Construct it once with the shared request configuration - * (server, auth, hooks, ...) and call the operation methods; every method + * (baseUrl, auth, hooks, ...) and call the operation methods; every method * forwards to the underlying channel function with that configuration applied. */ export class ${className} { diff --git a/src/codegen/generators/typescript/headers.ts b/src/codegen/generators/typescript/headers.ts index fd0d1cd0..4b234216 100644 --- a/src/codegen/generators/typescript/headers.ts +++ b/src/codegen/generators/typescript/headers.ts @@ -10,7 +10,12 @@ import {z} from 'zod'; import {defaultCodegenTypescriptModelinaOptions} from './utils'; import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; import {processAsyncAPIHeaders} from '../../inputs/asyncapi/generators/headers'; -import {processOpenAPIHeaders} from '../../inputs/openapi/generators/headers'; +import { + processOpenAPIHeaders, + createOpenAPIHeadersGenerator, + generateOpenAPIHeaderFunctions +} from '../../inputs/openapi/generators/headers'; +import {ConstrainedObjectModel} from '@asyncapi/modelina'; import { TS_DESCRIPTION_PRESET, TS_COMMON_PRESET, @@ -99,20 +104,11 @@ export interface ProcessedHeadersData { >; } -// Core generator function that works with processed data -export async function generateTypescriptHeadersCore({ - processedData, - context -}: { - processedData: ProcessedHeadersData; - context: TypescriptHeadersContext; -}): Promise<{ - channelModels: Record; - files: GeneratedFile[]; -}> { - const {generator} = context; - - const modelinaGenerator = new TypeScriptFileGenerator({ +function createAsyncAPIHeadersGenerator( + generator: TypescriptHeadersGeneratorInternal, + context: TypescriptHeadersContext +): TypeScriptFileGenerator { + return new TypeScriptFileGenerator({ ...defaultCodegenTypescriptModelinaOptions, constraints: { propertyKey: typeScriptDefaultPropertyKeyConstraints({ @@ -123,52 +119,95 @@ export async function generateTypescriptHeadersCore({ useJavascriptReservedKeywords: false, presets: [ TS_DESCRIPTION_PRESET, - { - preset: TS_COMMON_PRESET, - options: { - marshalling: true - } - }, - createValidationPreset( - { - includeValidation: generator.includeValidation - }, - context - ) + {preset: TS_COMMON_PRESET, options: {marshalling: true}}, + createValidationPreset({includeValidation: generator.includeValidation}, context) ] }); +} + +function appendOpenAPISerializerFunctions( + result: {models: OutputModel[]; files: GeneratedFile[]}, + headerFunctions: Record +): void { + for (const model of result.models) { + if (!(model.model instanceof ConstrainedObjectModel)) { + continue; + } + const constrainedModel = model.model as ConstrainedObjectModel; + const fns = generateOpenAPIHeaderFunctions(constrainedModel); + const modelFileName = `${model.modelName}.ts`; + const fileIndex = result.files.findIndex((f) => + f.path.endsWith(modelFileName) + ); + if (fileIndex !== -1) { + result.files[fileIndex] = { + ...result.files[fileIndex], + content: `${result.files[fileIndex].content}\n\n${fns}` + }; + } + const modelName = constrainedModel.name; + headerFunctions[modelName] = [`serialize${modelName}Headers`]; + } +} + +function deduplicateFiles(files: GeneratedFile[]): GeneratedFile[] { + const uniqueFiles: GeneratedFile[] = []; + const seenPaths = new Map(); + for (const file of files) { + const existingIndex = seenPaths.get(file.path); + if (existingIndex === undefined) { + seenPaths.set(file.path, uniqueFiles.length); + uniqueFiles.push(file); + } else { + uniqueFiles[existingIndex] = file; + } + } + return uniqueFiles; +} + +// Core generator function that works with processed data +export async function generateTypescriptHeadersCore({ + processedData, + context +}: { + processedData: ProcessedHeadersData; + context: TypescriptHeadersContext; +}): Promise<{ + channelModels: Record; + files: GeneratedFile[]; + headerFunctions: Record; +}> { + const {generator, inputType} = context; + const isOpenAPI = inputType === 'openapi'; + const modelinaGenerator = isOpenAPI + ? createOpenAPIHeadersGenerator() + : createAsyncAPIHeadersGenerator(generator, context); const channelModels: Record = {}; - const files: GeneratedFile[] = []; + const allFiles: GeneratedFile[] = []; + const headerFunctions: Record = {}; for (const [channelId, headerData] of Object.entries( processedData.channelHeaders )) { - if (headerData) { - const result = await generateModels({ - generator: modelinaGenerator, - input: headerData.schema, - outputPath: generator.outputPath - }); - channelModels[channelId] = - result.models.length > 0 ? result.models[0] : undefined; - files.push(...result.files); - } else { + if (!headerData) { channelModels[channelId] = undefined; + continue; } - } - - // Deduplicate files by path - const uniqueFiles: GeneratedFile[] = []; - const seenPaths = new Set(); - for (const file of files) { - if (!seenPaths.has(file.path)) { - seenPaths.add(file.path); - uniqueFiles.push(file); + const result = await generateModels({ + generator: modelinaGenerator, + input: headerData.schema, + outputPath: generator.outputPath + }); + if (isOpenAPI) { + appendOpenAPISerializerFunctions(result, headerFunctions); } + channelModels[channelId] = + result.models.length > 0 ? result.models[0] : undefined; + allFiles.push(...result.files); } - return {channelModels, files: uniqueFiles}; + return {channelModels, files: deduplicateFiles(allFiles), headerFunctions}; } // Main generator function that orchestrates input processing and generation @@ -204,14 +243,16 @@ export async function generateTypescriptHeaders( } // Generate models using processed data - const {channelModels, files} = await generateTypescriptHeadersCore({ - processedData, - context - }); + const {channelModels, files, headerFunctions} = + await generateTypescriptHeadersCore({ + processedData, + context + }); return { channelModels, generator, - files + files, + headerFunctions }; } diff --git a/src/codegen/generators/typescript/parameters.ts b/src/codegen/generators/typescript/parameters.ts index ebacba39..2f2b6a48 100644 --- a/src/codegen/generators/typescript/parameters.ts +++ b/src/codegen/generators/typescript/parameters.ts @@ -1,5 +1,9 @@ /* eslint-disable security/detect-object-injection, sonarjs/cognitive-complexity */ -import {OutputModel, TypeScriptFileGenerator} from '@asyncapi/modelina'; +import { + ConstrainedObjectModel, + OutputModel, + TypeScriptFileGenerator +} from '@asyncapi/modelina'; import {AsyncAPIDocumentInterface} from '@asyncapi/parser'; import { GenericCodegenContext, @@ -15,6 +19,7 @@ import { } from '../../inputs/asyncapi/generators/parameters'; import { createOpenAPIGenerator, + generateOpenAPIParameterFunctions, processOpenAPIParameters } from '../../inputs/openapi/generators/parameters'; import {createMissingInputDocumentError} from '../../errors'; @@ -88,6 +93,7 @@ export async function generateTypescriptParameters( const channelModels: Record = {}; const files: GeneratedFile[] = []; + const parameterFunctions: Record = {}; let processedSchemaData: ProcessedParameterSchemaData; let parameterGenerator: TypeScriptFileGenerator; @@ -131,6 +137,34 @@ export async function generateTypescriptParameters( input: schemaData.schema, outputPath: generator.outputPath }); + + // Post-process OpenAPI parameters: append standalone serialize/parse functions + if (inputType === 'openapi') { + for (const model of result.models) { + if (model.model instanceof ConstrainedObjectModel) { + const constrainedModel = model.model as ConstrainedObjectModel; + const fns = generateOpenAPIParameterFunctions(constrainedModel); + + const modelFileName = `${model.modelName}.ts`; + const fileIndex = result.files.findIndex((f) => + f.path.endsWith(modelFileName) + ); + if (fileIndex !== -1) { + result.files[fileIndex] = { + ...result.files[fileIndex], + content: `${result.files[fileIndex].content}\n\n${fns}` + }; + } + + const modelName = constrainedModel.name; + parameterFunctions[modelName] = [ + `serialize${modelName}Url`, + `parse${modelName}FromUrl` + ]; + } + } + } + channelModels[channelId] = result.models.length > 0 ? result.models[0] : undefined; files.push(...result.files); @@ -139,12 +173,15 @@ export async function generateTypescriptParameters( } } - // Deduplicate files by path + // Deduplicate files by path, keeping the last version (which has functions appended) const uniqueFiles: GeneratedFile[] = []; - const seenPaths = new Set(); + const seenPaths = new Map(); for (const file of files) { - if (!seenPaths.has(file.path)) { - seenPaths.add(file.path); + const existingIndex = seenPaths.get(file.path); + if (existingIndex !== undefined) { + uniqueFiles[existingIndex] = file; + } else { + seenPaths.set(file.path, uniqueFiles.length); uniqueFiles.push(file); } } @@ -152,6 +189,7 @@ export async function generateTypescriptParameters( return { channelModels, generator, - files: uniqueFiles + files: uniqueFiles, + parameterFunctions }; } diff --git a/src/codegen/inputs/openapi/generators/headers.ts b/src/codegen/inputs/openapi/generators/headers.ts index e87e0d84..9534e095 100644 --- a/src/codegen/inputs/openapi/generators/headers.ts +++ b/src/codegen/inputs/openapi/generators/headers.ts @@ -1,8 +1,46 @@ /* eslint-disable security/detect-object-injection */ import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; import {ProcessedHeadersData} from '../../../generators/typescript/headers'; -import {pascalCase} from '../../../generators/typescript/utils'; +import { + defaultCodegenTypescriptModelinaOptions, + pascalCase +} from '../../../generators/typescript/utils'; import {deriveOperationId} from '../utils'; +import { + ConstrainedObjectModel, + TS_DESCRIPTION_PRESET, + TypeScriptFileGenerator +} from '@asyncapi/modelina'; + +export function createOpenAPIHeadersGenerator() { + return new TypeScriptFileGenerator({ + ...defaultCodegenTypescriptModelinaOptions, + enumType: 'union', + useJavascriptReservedKeywords: false, + modelType: 'interface', + presets: [TS_DESCRIPTION_PRESET] + }); +} + +export function generateOpenAPIHeaderFunctions( + model: ConstrainedObjectModel +): string { + const modelName = model.name; + + const headerMappings = Object.values(model.properties) + .map((prop) => { + const wireName = prop.unconstrainedPropertyName; + const tsName = prop.propertyName; + return ` if (headers.${tsName} !== undefined) { result['${wireName}'] = String(headers.${tsName}); }`; + }) + .join('\n'); + + return `export function serialize${modelName}Headers(headers: ${modelName}): Record { + const result: Record = {}; +${headerMappings} + return result; +}`; +} // Helper function to convert OpenAPI header schema to JSON Schema function convertHeaderSchemaToJsonSchema(header: any): any { diff --git a/src/codegen/inputs/openapi/generators/parameters.ts b/src/codegen/inputs/openapi/generators/parameters.ts index a810b8f3..2d59a76e 100644 --- a/src/codegen/inputs/openapi/generators/parameters.ts +++ b/src/codegen/inputs/openapi/generators/parameters.ts @@ -206,12 +206,13 @@ export function createParameterSchema( } /** - * Generate additional content for OpenAPI parameter classes + * Collect path and query parameter configs from a model */ -function generateOpenAPIParameterMethods(model: ConstrainedObjectModel) { +function collectParameterConfigs(model: ConstrainedObjectModel): { + pathParams: ParameterConfig[]; + queryParams: ParameterConfig[]; +} { const properties = model.originalInput?.properties ?? {}; - - // Collect path and query parameters const pathParams: ParameterConfig[] = []; const queryParams: ParameterConfig[] = []; @@ -236,26 +237,228 @@ function generateOpenAPIParameterMethods(model: ConstrainedObjectModel) { } } - if (pathParams.length === 0 && queryParams.length === 0) { - return ''; + return {pathParams, queryParams}; +} + +function buildEncodeValues(param: ParameterConfig): { + encodeValue: string; + encodeScalarValue: string; +} { + const encoding = param.allowReserved ? '' : 'encodeURIComponent'; + const encodeValue = encoding ? `${encoding}(String(v))` : 'String(v)'; + const encodeScalarValue = encoding + ? `${encoding}(String(parameters.${param.propertyName}))` + : `String(parameters.${param.propertyName})`; + return {encodeValue, encodeScalarValue}; +} + +function buildSerializeQueryPart( + param: ParameterConfig, + encodeValue: string, + encodeScalarValue: string +): string { + if (param.style === 'form' && !param.explode) { + return ` if (parameters.${param.propertyName} !== undefined && parameters.${param.propertyName} !== null) { + const val = parameters.${param.propertyName}; + if (Array.isArray(val)) { + if (val.length === 0) { + parts.push('${param.name}='); + } else { + parts.push(\`${param.name}=\${val.map(v => ${encodeValue}).join(',')}\`); + } + } else { + parts.push(\`${param.name}=\${${encodeScalarValue}}\`); + } + }`; + } + if (param.style === 'form' && param.explode) { + return ` if (parameters.${param.propertyName} !== undefined && parameters.${param.propertyName} !== null) { + const val = parameters.${param.propertyName}; + if (Array.isArray(val)) { + val.forEach(v => parts.push(\`${param.name}=\${${encodeValue}}\`)); + } else { + parts.push(\`${param.name}=\${${encodeScalarValue}}\`); + } + }`; } + return ` if (parameters.${param.propertyName} !== undefined && parameters.${param.propertyName} !== null) { + parts.push(\`${param.name}=\${${encodeScalarValue}}\`); + }`; +} - // Generate both serialization and deserialization methods - const serializationMethods = generateSerializationMethods( - pathParams, - queryParams - ); - const deserializationMethods = generateDeserializationMethods( - pathParams, - queryParams, - model - ); - const extractPathParametersMethod = +function buildPathParamValidationCheck( + p: ParameterConfig, + isRequired: boolean +): string { + return isRequired + ? ` + if (pathValues['${p.name}'] === undefined) { + throw new Error(\`Required parameter '${p.name}' is missing from URL\`); + }` + : ''; +} + +function buildPathParamAssignment(p: ParameterConfig, propSchema: any): string { + const isNumber = + propSchema?.type === 'integer' || propSchema?.type === 'number'; + const paramType = getParameterType(propSchema); + const value = isNumber + ? `Number(pathValues['${p.name}'])` + : `pathValues['${p.name}'] as ${paramType}`; + return ` ${p.propertyName}: ${value}`; +} + +function buildParseQueryParam(param: ParameterConfig, propSchema: any): string { + const isArray = propSchema?.type === 'array' || propSchema?.items; + const isBoolean = propSchema?.type === 'boolean'; + const isNumber = + propSchema?.type === 'integer' || propSchema?.type === 'number'; + const paramType = getParameterType(propSchema); + + if (isArray && param.style === 'form' && !param.explode) { + return ` if (params.has('${param.name}')) { + const raw = params.get('${param.name}'); + if (raw === '') { + result.${param.propertyName} = []; + } else if (raw !== null) { + result.${param.propertyName} = raw.split(',') as ${paramType}; + } + }`; + } + if (isNumber) { + return ` if (params.has('${param.name}')) { + const raw = params.get('${param.name}'); + if (raw !== null) { + const num = Number(raw); + if (!isNaN(num)) { + result.${param.propertyName} = num; + } + } + }`; + } + if (isBoolean) { + return ` if (params.has('${param.name}')) { + const raw = params.get('${param.name}'); + if (raw !== null) { + result.${param.propertyName} = raw.toLowerCase() === 'true'; + } + }`; + } + const typecast = paramType !== 'string' ? ` as ${paramType}` : ''; + return ` if (params.has('${param.name}')) { + const raw = params.get('${param.name}'); + if (raw !== null) { + result.${param.propertyName} = raw${typecast}; + } + }`; +} + +/** + * Generate standalone exported functions for OpenAPI parameter interfaces. + * Returns the serialize and parse functions as a string to append to the model file. + */ +export function generateOpenAPIParameterFunctions( + model: ConstrainedObjectModel +): string { + const {pathParams, queryParams} = collectParameterConfigs(model); + const modelName = model.name; + + const pathSerializations = pathParams.map((param) => { + const {encodeScalarValue} = buildEncodeValues(param); + return ` url = url.replace(/\\{${param.name}\\}/g, ${encodeScalarValue});`; + }); + + const queryParts: string[] = []; + for (const param of queryParams) { + const {encodeValue, encodeScalarValue} = buildEncodeValues(param); + queryParts.push(buildSerializeQueryPart(param, encodeValue, encodeScalarValue)); + } + + const serializeBody = + [ + ...pathSerializations, + ...(queryParts.length > 0 + ? [ + '\n const parts: string[] = [];', + ...queryParts, + "\n const queryString = parts.join('&');\n if (queryString) {\n url += `?${queryString}`;\n }" + ] + : []) + ].join('\n') || ' // no parameters to substitute'; + + const pathParamNames = pathParams.map((p) => p.name); + const pathExtract = pathParams.length > 0 - ? generateExtractPathParametersMethod(pathParams, model) + ? ` const urlPath = url.indexOf('?') !== -1 ? url.slice(0, url.indexOf('?')) : url; + + const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/?]+)'); + const regex = new RegExp('^' + regexPattern + '$'); + const match = urlPath.match(regex); + + if (!match) { + throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); + } + + const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; + const pathValues: Record = {}; + paramNames.forEach((name, index) => { + pathValues[name] = decodeURIComponent(match[index + 1]); + }); + +${pathParams + .map((p) => { + const isRequired = model.originalInput?.required?.includes(p.name) ?? false; + return buildPathParamValidationCheck(p, isRequired); + }) + .join('')}` : ''; - return `${serializationMethods}${deserializationMethods}${extractPathParametersMethod}`; + const properties = model.originalInput?.properties ?? {}; + + const requiredPathArguments = pathParams + .map((p) => buildPathParamAssignment(p, properties[p.name])) + .join(',\n'); + + const resultInit = + pathParams.length > 0 + ? ` const result: ${modelName} = {\n${requiredPathArguments}\n };` + : ` const result: ${modelName} = {} as ${modelName};`; + + const queryExtract = + queryParams.length > 0 + ? ` + const qIdx = url.indexOf('?'); + if (qIdx === -1) { + return result; + } + + const queryString = url.slice(qIdx + 1); + if (!queryString) { + return result; + } + + const params = new URLSearchParams(queryString); + +${queryParams + .map((param) => buildParseQueryParam(param, properties[param.name])) + .join('\n')}` + : ''; + + return `export function serialize${modelName}Url(parameters: ${modelName}, basePath: string): string { + let url = basePath; + +${serializeBody} + + return url; +} + +export function parse${modelName}FromUrl(url: string, basePath: string): ${modelName} { +${pathExtract} +${resultInit} +${queryExtract} + + return result; +}`; } /** @@ -1259,17 +1462,7 @@ export function createOpenAPIGenerator() { ...defaultCodegenTypescriptModelinaOptions, enumType: 'union', useJavascriptReservedKeywords: false, - presets: [ - TS_DESCRIPTION_PRESET, - { - class: { - additionalContent: ({content, model}) => { - const additionalMethods = generateOpenAPIParameterMethods(model); - return `${content} -${additionalMethods}`; - } - } - } - ] + modelType: 'interface', + presets: [TS_DESCRIPTION_PRESET] }); } diff --git a/src/codegen/types.ts b/src/codegen/types.ts index e20eb7eb..dcfc1c5f 100644 --- a/src/codegen/types.ts +++ b/src/codegen/types.ts @@ -156,12 +156,16 @@ export interface ParameterRenderType { generator: GeneratorType; /** Generated files with path and content */ files: GeneratedFile[]; + /** Map from model name to list of exported standalone function names */ + parameterFunctions?: Record; } export interface HeadersRenderType { channelModels: Record; generator: GeneratorType; /** Generated files with path and content */ files: GeneratedFile[]; + /** Map from model name to list of exported standalone function names (OpenAPI only) */ + headerFunctions?: Record; } export interface TypesRenderType { result: string; diff --git a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap index ed56a781..1b0a03d2 100644 --- a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap @@ -11,6 +11,7 @@ exports[`channels typescript OpenAPI input should generate HTTP client protocol "import {Pet} from './../../../../../payloads/Pet'; import * as FindPetsByStatusAndCategoryResponseModule from './../../../../../payloads/FindPetsByStatusAndCategoryResponse'; import {FindPetsByStatusAndCategoryParameters} from './../../../../../parameters/FindPetsByStatusAndCategoryParameters'; +import {FindPetsByStatusAndCategoryHeaders, serializeFindPetsByStatusAndCategoryHeadersHeaders} from './../../../../../headers/FindPetsByStatusAndCategoryHeaders'; // ============================================================================ // Common Types - Shared across all HTTP client functions @@ -27,26 +28,6 @@ export interface HttpResponse { json: () => Record | Promise>; } -/** - * Pagination info extracted from response - */ -export interface PaginationInfo { - /** Total number of items (if available from headers like X-Total-Count) */ - totalCount?: number; - /** Total number of pages (if available) */ - totalPages?: number; - /** Current page/offset */ - currentOffset?: number; - /** Items per page */ - limit?: number; - /** Next cursor (for cursor-based pagination) */ - nextCursor?: string; - /** Previous cursor */ - prevCursor?: string; - /** Whether there are more items */ - hasMore?: boolean; -} - /** * Rich response wrapper returned by HTTP client functions */ @@ -61,16 +42,6 @@ export interface HttpClientResponse { headers: Record; /** Raw JSON response before deserialization */ rawData: Record; - /** Pagination info extracted from response (if applicable) */ - pagination?: PaginationInfo; - /** Fetch the next page (if pagination is configured and more data exists) */ - getNextPage?: () => Promise>; - /** Fetch the previous page (if pagination is configured) */ - getPrevPage?: () => Promise>; - /** Check if there's a next page */ - hasNextPage?: () => boolean; - /** Check if there's a previous page */ - hasPrevPage?: () => boolean; } /** @@ -165,69 +136,6 @@ const API_KEY_DEFAULTS = { in: 'header' as 'header' | 'query' | 'cookie' } as const; -// ============================================================================ -// Pagination Types -// ============================================================================ - -/** - * Where to place pagination parameters - */ -export type PaginationLocation = 'query' | 'header'; - -/** - * Offset-based pagination configuration - */ -export interface OffsetPagination { - type: 'offset'; - in?: PaginationLocation; // Where to place params (default: 'query') - offset: number; - limit: number; - offsetParam?: string; // Param name for offset (default: 'offset' for query, 'X-Offset' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Cursor-based pagination configuration - */ -export interface CursorPagination { - type: 'cursor'; - in?: PaginationLocation; // Where to place params (default: 'query') - cursor?: string; - limit?: number; - cursorParam?: string; // Param name for cursor (default: 'cursor' for query, 'X-Cursor' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Page-based pagination configuration - */ -export interface PagePagination { - type: 'page'; - in?: PaginationLocation; // Where to place params (default: 'query') - page: number; - pageSize: number; - pageParam?: string; // Param name for page (default: 'page' for query, 'X-Page' for header) - pageSizeParam?: string; // Param name for page size (default: 'pageSize' for query, 'X-Page-Size' for header) -} - -/** - * Range-based pagination (typically used with headers) - * Follows RFC 7233 style: Range: items=0-24 - */ -export interface RangePagination { - type: 'range'; - in?: 'header'; // Range pagination is typically header-only - start: number; - end: number; - unit?: string; // Range unit (default: 'items') - rangeHeader?: string; // Header name (default: 'Range') -} - -/** - * Union type for all pagination methods - */ -export type PaginationConfig = OffsetPagination | CursorPagination | PagePagination | RangePagination; - // ============================================================================ // Retry Configuration // ============================================================================ @@ -285,15 +193,11 @@ export interface HttpHooks { * Base context shared by all HTTP client functions */ export interface HttpClientContext { - server?: string; - path?: string; + baseUrl?: string; // Authentication - grouped for better autocomplete auth?: AuthConfig; - // Pagination configuration - pagination?: PaginationConfig; - // Retry configuration retry?: RetryConfig; @@ -303,8 +207,8 @@ export interface HttpClientContext { // Additional options additionalHeaders?: Record; - // Query parameters - queryParams?: Record; + // Extra query parameters not covered by the typed parameters interface + additionalQueryParams?: Record; } // ============================================================================ @@ -386,94 +290,6 @@ function applyAuth( return { headers, url }; } -/** - * Apply pagination parameters to URL and/or headers based on configuration - */ -function applyPagination( - pagination: PaginationConfig | undefined, - url: string, - headers: Record -): { url: string; headers: Record } { - if (!pagination) return { url, headers }; - - const location = pagination.in ?? 'query'; - const isHeader = location === 'header'; - - // Helper to get default param names based on location - const getDefaultName = (queryName: string, headerName: string) => - isHeader ? headerName : queryName; - - const queryParams = new URLSearchParams(); - const headerParams: Record = {}; - - const addParam = (name: string, value: string) => { - if (isHeader) { - headerParams[name] = value; - } else { - queryParams.append(name, value); - } - }; - - switch (pagination.type) { - case 'offset': - addParam( - pagination.offsetParam ?? getDefaultName('offset', 'X-Offset'), - String(pagination.offset) - ); - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - break; - - case 'cursor': - if (pagination.cursor) { - addParam( - pagination.cursorParam ?? getDefaultName('cursor', 'X-Cursor'), - pagination.cursor - ); - } - if (pagination.limit !== undefined) { - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - } - break; - - case 'page': - addParam( - pagination.pageParam ?? getDefaultName('page', 'X-Page'), - String(pagination.page) - ); - addParam( - pagination.pageSizeParam ?? getDefaultName('pageSize', 'X-Page-Size'), - String(pagination.pageSize) - ); - break; - - case 'range': { - // Range pagination is always header-based (RFC 7233 style) - const unit = pagination.unit ?? 'items'; - const headerName = pagination.rangeHeader ?? 'Range'; - headerParams[headerName] = \`\${unit}=\${pagination.start}-\${pagination.end}\`; - break; - } - } - - // Apply query params to URL - const queryString = queryParams.toString(); - if (queryString) { - const separator = url.includes('?') ? '&' : '?'; - url = \`\${url}\${separator}\${queryString}\`; - } - - // Merge header params - const updatedHeaders = { ...headers, ...headerParams }; - - return { url, headers: updatedHeaders }; -} - /** * Apply query parameters to URL */ @@ -617,210 +433,17 @@ function extractHeaders(response: HttpResponse): Record { } /** - * Extract pagination info from response headers - */ -function extractPaginationInfo( - headers: Record, - currentPagination?: PaginationConfig -): PaginationInfo | undefined { - const info: PaginationInfo = {}; - let hasPaginationInfo = false; - - // Common total count headers - const totalCount = headers['x-total-count'] || headers['x-total'] || headers['total-count']; - if (totalCount) { - info.totalCount = parseInt(totalCount, 10); - hasPaginationInfo = true; - } - - // Total pages - const totalPages = headers['x-total-pages'] || headers['x-page-count']; - if (totalPages) { - info.totalPages = parseInt(totalPages, 10); - hasPaginationInfo = true; - } - - // Next cursor - const nextCursor = headers['x-next-cursor'] || headers['x-cursor-next']; - if (nextCursor) { - info.nextCursor = nextCursor; - info.hasMore = true; - hasPaginationInfo = true; - } - - // Previous cursor - const prevCursor = headers['x-prev-cursor'] || headers['x-cursor-prev']; - if (prevCursor) { - info.prevCursor = prevCursor; - hasPaginationInfo = true; - } - - // Has more indicator - const hasMore = headers['x-has-more'] || headers['x-has-next']; - if (hasMore) { - info.hasMore = hasMore.toLowerCase() === 'true' || hasMore === '1'; - hasPaginationInfo = true; - } - - // Parse Link header (RFC 5988) - const linkHeader = headers['link']; - if (linkHeader) { - const links = parseLinkHeader(linkHeader); - if (links.next) { - info.hasMore = true; - hasPaginationInfo = true; - } - } - - // Include current pagination state - if (currentPagination) { - switch (currentPagination.type) { - case 'offset': - info.currentOffset = currentPagination.offset; - info.limit = currentPagination.limit; - break; - case 'cursor': - info.limit = currentPagination.limit; - break; - case 'page': - info.currentOffset = (currentPagination.page - 1) * currentPagination.pageSize; - info.limit = currentPagination.pageSize; - break; - case 'range': - info.currentOffset = currentPagination.start; - info.limit = currentPagination.end - currentPagination.start + 1; - break; - } - hasPaginationInfo = true; - } - - // Calculate hasMore based on total count - if (info.hasMore === undefined && info.totalCount !== undefined && - info.currentOffset !== undefined && info.limit !== undefined) { - info.hasMore = info.currentOffset + info.limit < info.totalCount; - } - - return hasPaginationInfo ? info : undefined; -} - -/** - * Parse RFC 5988 Link header - */ -function parseLinkHeader(header: string): Record { - const links: Record = {}; - const parts = header.split(','); - - for (const part of parts) { - const match = part.match(/<([^>]+)>;\\s*rel="?([^";\\s]+)"?/); - if (match) { - links[match[2]] = match[1]; - } - } - - return links; -} - -/** - * Create pagination helper functions for the response - */ -function createPaginationHelpers( - currentConfig: TContext, - paginationInfo: PaginationInfo | undefined, - requestFn: (config: TContext) => Promise> -): Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> { - const helpers: Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> = {}; - - if (!currentConfig.pagination) { - return helpers; - } - - const pagination = currentConfig.pagination; - - helpers.hasNextPage = () => { - if (paginationInfo?.hasMore !== undefined) return paginationInfo.hasMore; - if (paginationInfo?.nextCursor) return true; - if (paginationInfo?.totalCount !== undefined && - paginationInfo.currentOffset !== undefined && - paginationInfo.limit !== undefined) { - return paginationInfo.currentOffset + paginationInfo.limit < paginationInfo.totalCount; - } - return false; - }; - - helpers.hasPrevPage = () => { - if (paginationInfo?.prevCursor) return true; - if (paginationInfo?.currentOffset !== undefined) { - return paginationInfo.currentOffset > 0; - } - return false; - }; - - helpers.getNextPage = async () => { - let nextPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - nextPagination = { ...pagination, offset: pagination.offset + pagination.limit }; - break; - case 'cursor': - if (!paginationInfo?.nextCursor) throw new Error('No next cursor available'); - nextPagination = { ...pagination, cursor: paginationInfo.nextCursor }; - break; - case 'page': - nextPagination = { ...pagination, page: pagination.page + 1 }; - break; - case 'range': - const rangeSize = pagination.end - pagination.start + 1; - nextPagination = { ...pagination, start: pagination.end + 1, end: pagination.end + rangeSize }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: nextPagination }); - }; - - helpers.getPrevPage = async () => { - let prevPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - prevPagination = { ...pagination, offset: Math.max(0, pagination.offset - pagination.limit) }; - break; - case 'cursor': - if (!paginationInfo?.prevCursor) throw new Error('No previous cursor available'); - prevPagination = { ...pagination, cursor: paginationInfo.prevCursor }; - break; - case 'page': - prevPagination = { ...pagination, page: Math.max(1, pagination.page - 1) }; - break; - case 'range': - const size = pagination.end - pagination.start + 1; - const newStart = Math.max(0, pagination.start - size); - prevPagination = { ...pagination, start: newStart, end: newStart + size - 1 }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: prevPagination }); - }; - - return helpers; -} - -/** - * Builds a URL with path parameters replaced + * Builds a URL with path parameters replaced using a serializer function * @param server - Base server URL * @param pathTemplate - Path template with {param} placeholders - * @param parameters - Parameter object with getChannelWithParameters method + * @param serializeFn - Function that takes the path template and returns the serialized path */ -function buildUrlWithParameters string }>( +function buildUrlWithParameters( server: string, pathTemplate: string, - parameters: T + serializeFn: (path: string) => string ): string { - const path = parameters.getChannelWithParameters(pathTemplate); + const path = serializeFn(pathTemplate); return \`\${server}\${path}\`; } @@ -1000,7 +623,6 @@ async function handleTokenRefresh( export interface AddPetContext extends HttpClientContext { payload: Pet; - requestHeaders?: { marshal: () => string }; } /** @@ -1009,8 +631,7 @@ export interface AddPetContext extends HttpClientContext { async function addPet(context: AddPetContext): Promise> { // Apply defaults const config = { - path: '/pet', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1020,18 +641,11 @@ async function addPet(context: AddPetContext): Promise> } // Build headers - let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) - : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = \`\${config.server}\${config.path}\`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = \`\${config.baseUrl}/pet\`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1097,17 +711,13 @@ async function addPet(context: AddPetContext): Promise> // Extract response metadata const responseHeaders = extractHeaders(response); - const paginationInfo = extractPaginationInfo(responseHeaders, config.pagination); - // Build response wrapper with pagination helpers const result: HttpClientResponse = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, addPet), }; return result; @@ -1123,7 +733,6 @@ async function addPet(context: AddPetContext): Promise> export interface UpdatePetContext extends HttpClientContext { payload: Pet; - requestHeaders?: { marshal: () => string }; } /** @@ -1132,8 +741,7 @@ export interface UpdatePetContext extends HttpClientContext { async function updatePet(context: UpdatePetContext): Promise> { // Apply defaults const config = { - path: '/pet', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1143,18 +751,11 @@ async function updatePet(context: UpdatePetContext): Promise; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = \`\${config.server}\${config.path}\`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = \`\${config.baseUrl}/pet\`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1220,17 +821,13 @@ async function updatePet(context: UpdatePetContext): Promise = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, updatePet), }; return result; @@ -1246,7 +843,7 @@ async function updatePet(context: UpdatePetContext): Promise string }; + requestHeaders?: FindPetsByStatusAndCategoryHeaders; } /** @@ -1255,8 +852,7 @@ export interface FindPetsByStatusAndCategoryContext extends HttpClientContext { async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryContext): Promise> { // Apply defaults const config = { - path: '/pet/findByStatus/{status}/{categoryId}', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1266,18 +862,11 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC } // Build headers - let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) - : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders, ...(context.requestHeaders ? serializeFindPetsByStatusAndCategoryHeadersHeaders(context.requestHeaders) : {}) } as Record; // Build URL - let url = buildUrlWithParameters(config.server, '/pet/findByStatus/{status}/{categoryId}', context.parameters); - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = buildUrlWithParameters(config.baseUrl, '/pet/findByStatus/{status}/{categoryId}', (path) => serializeParameterUrl(context.parameters, path)); + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1343,17 +932,13 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Extract response metadata const responseHeaders = extractHeaders(response); - const paginationInfo = extractPaginationInfo(responseHeaders, config.pagination); - // Build response wrapper with pagination helpers const result: HttpClientResponse = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, findPetsByStatusAndCategory), }; return result; @@ -1975,26 +1560,6 @@ export interface HttpResponse { json: () => Record | Promise>; } -/** - * Pagination info extracted from response - */ -export interface PaginationInfo { - /** Total number of items (if available from headers like X-Total-Count) */ - totalCount?: number; - /** Total number of pages (if available) */ - totalPages?: number; - /** Current page/offset */ - currentOffset?: number; - /** Items per page */ - limit?: number; - /** Next cursor (for cursor-based pagination) */ - nextCursor?: string; - /** Previous cursor */ - prevCursor?: string; - /** Whether there are more items */ - hasMore?: boolean; -} - /** * Rich response wrapper returned by HTTP client functions */ @@ -2009,16 +1574,6 @@ export interface HttpClientResponse { headers: Record; /** Raw JSON response before deserialization */ rawData: Record; - /** Pagination info extracted from response (if applicable) */ - pagination?: PaginationInfo; - /** Fetch the next page (if pagination is configured and more data exists) */ - getNextPage?: () => Promise>; - /** Fetch the previous page (if pagination is configured) */ - getPrevPage?: () => Promise>; - /** Check if there's a next page */ - hasNextPage?: () => boolean; - /** Check if there's a previous page */ - hasPrevPage?: () => boolean; } /** @@ -2129,69 +1684,6 @@ const API_KEY_DEFAULTS = { in: 'header' as 'header' | 'query' | 'cookie' } as const; -// ============================================================================ -// Pagination Types -// ============================================================================ - -/** - * Where to place pagination parameters - */ -export type PaginationLocation = 'query' | 'header'; - -/** - * Offset-based pagination configuration - */ -export interface OffsetPagination { - type: 'offset'; - in?: PaginationLocation; // Where to place params (default: 'query') - offset: number; - limit: number; - offsetParam?: string; // Param name for offset (default: 'offset' for query, 'X-Offset' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Cursor-based pagination configuration - */ -export interface CursorPagination { - type: 'cursor'; - in?: PaginationLocation; // Where to place params (default: 'query') - cursor?: string; - limit?: number; - cursorParam?: string; // Param name for cursor (default: 'cursor' for query, 'X-Cursor' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Page-based pagination configuration - */ -export interface PagePagination { - type: 'page'; - in?: PaginationLocation; // Where to place params (default: 'query') - page: number; - pageSize: number; - pageParam?: string; // Param name for page (default: 'page' for query, 'X-Page' for header) - pageSizeParam?: string; // Param name for page size (default: 'pageSize' for query, 'X-Page-Size' for header) -} - -/** - * Range-based pagination (typically used with headers) - * Follows RFC 7233 style: Range: items=0-24 - */ -export interface RangePagination { - type: 'range'; - in?: 'header'; // Range pagination is typically header-only - start: number; - end: number; - unit?: string; // Range unit (default: 'items') - rangeHeader?: string; // Header name (default: 'Range') -} - -/** - * Union type for all pagination methods - */ -export type PaginationConfig = OffsetPagination | CursorPagination | PagePagination | RangePagination; - // ============================================================================ // Retry Configuration // ============================================================================ @@ -2249,15 +1741,11 @@ export interface HttpHooks { * Base context shared by all HTTP client functions */ export interface HttpClientContext { - server?: string; - path?: string; + baseUrl?: string; // Authentication - grouped for better autocomplete auth?: AuthConfig; - // Pagination configuration - pagination?: PaginationConfig; - // Retry configuration retry?: RetryConfig; @@ -2267,8 +1755,8 @@ export interface HttpClientContext { // Additional options additionalHeaders?: Record; - // Query parameters - queryParams?: Record; + // Extra query parameters not covered by the typed parameters interface + additionalQueryParams?: Record; } // ============================================================================ @@ -2360,94 +1848,6 @@ function applyAuth( return { headers, url }; } -/** - * Apply pagination parameters to URL and/or headers based on configuration - */ -function applyPagination( - pagination: PaginationConfig | undefined, - url: string, - headers: Record -): { url: string; headers: Record } { - if (!pagination) return { url, headers }; - - const location = pagination.in ?? 'query'; - const isHeader = location === 'header'; - - // Helper to get default param names based on location - const getDefaultName = (queryName: string, headerName: string) => - isHeader ? headerName : queryName; - - const queryParams = new URLSearchParams(); - const headerParams: Record = {}; - - const addParam = (name: string, value: string) => { - if (isHeader) { - headerParams[name] = value; - } else { - queryParams.append(name, value); - } - }; - - switch (pagination.type) { - case 'offset': - addParam( - pagination.offsetParam ?? getDefaultName('offset', 'X-Offset'), - String(pagination.offset) - ); - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - break; - - case 'cursor': - if (pagination.cursor) { - addParam( - pagination.cursorParam ?? getDefaultName('cursor', 'X-Cursor'), - pagination.cursor - ); - } - if (pagination.limit !== undefined) { - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - } - break; - - case 'page': - addParam( - pagination.pageParam ?? getDefaultName('page', 'X-Page'), - String(pagination.page) - ); - addParam( - pagination.pageSizeParam ?? getDefaultName('pageSize', 'X-Page-Size'), - String(pagination.pageSize) - ); - break; - - case 'range': { - // Range pagination is always header-based (RFC 7233 style) - const unit = pagination.unit ?? 'items'; - const headerName = pagination.rangeHeader ?? 'Range'; - headerParams[headerName] = \`\${unit}=\${pagination.start}-\${pagination.end}\`; - break; - } - } - - // Apply query params to URL - const queryString = queryParams.toString(); - if (queryString) { - const separator = url.includes('?') ? '&' : '?'; - url = \`\${url}\${separator}\${queryString}\`; - } - - // Merge header params - const updatedHeaders = { ...headers, ...headerParams }; - - return { url, headers: updatedHeaders }; -} - /** * Apply query parameters to URL */ @@ -2591,210 +1991,17 @@ function extractHeaders(response: HttpResponse): Record { } /** - * Extract pagination info from response headers - */ -function extractPaginationInfo( - headers: Record, - currentPagination?: PaginationConfig -): PaginationInfo | undefined { - const info: PaginationInfo = {}; - let hasPaginationInfo = false; - - // Common total count headers - const totalCount = headers['x-total-count'] || headers['x-total'] || headers['total-count']; - if (totalCount) { - info.totalCount = parseInt(totalCount, 10); - hasPaginationInfo = true; - } - - // Total pages - const totalPages = headers['x-total-pages'] || headers['x-page-count']; - if (totalPages) { - info.totalPages = parseInt(totalPages, 10); - hasPaginationInfo = true; - } - - // Next cursor - const nextCursor = headers['x-next-cursor'] || headers['x-cursor-next']; - if (nextCursor) { - info.nextCursor = nextCursor; - info.hasMore = true; - hasPaginationInfo = true; - } - - // Previous cursor - const prevCursor = headers['x-prev-cursor'] || headers['x-cursor-prev']; - if (prevCursor) { - info.prevCursor = prevCursor; - hasPaginationInfo = true; - } - - // Has more indicator - const hasMore = headers['x-has-more'] || headers['x-has-next']; - if (hasMore) { - info.hasMore = hasMore.toLowerCase() === 'true' || hasMore === '1'; - hasPaginationInfo = true; - } - - // Parse Link header (RFC 5988) - const linkHeader = headers['link']; - if (linkHeader) { - const links = parseLinkHeader(linkHeader); - if (links.next) { - info.hasMore = true; - hasPaginationInfo = true; - } - } - - // Include current pagination state - if (currentPagination) { - switch (currentPagination.type) { - case 'offset': - info.currentOffset = currentPagination.offset; - info.limit = currentPagination.limit; - break; - case 'cursor': - info.limit = currentPagination.limit; - break; - case 'page': - info.currentOffset = (currentPagination.page - 1) * currentPagination.pageSize; - info.limit = currentPagination.pageSize; - break; - case 'range': - info.currentOffset = currentPagination.start; - info.limit = currentPagination.end - currentPagination.start + 1; - break; - } - hasPaginationInfo = true; - } - - // Calculate hasMore based on total count - if (info.hasMore === undefined && info.totalCount !== undefined && - info.currentOffset !== undefined && info.limit !== undefined) { - info.hasMore = info.currentOffset + info.limit < info.totalCount; - } - - return hasPaginationInfo ? info : undefined; -} - -/** - * Parse RFC 5988 Link header - */ -function parseLinkHeader(header: string): Record { - const links: Record = {}; - const parts = header.split(','); - - for (const part of parts) { - const match = part.match(/<([^>]+)>;\\s*rel="?([^";\\s]+)"?/); - if (match) { - links[match[2]] = match[1]; - } - } - - return links; -} - -/** - * Create pagination helper functions for the response - */ -function createPaginationHelpers( - currentConfig: TContext, - paginationInfo: PaginationInfo | undefined, - requestFn: (config: TContext) => Promise> -): Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> { - const helpers: Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> = {}; - - if (!currentConfig.pagination) { - return helpers; - } - - const pagination = currentConfig.pagination; - - helpers.hasNextPage = () => { - if (paginationInfo?.hasMore !== undefined) return paginationInfo.hasMore; - if (paginationInfo?.nextCursor) return true; - if (paginationInfo?.totalCount !== undefined && - paginationInfo.currentOffset !== undefined && - paginationInfo.limit !== undefined) { - return paginationInfo.currentOffset + paginationInfo.limit < paginationInfo.totalCount; - } - return false; - }; - - helpers.hasPrevPage = () => { - if (paginationInfo?.prevCursor) return true; - if (paginationInfo?.currentOffset !== undefined) { - return paginationInfo.currentOffset > 0; - } - return false; - }; - - helpers.getNextPage = async () => { - let nextPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - nextPagination = { ...pagination, offset: pagination.offset + pagination.limit }; - break; - case 'cursor': - if (!paginationInfo?.nextCursor) throw new Error('No next cursor available'); - nextPagination = { ...pagination, cursor: paginationInfo.nextCursor }; - break; - case 'page': - nextPagination = { ...pagination, page: pagination.page + 1 }; - break; - case 'range': - const rangeSize = pagination.end - pagination.start + 1; - nextPagination = { ...pagination, start: pagination.end + 1, end: pagination.end + rangeSize }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: nextPagination }); - }; - - helpers.getPrevPage = async () => { - let prevPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - prevPagination = { ...pagination, offset: Math.max(0, pagination.offset - pagination.limit) }; - break; - case 'cursor': - if (!paginationInfo?.prevCursor) throw new Error('No previous cursor available'); - prevPagination = { ...pagination, cursor: paginationInfo.prevCursor }; - break; - case 'page': - prevPagination = { ...pagination, page: Math.max(1, pagination.page - 1) }; - break; - case 'range': - const size = pagination.end - pagination.start + 1; - const newStart = Math.max(0, pagination.start - size); - prevPagination = { ...pagination, start: newStart, end: newStart + size - 1 }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: prevPagination }); - }; - - return helpers; -} - -/** - * Builds a URL with path parameters replaced + * Builds a URL with path parameters replaced using a serializer function * @param server - Base server URL * @param pathTemplate - Path template with {param} placeholders - * @param parameters - Parameter object with getChannelWithParameters method + * @param serializeFn - Function that takes the path template and returns the serialized path */ -function buildUrlWithParameters string }>( +function buildUrlWithParameters( server: string, pathTemplate: string, - parameters: T + serializeFn: (path: string) => string ): string { - const path = parameters.getChannelWithParameters(pathTemplate); + const path = serializeFn(pathTemplate); return \`\${server}\${path}\`; } @@ -2972,9 +2179,7 @@ async function handleTokenRefresh( // Generated HTTP Client Functions // ============================================================================ -export interface GetPingRequestContext extends HttpClientContext { - requestHeaders?: { marshal: () => string }; -} +export interface GetPingRequestContext extends HttpClientContext {} /** * HTTP GET request to /ping @@ -2982,8 +2187,7 @@ export interface GetPingRequestContext extends HttpClientContext { async function getPingRequest(context: GetPingRequestContext = {}): Promise> { // Apply defaults const config = { - path: '/ping', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -2993,18 +2197,11 @@ async function getPingRequest(context: GetPingRequestContext = {}): Promise; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = \`\${config.server}\${config.path}\`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = \`\${config.baseUrl}/ping\`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -3070,17 +2267,13 @@ async function getPingRequest(context: GetPingRequestContext = {}): Promise = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, getPingRequest), }; return result; diff --git a/test/codegen/generators/typescript/__snapshots__/headers.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/headers.spec.ts.snap index d78d4721..0c589e7e 100644 --- a/test/codegen/generators/typescript/__snapshots__/headers.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/headers.spec.ts.snap @@ -1,184 +1,25 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`headers typescript should work with OpenAPI 2.0 inputs 1`] = ` -"import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import {default as addFormats} from 'ajv-formats'; -class DeletePetHeaders { - private _apiKey?: string; - - constructor(input: { - apiKey?: string, - }) { - this._apiKey = input.apiKey; - } - - get apiKey(): string | undefined { return this._apiKey; } - set apiKey(apiKey: string | undefined) { this._apiKey = apiKey; } - - public marshal() : string { - let json = '{' - if(this.apiKey !== undefined) { - json += \`"api_key": \${typeof this.apiKey === 'number' || typeof this.apiKey === 'boolean' ? this.apiKey : JSON.stringify(this.apiKey)},\`; - } - - //Remove potential last comma - return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`; - } - - public static unmarshal(json: string | object): DeletePetHeaders { - const obj = typeof json === "object" ? json : JSON.parse(json); - const instance = new DeletePetHeaders({} as any); - - if (obj["api_key"] !== undefined) { - instance.apiKey = obj["api_key"]; - } - - - return instance; - } - public static theCodeGenSchema = {"type":"object","additionalProperties":false,"properties":{"api_key":{"type":"string"}},"$id":"DeletePetHeaders","$schema":"http://json-schema.org/draft-07/schema"}; - public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { - const {data, ajvValidatorFunction} = context ?? {}; - // Intentionally parse JSON strings to support validation of marshalled output. - // Example: validate({data: marshal(obj)}) works because marshal returns JSON string. - // Note: String 'true' will be coerced to boolean true due to JSON.parse. - const parsedData = typeof data === 'string' ? JSON.parse(data) : data; - const validate = ajvValidatorFunction ?? this.createValidator(context) - return { - valid: validate(parsedData), - errors: validate.errors ?? undefined, - }; - } - public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { - const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; - addFormats(ajvInstance); - ajvInstance.addVocabulary(["xml", "example"]) - const validate = ajvInstance.compile(this.theCodeGenSchema); - return validate; - } - +" +interface DeletePetHeaders { + apiKey?: string; } export { DeletePetHeaders };" `; exports[`headers typescript should work with OpenAPI 3.0 inputs 1`] = ` -"import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import {default as addFormats} from 'ajv-formats'; -class DeletePetHeaders { - private _apiKey?: string; - - constructor(input: { - apiKey?: string, - }) { - this._apiKey = input.apiKey; - } - - get apiKey(): string | undefined { return this._apiKey; } - set apiKey(apiKey: string | undefined) { this._apiKey = apiKey; } - - public marshal() : string { - let json = '{' - if(this.apiKey !== undefined) { - json += \`"api_key": \${typeof this.apiKey === 'number' || typeof this.apiKey === 'boolean' ? this.apiKey : JSON.stringify(this.apiKey)},\`; - } - - //Remove potential last comma - return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`; - } - - public static unmarshal(json: string | object): DeletePetHeaders { - const obj = typeof json === "object" ? json : JSON.parse(json); - const instance = new DeletePetHeaders({} as any); - - if (obj["api_key"] !== undefined) { - instance.apiKey = obj["api_key"]; - } - - - return instance; - } - public static theCodeGenSchema = {"type":"object","additionalProperties":false,"properties":{"api_key":{"type":"string"}},"$id":"DeletePetHeaders","$schema":"http://json-schema.org/draft-07/schema"}; - public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { - const {data, ajvValidatorFunction} = context ?? {}; - // Intentionally parse JSON strings to support validation of marshalled output. - // Example: validate({data: marshal(obj)}) works because marshal returns JSON string. - // Note: String 'true' will be coerced to boolean true due to JSON.parse. - const parsedData = typeof data === 'string' ? JSON.parse(data) : data; - const validate = ajvValidatorFunction ?? this.createValidator(context) - return { - valid: validate(parsedData), - errors: validate.errors ?? undefined, - }; - } - public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { - const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; - addFormats(ajvInstance); - ajvInstance.addVocabulary(["xml", "example"]) - const validate = ajvInstance.compile(this.theCodeGenSchema); - return validate; - } - +" +interface DeletePetHeaders { + apiKey?: string; } export { DeletePetHeaders };" `; exports[`headers typescript should work with OpenAPI 3.1 inputs 1`] = ` -"import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import {default as addFormats} from 'ajv-formats'; -class DeletePetHeaders { - private _apiKey?: string; - - constructor(input: { - apiKey?: string, - }) { - this._apiKey = input.apiKey; - } - - get apiKey(): string | undefined { return this._apiKey; } - set apiKey(apiKey: string | undefined) { this._apiKey = apiKey; } - - public marshal() : string { - let json = '{' - if(this.apiKey !== undefined) { - json += \`"api_key": \${typeof this.apiKey === 'number' || typeof this.apiKey === 'boolean' ? this.apiKey : JSON.stringify(this.apiKey)},\`; - } - - //Remove potential last comma - return \`\${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}\`; - } - - public static unmarshal(json: string | object): DeletePetHeaders { - const obj = typeof json === "object" ? json : JSON.parse(json); - const instance = new DeletePetHeaders({} as any); - - if (obj["api_key"] !== undefined) { - instance.apiKey = obj["api_key"]; - } - - - return instance; - } - public static theCodeGenSchema = {"type":"object","additionalProperties":false,"properties":{"api_key":{"type":"string"}},"$id":"DeletePetHeaders","$schema":"http://json-schema.org/draft-07/schema"}; - public static validate(context?: {data: any, ajvValidatorFunction?: ValidateFunction, ajvInstance?: Ajv, ajvOptions?: AjvOptions}): { valid: boolean; errors?: ErrorObject[]; } { - const {data, ajvValidatorFunction} = context ?? {}; - // Intentionally parse JSON strings to support validation of marshalled output. - // Example: validate({data: marshal(obj)}) works because marshal returns JSON string. - // Note: String 'true' will be coerced to boolean true due to JSON.parse. - const parsedData = typeof data === 'string' ? JSON.parse(data) : data; - const validate = ajvValidatorFunction ?? this.createValidator(context) - return { - valid: validate(parsedData), - errors: validate.errors ?? undefined, - }; - } - public static createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { - const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; - addFormats(ajvInstance); - ajvInstance.addVocabulary(["xml", "example"]) - const validate = ajvInstance.compile(this.theCodeGenSchema); - return validate; - } - +" +interface DeletePetHeaders { + apiKey?: string; } export { DeletePetHeaders };" `; diff --git a/test/codegen/generators/typescript/__snapshots__/parameters.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/parameters.spec.ts.snap index 11d024f6..071d4f91 100644 --- a/test/codegen/generators/typescript/__snapshots__/parameters.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/parameters.spec.ts.snap @@ -199,4823 +199,427 @@ class MultipleParameterParameters { export { MultipleParameterParameters };" `; -exports[`parameters typescript openapi should use constrained property names and skip deserializeUrl without query parameters 1`] = ` +exports[`parameters typescript openapi should use constrained property names and skip parseFromUrl without query parameters 1`] = ` " -class GetDocumentParameters { - private _documentId: string; - - constructor(input: { - documentId: string, - }) { - this._documentId = input.documentId; - } - - get documentId(): string { return this._documentId; } - set documentId(documentId: string) { this._documentId = documentId; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: documentId (style: simple, explode: false) - if (this.documentId !== undefined && this.documentId !== null) { - const value = this.documentId; - if (Array.isArray(value)) { - result['documentId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['documentId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['documentId'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new GetDocumentParameters instance - */ - static fromUrl(url: string, basePath: string): GetDocumentParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new GetDocumentParameters({ documentId: pathParams.documentId }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { documentId: string } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'documentId': - result.documentId = decodeValue; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { GetDocumentParameters };" -`; - -exports[`parameters typescript openapi should use constrained property names and skip deserializeUrl without query parameters 2`] = ` -" -class GetTransactionsParameters { - private _skip?: number; - private _cvr?: string; - - constructor(input: { - skip?: number, - cvr?: string, - }) { - this._skip = input.skip; - this._cvr = input.cvr; - } - - get skip(): number | undefined { return this._skip; } - set skip(skip: number | undefined) { this._skip = skip; } - - get cvr(): string | undefined { return this._cvr; } - set cvr(cvr: string | undefined) { this._cvr = cvr; } - - - - /** - * Serialize query parameters according to OpenAPI 2.0/3.x specification - * @returns URLSearchParams object with serialized query parameters - */ - serializeQueryParameters(): URLSearchParams { - const params = new URLSearchParams(); - - // Serialize query parameter: Skip (style: form, explode: true) - if (this.skip !== undefined && this.skip !== null) { - const value = this.skip; - if (Array.isArray(value)) { - value.forEach(val => params.append('Skip', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('Skip', encodeURIComponent(String(value))); - } - } - // Serialize query parameter: Cvr (style: form, explode: true) - if (this.cvr !== undefined && this.cvr !== null) { - const value = this.cvr; - if (Array.isArray(value)) { - value.forEach(val => params.append('Cvr', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('Cvr', encodeURIComponent(String(value))); - } - } - - return params; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - - // Add query parameters - - const queryParams = this.serializeQueryParameters(); - const queryString = queryParams.toString(); - if (queryString) { - url += (url.includes('?') ? '&' : '?') + queryString; - } - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - /** - * Deserialize URL and populate instance properties from query parameters - * @param url The URL to parse (can be full URL or just query string) - */ - deserializeUrl(url: string): void { - // Extract query string from URL - let queryString = ''; - if (url.includes('?')) { - queryString = url.split('?')[1]; - } else if (url.includes('=')) { - // Assume it's already a query string - queryString = url; - } - - if (!queryString) { - return; - } - - const params = new URLSearchParams(queryString); - - // Deserialize query parameter: Skip (style: form, explode: true) - if (params.has('Skip')) { - const value = params.get('Skip'); - if (value) { - const decodedValue = decodeURIComponent(value); - const numValue = Number(decodedValue); - if (!isNaN(numValue)) { - this.skip = numValue; - } - } - } - // Deserialize query parameter: Cvr (style: form, explode: true) - if (params.has('Cvr')) { - const value = params.get('Cvr'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.cvr = decodedValue; - } - } - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new GetTransactionsParameters instance - */ - static fromUrl(url: string, basePath: string): GetTransactionsParameters { - - const instance = new GetTransactionsParameters({ }); - instance.deserializeUrl(url); - return instance; - } -} -export { GetTransactionsParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 1`] = ` -" -class DeleteOrderParameters { - private _orderId: number; - - constructor(input: { - orderId: number, - }) { - this._orderId = input.orderId; - } - - /** - * ID of the order that needs to be deleted - */ - get orderId(): number { return this._orderId; } - set orderId(orderId: number) { this._orderId = orderId; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: orderId (style: simple, explode: false) - if (this.orderId !== undefined && this.orderId !== null) { - const value = this.orderId; - if (Array.isArray(value)) { - result['orderId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['orderId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['orderId'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new DeleteOrderParameters instance - */ - static fromUrl(url: string, basePath: string): DeleteOrderParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new DeleteOrderParameters({ orderId: pathParams.orderId }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { orderId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'orderId': - result.orderId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { DeleteOrderParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 2`] = ` -" -class DeletePetParameters { - private _petId: number; - - constructor(input: { - petId: number, - }) { - this._petId = input.petId; - } - - /** - * Pet id to delete - */ - get petId(): number { return this._petId; } - set petId(petId: number) { this._petId = petId; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: petId (style: simple, explode: false) - if (this.petId !== undefined && this.petId !== null) { - const value = this.petId; - if (Array.isArray(value)) { - result['petId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['petId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['petId'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new DeletePetParameters instance - */ - static fromUrl(url: string, basePath: string): DeletePetParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new DeletePetParameters({ petId: pathParams.petId }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { petId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'petId': - result.petId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { DeletePetParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 3`] = ` -" -class DeleteUserParameters { - private _username: string; - - constructor(input: { - username: string, - }) { - this._username = input.username; - } - - /** - * The name that needs to be deleted - */ - get username(): string { return this._username; } - set username(username: string) { this._username = username; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: username (style: simple, explode: false) - if (this.username !== undefined && this.username !== null) { - const value = this.username; - if (Array.isArray(value)) { - result['username'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['username'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['username'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new DeleteUserParameters instance - */ - static fromUrl(url: string, basePath: string): DeleteUserParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new DeleteUserParameters({ username: pathParams.username }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { username: string } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'username': - result.username = decodeValue; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { DeleteUserParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 4`] = ` -" -class FindPetsByStatusParameters { - private _status: any[]; - - constructor(input: { - status: any[], - }) { - this._status = input.status; - } - - /** - * Status values that need to be considered for filter - */ - get status(): any[] { return this._status; } - set status(status: any[]) { this._status = status; } - - - - /** - * Serialize query parameters according to OpenAPI 2.0/3.x specification - * @returns URLSearchParams object with serialized query parameters - */ - serializeQueryParameters(): URLSearchParams { - const params = new URLSearchParams(); - - // Serialize query parameter: status (style: form, explode: true) - if (this.status !== undefined && this.status !== null) { - const value = this.status; - if (Array.isArray(value)) { - value.forEach(val => params.append('status', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('status', encodeURIComponent(String(value))); - } - } - - return params; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - - // Add query parameters - - const queryParams = this.serializeQueryParameters(); - const queryString = queryParams.toString(); - if (queryString) { - url += (url.includes('?') ? '&' : '?') + queryString; - } - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - /** - * Deserialize URL and populate instance properties from query parameters - * @param url The URL to parse (can be full URL or just query string) - */ - deserializeUrl(url: string): void { - // Extract query string from URL - let queryString = ''; - if (url.includes('?')) { - queryString = url.split('?')[1]; - } else if (url.includes('=')) { - // Assume it's already a query string - queryString = url; - } - - if (!queryString) { - return; - } - - const params = new URLSearchParams(queryString); - - // Deserialize query parameter: status (style: form, explode: true) - if (params.has('status')) { - const value = params.get('status'); - const allValues = params.getAll('status'); - if (allValues.length > 0) { - const decodedValues = allValues.map(val => decodeURIComponent(val)); - this.status = decodedValues as string[]; - } - } - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @param defaultStatus Default status values (required parameter) - * @returns A new FindPetsByStatusParameters instance - */ - static fromUrl(url: string, basePath: string, defaultStatus: string[] = []): FindPetsByStatusParameters { - - const instance = new FindPetsByStatusParameters({ status: defaultStatus }); - instance.deserializeUrl(url); - return instance; - } -} -export { FindPetsByStatusParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 5`] = ` -" -class FindPetsByTagsParameters { - private _tags: any[]; - - constructor(input: { - tags: any[], - }) { - this._tags = input.tags; - } - - /** - * Tags to filter by - */ - get tags(): any[] { return this._tags; } - set tags(tags: any[]) { this._tags = tags; } - - - - /** - * Serialize query parameters according to OpenAPI 2.0/3.x specification - * @returns URLSearchParams object with serialized query parameters - */ - serializeQueryParameters(): URLSearchParams { - const params = new URLSearchParams(); - - // Serialize query parameter: tags (style: form, explode: true) - if (this.tags !== undefined && this.tags !== null) { - const value = this.tags; - if (Array.isArray(value)) { - value.forEach(val => params.append('tags', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('tags', encodeURIComponent(String(value))); - } - } - - return params; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - - // Add query parameters - - const queryParams = this.serializeQueryParameters(); - const queryString = queryParams.toString(); - if (queryString) { - url += (url.includes('?') ? '&' : '?') + queryString; - } - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - /** - * Deserialize URL and populate instance properties from query parameters - * @param url The URL to parse (can be full URL or just query string) - */ - deserializeUrl(url: string): void { - // Extract query string from URL - let queryString = ''; - if (url.includes('?')) { - queryString = url.split('?')[1]; - } else if (url.includes('=')) { - // Assume it's already a query string - queryString = url; - } - - if (!queryString) { - return; - } - - const params = new URLSearchParams(queryString); - - // Deserialize query parameter: tags (style: form, explode: true) - if (params.has('tags')) { - const value = params.get('tags'); - const allValues = params.getAll('tags'); - if (allValues.length > 0) { - const decodedValues = allValues.map(val => decodeURIComponent(val)); - this.tags = decodedValues as string[]; - } - } - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @param defaultTags Default tags values (required parameter) - * @returns A new FindPetsByTagsParameters instance - */ - static fromUrl(url: string, basePath: string, defaultTags: string[] = []): FindPetsByTagsParameters { - - const instance = new FindPetsByTagsParameters({ tags: defaultTags }); - instance.deserializeUrl(url); - return instance; - } -} -export { FindPetsByTagsParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 6`] = ` -" -class GetOrderByIdParameters { - private _orderId: number; - - constructor(input: { - orderId: number, - }) { - this._orderId = input.orderId; - } - - /** - * ID of pet that needs to be fetched - */ - get orderId(): number { return this._orderId; } - set orderId(orderId: number) { this._orderId = orderId; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: orderId (style: simple, explode: false) - if (this.orderId !== undefined && this.orderId !== null) { - const value = this.orderId; - if (Array.isArray(value)) { - result['orderId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['orderId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['orderId'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new GetOrderByIdParameters instance - */ - static fromUrl(url: string, basePath: string): GetOrderByIdParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new GetOrderByIdParameters({ orderId: pathParams.orderId }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { orderId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'orderId': - result.orderId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { GetOrderByIdParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 7`] = ` -" -class GetPetByIdParameters { - private _petId: number; - - constructor(input: { - petId: number, - }) { - this._petId = input.petId; - } - - /** - * ID of pet to return - */ - get petId(): number { return this._petId; } - set petId(petId: number) { this._petId = petId; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: petId (style: simple, explode: false) - if (this.petId !== undefined && this.petId !== null) { - const value = this.petId; - if (Array.isArray(value)) { - result['petId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['petId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['petId'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new GetPetByIdParameters instance - */ - static fromUrl(url: string, basePath: string): GetPetByIdParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new GetPetByIdParameters({ petId: pathParams.petId }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { petId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'petId': - result.petId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { GetPetByIdParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 8`] = ` -" -class GetUserByNameParameters { - private _username: string; - - constructor(input: { - username: string, - }) { - this._username = input.username; - } - - /** - * The name that needs to be fetched. Use user1 for testing. - */ - get username(): string { return this._username; } - set username(username: string) { this._username = username; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: username (style: simple, explode: false) - if (this.username !== undefined && this.username !== null) { - const value = this.username; - if (Array.isArray(value)) { - result['username'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['username'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['username'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new GetUserByNameParameters instance - */ - static fromUrl(url: string, basePath: string): GetUserByNameParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new GetUserByNameParameters({ username: pathParams.username }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { username: string } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'username': - result.username = decodeValue; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { GetUserByNameParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 9`] = ` -" -class LoginUserParameters { - private _username: string; - private _password: string; - - constructor(input: { - username: string, - password: string, - }) { - this._username = input.username; - this._password = input.password; - } - - /** - * The user name for login - */ - get username(): string { return this._username; } - set username(username: string) { this._username = username; } - - /** - * The password for login in clear text - */ - get password(): string { return this._password; } - set password(password: string) { this._password = password; } - - - - /** - * Serialize query parameters according to OpenAPI 2.0/3.x specification - * @returns URLSearchParams object with serialized query parameters - */ - serializeQueryParameters(): URLSearchParams { - const params = new URLSearchParams(); - - // Serialize query parameter: username (style: form, explode: true) - if (this.username !== undefined && this.username !== null) { - const value = this.username; - if (Array.isArray(value)) { - value.forEach(val => params.append('username', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('username', encodeURIComponent(String(value))); - } - } - // Serialize query parameter: password (style: form, explode: true) - if (this.password !== undefined && this.password !== null) { - const value = this.password; - if (Array.isArray(value)) { - value.forEach(val => params.append('password', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('password', encodeURIComponent(String(value))); - } - } - - return params; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - - // Add query parameters - - const queryParams = this.serializeQueryParameters(); - const queryString = queryParams.toString(); - if (queryString) { - url += (url.includes('?') ? '&' : '?') + queryString; - } - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - /** - * Deserialize URL and populate instance properties from query parameters - * @param url The URL to parse (can be full URL or just query string) - */ - deserializeUrl(url: string): void { - // Extract query string from URL - let queryString = ''; - if (url.includes('?')) { - queryString = url.split('?')[1]; - } else if (url.includes('=')) { - // Assume it's already a query string - queryString = url; - } - - if (!queryString) { - return; - } - - const params = new URLSearchParams(queryString); - - // Deserialize query parameter: username (style: form, explode: true) - if (params.has('username')) { - const value = params.get('username'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.username = decodedValue; - } - } - // Deserialize query parameter: password (style: form, explode: true) - if (params.has('password')) { - const value = params.get('password'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.password = decodedValue; - } - } - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @param defaultUsername Default username values (required parameter) - * @param defaultPassword Default password values (required parameter) - * @returns A new LoginUserParameters instance - */ - static fromUrl(url: string, basePath: string, defaultUsername: string = "", defaultPassword: string = ""): LoginUserParameters { - - const instance = new LoginUserParameters({ username: defaultUsername, password: defaultPassword }); - instance.deserializeUrl(url); - return instance; - } -} -export { LoginUserParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 10`] = ` -" -class UpdatePetWithFormParameters { - private _petId: number; - - constructor(input: { - petId: number, - }) { - this._petId = input.petId; - } - - /** - * ID of pet that needs to be updated - */ - get petId(): number { return this._petId; } - set petId(petId: number) { this._petId = petId; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: petId (style: simple, explode: false) - if (this.petId !== undefined && this.petId !== null) { - const value = this.petId; - if (Array.isArray(value)) { - result['petId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['petId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['petId'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new UpdatePetWithFormParameters instance - */ - static fromUrl(url: string, basePath: string): UpdatePetWithFormParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new UpdatePetWithFormParameters({ petId: pathParams.petId }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { petId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'petId': - result.petId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { UpdatePetWithFormParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 11`] = ` -" -class UpdateUserParameters { - private _username: string; - - constructor(input: { - username: string, - }) { - this._username = input.username; - } - - /** - * name that need to be updated - */ - get username(): string { return this._username; } - set username(username: string) { this._username = username; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: username (style: simple, explode: false) - if (this.username !== undefined && this.username !== null) { - const value = this.username; - if (Array.isArray(value)) { - result['username'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['username'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['username'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new UpdateUserParameters instance - */ - static fromUrl(url: string, basePath: string): UpdateUserParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new UpdateUserParameters({ username: pathParams.username }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { username: string } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'username': - result.username = decodeValue; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { UpdateUserParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 12`] = ` -" -class UploadFileParameters { - private _petId: number; - - constructor(input: { - petId: number, - }) { - this._petId = input.petId; - } - - /** - * ID of pet to update - */ - get petId(): number { return this._petId; } - set petId(petId: number) { this._petId = petId; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: petId (style: simple, explode: false) - if (this.petId !== undefined && this.petId !== null) { - const value = this.petId; - if (Array.isArray(value)) { - result['petId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['petId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['petId'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new UploadFileParameters instance - */ - static fromUrl(url: string, basePath: string): UploadFileParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new UploadFileParameters({ petId: pathParams.petId }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { petId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'petId': - result.petId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { UploadFileParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 1`] = ` -" -class DeleteOrderParameters { - private _orderId: string; - - constructor(input: { - orderId: string, - }) { - this._orderId = input.orderId; - } - - /** - * ID of the order that needs to be deleted - */ - get orderId(): string { return this._orderId; } - set orderId(orderId: string) { this._orderId = orderId; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: orderId (style: simple, explode: false) - if (this.orderId !== undefined && this.orderId !== null) { - const value = this.orderId; - if (Array.isArray(value)) { - result['orderId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['orderId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['orderId'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new DeleteOrderParameters instance - */ - static fromUrl(url: string, basePath: string): DeleteOrderParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new DeleteOrderParameters({ orderId: pathParams.orderId }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { orderId: string } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'orderId': - result.orderId = decodeValue; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { DeleteOrderParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 2`] = ` -" -class DeletePetParameters { - private _petId: number; - - constructor(input: { - petId: number, - }) { - this._petId = input.petId; - } - - /** - * Pet id to delete - */ - get petId(): number { return this._petId; } - set petId(petId: number) { this._petId = petId; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: petId (style: simple, explode: false) - if (this.petId !== undefined && this.petId !== null) { - const value = this.petId; - if (Array.isArray(value)) { - result['petId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['petId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['petId'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new DeletePetParameters instance - */ - static fromUrl(url: string, basePath: string): DeletePetParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new DeletePetParameters({ petId: pathParams.petId }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { petId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'petId': - result.petId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { DeletePetParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 3`] = ` -" -class DeleteUserParameters { - private _username: string; - - constructor(input: { - username: string, - }) { - this._username = input.username; - } - - /** - * The name that needs to be deleted - */ - get username(): string { return this._username; } - set username(username: string) { this._username = username; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: username (style: simple, explode: false) - if (this.username !== undefined && this.username !== null) { - const value = this.username; - if (Array.isArray(value)) { - result['username'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['username'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['username'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new DeleteUserParameters instance - */ - static fromUrl(url: string, basePath: string): DeleteUserParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new DeleteUserParameters({ username: pathParams.username }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { username: string } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'username': - result.username = decodeValue; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { DeleteUserParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 4`] = ` -"import {StatusItem} from './StatusItem'; -class FindPetsByStatusParameters { - private _status: StatusItem[]; - - constructor(input: { - status: StatusItem[], - }) { - this._status = input.status; - } - - /** - * Status values that need to be considered for filter - */ - get status(): StatusItem[] { return this._status; } - set status(status: StatusItem[]) { this._status = status; } - - - - /** - * Serialize query parameters according to OpenAPI 2.0/3.x specification - * @returns URLSearchParams object with serialized query parameters - */ - serializeQueryParameters(): URLSearchParams { - const params = new URLSearchParams(); - - // Serialize query parameter: status (style: form, explode: false) - if (this.status !== undefined && this.status !== null) { - const value = this.status; - if (Array.isArray(value)) { - params.append('status', value.map(val => encodeURIComponent(String(val))).join(',')); - } else if (typeof value === 'object' && value !== null) { - params.append('status', Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(',')); - } else { - params.append('status', encodeURIComponent(String(value))); - } - } - - return params; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - - // Add query parameters - - const queryParams = this.serializeQueryParameters(); - const queryString = queryParams.toString(); - if (queryString) { - url += (url.includes('?') ? '&' : '?') + queryString; - } - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - /** - * Deserialize URL and populate instance properties from query parameters - * @param url The URL to parse (can be full URL or just query string) - */ - deserializeUrl(url: string): void { - // Extract query string from URL - let queryString = ''; - if (url.includes('?')) { - queryString = url.split('?')[1]; - } else if (url.includes('=')) { - // Assume it's already a query string - queryString = url; - } - - if (!queryString) { - return; - } - - const params = new URLSearchParams(queryString); - - // Deserialize query parameter: status (style: form, explode: false) - if (params.has('status')) { - const value = params.get('status'); - if (value === '') { - this.status = []; - } else if (value) { - // Split by comma and decode - const decodedValues = value.split(',').map(val => decodeURIComponent(val.trim())); - this.status = decodedValues as ("available" | "pending" | "sold")[]; - } - } - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @param defaultStatus Default status values (required parameter) - * @returns A new FindPetsByStatusParameters instance - */ - static fromUrl(url: string, basePath: string, defaultStatus: ("available" | "pending" | "sold")[] = []): FindPetsByStatusParameters { - - const instance = new FindPetsByStatusParameters({ status: defaultStatus }); - instance.deserializeUrl(url); - return instance; - } -} -export { FindPetsByStatusParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 5`] = ` -" -class FindPetsByTagsParameters { - private _tags: string[]; - - constructor(input: { - tags: string[], - }) { - this._tags = input.tags; - } - - /** - * Tags to filter by - */ - get tags(): string[] { return this._tags; } - set tags(tags: string[]) { this._tags = tags; } - - - - /** - * Serialize query parameters according to OpenAPI 2.0/3.x specification - * @returns URLSearchParams object with serialized query parameters - */ - serializeQueryParameters(): URLSearchParams { - const params = new URLSearchParams(); - - // Serialize query parameter: tags (style: form, explode: false) - if (this.tags !== undefined && this.tags !== null) { - const value = this.tags; - if (Array.isArray(value)) { - params.append('tags', value.map(val => encodeURIComponent(String(val))).join(',')); - } else if (typeof value === 'object' && value !== null) { - params.append('tags', Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(',')); - } else { - params.append('tags', encodeURIComponent(String(value))); - } - } - - return params; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - - // Add query parameters - - const queryParams = this.serializeQueryParameters(); - const queryString = queryParams.toString(); - if (queryString) { - url += (url.includes('?') ? '&' : '?') + queryString; - } - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - /** - * Deserialize URL and populate instance properties from query parameters - * @param url The URL to parse (can be full URL or just query string) - */ - deserializeUrl(url: string): void { - // Extract query string from URL - let queryString = ''; - if (url.includes('?')) { - queryString = url.split('?')[1]; - } else if (url.includes('=')) { - // Assume it's already a query string - queryString = url; - } - - if (!queryString) { - return; - } - - const params = new URLSearchParams(queryString); - - // Deserialize query parameter: tags (style: form, explode: false) - if (params.has('tags')) { - const value = params.get('tags'); - if (value === '') { - this.tags = []; - } else if (value) { - // Split by comma and decode - const decodedValues = value.split(',').map(val => decodeURIComponent(val.trim())); - this.tags = decodedValues as string[]; - } - } - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @param defaultTags Default tags values (required parameter) - * @returns A new FindPetsByTagsParameters instance - */ - static fromUrl(url: string, basePath: string, defaultTags: string[] = []): FindPetsByTagsParameters { - - const instance = new FindPetsByTagsParameters({ tags: defaultTags }); - instance.deserializeUrl(url); - return instance; - } -} -export { FindPetsByTagsParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 6`] = ` -" -class GetOrderByIdParameters { - private _orderId: number; - - constructor(input: { - orderId: number, - }) { - this._orderId = input.orderId; - } - - /** - * ID of pet that needs to be fetched - */ - get orderId(): number { return this._orderId; } - set orderId(orderId: number) { this._orderId = orderId; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: orderId (style: simple, explode: false) - if (this.orderId !== undefined && this.orderId !== null) { - const value = this.orderId; - if (Array.isArray(value)) { - result['orderId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['orderId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['orderId'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new GetOrderByIdParameters instance - */ - static fromUrl(url: string, basePath: string): GetOrderByIdParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new GetOrderByIdParameters({ orderId: pathParams.orderId }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { orderId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'orderId': - result.orderId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { GetOrderByIdParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 7`] = ` -" -class GetPetByIdParameters { - private _petId: number; - - constructor(input: { - petId: number, - }) { - this._petId = input.petId; - } - - /** - * ID of pet to return - */ - get petId(): number { return this._petId; } - set petId(petId: number) { this._petId = petId; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: petId (style: simple, explode: false) - if (this.petId !== undefined && this.petId !== null) { - const value = this.petId; - if (Array.isArray(value)) { - result['petId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['petId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['petId'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new GetPetByIdParameters instance - */ - static fromUrl(url: string, basePath: string): GetPetByIdParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new GetPetByIdParameters({ petId: pathParams.petId }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { petId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'petId': - result.petId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { GetPetByIdParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 8`] = ` -" -class GetUserByNameParameters { - private _username: string; - - constructor(input: { - username: string, - }) { - this._username = input.username; - } - - /** - * The name that needs to be fetched. Use user1 for testing. - */ - get username(): string { return this._username; } - set username(username: string) { this._username = username; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: username (style: simple, explode: false) - if (this.username !== undefined && this.username !== null) { - const value = this.username; - if (Array.isArray(value)) { - result['username'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['username'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['username'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new GetUserByNameParameters instance - */ - static fromUrl(url: string, basePath: string): GetUserByNameParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new GetUserByNameParameters({ username: pathParams.username }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { username: string } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'username': - result.username = decodeValue; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { GetUserByNameParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 9`] = ` -" -class LoginUserParameters { - private _username: string; - private _password: string; - - constructor(input: { - username: string, - password: string, - }) { - this._username = input.username; - this._password = input.password; - } - - /** - * The user name for login - */ - get username(): string { return this._username; } - set username(username: string) { this._username = username; } - - /** - * The password for login in clear text - */ - get password(): string { return this._password; } - set password(password: string) { this._password = password; } - - - - /** - * Serialize query parameters according to OpenAPI 2.0/3.x specification - * @returns URLSearchParams object with serialized query parameters - */ - serializeQueryParameters(): URLSearchParams { - const params = new URLSearchParams(); - - // Serialize query parameter: username (style: form, explode: true) - if (this.username !== undefined && this.username !== null) { - const value = this.username; - if (Array.isArray(value)) { - value.forEach(val => params.append('username', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('username', encodeURIComponent(String(value))); - } - } - // Serialize query parameter: password (style: form, explode: true) - if (this.password !== undefined && this.password !== null) { - const value = this.password; - if (Array.isArray(value)) { - value.forEach(val => params.append('password', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('password', encodeURIComponent(String(value))); - } - } - - return params; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - - // Add query parameters - - const queryParams = this.serializeQueryParameters(); - const queryString = queryParams.toString(); - if (queryString) { - url += (url.includes('?') ? '&' : '?') + queryString; - } - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - /** - * Deserialize URL and populate instance properties from query parameters - * @param url The URL to parse (can be full URL or just query string) - */ - deserializeUrl(url: string): void { - // Extract query string from URL - let queryString = ''; - if (url.includes('?')) { - queryString = url.split('?')[1]; - } else if (url.includes('=')) { - // Assume it's already a query string - queryString = url; - } - - if (!queryString) { - return; - } - - const params = new URLSearchParams(queryString); - - // Deserialize query parameter: username (style: form, explode: true) - if (params.has('username')) { - const value = params.get('username'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.username = decodedValue; - } - } - // Deserialize query parameter: password (style: form, explode: true) - if (params.has('password')) { - const value = params.get('password'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.password = decodedValue; - } - } - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @param defaultUsername Default username values (required parameter) - * @param defaultPassword Default password values (required parameter) - * @returns A new LoginUserParameters instance - */ - static fromUrl(url: string, basePath: string, defaultUsername: string = "", defaultPassword: string = ""): LoginUserParameters { - - const instance = new LoginUserParameters({ username: defaultUsername, password: defaultPassword }); - instance.deserializeUrl(url); - return instance; - } -} -export { LoginUserParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 10`] = ` -" -class UpdatePetWithFormParameters { - private _petId: number; - - constructor(input: { - petId: number, - }) { - this._petId = input.petId; - } - - /** - * ID of pet that needs to be updated - */ - get petId(): number { return this._petId; } - set petId(petId: number) { this._petId = petId; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: petId (style: simple, explode: false) - if (this.petId !== undefined && this.petId !== null) { - const value = this.petId; - if (Array.isArray(value)) { - result['petId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['petId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['petId'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new UpdatePetWithFormParameters instance - */ - static fromUrl(url: string, basePath: string): UpdatePetWithFormParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new UpdatePetWithFormParameters({ petId: pathParams.petId }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { petId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'petId': - result.petId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { UpdatePetWithFormParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 11`] = ` -" -class UpdateUserParameters { - private _username: string; - - constructor(input: { - username: string, - }) { - this._username = input.username; - } - - /** - * name that need to be deleted - */ - get username(): string { return this._username; } - set username(username: string) { this._username = username; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: username (style: simple, explode: false) - if (this.username !== undefined && this.username !== null) { - const value = this.username; - if (Array.isArray(value)) { - result['username'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['username'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['username'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new UpdateUserParameters instance - */ - static fromUrl(url: string, basePath: string): UpdateUserParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new UpdateUserParameters({ username: pathParams.username }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { username: string } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'username': - result.username = decodeValue; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { UpdateUserParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 12`] = ` -" -class UploadFileParameters { - private _petId: number; - - constructor(input: { - petId: number, - }) { - this._petId = input.petId; - } - - /** - * ID of pet to update - */ - get petId(): number { return this._petId; } - set petId(petId: number) { this._petId = petId; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: petId (style: simple, explode: false) - if (this.petId !== undefined && this.petId !== null) { - const value = this.petId; - if (Array.isArray(value)) { - result['petId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['petId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['petId'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new UploadFileParameters instance - */ - static fromUrl(url: string, basePath: string): UploadFileParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new UploadFileParameters({ petId: pathParams.petId }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { petId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'petId': - result.petId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { UploadFileParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 1`] = ` -" -class DeleteOrderParameters { - private _orderId: string; - - constructor(input: { - orderId: string, - }) { - this._orderId = input.orderId; - } - - /** - * ID of the order that needs to be deleted - */ - get orderId(): string { return this._orderId; } - set orderId(orderId: string) { this._orderId = orderId; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: orderId (style: simple, explode: false) - if (this.orderId !== undefined && this.orderId !== null) { - const value = this.orderId; - if (Array.isArray(value)) { - result['orderId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['orderId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['orderId'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new DeleteOrderParameters instance - */ - static fromUrl(url: string, basePath: string): DeleteOrderParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new DeleteOrderParameters({ orderId: pathParams.orderId }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { orderId: string } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'orderId': - result.orderId = decodeValue; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } -} -export { DeleteOrderParameters };" -`; - -exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 2`] = ` -" -class DeletePetParameters { - private _petId: number; - - constructor(input: { - petId: number, - }) { - this._petId = input.petId; - } - - /** - * Pet id to delete - */ - get petId(): number { return this._petId; } - set petId(petId: number) { this._petId = petId; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: petId (style: simple, explode: false) - if (this.petId !== undefined && this.petId !== null) { - const value = this.petId; - if (Array.isArray(value)) { - result['petId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['petId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['petId'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new DeletePetParameters instance - */ - static fromUrl(url: string, basePath: string): DeletePetParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new DeletePetParameters({ petId: pathParams.petId }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { petId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'petId': - result.petId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } +interface GetDocumentParameters { + documentId: string; } -export { DeletePetParameters };" +export { GetDocumentParameters };" `; -exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 3`] = ` +exports[`parameters typescript openapi should use constrained property names and skip parseFromUrl without query parameters 2`] = ` " -class DeleteUserParameters { - private _username: string; - - constructor(input: { - username: string, - }) { - this._username = input.username; - } - - /** - * The name that needs to be deleted - */ - get username(): string { return this._username; } - set username(username: string) { this._username = username; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: username (style: simple, explode: false) - if (this.username !== undefined && this.username !== null) { - const value = this.username; - if (Array.isArray(value)) { - result['username'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['username'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['username'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } +interface GetTransactionsParameters { + skip?: number; + cvr?: string; +} +export { GetTransactionsParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 1`] = ` +" +interface DeleteOrderParameters { /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced + * ID of the order that needs to be deleted */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } + orderId: number; +} +export { DeleteOrderParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 2`] = ` +" +interface DeletePetParameters { /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new DeleteUserParameters instance + * Pet id to delete */ - static fromUrl(url: string, basePath: string): DeleteUserParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new DeleteUserParameters({ username: pathParams.username }); - return instance; - } + petId: number; +} +export { DeletePetParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 3`] = ` +" +interface DeleteUserParameters { /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values + * The name that needs to be deleted */ - private static extractPathParameters(url: string, basePath: string): { username: string } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'username': - result.username = decodeValue; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } + username: string; } export { DeleteUserParameters };" `; -exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 4`] = ` -"import {StatusItem} from './StatusItem'; -class FindPetsByStatusParameters { - private _status: StatusItem[]; - - constructor(input: { - status: StatusItem[], - }) { - this._status = input.status; - } - +exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 4`] = ` +" +interface FindPetsByStatusParameters { /** * Status values that need to be considered for filter */ - get status(): StatusItem[] { return this._status; } - set status(status: StatusItem[]) { this._status = status; } - - - - /** - * Serialize query parameters according to OpenAPI 2.0/3.x specification - * @returns URLSearchParams object with serialized query parameters - */ - serializeQueryParameters(): URLSearchParams { - const params = new URLSearchParams(); - - // Serialize query parameter: status (style: form, explode: false) - if (this.status !== undefined && this.status !== null) { - const value = this.status; - if (Array.isArray(value)) { - params.append('status', value.map(val => encodeURIComponent(String(val))).join(',')); - } else if (typeof value === 'object' && value !== null) { - params.append('status', Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(',')); - } else { - params.append('status', encodeURIComponent(String(value))); - } - } - - return params; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - - // Add query parameters - - const queryParams = this.serializeQueryParameters(); - const queryString = queryParams.toString(); - if (queryString) { - url += (url.includes('?') ? '&' : '?') + queryString; - } - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - /** - * Deserialize URL and populate instance properties from query parameters - * @param url The URL to parse (can be full URL or just query string) - */ - deserializeUrl(url: string): void { - // Extract query string from URL - let queryString = ''; - if (url.includes('?')) { - queryString = url.split('?')[1]; - } else if (url.includes('=')) { - // Assume it's already a query string - queryString = url; - } - - if (!queryString) { - return; - } - - const params = new URLSearchParams(queryString); - - // Deserialize query parameter: status (style: form, explode: false) - if (params.has('status')) { - const value = params.get('status'); - if (value === '') { - this.status = []; - } else if (value) { - // Split by comma and decode - const decodedValues = value.split(',').map(val => decodeURIComponent(val.trim())); - this.status = decodedValues as ("available" | "pending" | "sold")[]; - } - } - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @param defaultStatus Default status values (required parameter) - * @returns A new FindPetsByStatusParameters instance - */ - static fromUrl(url: string, basePath: string, defaultStatus: ("available" | "pending" | "sold")[] = []): FindPetsByStatusParameters { - - const instance = new FindPetsByStatusParameters({ status: defaultStatus }); - instance.deserializeUrl(url); - return instance; - } + status: any[]; } export { FindPetsByStatusParameters };" `; -exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 5`] = ` +exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 5`] = ` " -class FindPetsByTagsParameters { - private _tags: string[]; - - constructor(input: { - tags: string[], - }) { - this._tags = input.tags; - } - +interface FindPetsByTagsParameters { /** * Tags to filter by */ - get tags(): string[] { return this._tags; } - set tags(tags: string[]) { this._tags = tags; } - - - - /** - * Serialize query parameters according to OpenAPI 2.0/3.x specification - * @returns URLSearchParams object with serialized query parameters - */ - serializeQueryParameters(): URLSearchParams { - const params = new URLSearchParams(); - - // Serialize query parameter: tags (style: form, explode: false) - if (this.tags !== undefined && this.tags !== null) { - const value = this.tags; - if (Array.isArray(value)) { - params.append('tags', value.map(val => encodeURIComponent(String(val))).join(',')); - } else if (typeof value === 'object' && value !== null) { - params.append('tags', Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(',')); - } else { - params.append('tags', encodeURIComponent(String(value))); - } - } - - return params; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - - // Add query parameters - - const queryParams = this.serializeQueryParameters(); - const queryString = queryParams.toString(); - if (queryString) { - url += (url.includes('?') ? '&' : '?') + queryString; - } - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - /** - * Deserialize URL and populate instance properties from query parameters - * @param url The URL to parse (can be full URL or just query string) - */ - deserializeUrl(url: string): void { - // Extract query string from URL - let queryString = ''; - if (url.includes('?')) { - queryString = url.split('?')[1]; - } else if (url.includes('=')) { - // Assume it's already a query string - queryString = url; - } - - if (!queryString) { - return; - } - - const params = new URLSearchParams(queryString); - - // Deserialize query parameter: tags (style: form, explode: false) - if (params.has('tags')) { - const value = params.get('tags'); - if (value === '') { - this.tags = []; - } else if (value) { - // Split by comma and decode - const decodedValues = value.split(',').map(val => decodeURIComponent(val.trim())); - this.tags = decodedValues as string[]; - } - } - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @param defaultTags Default tags values (required parameter) - * @returns A new FindPetsByTagsParameters instance - */ - static fromUrl(url: string, basePath: string, defaultTags: string[] = []): FindPetsByTagsParameters { - - const instance = new FindPetsByTagsParameters({ tags: defaultTags }); - instance.deserializeUrl(url); - return instance; - } + tags: any[]; } export { FindPetsByTagsParameters };" `; -exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 6`] = ` +exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 6`] = ` " -class GetOrderByIdParameters { - private _orderId: number; - - constructor(input: { - orderId: number, - }) { - this._orderId = input.orderId; - } - +interface GetOrderByIdParameters { /** * ID of pet that needs to be fetched */ - get orderId(): number { return this._orderId; } - set orderId(orderId: number) { this._orderId = orderId; } - - - - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: orderId (style: simple, explode: false) - if (this.orderId !== undefined && this.orderId !== null) { - const value = this.orderId; - if (Array.isArray(value)) { - result['orderId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['orderId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['orderId'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } - - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new GetOrderByIdParameters instance - */ - static fromUrl(url: string, basePath: string): GetOrderByIdParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new GetOrderByIdParameters({ orderId: pathParams.orderId }); - return instance; - } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { orderId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'orderId': - result.orderId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } + orderId: number; } export { GetOrderByIdParameters };" `; -exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 7`] = ` +exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 7`] = ` " -class GetPetByIdParameters { - private _petId: number; - - constructor(input: { - petId: number, - }) { - this._petId = input.petId; - } - +interface GetPetByIdParameters { /** * ID of pet to return */ - get petId(): number { return this._petId; } - set petId(petId: number) { this._petId = petId; } - - + petId: number; +} +export { GetPetByIdParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 8`] = ` +" +interface GetUserByNameParameters { /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: petId (style: simple, explode: false) - if (this.petId !== undefined && this.petId !== null) { - const value = this.petId; - if (Array.isArray(value)) { - result['petId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['petId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['petId'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters + * The name that needs to be fetched. Use user1 for testing. */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } + username: string; +} +export { GetUserByNameParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 9`] = ` +" +interface LoginUserParameters { /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced + * The user name for login */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } - + username: string; /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new GetPetByIdParameters instance + * The password for login in clear text */ - static fromUrl(url: string, basePath: string): GetPetByIdParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new GetPetByIdParameters({ petId: pathParams.petId }); - return instance; - } + password: string; +} +export { LoginUserParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 10`] = ` +" +interface UpdatePetWithFormParameters { /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values + * ID of pet that needs to be updated */ - private static extractPathParameters(url: string, basePath: string): { petId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'petId': - result.petId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } + petId: number; } -export { GetPetByIdParameters };" +export { UpdatePetWithFormParameters };" `; -exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 8`] = ` +exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 11`] = ` " -class GetUserByNameParameters { - private _username: string; - - constructor(input: { - username: string, - }) { - this._username = input.username; - } - +interface UpdateUserParameters { /** - * The name that needs to be fetched. Use user1 for testing. + * name that need to be updated */ - get username(): string { return this._username; } - set username(username: string) { this._username = username; } - - + username: string; +} +export { UpdateUserParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 12`] = ` +" +interface UploadFileParameters { /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: username (style: simple, explode: false) - if (this.username !== undefined && this.username !== null) { - const value = this.username; - if (Array.isArray(value)) { - result['username'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['username'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['username'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters + * ID of pet to update */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } + petId: number; +} +export { UploadFileParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 1`] = ` +" +interface DeleteOrderParameters { /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced + * ID of the order that needs to be deleted */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } + orderId: string; +} +export { DeleteOrderParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 2`] = ` +" +interface DeletePetParameters { /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new GetUserByNameParameters instance + * Pet id to delete */ - static fromUrl(url: string, basePath: string): GetUserByNameParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new GetUserByNameParameters({ username: pathParams.username }); - return instance; - } + petId: number; +} +export { DeletePetParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 3`] = ` +" +interface DeleteUserParameters { /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values + * The name that needs to be deleted */ - private static extractPathParameters(url: string, basePath: string): { username: string } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'username': - result.username = decodeValue; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } + username: string; } -export { GetUserByNameParameters };" +export { DeleteUserParameters };" `; -exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 9`] = ` -" -class LoginUserParameters { - private _username: string; - private _password: string; - - constructor(input: { - username: string, - password: string, - }) { - this._username = input.username; - this._password = input.password; - } - +exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 4`] = ` +"import {StatusItem} from './StatusItem'; +interface FindPetsByStatusParameters { /** - * The user name for login + * Status values that need to be considered for filter */ - get username(): string { return this._username; } - set username(username: string) { this._username = username; } + status: StatusItem[]; +} +export { FindPetsByStatusParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 5`] = ` +" +interface FindPetsByTagsParameters { /** - * The password for login in clear text + * Tags to filter by */ - get password(): string { return this._password; } - set password(password: string) { this._password = password; } - - + tags: string[]; +} +export { FindPetsByTagsParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 6`] = ` +" +interface GetOrderByIdParameters { /** - * Serialize query parameters according to OpenAPI 2.0/3.x specification - * @returns URLSearchParams object with serialized query parameters + * ID of pet that needs to be fetched */ - serializeQueryParameters(): URLSearchParams { - const params = new URLSearchParams(); - - // Serialize query parameter: username (style: form, explode: true) - if (this.username !== undefined && this.username !== null) { - const value = this.username; - if (Array.isArray(value)) { - value.forEach(val => params.append('username', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('username', encodeURIComponent(String(value))); - } - } - // Serialize query parameter: password (style: form, explode: true) - if (this.password !== undefined && this.password !== null) { - const value = this.password; - if (Array.isArray(value)) { - value.forEach(val => params.append('password', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('password', encodeURIComponent(String(value))); - } - } - - return params; - } + orderId: number; +} +export { GetOrderByIdParameters };" +`; + +exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 7`] = ` +" +interface GetPetByIdParameters { /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters + * ID of pet to return */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - - // Add query parameters - - const queryParams = this.serializeQueryParameters(); - const queryString = queryParams.toString(); - if (queryString) { - url += (url.includes('?') ? '&' : '?') + queryString; - } - - return url; - } + petId: number; +} +export { GetPetByIdParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 8`] = ` +" +interface GetUserByNameParameters { /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced + * The name that needs to be fetched. Use user1 for testing. */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } + username: string; +} +export { GetUserByNameParameters };" +`; + +exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 9`] = ` +" +interface LoginUserParameters { /** - * Deserialize URL and populate instance properties from query parameters - * @param url The URL to parse (can be full URL or just query string) + * The user name for login */ - deserializeUrl(url: string): void { - // Extract query string from URL - let queryString = ''; - if (url.includes('?')) { - queryString = url.split('?')[1]; - } else if (url.includes('=')) { - // Assume it's already a query string - queryString = url; - } - - if (!queryString) { - return; - } - - const params = new URLSearchParams(queryString); - - // Deserialize query parameter: username (style: form, explode: true) - if (params.has('username')) { - const value = params.get('username'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.username = decodedValue; - } - } - // Deserialize query parameter: password (style: form, explode: true) - if (params.has('password')) { - const value = params.get('password'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.password = decodedValue; - } - } - } - + username: string; /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @param defaultUsername Default username values (required parameter) - * @param defaultPassword Default password values (required parameter) - * @returns A new LoginUserParameters instance + * The password for login in clear text */ - static fromUrl(url: string, basePath: string, defaultUsername: string = "", defaultPassword: string = ""): LoginUserParameters { - - const instance = new LoginUserParameters({ username: defaultUsername, password: defaultPassword }); - instance.deserializeUrl(url); - return instance; - } + password: string; } export { LoginUserParameters };" `; -exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 10`] = ` +exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 10`] = ` " -class UpdatePetWithFormParameters { - private _petId: number; - - constructor(input: { - petId: number, - }) { - this._petId = input.petId; - } - +interface UpdatePetWithFormParameters { /** * ID of pet that needs to be updated */ - get petId(): number { return this._petId; } - set petId(petId: number) { this._petId = petId; } - - + petId: number; +} +export { UpdatePetWithFormParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 11`] = ` +" +interface UpdateUserParameters { /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: petId (style: simple, explode: false) - if (this.petId !== undefined && this.petId !== null) { - const value = this.petId; - if (Array.isArray(value)) { - result['petId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['petId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['petId'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters + * name that need to be deleted */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } + username: string; +} +export { UpdateUserParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 12`] = ` +" +interface UploadFileParameters { /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced + * ID of pet to update */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } + petId: number; +} +export { UploadFileParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 1`] = ` +" +interface DeleteOrderParameters { /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new UpdatePetWithFormParameters instance + * ID of the order that needs to be deleted */ - static fromUrl(url: string, basePath: string): UpdatePetWithFormParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new UpdatePetWithFormParameters({ petId: pathParams.petId }); - return instance; - } + orderId: string; +} +export { DeleteOrderParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 2`] = ` +" +interface DeletePetParameters { /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values + * Pet id to delete */ - private static extractPathParameters(url: string, basePath: string): { petId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'petId': - result.petId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } + petId: number; } -export { UpdatePetWithFormParameters };" +export { DeletePetParameters };" `; -exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 11`] = ` +exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 3`] = ` " -class UpdateUserParameters { - private _username: string; - - constructor(input: { - username: string, - }) { - this._username = input.username; - } - +interface DeleteUserParameters { /** - * name that need to be deleted + * The name that needs to be deleted */ - get username(): string { return this._username; } - set username(username: string) { this._username = username; } - - + username: string; +} +export { DeleteUserParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 4`] = ` +"import {StatusItem} from './StatusItem'; +interface FindPetsByStatusParameters { /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: username (style: simple, explode: false) - if (this.username !== undefined && this.username !== null) { - const value = this.username; - if (Array.isArray(value)) { - result['username'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['username'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['username'] = encodeURIComponent(String(value)); - } - } - - return result; - } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters + * Status values that need to be considered for filter */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } + status: StatusItem[]; +} +export { FindPetsByStatusParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 5`] = ` +" +interface FindPetsByTagsParameters { /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced + * Tags to filter by */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } + tags: string[]; +} +export { FindPetsByTagsParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 6`] = ` +" +interface GetOrderByIdParameters { /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new UpdateUserParameters instance + * ID of pet that needs to be fetched */ - static fromUrl(url: string, basePath: string): UpdateUserParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new UpdateUserParameters({ username: pathParams.username }); - return instance; - } + orderId: number; +} +export { GetOrderByIdParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 7`] = ` +" +interface GetPetByIdParameters { /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values + * ID of pet to return */ - private static extractPathParameters(url: string, basePath: string): { username: string } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'username': - result.username = decodeValue; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } + petId: number; } -export { UpdateUserParameters };" +export { GetPetByIdParameters };" `; -exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 12`] = ` +exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 8`] = ` " -class UploadFileParameters { - private _petId: number; - - constructor(input: { - petId: number, - }) { - this._petId = input.petId; - } - +interface GetUserByNameParameters { /** - * ID of pet to update + * The name that needs to be fetched. Use user1 for testing. */ - get petId(): number { return this._petId; } - set petId(petId: number) { this._petId = petId; } - - + username: string; +} +export { GetUserByNameParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 9`] = ` +" +interface LoginUserParameters { /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution + * The user name for login */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: petId (style: simple, explode: false) - if (this.petId !== undefined && this.petId !== null) { - const value = this.petId; - if (Array.isArray(value)) { - result['petId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['petId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); - } else { - result['petId'] = encodeURIComponent(String(value)); - } - } - - return result; - } + username: string; /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters + * The password for login in clear text */ - serializeUrl(basePath: string): string { - let url = basePath; - - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); - } - - // Add query parameters - - - return url; - } + password: string; +} +export { LoginUserParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 10`] = ` +" +interface UpdatePetWithFormParameters { /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced + * ID of pet that needs to be updated */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); - } + petId: number; +} +export { UpdatePetWithFormParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 11`] = ` +" +interface UpdateUserParameters { /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new UploadFileParameters instance + * name that need to be deleted */ - static fromUrl(url: string, basePath: string): UploadFileParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new UploadFileParameters({ petId: pathParams.petId }); - return instance; - } + username: string; +} +export { UpdateUserParameters };" +`; +exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 12`] = ` +" +interface UploadFileParameters { /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values + * ID of pet to update */ - private static extractPathParameters(url: string, basePath: string): { petId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); - } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'petId': - result.petId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; - } + petId: number; } export { UploadFileParameters };" `; diff --git a/test/codegen/generators/typescript/channels.spec.ts b/test/codegen/generators/typescript/channels.spec.ts index 54fe1832..ec497e9d 100644 --- a/test/codegen/generators/typescript/channels.spec.ts +++ b/test/codegen/generators/typescript/channels.spec.ts @@ -602,6 +602,39 @@ describe('channels', () => { files: [] }; + const xRequestIdProperty = new ConstrainedObjectPropertyModel( + 'xRequestId', + 'X-Request-ID', + false, + new ConstrainedStringModel('string', undefined, {}, 'string') + ); + const acceptLanguageProperty = new ConstrainedObjectPropertyModel( + 'acceptLanguage', + 'Accept-Language', + false, + new ConstrainedStringModel('string', undefined, {}, 'string') + ); + const findPetsByStatusAndCategoryHeadersModel = new OutputModel( + '', + new ConstrainedObjectModel('FindPetsByStatusAndCategoryHeaders', undefined, {}, 'FindPetsByStatusAndCategoryHeaders', { + xRequestId: xRequestIdProperty, + acceptLanguage: acceptLanguageProperty + }), + 'FindPetsByStatusAndCategoryHeaders', + {models: {}, originalInput: undefined}, + [] + ); + const headersDependency: TypeScriptHeadersRenderType = { + channelModels: { + findPetsByStatusAndCategory: findPetsByStatusAndCategoryHeadersModel + }, + generator: {outputPath: './headers'} as any, + files: [], + headerFunctions: { + FindPetsByStatusAndCategoryHeaders: ['serializeFindPetsByStatusAndCategoryHeadersHeaders'] + } + }; + const generatedChannels = await generateTypeScriptChannels({ generator: { ...defaultTypeScriptChannelsGenerator, @@ -615,7 +648,7 @@ describe('channels', () => { dependencyOutputs: { 'parameters-typescript': parametersDependency, 'payloads-typescript': payloadsDependency, - 'headers-typescript': createHeadersDependency() + 'headers-typescript': headersDependency } }); diff --git a/test/codegen/generators/typescript/parameters.spec.ts b/test/codegen/generators/typescript/parameters.spec.ts index 7f15e871..d32c4767 100644 --- a/test/codegen/generators/typescript/parameters.spec.ts +++ b/test/codegen/generators/typescript/parameters.spec.ts @@ -9,7 +9,7 @@ describe('parameters', () => { describe('asyncapi', () => { it('should work with AsyncAPI that contains parameters', async () => { const parsedAsyncAPIDocument = await loadAsyncapiDocument(path.resolve(__dirname, '../../../configs/parameters.yaml')); - + const renderedContent = await generateTypescriptParameters({ generator: { serializationType: 'json', @@ -28,7 +28,7 @@ describe('parameters', () => { }); it('should work with AsyncAPI v2 that contains parameters and const parameters', async () => { const parsedAsyncAPIDocument = await loadAsyncapiDocument(path.resolve(__dirname, '../../../configs/parameters-v2.yaml')); - + const renderedContent = await generateTypescriptParameters({ generator: { serializationType: 'json', @@ -47,7 +47,7 @@ describe('parameters', () => { }); it('should work with no channels', async () => { const parsedAsyncAPIDocument = await loadAsyncapiDocument(path.resolve(__dirname, '../../../configs/parameters-no-channels.yaml')); - + const renderedContent = await generateTypescriptParameters({ generator: { serializationType: 'json', @@ -64,11 +64,11 @@ describe('parameters', () => { expect(Object.keys(renderedContent.channelModels).length).toEqual(0); }); }); - + describe('openapi', () => { it('should work with OpenAPI 3.0 that contains parameters', async () => { const parsedOpenAPIDocument = await loadOpenapiDocument(path.resolve(__dirname, '../../../configs/openapi-3.json')); - + const renderedContent = await generateTypescriptParameters({ generator: { serializationType: 'json', @@ -82,7 +82,7 @@ describe('parameters', () => { openapiDocument: parsedOpenAPIDocument, dependencyOutputs: { } }); - + expect(Object.keys(renderedContent.channelModels).length).toEqual(12); // Check that parameters were generated for operations with path/query parameters expect(renderedContent.channelModels['deleteOrder']?.result).toMatchSnapshot(); @@ -98,10 +98,10 @@ describe('parameters', () => { expect(renderedContent.channelModels['updateUser']?.result).toMatchSnapshot(); expect(renderedContent.channelModels['uploadFile']?.result).toMatchSnapshot(); }); - + it('should work with OpenAPI 3.1 that contains parameters', async () => { const parsedOpenAPIDocument = await loadOpenapiDocument(path.resolve(__dirname, '../../../configs/openapi-3_1.json')); - + const renderedContent = await generateTypescriptParameters({ generator: { serializationType: 'json', @@ -115,7 +115,7 @@ describe('parameters', () => { openapiDocument: parsedOpenAPIDocument, dependencyOutputs: { } }); - + expect(Object.keys(renderedContent.channelModels).length).toEqual(12); // Check that parameters were generated for operations with path/query parameters expect(renderedContent.channelModels['deleteOrder']?.result).toMatchSnapshot(); @@ -131,10 +131,10 @@ describe('parameters', () => { expect(renderedContent.channelModels['updateUser']?.result).toMatchSnapshot(); expect(renderedContent.channelModels['uploadFile']?.result).toMatchSnapshot(); }); - + it('should work with OpenAPI 2.0 that contains parameters', async () => { const parsedOpenAPIDocument = await loadOpenapiDocument(path.resolve(__dirname, '../../../configs/openapi-2.json')); - + const renderedContent = await generateTypescriptParameters({ generator: { serializationType: 'json', @@ -148,7 +148,7 @@ describe('parameters', () => { openapiDocument: parsedOpenAPIDocument, dependencyOutputs: { } }); - + expect(Object.keys(renderedContent.channelModels).length).toEqual(12); // Check that some parameters were generated (the exact operation IDs may differ in OpenAPI 2.0) expect(renderedContent.channelModels['deleteOrder']?.result).toMatchSnapshot(); @@ -164,8 +164,8 @@ describe('parameters', () => { expect(renderedContent.channelModels['updateUser']?.result).toMatchSnapshot(); expect(renderedContent.channelModels['uploadFile']?.result).toMatchSnapshot(); }); - - it('should use constrained property names and skip deserializeUrl without query parameters', async () => { + + it('should use constrained property names and skip parseFromUrl without query parameters', async () => { const openAPIDoc: OpenAPIV3.Document = { openapi: '3.0.0', info: { @@ -224,23 +224,25 @@ describe('parameters', () => { dependencyOutputs: { } }); - // Path-only parameters must not reference the deserializeUrl method that only exists with query parameters - const pathOnlyResult = renderedContent.channelModels['getDocument']?.result; - expect(pathOnlyResult).toBeDefined(); - expect(pathOnlyResult).not.toContain('deserializeUrl'); + // Path-only parameters: interface exists but no parseFromUrl function (no query params) + expect(renderedContent.channelModels['getDocument']).toBeDefined(); + const pathOnlyFile = renderedContent.files.find(f => f.path.endsWith('GetDocumentParameters.ts')); + expect(pathOnlyFile).toBeDefined(); + expect(pathOnlyFile?.content).not.toContain('parseFromUrl'); - // Non-camelCase parameter names must access the constrained property while keeping the raw wire name - const queryResult = renderedContent.channelModels['getTransactions']?.result; - expect(queryResult).toBeDefined(); - expect(queryResult).not.toContain('this.Skip'); - expect(queryResult).not.toContain('this.Cvr'); - expect(queryResult).toContain('this.skip'); - expect(queryResult).toContain('this.cvr'); - expect(queryResult).toContain(`params.append('Skip'`); - expect(queryResult).toContain(`params.has('Cvr')`); + // Non-camelCase names: serialize/parse functions use constrained property names and raw wire names + expect(renderedContent.channelModels['getTransactions']).toBeDefined(); + const queryFile = renderedContent.files.find(f => f.path.endsWith('GetTransactionsParameters.ts')); + expect(queryFile).toBeDefined(); + expect(queryFile?.content).not.toContain('parameters.Skip'); + expect(queryFile?.content).not.toContain('parameters.Cvr'); + expect(queryFile?.content).toContain('parameters.skip'); + expect(queryFile?.content).toContain('parameters.cvr'); + expect(queryFile?.content).toContain(`'Skip'`); + expect(queryFile?.content).toContain(`'Cvr'`); - expect(pathOnlyResult).toMatchSnapshot(); - expect(queryResult).toMatchSnapshot(); + expect(renderedContent.channelModels['getDocument']?.result).toMatchSnapshot(); + expect(renderedContent.channelModels['getTransactions']?.result).toMatchSnapshot(); }); it('should handle empty OpenAPI document with no operations', async () => { @@ -253,7 +255,7 @@ describe('parameters', () => { }, paths: {} }; - + const renderedContent = await generateTypescriptParameters({ generator: { serializationType: 'json', @@ -267,7 +269,7 @@ describe('parameters', () => { openapiDocument: minimalOpenAPIDoc, dependencyOutputs: { } }); - + expect(Object.keys(renderedContent.channelModels).length).toEqual(0); }); }); diff --git a/test/runtime/typescript/package-lock.json b/test/runtime/typescript/package-lock.json index 57820879..2b2c4bb7 100644 --- a/test/runtime/typescript/package-lock.json +++ b/test/runtime/typescript/package-lock.json @@ -1002,6 +1002,162 @@ } } }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.3.66", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.66.tgz", + "integrity": "sha512-UijJsvuLy73vxeVYEy7urIHksXS+3BdvJ9s9AY+bRMSQW483NO7RLp8g4FdTyJbRaN0BH15SQnY0dcjQBkVuHw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.3.66", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.66.tgz", + "integrity": "sha512-xGsHKvViQnwTNLF30Y/5OqWdnN6RsiyUI8awZXfz1sHcXCEaLe+v+WLQ+/E8sgw0YUkYVHzzfV/sAN2CezJK5Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.3.66", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.66.tgz", + "integrity": "sha512-gNbLcSIV2pq90BkMSpzvK4xPXOl8GEF3YR4NaqF0CYSzQsVXXTTqMuX/r26xNYudBKzH0345S1MpoRk2qricnA==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.3.66", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.66.tgz", + "integrity": "sha512-cJSQ0oplyWbJqy4rzVcnBYLAi6z1QT3QCcR7iAey0aAmCvfRBZJfXlyjggMjn4iosuadkauwCZR1xYNhBDRn7w==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.3.66", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.66.tgz", + "integrity": "sha512-GDQZpcB9aGxG9PTA2shdIkoMZlGK5omJ8NR49uoBTtLBVYiGeXAwV0U1Uaw8kXEZj9i7wZDkvjzjSaNH3evRsg==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.3.66", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.66.tgz", + "integrity": "sha512-lg8E4O/Pd9KfK0lajdinVMuGME8dSv7V9arhEpmlfGE2eXSDCWqDn5Htk5QVBstt9lt1lsRhWHJ/YYc2eQY30Q==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.3.66", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.66.tgz", + "integrity": "sha512-lo8ZcAO/zL2pZWH+LZIyge8u2MklaeuT6+FpVVpBFktMVdYXbaVtzpvWbgRFBZHvL3SRDF+u8jxjtkXhvGUpTw==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.3.66", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.66.tgz", + "integrity": "sha512-cQoVwBuJY5WkHbfpCOlndNwYr1ZThatRjQQvKy540NUIeAEk9Fa6ozlDBtU75UdaWKtUG6YQ/bWz+KTemheVxw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.3.66", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.66.tgz", + "integrity": "sha512-y/FrAIINK4UBeUQQknGlWXEyjo+MBvjF7WkUf2KP7sNr9EHHy8+dXohAGd5Anz0eJrqOM1ZXR/GEjxRp7bGQ1Q==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, "node_modules/@swc/core-win32-x64-msvc": { "version": "1.3.66", "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.66.tgz", @@ -2590,6 +2746,20 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 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 6dcb861d..d4c4c6d5 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 @@ -7,7 +7,7 @@ import {ItemStatus} from './payload/ItemStatus'; import {PetOrder} from './payload/PetOrder'; import {AUser} from './payload/AUser'; import {AnUploadedResponse} from './payload/AnUploadedResponse'; -import {FindPetsByStatusAndCategoryParameters} from './parameter/FindPetsByStatusAndCategoryParameters'; +import {FindPetsByStatusAndCategoryParameters, serializeFindPetsByStatusAndCategoryParametersUrl, parseFindPetsByStatusAndCategoryParametersFromUrl} from './parameter/FindPetsByStatusAndCategoryParameters'; import {FindPetsByStatusAndCategoryHeaders} from './headers/FindPetsByStatusAndCategoryHeaders'; // ============================================================================ @@ -808,17 +808,17 @@ function createPaginationHelpers( } /** - * Builds a URL with path parameters replaced + * Builds a URL with path parameters replaced using a serializer function * @param server - Base server URL * @param pathTemplate - Path template with {param} placeholders - * @param parameters - Parameter object with getChannelWithParameters method + * @param serializeFn - Function that takes the path template and returns the serialized path */ -function buildUrlWithParameters string }>( +function buildUrlWithParameters( server: string, pathTemplate: string, - parameters: T + serializeFn: (path: string) => string ): string { - const path = parameters.getChannelWithParameters(pathTemplate); + const path = serializeFn(pathTemplate); return `${server}${path}`; } @@ -1243,7 +1243,7 @@ async function updatePet(context: UpdatePetContext): Promise string }; + parameters: FindPetsByStatusAndCategoryParameters; requestHeaders?: { marshal: () => string }; } @@ -1269,7 +1269,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = buildUrlWithParameters(config.server, '/pet/findByStatus/{status}/{categoryId}', context.parameters); + let url = buildUrlWithParameters(config.server, '/pet/findByStatus/{status}/{categoryId}', (path) => serializeFindPetsByStatusAndCategoryParametersUrl(context.parameters, path)); url = applyQueryParams(config.queryParams, url); // Apply pagination (can affect URL and/or headers) diff --git a/test/runtime/typescript/src/openapi-path-organization/channels/parameter/FindPetsByStatusAndCategoryParameters.ts b/test/runtime/typescript/src/openapi-path-organization/channels/parameter/FindPetsByStatusAndCategoryParameters.ts index f90e6da9..23f9ee81 100644 --- a/test/runtime/typescript/src/openapi-path-organization/channels/parameter/FindPetsByStatusAndCategoryParameters.ts +++ b/test/runtime/typescript/src/openapi-path-organization/channels/parameter/FindPetsByStatusAndCategoryParameters.ts @@ -2,390 +2,213 @@ import {Status} from './Status'; import {SortBy} from './SortBy'; import {SortOrder} from './SortOrder'; import {Format} from './Format'; -class FindPetsByStatusAndCategoryParameters { - private _status: Status; - private _categoryId: number; - private _limit?: number; - private _offset?: number; - private _sortBy?: SortBy; - private _sortOrder?: SortOrder; - private _tags?: string[]; - private _includePetDetails?: boolean; - private _format?: Format; - - constructor(input: { - status: Status, - categoryId: number, - limit?: number, - offset?: number, - sortBy?: SortBy, - sortOrder?: SortOrder, - tags?: string[], - includePetDetails?: boolean, - format?: Format, - }) { - this._status = input.status; - this._categoryId = input.categoryId; - this._limit = input.limit; - this._offset = input.offset; - this._sortBy = input.sortBy; - this._sortOrder = input.sortOrder; - this._tags = input.tags; - this._includePetDetails = input.includePetDetails; - this._format = input.format; - } - +interface FindPetsByStatusAndCategoryParameters { /** * Status value that needs to be considered for filter */ - get status(): Status { return this._status; } - set status(status: Status) { this._status = status; } - + status: Status; /** * Category ID to filter pets by */ - get categoryId(): number { return this._categoryId; } - set categoryId(categoryId: number) { this._categoryId = categoryId; } - + categoryId: number; /** * Maximum number of pets to return */ - get limit(): number | undefined { return this._limit; } - set limit(limit: number | undefined) { this._limit = limit; } - + limit?: number; /** * Number of pets to skip before returning results */ - get offset(): number | undefined { return this._offset; } - set offset(offset: number | undefined) { this._offset = offset; } - + offset?: number; /** * Sort pets by specified field */ - get sortBy(): SortBy | undefined { return this._sortBy; } - set sortBy(sortBy: SortBy | undefined) { this._sortBy = sortBy; } - + sortBy?: SortBy; /** * Sort order for results */ - get sortOrder(): SortOrder | undefined { return this._sortOrder; } - set sortOrder(sortOrder: SortOrder | undefined) { this._sortOrder = sortOrder; } - + sortOrder?: SortOrder; /** * Filter pets by tags (comma-separated) */ - get tags(): string[] | undefined { return this._tags; } - set tags(tags: string[] | undefined) { this._tags = tags; } - + tags?: string[]; /** * Include detailed pet information in response */ - get includePetDetails(): boolean | undefined { return this._includePetDetails; } - set includePetDetails(includePetDetails: boolean | undefined) { this._includePetDetails = includePetDetails; } - + includePetDetails?: boolean; /** * Response format preference */ - get format(): Format | undefined { return this._format; } - set format(format: Format | undefined) { this._format = format; } + format?: Format; +} +export { FindPetsByStatusAndCategoryParameters }; +export function serializeFindPetsByStatusAndCategoryParametersUrl(parameters: FindPetsByStatusAndCategoryParameters, basePath: string): string { + let url = basePath; + url = url.replace(/\{status\}/g, encodeURIComponent(String(parameters.status))); + url = url.replace(/\{categoryId\}/g, encodeURIComponent(String(parameters.categoryId))); - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: status (style: simple, explode: false) - if (this.status !== undefined && this.status !== null) { - const value = this.status; - if (Array.isArray(value)) { - result['status'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['status'] = Object.entries(value).map(([key, val]) => `${encodeURIComponent(key)},${encodeURIComponent(String(val))}`).join(','); - } else { - result['status'] = encodeURIComponent(String(value)); - } + const parts: string[] = []; + if (parameters.limit !== undefined && parameters.limit !== null) { + const val = parameters.limit; + if (Array.isArray(val)) { + val.forEach(v => parts.push(`limit=${encodeURIComponent(String(v))}`)); + } else { + parts.push(`limit=${encodeURIComponent(String(parameters.limit))}`); } - // Serialize path parameter: categoryId (style: simple, explode: false) - if (this.categoryId !== undefined && this.categoryId !== null) { - const value = this.categoryId; - if (Array.isArray(value)) { - result['categoryId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['categoryId'] = Object.entries(value).map(([key, val]) => `${encodeURIComponent(key)},${encodeURIComponent(String(val))}`).join(','); - } else { - result['categoryId'] = encodeURIComponent(String(value)); - } - } - - return result; } - /** - * Serialize query parameters according to OpenAPI 2.0/3.x specification - * @returns URLSearchParams object with serialized query parameters - */ - serializeQueryParameters(): URLSearchParams { - const params = new URLSearchParams(); - - // Serialize query parameter: limit (style: form, explode: true) - if (this.limit !== undefined && this.limit !== null) { - const value = this.limit; - if (Array.isArray(value)) { - value.forEach(val => params.append('limit', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('limit', encodeURIComponent(String(value))); - } - } - // Serialize query parameter: offset (style: form, explode: true) - if (this.offset !== undefined && this.offset !== null) { - const value = this.offset; - if (Array.isArray(value)) { - value.forEach(val => params.append('offset', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('offset', encodeURIComponent(String(value))); - } + if (parameters.offset !== undefined && parameters.offset !== null) { + const val = parameters.offset; + if (Array.isArray(val)) { + val.forEach(v => parts.push(`offset=${encodeURIComponent(String(v))}`)); + } else { + parts.push(`offset=${encodeURIComponent(String(parameters.offset))}`); } - // Serialize query parameter: sortBy (style: form, explode: true) - if (this.sortBy !== undefined && this.sortBy !== null) { - const value = this.sortBy; - if (Array.isArray(value)) { - value.forEach(val => params.append('sortBy', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('sortBy', encodeURIComponent(String(value))); - } + } + if (parameters.sortBy !== undefined && parameters.sortBy !== null) { + const val = parameters.sortBy; + if (Array.isArray(val)) { + val.forEach(v => parts.push(`sortBy=${encodeURIComponent(String(v))}`)); + } else { + parts.push(`sortBy=${encodeURIComponent(String(parameters.sortBy))}`); } - // Serialize query parameter: sortOrder (style: form, explode: true) - if (this.sortOrder !== undefined && this.sortOrder !== null) { - const value = this.sortOrder; - if (Array.isArray(value)) { - value.forEach(val => params.append('sortOrder', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('sortOrder', encodeURIComponent(String(value))); - } + } + if (parameters.sortOrder !== undefined && parameters.sortOrder !== null) { + const val = parameters.sortOrder; + if (Array.isArray(val)) { + val.forEach(v => parts.push(`sortOrder=${encodeURIComponent(String(v))}`)); + } else { + parts.push(`sortOrder=${encodeURIComponent(String(parameters.sortOrder))}`); } - // Serialize query parameter: tags (style: form, explode: false) - if (this.tags !== undefined && this.tags !== null) { - const value = this.tags; - if (Array.isArray(value)) { - params.append('tags', value.map(val => encodeURIComponent(String(val))).join(',')); - } else if (typeof value === 'object' && value !== null) { - params.append('tags', Object.entries(value).map(([key, val]) => `${encodeURIComponent(key)},${encodeURIComponent(String(val))}`).join(',')); + } + if (parameters.tags !== undefined && parameters.tags !== null) { + const val = parameters.tags; + if (Array.isArray(val)) { + if (val.length === 0) { + parts.push('tags='); } else { - params.append('tags', encodeURIComponent(String(value))); + parts.push(`tags=${val.map(v => encodeURIComponent(String(v))).join(',')}`); } + } else { + parts.push(`tags=${encodeURIComponent(String(parameters.tags))}`); } - // Serialize query parameter: includePetDetails (style: form, explode: true) - if (this.includePetDetails !== undefined && this.includePetDetails !== null) { - const value = this.includePetDetails; - if (Array.isArray(value)) { - value.forEach(val => params.append('includePetDetails', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('includePetDetails', encodeURIComponent(String(value))); - } + } + if (parameters.includePetDetails !== undefined && parameters.includePetDetails !== null) { + const val = parameters.includePetDetails; + if (Array.isArray(val)) { + val.forEach(v => parts.push(`includePetDetails=${encodeURIComponent(String(v))}`)); + } else { + parts.push(`includePetDetails=${encodeURIComponent(String(parameters.includePetDetails))}`); } - // Serialize query parameter: format (style: form, explode: true) - if (this.format !== undefined && this.format !== null) { - const value = this.format; - if (Array.isArray(value)) { - value.forEach(val => params.append('format', String(val))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(key, String(val))); - } else { - params.append('format', String(value)); - } + } + if (parameters.format !== undefined && parameters.format !== null) { + const val = parameters.format; + if (Array.isArray(val)) { + val.forEach(v => parts.push(`format=${String(v)}`)); + } else { + parts.push(`format=${String(parameters.format)}`); } - - return params; } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(`{${name}}`, 'g'), value); - } + const queryString = parts.join('&'); + if (queryString) { + url += `?${queryString}`; + } - // Add query parameters - - const queryParams = this.serializeQueryParameters(); - const queryString = queryParams.toString(); - if (queryString) { - url += (url.includes('?') ? '&' : '?') + queryString; - } + return url; +} - return url; +export function parseFindPetsByStatusAndCategoryParametersFromUrl(url: string, basePath: string): FindPetsByStatusAndCategoryParameters { + const urlPath = url.indexOf('?') !== -1 ? url.slice(0, url.indexOf('?')) : url; + + const regexPattern = basePath.replace(/\{([^}]+)\}/g, '([^/?]+)'); + const regex = new RegExp('^' + regexPattern + '$'); + const match = urlPath.match(regex); + + if (!match) { + throw new Error(`URL path '${urlPath}' does not match base path template '${basePath}'`); } - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); + const paramNames = basePath.match(/\{([^}]+)\}/g)?.map(p => p.slice(1, -1)) || []; + const pathValues: Record = {}; + paramNames.forEach((name, index) => { + pathValues[name] = decodeURIComponent(match[index + 1]); + }); + + + if (pathValues['status'] === undefined) { + throw new Error(`Required parameter 'status' is missing from URL`); } - /** - * Deserialize URL and populate instance properties from query parameters - * @param url The URL to parse (can be full URL or just query string) - */ - deserializeUrl(url: string): void { - // Extract query string from URL - let queryString = ''; - if (url.includes('?')) { - queryString = url.split('?')[1]; - } else if (url.includes('=')) { - // Assume it's already a query string - queryString = url; - } + if (pathValues['categoryId'] === undefined) { + throw new Error(`Required parameter 'categoryId' is missing from URL`); + } + const result: FindPetsByStatusAndCategoryParameters = { + status: pathValues['status'] as "available" | "pending" | "sold", + categoryId: Number(pathValues['categoryId']) + }; - if (!queryString) { - return; - } + const qIdx = url.indexOf('?'); + if (qIdx === -1) { + return result; + } - const params = new URLSearchParams(queryString); + const queryString = url.slice(qIdx + 1); + if (!queryString) { + return result; + } - // Deserialize query parameter: limit (style: form, explode: true) - if (params.has('limit')) { - const value = params.get('limit'); - if (value) { - const decodedValue = decodeURIComponent(value); - const numValue = Number(decodedValue); - if (!isNaN(numValue)) { - this.limit = numValue; - } - } - } - // Deserialize query parameter: offset (style: form, explode: true) - if (params.has('offset')) { - const value = params.get('offset'); - if (value) { - const decodedValue = decodeURIComponent(value); - const numValue = Number(decodedValue); - if (!isNaN(numValue)) { - this.offset = numValue; - } - } - } - // Deserialize query parameter: sortBy (style: form, explode: true) - if (params.has('sortBy')) { - const value = params.get('sortBy'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.sortBy = decodedValue as "name" | "id" | "category" | "status"; + const params = new URLSearchParams(queryString); + + if (params.has('limit')) { + const raw = params.get('limit'); + if (raw !== null) { + const num = Number(raw); + if (!isNaN(num)) { + result.limit = num; } } - // Deserialize query parameter: sortOrder (style: form, explode: true) - if (params.has('sortOrder')) { - const value = params.get('sortOrder'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.sortOrder = decodedValue as "asc" | "desc"; + } + if (params.has('offset')) { + const raw = params.get('offset'); + if (raw !== null) { + const num = Number(raw); + if (!isNaN(num)) { + result.offset = num; } } - // Deserialize query parameter: tags (style: form, explode: false) - if (params.has('tags')) { - const value = params.get('tags'); - if (value === '') { - this.tags = []; - } else if (value) { - // Split by comma and decode - const decodedValues = value.split(',').map(val => decodeURIComponent(val.trim())); - this.tags = decodedValues as string[]; - } + } + if (params.has('sortBy')) { + const raw = params.get('sortBy'); + if (raw !== null) { + result.sortBy = raw as "name" | "id" | "category" | "status"; } - // Deserialize query parameter: includePetDetails (style: form, explode: true) - if (params.has('includePetDetails')) { - const value = params.get('includePetDetails'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.includePetDetails = decodedValue.toLowerCase() === 'true'; - } + } + if (params.has('sortOrder')) { + const raw = params.get('sortOrder'); + if (raw !== null) { + result.sortOrder = raw as "asc" | "desc"; } - // Deserialize query parameter: format (style: form, explode: true) - if (params.has('format')) { - const value = params.get('format'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.format = decodedValue as "json" | "xml" | "csv"; - } + } + if (params.has('tags')) { + const raw = params.get('tags'); + if (raw === '') { + result.tags = []; + } else if (raw !== null) { + result.tags = raw.split(',') as string[]; } } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new FindPetsByStatusAndCategoryParameters instance - */ - static fromUrl(url: string, basePath: string): FindPetsByStatusAndCategoryParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new FindPetsByStatusAndCategoryParameters({ status: pathParams.status, categoryId: pathParams.categoryId }); - instance.deserializeUrl(url); - return instance; + if (params.has('includePetDetails')) { + const raw = params.get('includePetDetails'); + if (raw !== null) { + result.includePetDetails = raw.toLowerCase() === 'true'; + } } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { status: "available" | "pending" | "sold", categoryId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\{([^}]+)\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(`URL path '${urlPath}' does not match base path template '${basePath}'`); + if (params.has('format')) { + const raw = params.get('format'); + if (raw !== null) { + result.format = raw as "json" | "xml" | "csv"; } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\{([^}]+)\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'status': - result.status = decodeValue as "available" | "pending" | "sold"; - break; - case 'categoryId': - result.categoryId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; } -} -export { FindPetsByStatusAndCategoryParameters }; \ No newline at end of file + + return result; +} \ No newline at end of file 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 355e2c5a..22de75ec 100644 --- a/test/runtime/typescript/src/openapi-primitive/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi-primitive/channels/http_client.ts @@ -1,7 +1,5 @@ import * as GetEchoResponse_200Module from './../payloads/GetEchoResponse_200'; import * as GetCountResponse_200Module from './../payloads/GetCountResponse_200'; -import { URLSearchParams, URL } from 'url'; -import * as NodeFetch from 'node-fetch'; // ============================================================================ // Common Types - Shared across all HTTP client functions @@ -71,7 +69,7 @@ export interface HttpRequestParams { url: string; headers?: Record; method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'; - credentials?: RequestCredentials; + credentials?: 'omit' | 'include' | 'same-origin'; body?: any; } @@ -268,7 +266,7 @@ export interface HttpHooks { /** * The actual request implementation - allows swapping fetch for axios, etc. - * Default: uses node-fetch + * Default: uses the global fetch (Node.js 18+) */ makeRequest?: (params: HttpRequestParams) => Promise; @@ -332,13 +330,25 @@ const DEFAULT_RETRY_CONFIG: Required = { }; /** - * Default request hook implementation using node-fetch + * Default request hook implementation using the global fetch (Node.js 18+) */ const defaultMakeRequest = async (params: HttpRequestParams): Promise => { - return NodeFetch.default(params.url, { + // Build a Headers object so multi-value headers (string[]) are preserved - + // the global fetch's HeadersInit only accepts string values in a plain object. + const headers = new Headers(); + for (const [name, value] of Object.entries(params.headers ?? {})) { + if (Array.isArray(value)) { + for (const entry of value) { + headers.append(name, entry); + } + } else { + headers.set(name, value); + } + } + return fetch(params.url, { body: params.body, method: params.method, - headers: params.headers + headers }) as unknown as HttpResponse; }; @@ -815,17 +825,17 @@ function createPaginationHelpers( } /** - * Builds a URL with path parameters replaced + * Builds a URL with path parameters replaced using a serializer function * @param server - Base server URL * @param pathTemplate - Path template with {param} placeholders - * @param parameters - Parameter object with getChannelWithParameters method + * @param serializeFn - Function that takes the path template and returns the serialized path */ -function buildUrlWithParameters string }>( +function buildUrlWithParameters( server: string, pathTemplate: string, - parameters: T + serializeFn: (path: string) => string ): string { - const path = parameters.getChannelWithParameters(pathTemplate); + const path = serializeFn(pathTemplate); return `${server}${path}`; } @@ -924,7 +934,7 @@ async function handleOAuth2TokenFlow( params.delete('client_secret'); } - const tokenResponse = await NodeFetch.default(auth.tokenUrl, { + const tokenResponse = await fetch(auth.tokenUrl, { method: 'POST', headers: authHeaders, body: params.toString() @@ -964,7 +974,7 @@ async function handleTokenRefresh( ): Promise { if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; - const refreshResponse = await NodeFetch.default(auth.tokenUrl, { + const refreshResponse = await fetch(auth.tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' 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 6dcb861d..d4c4c6d5 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 @@ -7,7 +7,7 @@ import {ItemStatus} from './payload/ItemStatus'; import {PetOrder} from './payload/PetOrder'; import {AUser} from './payload/AUser'; import {AnUploadedResponse} from './payload/AnUploadedResponse'; -import {FindPetsByStatusAndCategoryParameters} from './parameter/FindPetsByStatusAndCategoryParameters'; +import {FindPetsByStatusAndCategoryParameters, serializeFindPetsByStatusAndCategoryParametersUrl, parseFindPetsByStatusAndCategoryParametersFromUrl} from './parameter/FindPetsByStatusAndCategoryParameters'; import {FindPetsByStatusAndCategoryHeaders} from './headers/FindPetsByStatusAndCategoryHeaders'; // ============================================================================ @@ -808,17 +808,17 @@ function createPaginationHelpers( } /** - * Builds a URL with path parameters replaced + * Builds a URL with path parameters replaced using a serializer function * @param server - Base server URL * @param pathTemplate - Path template with {param} placeholders - * @param parameters - Parameter object with getChannelWithParameters method + * @param serializeFn - Function that takes the path template and returns the serialized path */ -function buildUrlWithParameters string }>( +function buildUrlWithParameters( server: string, pathTemplate: string, - parameters: T + serializeFn: (path: string) => string ): string { - const path = parameters.getChannelWithParameters(pathTemplate); + const path = serializeFn(pathTemplate); return `${server}${path}`; } @@ -1243,7 +1243,7 @@ async function updatePet(context: UpdatePetContext): Promise string }; + parameters: FindPetsByStatusAndCategoryParameters; requestHeaders?: { marshal: () => string }; } @@ -1269,7 +1269,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = buildUrlWithParameters(config.server, '/pet/findByStatus/{status}/{categoryId}', context.parameters); + let url = buildUrlWithParameters(config.server, '/pet/findByStatus/{status}/{categoryId}', (path) => serializeFindPetsByStatusAndCategoryParametersUrl(context.parameters, path)); url = applyQueryParams(config.queryParams, url); // Apply pagination (can affect URL and/or headers) diff --git a/test/runtime/typescript/src/openapi-tag-organization/channels/parameter/FindPetsByStatusAndCategoryParameters.ts b/test/runtime/typescript/src/openapi-tag-organization/channels/parameter/FindPetsByStatusAndCategoryParameters.ts index f90e6da9..23f9ee81 100644 --- a/test/runtime/typescript/src/openapi-tag-organization/channels/parameter/FindPetsByStatusAndCategoryParameters.ts +++ b/test/runtime/typescript/src/openapi-tag-organization/channels/parameter/FindPetsByStatusAndCategoryParameters.ts @@ -2,390 +2,213 @@ import {Status} from './Status'; import {SortBy} from './SortBy'; import {SortOrder} from './SortOrder'; import {Format} from './Format'; -class FindPetsByStatusAndCategoryParameters { - private _status: Status; - private _categoryId: number; - private _limit?: number; - private _offset?: number; - private _sortBy?: SortBy; - private _sortOrder?: SortOrder; - private _tags?: string[]; - private _includePetDetails?: boolean; - private _format?: Format; - - constructor(input: { - status: Status, - categoryId: number, - limit?: number, - offset?: number, - sortBy?: SortBy, - sortOrder?: SortOrder, - tags?: string[], - includePetDetails?: boolean, - format?: Format, - }) { - this._status = input.status; - this._categoryId = input.categoryId; - this._limit = input.limit; - this._offset = input.offset; - this._sortBy = input.sortBy; - this._sortOrder = input.sortOrder; - this._tags = input.tags; - this._includePetDetails = input.includePetDetails; - this._format = input.format; - } - +interface FindPetsByStatusAndCategoryParameters { /** * Status value that needs to be considered for filter */ - get status(): Status { return this._status; } - set status(status: Status) { this._status = status; } - + status: Status; /** * Category ID to filter pets by */ - get categoryId(): number { return this._categoryId; } - set categoryId(categoryId: number) { this._categoryId = categoryId; } - + categoryId: number; /** * Maximum number of pets to return */ - get limit(): number | undefined { return this._limit; } - set limit(limit: number | undefined) { this._limit = limit; } - + limit?: number; /** * Number of pets to skip before returning results */ - get offset(): number | undefined { return this._offset; } - set offset(offset: number | undefined) { this._offset = offset; } - + offset?: number; /** * Sort pets by specified field */ - get sortBy(): SortBy | undefined { return this._sortBy; } - set sortBy(sortBy: SortBy | undefined) { this._sortBy = sortBy; } - + sortBy?: SortBy; /** * Sort order for results */ - get sortOrder(): SortOrder | undefined { return this._sortOrder; } - set sortOrder(sortOrder: SortOrder | undefined) { this._sortOrder = sortOrder; } - + sortOrder?: SortOrder; /** * Filter pets by tags (comma-separated) */ - get tags(): string[] | undefined { return this._tags; } - set tags(tags: string[] | undefined) { this._tags = tags; } - + tags?: string[]; /** * Include detailed pet information in response */ - get includePetDetails(): boolean | undefined { return this._includePetDetails; } - set includePetDetails(includePetDetails: boolean | undefined) { this._includePetDetails = includePetDetails; } - + includePetDetails?: boolean; /** * Response format preference */ - get format(): Format | undefined { return this._format; } - set format(format: Format | undefined) { this._format = format; } + format?: Format; +} +export { FindPetsByStatusAndCategoryParameters }; +export function serializeFindPetsByStatusAndCategoryParametersUrl(parameters: FindPetsByStatusAndCategoryParameters, basePath: string): string { + let url = basePath; + url = url.replace(/\{status\}/g, encodeURIComponent(String(parameters.status))); + url = url.replace(/\{categoryId\}/g, encodeURIComponent(String(parameters.categoryId))); - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: status (style: simple, explode: false) - if (this.status !== undefined && this.status !== null) { - const value = this.status; - if (Array.isArray(value)) { - result['status'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['status'] = Object.entries(value).map(([key, val]) => `${encodeURIComponent(key)},${encodeURIComponent(String(val))}`).join(','); - } else { - result['status'] = encodeURIComponent(String(value)); - } + const parts: string[] = []; + if (parameters.limit !== undefined && parameters.limit !== null) { + const val = parameters.limit; + if (Array.isArray(val)) { + val.forEach(v => parts.push(`limit=${encodeURIComponent(String(v))}`)); + } else { + parts.push(`limit=${encodeURIComponent(String(parameters.limit))}`); } - // Serialize path parameter: categoryId (style: simple, explode: false) - if (this.categoryId !== undefined && this.categoryId !== null) { - const value = this.categoryId; - if (Array.isArray(value)) { - result['categoryId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['categoryId'] = Object.entries(value).map(([key, val]) => `${encodeURIComponent(key)},${encodeURIComponent(String(val))}`).join(','); - } else { - result['categoryId'] = encodeURIComponent(String(value)); - } - } - - return result; } - /** - * Serialize query parameters according to OpenAPI 2.0/3.x specification - * @returns URLSearchParams object with serialized query parameters - */ - serializeQueryParameters(): URLSearchParams { - const params = new URLSearchParams(); - - // Serialize query parameter: limit (style: form, explode: true) - if (this.limit !== undefined && this.limit !== null) { - const value = this.limit; - if (Array.isArray(value)) { - value.forEach(val => params.append('limit', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('limit', encodeURIComponent(String(value))); - } - } - // Serialize query parameter: offset (style: form, explode: true) - if (this.offset !== undefined && this.offset !== null) { - const value = this.offset; - if (Array.isArray(value)) { - value.forEach(val => params.append('offset', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('offset', encodeURIComponent(String(value))); - } + if (parameters.offset !== undefined && parameters.offset !== null) { + const val = parameters.offset; + if (Array.isArray(val)) { + val.forEach(v => parts.push(`offset=${encodeURIComponent(String(v))}`)); + } else { + parts.push(`offset=${encodeURIComponent(String(parameters.offset))}`); } - // Serialize query parameter: sortBy (style: form, explode: true) - if (this.sortBy !== undefined && this.sortBy !== null) { - const value = this.sortBy; - if (Array.isArray(value)) { - value.forEach(val => params.append('sortBy', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('sortBy', encodeURIComponent(String(value))); - } + } + if (parameters.sortBy !== undefined && parameters.sortBy !== null) { + const val = parameters.sortBy; + if (Array.isArray(val)) { + val.forEach(v => parts.push(`sortBy=${encodeURIComponent(String(v))}`)); + } else { + parts.push(`sortBy=${encodeURIComponent(String(parameters.sortBy))}`); } - // Serialize query parameter: sortOrder (style: form, explode: true) - if (this.sortOrder !== undefined && this.sortOrder !== null) { - const value = this.sortOrder; - if (Array.isArray(value)) { - value.forEach(val => params.append('sortOrder', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('sortOrder', encodeURIComponent(String(value))); - } + } + if (parameters.sortOrder !== undefined && parameters.sortOrder !== null) { + const val = parameters.sortOrder; + if (Array.isArray(val)) { + val.forEach(v => parts.push(`sortOrder=${encodeURIComponent(String(v))}`)); + } else { + parts.push(`sortOrder=${encodeURIComponent(String(parameters.sortOrder))}`); } - // Serialize query parameter: tags (style: form, explode: false) - if (this.tags !== undefined && this.tags !== null) { - const value = this.tags; - if (Array.isArray(value)) { - params.append('tags', value.map(val => encodeURIComponent(String(val))).join(',')); - } else if (typeof value === 'object' && value !== null) { - params.append('tags', Object.entries(value).map(([key, val]) => `${encodeURIComponent(key)},${encodeURIComponent(String(val))}`).join(',')); + } + if (parameters.tags !== undefined && parameters.tags !== null) { + const val = parameters.tags; + if (Array.isArray(val)) { + if (val.length === 0) { + parts.push('tags='); } else { - params.append('tags', encodeURIComponent(String(value))); + parts.push(`tags=${val.map(v => encodeURIComponent(String(v))).join(',')}`); } + } else { + parts.push(`tags=${encodeURIComponent(String(parameters.tags))}`); } - // Serialize query parameter: includePetDetails (style: form, explode: true) - if (this.includePetDetails !== undefined && this.includePetDetails !== null) { - const value = this.includePetDetails; - if (Array.isArray(value)) { - value.forEach(val => params.append('includePetDetails', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('includePetDetails', encodeURIComponent(String(value))); - } + } + if (parameters.includePetDetails !== undefined && parameters.includePetDetails !== null) { + const val = parameters.includePetDetails; + if (Array.isArray(val)) { + val.forEach(v => parts.push(`includePetDetails=${encodeURIComponent(String(v))}`)); + } else { + parts.push(`includePetDetails=${encodeURIComponent(String(parameters.includePetDetails))}`); } - // Serialize query parameter: format (style: form, explode: true) - if (this.format !== undefined && this.format !== null) { - const value = this.format; - if (Array.isArray(value)) { - value.forEach(val => params.append('format', String(val))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(key, String(val))); - } else { - params.append('format', String(value)); - } + } + if (parameters.format !== undefined && parameters.format !== null) { + const val = parameters.format; + if (Array.isArray(val)) { + val.forEach(v => parts.push(`format=${String(v)}`)); + } else { + parts.push(`format=${String(parameters.format)}`); } - - return params; } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(`{${name}}`, 'g'), value); - } + const queryString = parts.join('&'); + if (queryString) { + url += `?${queryString}`; + } - // Add query parameters - - const queryParams = this.serializeQueryParameters(); - const queryString = queryParams.toString(); - if (queryString) { - url += (url.includes('?') ? '&' : '?') + queryString; - } + return url; +} - return url; +export function parseFindPetsByStatusAndCategoryParametersFromUrl(url: string, basePath: string): FindPetsByStatusAndCategoryParameters { + const urlPath = url.indexOf('?') !== -1 ? url.slice(0, url.indexOf('?')) : url; + + const regexPattern = basePath.replace(/\{([^}]+)\}/g, '([^/?]+)'); + const regex = new RegExp('^' + regexPattern + '$'); + const match = urlPath.match(regex); + + if (!match) { + throw new Error(`URL path '${urlPath}' does not match base path template '${basePath}'`); } - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); + const paramNames = basePath.match(/\{([^}]+)\}/g)?.map(p => p.slice(1, -1)) || []; + const pathValues: Record = {}; + paramNames.forEach((name, index) => { + pathValues[name] = decodeURIComponent(match[index + 1]); + }); + + + if (pathValues['status'] === undefined) { + throw new Error(`Required parameter 'status' is missing from URL`); } - /** - * Deserialize URL and populate instance properties from query parameters - * @param url The URL to parse (can be full URL or just query string) - */ - deserializeUrl(url: string): void { - // Extract query string from URL - let queryString = ''; - if (url.includes('?')) { - queryString = url.split('?')[1]; - } else if (url.includes('=')) { - // Assume it's already a query string - queryString = url; - } + if (pathValues['categoryId'] === undefined) { + throw new Error(`Required parameter 'categoryId' is missing from URL`); + } + const result: FindPetsByStatusAndCategoryParameters = { + status: pathValues['status'] as "available" | "pending" | "sold", + categoryId: Number(pathValues['categoryId']) + }; - if (!queryString) { - return; - } + const qIdx = url.indexOf('?'); + if (qIdx === -1) { + return result; + } - const params = new URLSearchParams(queryString); + const queryString = url.slice(qIdx + 1); + if (!queryString) { + return result; + } - // Deserialize query parameter: limit (style: form, explode: true) - if (params.has('limit')) { - const value = params.get('limit'); - if (value) { - const decodedValue = decodeURIComponent(value); - const numValue = Number(decodedValue); - if (!isNaN(numValue)) { - this.limit = numValue; - } - } - } - // Deserialize query parameter: offset (style: form, explode: true) - if (params.has('offset')) { - const value = params.get('offset'); - if (value) { - const decodedValue = decodeURIComponent(value); - const numValue = Number(decodedValue); - if (!isNaN(numValue)) { - this.offset = numValue; - } - } - } - // Deserialize query parameter: sortBy (style: form, explode: true) - if (params.has('sortBy')) { - const value = params.get('sortBy'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.sortBy = decodedValue as "name" | "id" | "category" | "status"; + const params = new URLSearchParams(queryString); + + if (params.has('limit')) { + const raw = params.get('limit'); + if (raw !== null) { + const num = Number(raw); + if (!isNaN(num)) { + result.limit = num; } } - // Deserialize query parameter: sortOrder (style: form, explode: true) - if (params.has('sortOrder')) { - const value = params.get('sortOrder'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.sortOrder = decodedValue as "asc" | "desc"; + } + if (params.has('offset')) { + const raw = params.get('offset'); + if (raw !== null) { + const num = Number(raw); + if (!isNaN(num)) { + result.offset = num; } } - // Deserialize query parameter: tags (style: form, explode: false) - if (params.has('tags')) { - const value = params.get('tags'); - if (value === '') { - this.tags = []; - } else if (value) { - // Split by comma and decode - const decodedValues = value.split(',').map(val => decodeURIComponent(val.trim())); - this.tags = decodedValues as string[]; - } + } + if (params.has('sortBy')) { + const raw = params.get('sortBy'); + if (raw !== null) { + result.sortBy = raw as "name" | "id" | "category" | "status"; } - // Deserialize query parameter: includePetDetails (style: form, explode: true) - if (params.has('includePetDetails')) { - const value = params.get('includePetDetails'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.includePetDetails = decodedValue.toLowerCase() === 'true'; - } + } + if (params.has('sortOrder')) { + const raw = params.get('sortOrder'); + if (raw !== null) { + result.sortOrder = raw as "asc" | "desc"; } - // Deserialize query parameter: format (style: form, explode: true) - if (params.has('format')) { - const value = params.get('format'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.format = decodedValue as "json" | "xml" | "csv"; - } + } + if (params.has('tags')) { + const raw = params.get('tags'); + if (raw === '') { + result.tags = []; + } else if (raw !== null) { + result.tags = raw.split(',') as string[]; } } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new FindPetsByStatusAndCategoryParameters instance - */ - static fromUrl(url: string, basePath: string): FindPetsByStatusAndCategoryParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new FindPetsByStatusAndCategoryParameters({ status: pathParams.status, categoryId: pathParams.categoryId }); - instance.deserializeUrl(url); - return instance; + if (params.has('includePetDetails')) { + const raw = params.get('includePetDetails'); + if (raw !== null) { + result.includePetDetails = raw.toLowerCase() === 'true'; + } } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { status: "available" | "pending" | "sold", categoryId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\{([^}]+)\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(`URL path '${urlPath}' does not match base path template '${basePath}'`); + if (params.has('format')) { + const raw = params.get('format'); + if (raw !== null) { + result.format = raw as "json" | "xml" | "csv"; } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\{([^}]+)\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'status': - result.status = decodeValue as "available" | "pending" | "sold"; - break; - case 'categoryId': - result.categoryId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; } -} -export { FindPetsByStatusAndCategoryParameters }; \ No newline at end of file + + return result; +} \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/channels/http_client.ts b/test/runtime/typescript/src/openapi/channels/http_client.ts index c2b07ea4..205a85f9 100644 --- a/test/runtime/typescript/src/openapi/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi/channels/http_client.ts @@ -7,7 +7,7 @@ import {ItemStatus} from './../payloads/ItemStatus'; import {PetOrder} from './../payloads/PetOrder'; import {AUser} from './../payloads/AUser'; import {AnUploadedResponse} from './../payloads/AnUploadedResponse'; -import {FindPetsByStatusAndCategoryParameters} from './../parameters/FindPetsByStatusAndCategoryParameters'; +import {FindPetsByStatusAndCategoryParameters, serializeFindPetsByStatusAndCategoryParametersUrl, parseFindPetsByStatusAndCategoryParametersFromUrl} from './../parameters/FindPetsByStatusAndCategoryParameters'; import {FindPetsByStatusAndCategoryHeaders} from './../headers/FindPetsByStatusAndCategoryHeaders'; // ============================================================================ @@ -808,17 +808,17 @@ function createPaginationHelpers( } /** - * Builds a URL with path parameters replaced + * Builds a URL with path parameters replaced using a serializer function * @param server - Base server URL * @param pathTemplate - Path template with {param} placeholders - * @param parameters - Parameter object with getChannelWithParameters method + * @param serializeFn - Function that takes the path template and returns the serialized path */ -function buildUrlWithParameters string }>( +function buildUrlWithParameters( server: string, pathTemplate: string, - parameters: T + serializeFn: (path: string) => string ): string { - const path = parameters.getChannelWithParameters(pathTemplate); + const path = serializeFn(pathTemplate); return `${server}${path}`; } @@ -1243,7 +1243,7 @@ async function updatePet(context: UpdatePetContext): Promise string }; + parameters: FindPetsByStatusAndCategoryParameters; requestHeaders?: { marshal: () => string }; } @@ -1269,7 +1269,7 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = buildUrlWithParameters(config.server, '/pet/findByStatus/{status}/{categoryId}', context.parameters); + let url = buildUrlWithParameters(config.server, '/pet/findByStatus/{status}/{categoryId}', (path) => serializeFindPetsByStatusAndCategoryParametersUrl(context.parameters, path)); url = applyQueryParams(config.queryParams, url); // Apply pagination (can affect URL and/or headers) diff --git a/test/runtime/typescript/src/openapi/parameters/FindPetsByStatusAndCategoryParameters.ts b/test/runtime/typescript/src/openapi/parameters/FindPetsByStatusAndCategoryParameters.ts index f90e6da9..23f9ee81 100644 --- a/test/runtime/typescript/src/openapi/parameters/FindPetsByStatusAndCategoryParameters.ts +++ b/test/runtime/typescript/src/openapi/parameters/FindPetsByStatusAndCategoryParameters.ts @@ -2,390 +2,213 @@ import {Status} from './Status'; import {SortBy} from './SortBy'; import {SortOrder} from './SortOrder'; import {Format} from './Format'; -class FindPetsByStatusAndCategoryParameters { - private _status: Status; - private _categoryId: number; - private _limit?: number; - private _offset?: number; - private _sortBy?: SortBy; - private _sortOrder?: SortOrder; - private _tags?: string[]; - private _includePetDetails?: boolean; - private _format?: Format; - - constructor(input: { - status: Status, - categoryId: number, - limit?: number, - offset?: number, - sortBy?: SortBy, - sortOrder?: SortOrder, - tags?: string[], - includePetDetails?: boolean, - format?: Format, - }) { - this._status = input.status; - this._categoryId = input.categoryId; - this._limit = input.limit; - this._offset = input.offset; - this._sortBy = input.sortBy; - this._sortOrder = input.sortOrder; - this._tags = input.tags; - this._includePetDetails = input.includePetDetails; - this._format = input.format; - } - +interface FindPetsByStatusAndCategoryParameters { /** * Status value that needs to be considered for filter */ - get status(): Status { return this._status; } - set status(status: Status) { this._status = status; } - + status: Status; /** * Category ID to filter pets by */ - get categoryId(): number { return this._categoryId; } - set categoryId(categoryId: number) { this._categoryId = categoryId; } - + categoryId: number; /** * Maximum number of pets to return */ - get limit(): number | undefined { return this._limit; } - set limit(limit: number | undefined) { this._limit = limit; } - + limit?: number; /** * Number of pets to skip before returning results */ - get offset(): number | undefined { return this._offset; } - set offset(offset: number | undefined) { this._offset = offset; } - + offset?: number; /** * Sort pets by specified field */ - get sortBy(): SortBy | undefined { return this._sortBy; } - set sortBy(sortBy: SortBy | undefined) { this._sortBy = sortBy; } - + sortBy?: SortBy; /** * Sort order for results */ - get sortOrder(): SortOrder | undefined { return this._sortOrder; } - set sortOrder(sortOrder: SortOrder | undefined) { this._sortOrder = sortOrder; } - + sortOrder?: SortOrder; /** * Filter pets by tags (comma-separated) */ - get tags(): string[] | undefined { return this._tags; } - set tags(tags: string[] | undefined) { this._tags = tags; } - + tags?: string[]; /** * Include detailed pet information in response */ - get includePetDetails(): boolean | undefined { return this._includePetDetails; } - set includePetDetails(includePetDetails: boolean | undefined) { this._includePetDetails = includePetDetails; } - + includePetDetails?: boolean; /** * Response format preference */ - get format(): Format | undefined { return this._format; } - set format(format: Format | undefined) { this._format = format; } + format?: Format; +} +export { FindPetsByStatusAndCategoryParameters }; +export function serializeFindPetsByStatusAndCategoryParametersUrl(parameters: FindPetsByStatusAndCategoryParameters, basePath: string): string { + let url = basePath; + url = url.replace(/\{status\}/g, encodeURIComponent(String(parameters.status))); + url = url.replace(/\{categoryId\}/g, encodeURIComponent(String(parameters.categoryId))); - /** - * Serialize path parameters according to OpenAPI 2.0/3.x specification - * @returns Record of parameter names to their serialized values for path substitution - */ - serializePathParameters(): Record { - const result: Record = {}; - - // Serialize path parameter: status (style: simple, explode: false) - if (this.status !== undefined && this.status !== null) { - const value = this.status; - if (Array.isArray(value)) { - result['status'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['status'] = Object.entries(value).map(([key, val]) => `${encodeURIComponent(key)},${encodeURIComponent(String(val))}`).join(','); - } else { - result['status'] = encodeURIComponent(String(value)); - } + const parts: string[] = []; + if (parameters.limit !== undefined && parameters.limit !== null) { + const val = parameters.limit; + if (Array.isArray(val)) { + val.forEach(v => parts.push(`limit=${encodeURIComponent(String(v))}`)); + } else { + parts.push(`limit=${encodeURIComponent(String(parameters.limit))}`); } - // Serialize path parameter: categoryId (style: simple, explode: false) - if (this.categoryId !== undefined && this.categoryId !== null) { - const value = this.categoryId; - if (Array.isArray(value)) { - result['categoryId'] = value.map(val => encodeURIComponent(String(val))).join(','); - } else if (typeof value === 'object' && value !== null) { - result['categoryId'] = Object.entries(value).map(([key, val]) => `${encodeURIComponent(key)},${encodeURIComponent(String(val))}`).join(','); - } else { - result['categoryId'] = encodeURIComponent(String(value)); - } - } - - return result; } - /** - * Serialize query parameters according to OpenAPI 2.0/3.x specification - * @returns URLSearchParams object with serialized query parameters - */ - serializeQueryParameters(): URLSearchParams { - const params = new URLSearchParams(); - - // Serialize query parameter: limit (style: form, explode: true) - if (this.limit !== undefined && this.limit !== null) { - const value = this.limit; - if (Array.isArray(value)) { - value.forEach(val => params.append('limit', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('limit', encodeURIComponent(String(value))); - } - } - // Serialize query parameter: offset (style: form, explode: true) - if (this.offset !== undefined && this.offset !== null) { - const value = this.offset; - if (Array.isArray(value)) { - value.forEach(val => params.append('offset', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('offset', encodeURIComponent(String(value))); - } + if (parameters.offset !== undefined && parameters.offset !== null) { + const val = parameters.offset; + if (Array.isArray(val)) { + val.forEach(v => parts.push(`offset=${encodeURIComponent(String(v))}`)); + } else { + parts.push(`offset=${encodeURIComponent(String(parameters.offset))}`); } - // Serialize query parameter: sortBy (style: form, explode: true) - if (this.sortBy !== undefined && this.sortBy !== null) { - const value = this.sortBy; - if (Array.isArray(value)) { - value.forEach(val => params.append('sortBy', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('sortBy', encodeURIComponent(String(value))); - } + } + if (parameters.sortBy !== undefined && parameters.sortBy !== null) { + const val = parameters.sortBy; + if (Array.isArray(val)) { + val.forEach(v => parts.push(`sortBy=${encodeURIComponent(String(v))}`)); + } else { + parts.push(`sortBy=${encodeURIComponent(String(parameters.sortBy))}`); } - // Serialize query parameter: sortOrder (style: form, explode: true) - if (this.sortOrder !== undefined && this.sortOrder !== null) { - const value = this.sortOrder; - if (Array.isArray(value)) { - value.forEach(val => params.append('sortOrder', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('sortOrder', encodeURIComponent(String(value))); - } + } + if (parameters.sortOrder !== undefined && parameters.sortOrder !== null) { + const val = parameters.sortOrder; + if (Array.isArray(val)) { + val.forEach(v => parts.push(`sortOrder=${encodeURIComponent(String(v))}`)); + } else { + parts.push(`sortOrder=${encodeURIComponent(String(parameters.sortOrder))}`); } - // Serialize query parameter: tags (style: form, explode: false) - if (this.tags !== undefined && this.tags !== null) { - const value = this.tags; - if (Array.isArray(value)) { - params.append('tags', value.map(val => encodeURIComponent(String(val))).join(',')); - } else if (typeof value === 'object' && value !== null) { - params.append('tags', Object.entries(value).map(([key, val]) => `${encodeURIComponent(key)},${encodeURIComponent(String(val))}`).join(',')); + } + if (parameters.tags !== undefined && parameters.tags !== null) { + const val = parameters.tags; + if (Array.isArray(val)) { + if (val.length === 0) { + parts.push('tags='); } else { - params.append('tags', encodeURIComponent(String(value))); + parts.push(`tags=${val.map(v => encodeURIComponent(String(v))).join(',')}`); } + } else { + parts.push(`tags=${encodeURIComponent(String(parameters.tags))}`); } - // Serialize query parameter: includePetDetails (style: form, explode: true) - if (this.includePetDetails !== undefined && this.includePetDetails !== null) { - const value = this.includePetDetails; - if (Array.isArray(value)) { - value.forEach(val => params.append('includePetDetails', encodeURIComponent(String(val)))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); - } else { - params.append('includePetDetails', encodeURIComponent(String(value))); - } + } + if (parameters.includePetDetails !== undefined && parameters.includePetDetails !== null) { + const val = parameters.includePetDetails; + if (Array.isArray(val)) { + val.forEach(v => parts.push(`includePetDetails=${encodeURIComponent(String(v))}`)); + } else { + parts.push(`includePetDetails=${encodeURIComponent(String(parameters.includePetDetails))}`); } - // Serialize query parameter: format (style: form, explode: true) - if (this.format !== undefined && this.format !== null) { - const value = this.format; - if (Array.isArray(value)) { - value.forEach(val => params.append('format', String(val))); - } else if (typeof value === 'object' && value !== null) { - Object.entries(value).forEach(([key, val]) => params.append(key, String(val))); - } else { - params.append('format', String(value)); - } + } + if (parameters.format !== undefined && parameters.format !== null) { + const val = parameters.format; + if (Array.isArray(val)) { + val.forEach(v => parts.push(`format=${String(v)}`)); + } else { + parts.push(`format=${String(parameters.format)}`); } - - return params; } - /** - * Get the complete serialized URL with path and query parameters - * @param basePath The base path template (e.g., '/users/{id}') - * @returns The complete URL with serialized parameters - */ - serializeUrl(basePath: string): string { - let url = basePath; - // Replace path parameters - - const pathParams = this.serializePathParameters(); - for (const [name, value] of Object.entries(pathParams)) { - url = url.replace(new RegExp(`{${name}}`, 'g'), value); - } + const queryString = parts.join('&'); + if (queryString) { + url += `?${queryString}`; + } - // Add query parameters - - const queryParams = this.serializeQueryParameters(); - const queryString = queryParams.toString(); - if (queryString) { - url += (url.includes('?') ? '&' : '?') + queryString; - } + return url; +} - return url; +export function parseFindPetsByStatusAndCategoryParametersFromUrl(url: string, basePath: string): FindPetsByStatusAndCategoryParameters { + const urlPath = url.indexOf('?') !== -1 ? url.slice(0, url.indexOf('?')) : url; + + const regexPattern = basePath.replace(/\{([^}]+)\}/g, '([^/?]+)'); + const regex = new RegExp('^' + regexPattern + '$'); + const match = urlPath.match(regex); + + if (!match) { + throw new Error(`URL path '${urlPath}' does not match base path template '${basePath}'`); } - /** - * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns The path with parameters replaced - */ - getChannelWithParameters(basePath: string): string { - return this.serializeUrl(basePath); + const paramNames = basePath.match(/\{([^}]+)\}/g)?.map(p => p.slice(1, -1)) || []; + const pathValues: Record = {}; + paramNames.forEach((name, index) => { + pathValues[name] = decodeURIComponent(match[index + 1]); + }); + + + if (pathValues['status'] === undefined) { + throw new Error(`Required parameter 'status' is missing from URL`); } - /** - * Deserialize URL and populate instance properties from query parameters - * @param url The URL to parse (can be full URL or just query string) - */ - deserializeUrl(url: string): void { - // Extract query string from URL - let queryString = ''; - if (url.includes('?')) { - queryString = url.split('?')[1]; - } else if (url.includes('=')) { - // Assume it's already a query string - queryString = url; - } + if (pathValues['categoryId'] === undefined) { + throw new Error(`Required parameter 'categoryId' is missing from URL`); + } + const result: FindPetsByStatusAndCategoryParameters = { + status: pathValues['status'] as "available" | "pending" | "sold", + categoryId: Number(pathValues['categoryId']) + }; - if (!queryString) { - return; - } + const qIdx = url.indexOf('?'); + if (qIdx === -1) { + return result; + } - const params = new URLSearchParams(queryString); + const queryString = url.slice(qIdx + 1); + if (!queryString) { + return result; + } - // Deserialize query parameter: limit (style: form, explode: true) - if (params.has('limit')) { - const value = params.get('limit'); - if (value) { - const decodedValue = decodeURIComponent(value); - const numValue = Number(decodedValue); - if (!isNaN(numValue)) { - this.limit = numValue; - } - } - } - // Deserialize query parameter: offset (style: form, explode: true) - if (params.has('offset')) { - const value = params.get('offset'); - if (value) { - const decodedValue = decodeURIComponent(value); - const numValue = Number(decodedValue); - if (!isNaN(numValue)) { - this.offset = numValue; - } - } - } - // Deserialize query parameter: sortBy (style: form, explode: true) - if (params.has('sortBy')) { - const value = params.get('sortBy'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.sortBy = decodedValue as "name" | "id" | "category" | "status"; + const params = new URLSearchParams(queryString); + + if (params.has('limit')) { + const raw = params.get('limit'); + if (raw !== null) { + const num = Number(raw); + if (!isNaN(num)) { + result.limit = num; } } - // Deserialize query parameter: sortOrder (style: form, explode: true) - if (params.has('sortOrder')) { - const value = params.get('sortOrder'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.sortOrder = decodedValue as "asc" | "desc"; + } + if (params.has('offset')) { + const raw = params.get('offset'); + if (raw !== null) { + const num = Number(raw); + if (!isNaN(num)) { + result.offset = num; } } - // Deserialize query parameter: tags (style: form, explode: false) - if (params.has('tags')) { - const value = params.get('tags'); - if (value === '') { - this.tags = []; - } else if (value) { - // Split by comma and decode - const decodedValues = value.split(',').map(val => decodeURIComponent(val.trim())); - this.tags = decodedValues as string[]; - } + } + if (params.has('sortBy')) { + const raw = params.get('sortBy'); + if (raw !== null) { + result.sortBy = raw as "name" | "id" | "category" | "status"; } - // Deserialize query parameter: includePetDetails (style: form, explode: true) - if (params.has('includePetDetails')) { - const value = params.get('includePetDetails'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.includePetDetails = decodedValue.toLowerCase() === 'true'; - } + } + if (params.has('sortOrder')) { + const raw = params.get('sortOrder'); + if (raw !== null) { + result.sortOrder = raw as "asc" | "desc"; } - // Deserialize query parameter: format (style: form, explode: true) - if (params.has('format')) { - const value = params.get('format'); - if (value) { - const decodedValue = decodeURIComponent(value); - this.format = decodedValue as "json" | "xml" | "csv"; - } + } + if (params.has('tags')) { + const raw = params.get('tags'); + if (raw === '') { + result.tags = []; + } else if (raw !== null) { + result.tags = raw.split(',') as string[]; } } - - /** - * Static method to create a new instance from a URL - * @param url The URL to parse - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - - * @returns A new FindPetsByStatusAndCategoryParameters instance - */ - static fromUrl(url: string, basePath: string): FindPetsByStatusAndCategoryParameters { - // Extract path parameters from URL - const pathParams = this.extractPathParameters(url, basePath); - const instance = new FindPetsByStatusAndCategoryParameters({ status: pathParams.status, categoryId: pathParams.categoryId }); - instance.deserializeUrl(url); - return instance; + if (params.has('includePetDetails')) { + const raw = params.get('includePetDetails'); + if (raw !== null) { + result.includePetDetails = raw.toLowerCase() === 'true'; + } } - - /** - * Extract path parameters from a URL using a base path template - * @param url The URL to extract parameters from - * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') - * @returns Object containing extracted path parameter values - */ - private static extractPathParameters(url: string, basePath: string): { status: "available" | "pending" | "sold", categoryId: number } { - // Remove query string from URL for path matching - const urlPath = url.split('?')[0]; - - // Create regex pattern from base path template - const regexPattern = basePath.replace(/\{([^}]+)\}/g, '([^/]+)'); - const regex = new RegExp('^' + regexPattern + '$'); - - const match = urlPath.match(regex); - if (!match) { - throw new Error(`URL path '${urlPath}' does not match base path template '${basePath}'`); + if (params.has('format')) { + const raw = params.get('format'); + if (raw !== null) { + result.format = raw as "json" | "xml" | "csv"; } - - // Extract parameter names from base path template - const paramNames = basePath.match(/\{([^}]+)\}/g)?.map(p => p.slice(1, -1)) || []; - - // Map matched values to parameter names - const result: any = {}; - paramNames.forEach((paramName, index) => { - const rawValue = match[index + 1]; - const decodeValue = decodeURIComponent(rawValue); - switch (paramName) { - case 'status': - result.status = decodeValue as "available" | "pending" | "sold"; - break; - case 'categoryId': - result.categoryId = Number(decodeValue) as number; - break; - default: - result[paramName] = decodeValue; - } - }); - - return result; } -} -export { FindPetsByStatusAndCategoryParameters }; \ No newline at end of file + + return result; +} \ No newline at end of file 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 0eaee9d3..6da670d9 100644 --- a/test/runtime/typescript/src/request-reply/channels/http_client.ts +++ b/test/runtime/typescript/src/request-reply/channels/http_client.ts @@ -835,17 +835,17 @@ function createPaginationHelpers( } /** - * Builds a URL with path parameters replaced + * Builds a URL with path parameters replaced using a serializer function * @param server - Base server URL * @param pathTemplate - Path template with {param} placeholders - * @param parameters - Parameter object with getChannelWithParameters method + * @param serializeFn - Function that takes the path template and returns the serialized path */ -function buildUrlWithParameters string }>( +function buildUrlWithParameters( server: string, pathTemplate: string, - parameters: T + serializeFn: (path: string) => string ): string { - const path = parameters.getChannelWithParameters(pathTemplate); + const path = serializeFn(pathTemplate); return `${server}${path}`; } @@ -2029,7 +2029,7 @@ async function getGetUserItem(context: GetGetUserItemContext): Promise; // Build URL - let url = buildUrlWithParameters(config.server, '/users/{userId}/items/{itemId}', context.parameters); + let url = buildUrlWithParameters(config.server, '/users/{userId}/items/{itemId}', (path) => context.parameters.getChannelWithParameters(path)); url = applyQueryParams(config.queryParams, url); // Apply pagination (can affect URL and/or headers) @@ -2153,7 +2153,7 @@ async function putUpdateUserItem(context: PutUpdateUserItemContext): Promise; // Build URL - let url = buildUrlWithParameters(config.server, '/users/{userId}/items/{itemId}', context.parameters); + let url = buildUrlWithParameters(config.server, '/users/{userId}/items/{itemId}', (path) => context.parameters.getChannelWithParameters(path)); url = applyQueryParams(config.queryParams, url); // Apply pagination (can affect URL and/or headers) diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/api_auth.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/api_auth.spec.ts index 43f99206..97a8143c 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/api_auth.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/api_auth.spec.ts @@ -29,7 +29,7 @@ describe('HTTP Client - API Key and Basic Authentication', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: API_KEY, @@ -64,7 +64,7 @@ describe('HTTP Client - API Key and Basic Authentication', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: API_KEY, @@ -109,7 +109,7 @@ describe('HTTP Client - API Key and Basic Authentication', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'basic', username: USERNAME, @@ -149,7 +149,7 @@ describe('HTTP Client - API Key and Basic Authentication', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'bearer', token: BEARER_TOKEN @@ -176,7 +176,7 @@ describe('HTTP Client - API Key and Basic Authentication', () => { try { await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: 'wrong-api-key', diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/authentication.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/authentication.spec.ts index 646ed2c3..f42b5391 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/authentication.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/authentication.spec.ts @@ -27,7 +27,7 @@ describe('HTTP Client - Authentication', () => { const auth: AuthConfig = { type: 'bearer', token: 'test-token-123' }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -54,7 +54,7 @@ describe('HTTP Client - Authentication', () => { const auth: AuthConfig = { type: 'basic', username: 'user', password: 'pass' }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -82,7 +82,7 @@ describe('HTTP Client - Authentication', () => { const auth: AuthConfig = { type: 'apiKey', key: 'my-api-key-123' }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -112,7 +112,7 @@ describe('HTTP Client - Authentication', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -142,7 +142,7 @@ describe('HTTP Client - Authentication', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -167,14 +167,14 @@ describe('HTTP Client - Authentication', () => { return runWithServer(app, port, async (_server, actualPort) => { await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: 'secret-key', name: 'api_key', in: 'query' }, - queryParams: { + additionalQueryParams: { filter: 'active' } }); @@ -206,7 +206,7 @@ describe('HTTP Client - Authentication', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -235,7 +235,7 @@ describe('HTTP Client - Authentication', () => { return runWithServer(app, port, async (_server, actualPort) => { await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'bearer', token: 'my-token' }, additionalHeaders: { 'X-Custom-Header': 'custom-value', diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/basics.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/basics.spec.ts index 900ab24a..5bb6efc9 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/basics.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/basics.spec.ts @@ -27,7 +27,7 @@ describe('HTTP Client - Basics', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` }); expect(response.data).toBeDefined(); @@ -39,33 +39,6 @@ describe('HTTP Client - Basics', () => { expect(response.rawData).toBeDefined(); }); }); - - it('should include pagination info from response headers', async () => { - const { app, router, port } = createTestServer(); - - const replyMessage = new Pong({ additionalProperties: new Map([['page', 1]]) }); - - router.get('/ping', (req, res) => { - res.setHeader('Content-Type', 'application/json'); - res.setHeader('X-Total-Count', '100'); - res.setHeader('X-Has-More', 'true'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination: { type: 'offset', offset: 0, limit: 20 } - }); - - expect(response.pagination).toBeDefined(); - expect(response.pagination?.totalCount).toBe(100); - expect(response.pagination?.hasMore).toBe(true); - expect(response.pagination?.currentOffset).toBe(0); - expect(response.pagination?.limit).toBe(20); - }); - }); }); describe('query parameters', () => { @@ -86,8 +59,8 @@ describe('HTTP Client - Basics', () => { return runWithServer(app, port, async (_server, actualPort) => { await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - queryParams: { + baseUrl: `http://localhost:${actualPort}`, + additionalQueryParams: { filter: 'active', sort: 'name' } @@ -109,7 +82,7 @@ describe('HTTP Client - Basics', () => { return runWithServer(app, port, async (_server, actualPort) => { await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` })).rejects.toThrow('Unauthorized'); }); }); @@ -123,7 +96,7 @@ describe('HTTP Client - Basics', () => { return runWithServer(app, port, async (_server, actualPort) => { await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` })).rejects.toThrow('Forbidden'); }); }); @@ -137,7 +110,7 @@ describe('HTTP Client - Basics', () => { return runWithServer(app, port, async (_server, actualPort) => { await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` })).rejects.toThrow('Not Found'); }); }); @@ -151,7 +124,7 @@ describe('HTTP Client - Basics', () => { return runWithServer(app, port, async (_server, actualPort) => { await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` })).rejects.toThrow('Internal Server Error'); }); }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/hooks.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/hooks.spec.ts index 60d094ae..3864aad5 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/hooks.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/hooks.spec.ts @@ -36,7 +36,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); @@ -72,7 +72,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); @@ -106,7 +106,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); @@ -136,7 +136,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); @@ -169,7 +169,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); @@ -209,7 +209,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); @@ -243,7 +243,7 @@ describe('HTTP Client - Hooks', () => { try { await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); } catch (error) { @@ -271,7 +271,7 @@ describe('HTTP Client - Hooks', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks })).rejects.toThrow(/Request to.*failed/); }); @@ -297,7 +297,7 @@ describe('HTTP Client - Hooks', () => { try { await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); } catch (error) { @@ -336,7 +336,7 @@ describe('HTTP Client - Hooks', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); @@ -381,7 +381,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks }); @@ -426,7 +426,7 @@ describe('HTTP Client - Hooks', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, hooks, retry }); @@ -436,44 +436,5 @@ describe('HTTP Client - Hooks', () => { expect(requestCount).toBe(2); }); }); - - it('should work with hooks and pagination', async () => { - const { app, router, port } = createTestServer(); - - const hookCalls: { method: string; offset?: string }[] = []; - - router.get('/ping', (req, res) => { - const replyMessage = new Pong({}); - res.setHeader('Content-Type', 'application/json'); - res.setHeader('X-Total-Count', '60'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const hooks: HttpHooks = { - beforeRequest: (params) => { - const url = new URL(params.url); - hookCalls.push({ - method: params.method, - offset: url.searchParams.get('offset') || undefined - }); - return params; - } - }; - - const page1 = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - hooks, - pagination: { type: 'offset', offset: 0, limit: 20 } - }); - - await page1.getNextPage!(); - - expect(hookCalls.length).toBe(2); - expect(hookCalls[0].offset).toBe('0'); - expect(hookCalls[1].offset).toBe('20'); - }); - }); }); }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/methods.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/methods.spec.ts index dc5a2f87..9a061a9a 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/methods.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/methods.spec.ts @@ -31,7 +31,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` }); expect(receivedMethod).toBe('GET'); @@ -60,7 +60,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, payload: requestMessage }); @@ -91,7 +91,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await putPingPutRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, payload: requestMessage }); @@ -118,7 +118,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { await putPingPutRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, payload: requestMessage, auth: { type: 'bearer', token: 'put-token' } }); @@ -143,9 +143,9 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { await putPingPutRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, payload: requestMessage, - queryParams: { version: '2' } + additionalQueryParams: { version: '2' } }); expect(receivedQuery.version).toBe('2'); @@ -169,7 +169,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await deletePingDeleteRequest({ - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` }); expect(receivedMethod).toBe('DELETE'); @@ -193,7 +193,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { await deletePingDeleteRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'bearer', token: 'delete-token' } }); @@ -221,7 +221,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await patchPingPatchRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, payload: requestMessage }); @@ -231,33 +231,6 @@ describe('HTTP Client - HTTP Methods', () => { expect(response.data).toBeDefined(); }); }); - - it('should support PATCH with pagination', async () => { - const { app, router, port } = createTestServer(); - - const requestMessage = new Ping({}); - const replyMessage = new Pong({}); - let receivedOffset: string | undefined; - - router.patch('/ping', (req, res) => { - receivedOffset = req.query.offset as string; - res.setHeader('Content-Type', 'application/json'); - res.setHeader('X-Total-Count', '50'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const response = await patchPingPatchRequest({ - server: `http://localhost:${actualPort}`, - payload: requestMessage, - pagination: { type: 'offset', offset: 10, limit: 5 } - }); - - expect(receivedOffset).toBe('10'); - expect(response.pagination).toBeDefined(); - }); - }); }); describe('HEAD method', () => { @@ -278,7 +251,7 @@ describe('HTTP Client - HTTP Methods', () => { // This test verifies the method is sent correctly even if parsing fails try { await headPingHeadRequest({ - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` }); } catch (error) { // Expected - HEAD responses have no body to parse @@ -304,7 +277,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { try { await headPingHeadRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: 'head-key' } }); } catch (error) { @@ -334,7 +307,7 @@ describe('HTTP Client - HTTP Methods', () => { // OPTIONS requests typically return no body try { await optionsPingOptionsRequest({ - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` }); } catch (error) { // Expected - OPTIONS responses typically have no body @@ -357,7 +330,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { await expect(putPingPutRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, payload: requestMessage })).rejects.toThrow('Not Found'); }); @@ -372,7 +345,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { await expect(deletePingDeleteRequest({ - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` })).rejects.toThrow('Internal Server Error'); }); }); @@ -388,7 +361,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { await expect(patchPingPatchRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, payload: requestMessage })).rejects.toThrow('Forbidden'); }); @@ -416,7 +389,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await putPingPutRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, payload: requestMessage, retry: { maxRetries: 3, initialDelayMs: 50, retryableStatusCodes: [503] } }); @@ -445,7 +418,7 @@ describe('HTTP Client - HTTP Methods', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await deletePingDeleteRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry: { maxRetries: 3, initialDelayMs: 50, retryableStatusCodes: [502] } }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2.spec.ts index 16b448e4..dfcae0b1 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2.spec.ts @@ -50,7 +50,7 @@ describe('HTTP Client - OAuth2', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -76,7 +76,7 @@ describe('HTTP Client - OAuth2', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth })).rejects.toThrow('OAuth2 Client Credentials flow requires tokenUrl'); }); @@ -120,7 +120,7 @@ describe('HTTP Client - OAuth2', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -147,7 +147,7 @@ describe('HTTP Client - OAuth2', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth })).rejects.toThrow('OAuth2 Password flow requires username'); }); @@ -194,7 +194,7 @@ describe('HTTP Client - OAuth2', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -262,7 +262,7 @@ describe('HTTP Client - OAuth2', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth, retry }); @@ -324,7 +324,7 @@ describe('HTTP Client - OAuth2', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth, retry }); @@ -383,7 +383,7 @@ describe('HTTP Client - OAuth2', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth, retry }); @@ -438,7 +438,7 @@ describe('HTTP Client - OAuth2', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth, retry })).rejects.toThrow(); @@ -494,7 +494,7 @@ describe('HTTP Client - OAuth2', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth, retry }); @@ -538,7 +538,7 @@ describe('HTTP Client - OAuth2', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -571,7 +571,7 @@ describe('HTTP Client - OAuth2', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -607,7 +607,7 @@ describe('HTTP Client - OAuth2', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); @@ -654,7 +654,7 @@ describe('HTTP Client - OAuth2', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_client_credentials.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_client_credentials.spec.ts index feae72fb..e71dddbf 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_client_credentials.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_client_credentials.spec.ts @@ -75,7 +75,7 @@ describe('HTTP Client - OAuth2 Client Credentials Flow', () => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', flow: 'client_credentials', @@ -116,7 +116,7 @@ describe('HTTP Client - OAuth2 Client Credentials Flow', () => { try { await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', flow: 'client_credentials', @@ -139,7 +139,7 @@ describe('HTTP Client - OAuth2 Client Credentials Flow', () => { // Using as any to bypass TypeScript's type checking for this test await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + baseUrl: `http://localhost:${port}`, auth: { type: 'oauth2', flow: 'client_credentials', @@ -216,7 +216,7 @@ describe('HTTP Client - OAuth2 Client Credentials Flow', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', flow: 'client_credentials', diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_implicit_flow.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_implicit_flow.spec.ts index 4a836c8f..287cbe16 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_implicit_flow.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_implicit_flow.spec.ts @@ -63,7 +63,7 @@ describe('HTTP Client - OAuth2 Pre-obtained Access Token', () => { // (implicit, authorization_code via PKCE, etc.) const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', accessToken: ACCESS_TOKEN @@ -126,7 +126,7 @@ describe('HTTP Client - OAuth2 Pre-obtained Access Token', () => { // Use pre-obtained token with refresh capability const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', accessToken: EXPIRED_ACCESS_TOKEN, @@ -164,7 +164,7 @@ describe('HTTP Client - OAuth2 Pre-obtained Access Token', () => { // Simplest OAuth2 usage - just pass the access token const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', accessToken: ACCESS_TOKEN @@ -194,7 +194,7 @@ describe('HTTP Client - OAuth2 Pre-obtained Access Token', () => { // OAuth2 config without access token and no server-side flow await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2' // No accessToken, no flow - should make request without auth header diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_password_flow.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_password_flow.spec.ts index 623f1672..dc86581d 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_password_flow.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_password_flow.spec.ts @@ -75,7 +75,7 @@ describe('HTTP Client - OAuth2 Password Flow', () => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', flow: 'password', @@ -121,7 +121,7 @@ describe('HTTP Client - OAuth2 Password Flow', () => { try { await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', flow: 'password', @@ -148,7 +148,7 @@ describe('HTTP Client - OAuth2 Password Flow', () => { // Using as any to bypass TypeScript's type checking for this test await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${port}`, + baseUrl: `http://localhost:${port}`, auth: { type: 'oauth2', flow: 'password', @@ -214,7 +214,7 @@ describe('HTTP Client - OAuth2 Password Flow', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', flow: 'password', diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_refresh_token.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_refresh_token.spec.ts index bcd2a6c8..ee2b9183 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_refresh_token.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/oauth2_refresh_token.spec.ts @@ -90,7 +90,7 @@ describe('HTTP Client - OAuth2 Refresh Token Flow', () => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', clientId: CLIENT_ID, @@ -142,7 +142,7 @@ describe('HTTP Client - OAuth2 Refresh Token Flow', () => { try { await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', clientId: CLIENT_ID, @@ -176,7 +176,7 @@ describe('HTTP Client - OAuth2 Refresh Token Flow', () => { try { await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', accessToken: EXPIRED_ACCESS_TOKEN, @@ -242,7 +242,7 @@ describe('HTTP Client - OAuth2 Refresh Token Flow', () => { const response = await postPingPostRequest({ payload: requestMessage, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', clientId: CLIENT_ID, diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/openapi.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/openapi.spec.ts index 972bcdd4..bbec06ec 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/openapi.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/openapi.spec.ts @@ -29,7 +29,7 @@ describe('HTTP Client - OpenAPI Generated', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await addPet({ payload: requestPet, - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` }); expect(response.data).toBeDefined(); @@ -58,7 +58,7 @@ describe('HTTP Client - OpenAPI Generated', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await updatePet({ payload: requestPet, - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` }); expect(receivedMethod).toBe('PUT'); @@ -83,14 +83,14 @@ describe('HTTP Client - OpenAPI Generated', () => { }); return runWithServer(app, port, async (_server, actualPort) => { - const params = new FindPetsByStatusAndCategoryParameters({ + const params: FindPetsByStatusAndCategoryParameters = { status: 'available', categoryId: 123 - }); + }; const response = await findPetsByStatusAndCategory({ parameters: params, - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` }); expect(receivedPath).toContain('available'); @@ -112,7 +112,7 @@ describe('HTTP Client - OpenAPI Generated', () => { const pet = new APet({ name: 'Test', photoUrls: [] }); await expect(addPet({ payload: pet, - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` })).rejects.toThrow(); }); }); @@ -125,14 +125,14 @@ describe('HTTP Client - OpenAPI Generated', () => { }); return runWithServer(app, port, async (_server, actualPort) => { - const params = new FindPetsByStatusAndCategoryParameters({ - status: 'invalid', + const params: FindPetsByStatusAndCategoryParameters = { + status: 'invalid' as any, categoryId: 999 - }); + }; await expect(findPetsByStatusAndCategory({ parameters: params, - server: `http://localhost:${actualPort}` + baseUrl: `http://localhost:${actualPort}` })).rejects.toThrow('Not Found'); }); }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/pagination.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/pagination.spec.ts deleted file mode 100644 index ee74bc9f..00000000 --- a/test/runtime/typescript/test/channels/request_reply/http_client/pagination.spec.ts +++ /dev/null @@ -1,396 +0,0 @@ -/* eslint-disable no-console */ -import { Pong } from "../../../../src/request-reply/payloads/Pong"; -import { createTestServer, runWithServer } from './test-utils'; -import { - getPingGetRequest, - PaginationConfig, -} from '../../../../src/request-reply/channels/http_client'; - -jest.setTimeout(15000); - -describe('HTTP Client - Pagination', () => { - describe('offset pagination', () => { - it('should add offset pagination params to query string', async () => { - const { app, router, port } = createTestServer(); - - const replyMessage = new Pong({}); - let receivedOffset: string | undefined; - let receivedLimit: string | undefined; - - router.get('/ping', (req, res) => { - receivedOffset = req.query.offset as string; - receivedLimit = req.query.limit as string; - res.setHeader('Content-Type', 'application/json'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const pagination: PaginationConfig = { - type: 'offset', - offset: 20, - limit: 10 - }; - - await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination - }); - - expect(receivedOffset).toBe('20'); - expect(receivedLimit).toBe('10'); - }); - }); - - it('should add pagination params to headers when in: header', async () => { - const { app, router, port } = createTestServer(); - - const replyMessage = new Pong({}); - let receivedOffsetHeader: string | undefined; - let receivedLimitHeader: string | undefined; - - router.get('/ping', (req, res) => { - receivedOffsetHeader = req.headers['x-offset'] as string; - receivedLimitHeader = req.headers['x-limit'] as string; - res.setHeader('Content-Type', 'application/json'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const pagination: PaginationConfig = { - type: 'offset', - in: 'header', - offset: 50, - limit: 25 - }; - - await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination - }); - - expect(receivedOffsetHeader).toBe('50'); - expect(receivedLimitHeader).toBe('25'); - }); - }); - - it('should use custom pagination param names', async () => { - const { app, router, port } = createTestServer(); - - const replyMessage = new Pong({}); - let receivedSkip: string | undefined; - let receivedTake: string | undefined; - - router.get('/ping', (req, res) => { - receivedSkip = req.query.skip as string; - receivedTake = req.query.take as string; - res.setHeader('Content-Type', 'application/json'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const pagination: PaginationConfig = { - type: 'offset', - offset: 100, - limit: 50, - offsetParam: 'skip', - limitParam: 'take' - }; - - await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination - }); - - expect(receivedSkip).toBe('100'); - expect(receivedTake).toBe('50'); - }); - }); - }); - - describe('cursor pagination', () => { - it('should add cursor pagination params', async () => { - const { app, router, port } = createTestServer(); - - const replyMessage = new Pong({}); - let receivedCursor: string | undefined; - let receivedLimit: string | undefined; - - router.get('/ping', (req, res) => { - receivedCursor = req.query.cursor as string; - receivedLimit = req.query.limit as string; - res.setHeader('Content-Type', 'application/json'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const pagination: PaginationConfig = { - type: 'cursor', - cursor: 'abc123xyz', - limit: 15 - }; - - await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination - }); - - expect(receivedCursor).toBe('abc123xyz'); - expect(receivedLimit).toBe('15'); - }); - }); - - it('should handle cursor-based pagination with next cursor from headers', async () => { - const { app, router, port } = createTestServer(); - - const cursors: (string | undefined)[] = []; - - router.get('/ping', (req, res) => { - const cursor = req.query.cursor as string | undefined; - cursors.push(cursor); - - const replyMessage = new Pong({}); - res.setHeader('Content-Type', 'application/json'); - - if (!cursor) { - res.setHeader('X-Next-Cursor', 'cursor-page-2'); - } else if (cursor === 'cursor-page-2') { - res.setHeader('X-Next-Cursor', 'cursor-page-3'); - } - - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const page1 = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination: { type: 'cursor', limit: 10 } - }); - - expect(page1.pagination?.nextCursor).toBe('cursor-page-2'); - expect(page1.hasNextPage?.()).toBe(true); - - const page2 = await page1.getNextPage!(); - expect(page2.pagination?.nextCursor).toBe('cursor-page-3'); - - const page3 = await page2.getNextPage!(); - expect(page3.pagination?.nextCursor).toBeUndefined(); - expect(page3.hasNextPage?.()).toBe(false); - - expect(cursors).toEqual([undefined, 'cursor-page-2', 'cursor-page-3']); - }); - }); - }); - - describe('page pagination', () => { - it('should add page-based pagination params', async () => { - const { app, router, port } = createTestServer(); - - const replyMessage = new Pong({}); - let receivedPage: string | undefined; - let receivedPageSize: string | undefined; - - router.get('/ping', (req, res) => { - receivedPage = req.query.page as string; - receivedPageSize = req.query.pageSize as string; - res.setHeader('Content-Type', 'application/json'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const pagination: PaginationConfig = { - type: 'page', - page: 3, - pageSize: 25 - }; - - await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination - }); - - expect(receivedPage).toBe('3'); - expect(receivedPageSize).toBe('25'); - }); - }); - }); - - describe('range pagination', () => { - it('should add Range header for range pagination', async () => { - const { app, router, port } = createTestServer(); - - const replyMessage = new Pong({}); - let receivedRangeHeader: string | undefined; - - router.get('/ping', (req, res) => { - receivedRangeHeader = req.headers['range'] as string; - res.setHeader('Content-Type', 'application/json'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const pagination: PaginationConfig = { - type: 'range', - start: 0, - end: 24, - unit: 'items' - }; - - await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination - }); - - expect(receivedRangeHeader).toBe('items=0-24'); - }); - }); - - it('should handle range pagination for page navigation', async () => { - const { app, router, port } = createTestServer(); - - const ranges: string[] = []; - - router.get('/ping', (req, res) => { - ranges.push(req.headers['range'] as string); - const replyMessage = new Pong({}); - res.setHeader('Content-Type', 'application/json'); - res.setHeader('X-Total-Count', '100'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const page1 = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination: { type: 'range', start: 0, end: 24, unit: 'items' } - }); - - expect(page1.hasNextPage?.()).toBe(true); - - await page1.getNextPage!(); - - expect(ranges).toEqual(['items=0-24', 'items=25-49']); - }); - }); - }); - - describe('pagination helpers', () => { - it('should provide pagination helpers when pagination is configured', async () => { - const { app, router, port } = createTestServer(); - - const replyMessage = new Pong({}); - - router.get('/ping', (req, res) => { - res.setHeader('Content-Type', 'application/json'); - res.setHeader('X-Total-Count', '100'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination: { type: 'offset', offset: 0, limit: 20 } - }); - - expect(response.hasNextPage).toBeDefined(); - expect(response.hasPrevPage).toBeDefined(); - expect(response.getNextPage).toBeDefined(); - expect(response.getPrevPage).toBeDefined(); - expect(response.hasNextPage?.()).toBe(true); - expect(response.hasPrevPage?.()).toBe(false); - }); - }); - - it('should navigate through pages with getNextPage', async () => { - const { app, router, port } = createTestServer(); - - let requestCount = 0; - const offsets: string[] = []; - - router.get('/ping', (req, res) => { - requestCount++; - offsets.push(req.query.offset as string); - const replyMessage = new Pong({ additionalProperties: new Map([['page', requestCount]]) }); - res.setHeader('Content-Type', 'application/json'); - res.setHeader('X-Total-Count', '100'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const page1 = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination: { type: 'offset', offset: 0, limit: 20 } - }); - - expect(page1.hasNextPage?.()).toBe(true); - - const page2 = await page1.getNextPage!(); - expect(page2.pagination?.currentOffset).toBe(20); - - const page3 = await page2.getNextPage!(); - expect(page3.pagination?.currentOffset).toBe(40); - - expect(requestCount).toBe(3); - expect(offsets).toEqual(['0', '20', '40']); - }); - }); - - it('should navigate backwards with getPrevPage', async () => { - const { app, router, port } = createTestServer(); - - const offsets: string[] = []; - - router.get('/ping', (req, res) => { - offsets.push(req.query.offset as string); - const replyMessage = new Pong({}); - res.setHeader('Content-Type', 'application/json'); - res.setHeader('X-Total-Count', '100'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const page = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination: { type: 'offset', offset: 60, limit: 20 } - }); - - expect(page.hasPrevPage?.()).toBe(true); - - const prevPage = await page.getPrevPage!(); - expect(prevPage.pagination?.currentOffset).toBe(40); - - expect(offsets).toEqual(['60', '40']); - }); - }); - - it('should parse Link header for pagination info', async () => { - const { app, router, port } = createTestServer(); - - router.get('/ping', (req, res) => { - const replyMessage = new Pong({}); - res.setHeader('Content-Type', 'application/json'); - res.setHeader('Link', '; rel="next", ; rel="last"'); - res.write(replyMessage.marshal()); - res.end(); - }); - - return runWithServer(app, port, async (_server, actualPort) => { - const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, - pagination: { type: 'page', page: 1, pageSize: 20 } - }); - - expect(response.pagination?.hasMore).toBe(true); - }); - }); - }); -}); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts index 57ccf4a7..2f0091b3 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts @@ -34,7 +34,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); const response = await getGetUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters }); @@ -63,12 +63,12 @@ describe('HTTP Client - Parameters and Headers', () => { return runWithServer(app, port, async (_server, actualPort) => { await getGetUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'alice', itemId: '100' }) }); await getGetUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'bob', itemId: '200' }) }); @@ -98,7 +98,7 @@ describe('HTTP Client - Parameters and Headers', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await getGetUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'secure-user', itemId: '999' }), auth: { type: 'bearer', token: 'secret-token' } }); @@ -130,9 +130,9 @@ describe('HTTP Client - Parameters and Headers', () => { return runWithServer(app, port, async (_server, actualPort) => { await getGetUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'user1', itemId: '42' }), - queryParams: { + additionalQueryParams: { include: 'metadata', fields: 'name,quantity' } @@ -177,7 +177,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); const response = await putUpdateUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'user-1', itemId: '100' }), payload, requestHeaders: headers @@ -216,7 +216,7 @@ describe('HTTP Client - Parameters and Headers', () => { }); const response = await putUpdateUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'u1', itemId: '1' }), payload, requestHeaders: headers @@ -247,7 +247,7 @@ describe('HTTP Client - Parameters and Headers', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await putUpdateUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'u', itemId: '1' }), payload: new ItemRequest({ name: 'Item' }), requestHeaders: new ItemRequestHeaders({ xCorrelationId: 'corr-id' }), @@ -281,7 +281,7 @@ describe('HTTP Client - Parameters and Headers', () => { return runWithServer(app, port, async (_server, actualPort) => { await putUpdateUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'u', itemId: '1' }), payload: new ItemRequest({ name: 'Secure Item' }), requestHeaders: new ItemRequestHeaders({ xCorrelationId: 'secure-corr' }), @@ -328,7 +328,7 @@ describe('HTTP Client - Parameters and Headers', () => { return runWithServer(app, port, async (_server, actualPort) => { const response = await putUpdateUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters: new UserItemsParameters({ userId: 'full-user', itemId: '999' }), payload: new ItemRequest({ name: 'Complete Item', @@ -340,7 +340,7 @@ describe('HTTP Client - Parameters and Headers', () => { xRequestId: 'full-req-id' }), auth: { type: 'basic', username: 'admin', password: 'secret' }, - queryParams: { verbose: 'true' } + additionalQueryParams: { verbose: 'true' } }); expect(receivedData).toBeDefined(); @@ -372,7 +372,7 @@ describe('HTTP Client - Parameters and Headers', () => { try { const response = await getGetUserItem({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, parameters }); expect(response.status).toBe(404); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/primitive-response.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/primitive-response.spec.ts index 9ad51ebc..18145c4a 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/primitive-response.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/primitive-response.spec.ts @@ -23,7 +23,7 @@ describe('HTTP Client - primitive response payloads', () => { }); return runWithServer(app, port, async (_server, actualPort) => { - const response = await getEcho({ server: `http://localhost:${actualPort}` }); + const response = await getEcho({ baseUrl: `http://localhost:${actualPort}` }); expect(response.status).toBe(200); expect(response.data).toBe('hello world'); @@ -39,7 +39,7 @@ describe('HTTP Client - primitive response payloads', () => { }); return runWithServer(app, port, async (_server, actualPort) => { - const response = await getCount({ server: `http://localhost:${actualPort}` }); + const response = await getCount({ baseUrl: `http://localhost:${actualPort}` }); expect(response.status).toBe(200); expect(response.data).toBe(42); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/retry.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/retry.spec.ts index 586a07c3..083d0c8b 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/retry.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/retry.spec.ts @@ -35,7 +35,7 @@ describe('HTTP Client - Retry Logic', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry }); @@ -73,7 +73,7 @@ describe('HTTP Client - Retry Logic', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry }); @@ -114,7 +114,7 @@ describe('HTTP Client - Retry Logic', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry }); @@ -143,7 +143,7 @@ describe('HTTP Client - Retry Logic', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry })).rejects.toThrow('Internal Server Error'); @@ -183,7 +183,7 @@ describe('HTTP Client - Retry Logic', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry }); @@ -225,7 +225,7 @@ describe('HTTP Client - Retry Logic', () => { }; await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry }); @@ -254,7 +254,7 @@ describe('HTTP Client - Retry Logic', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry })).rejects.toThrow(); @@ -286,7 +286,7 @@ describe('HTTP Client - Retry Logic', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry }); @@ -313,7 +313,7 @@ describe('HTTP Client - Retry Logic', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry })).rejects.toThrow('Internal Server Error'); @@ -345,7 +345,7 @@ describe('HTTP Client - Retry Logic', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry }); @@ -378,7 +378,7 @@ describe('HTTP Client - Retry Logic', () => { }; const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry }); @@ -394,7 +394,7 @@ describe('HTTP Client - Retry Logic', () => { const unusedPort = 59999; await expect(getPingGetRequest({ - server: `http://localhost:${unusedPort}` + baseUrl: `http://localhost:${unusedPort}` })).rejects.toThrow(); }); @@ -411,7 +411,7 @@ describe('HTTP Client - Retry Logic', () => { }; await expect(getPingGetRequest({ - server: `http://localhost:${unusedPort}`, + baseUrl: `http://localhost:${unusedPort}`, retry })).rejects.toThrow(); @@ -440,7 +440,7 @@ describe('HTTP Client - Retry Logic', () => { // Request should succeed on first try const response = await getPingGetRequest({ - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, retry }); diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts index 65993115..b6a90bd7 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/security_schemes.spec.ts @@ -84,7 +84,7 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { // from the spec are documented in the generated interface await addPet({ payload: requestPet, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: 'my-secret-api-key' @@ -123,7 +123,7 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { // Use the header name from the spec await addPet({ payload: requestPet, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'apiKey', key: 'my-secret-api-key', @@ -162,7 +162,7 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { // With a pre-obtained token, oauth2 works const response = await addPet({ payload: requestPet, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', accessToken: 'pre-obtained-token' @@ -198,7 +198,7 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { // The generated interface documents these in the JSDoc await addPet({ payload: requestPet, - server: `http://localhost:${actualPort}`, + baseUrl: `http://localhost:${actualPort}`, auth: { type: 'oauth2', accessToken: 'token', @@ -219,13 +219,13 @@ describe('HTTP Client - Security Schemes from OpenAPI', () => { // // await addPet({ // payload: requestPet, - // server: `http://localhost:${actualPort}`, + // baseUrl: `http://localhost:${actualPort}`, // auth: { type: 'basic', username: 'user', password: 'pass' } // TypeScript Error! // }); // // await addPet({ // payload: requestPet, - // server: `http://localhost:${actualPort}`, + // baseUrl: `http://localhost:${actualPort}`, // auth: { type: 'bearer', token: 'token' } // TypeScript Error! // }); diff --git a/test/runtime/typescript/test/parameters.spec.ts b/test/runtime/typescript/test/parameters.spec.ts index 259a96ef..7b3df65f 100644 --- a/test/runtime/typescript/test/parameters.spec.ts +++ b/test/runtime/typescript/test/parameters.spec.ts @@ -1,5 +1,5 @@ import { UserSignedupParameters } from '../src/parameters/UserSignedupParameters'; -import { FindPetsByStatusAndCategoryParameters } from '../src/openapi/parameters/FindPetsByStatusAndCategoryParameters'; +import { FindPetsByStatusAndCategoryParameters, serializeFindPetsByStatusAndCategoryParametersUrl, parseFindPetsByStatusAndCategoryParametersFromUrl } from '../src/openapi/parameters/FindPetsByStatusAndCategoryParameters'; describe('parameters', () => { const testObject = new UserSignedupParameters({ myParameter: 'parameter', @@ -17,26 +17,24 @@ describe('parameters', () => { describe('openapi', () => { describe('FindPetsByStatusAndCategoryParameters', () => { test('should roundtrip serialize and deserialize with only required path parameters', () => { - const original = new FindPetsByStatusAndCategoryParameters({ + const original: FindPetsByStatusAndCategoryParameters = { status: 'available', categoryId: 123 - }); + }; const basePath = '/pet/findByStatus/{status}/{categoryId}'; - const serializedUrl = original.serializeUrl(basePath); - - // Should have path parameters substituted but no query parameters + const serializedUrl = serializeFindPetsByStatusAndCategoryParametersUrl(original, basePath); + expect(serializedUrl).toEqual('/pet/findByStatus/available/123'); - const deserialized = FindPetsByStatusAndCategoryParameters.fromUrl(serializedUrl, basePath); - - // Path parameters are extracted from URL in deserialization + const deserialized = parseFindPetsByStatusAndCategoryParametersFromUrl(serializedUrl, basePath); + expect(deserialized.status).toEqual(original.status); expect(deserialized.categoryId).toEqual(original.categoryId); }); test('should roundtrip serialize and deserialize with all parameters', () => { - const original = new FindPetsByStatusAndCategoryParameters({ + const original: FindPetsByStatusAndCategoryParameters = { status: 'pending', categoryId: 456, limit: 50, @@ -46,28 +44,24 @@ describe('parameters', () => { tags: ['friendly', 'small'], includePetDetails: true, format: 'json' - }); + }; const basePath = '/pet/findByStatus/{status}/{categoryId}'; - const serializedUrl = original.serializeUrl(basePath); - - // Should have both path parameters substituted and query parameters + const serializedUrl = serializeFindPetsByStatusAndCategoryParametersUrl(original, basePath); + expect(serializedUrl).toContain('/pet/findByStatus/pending/456'); expect(serializedUrl).toContain('limit=50'); expect(serializedUrl).toContain('offset=10'); expect(serializedUrl).toContain('sortBy=name'); expect(serializedUrl).toContain('sortOrder=desc'); - expect(serializedUrl).toContain('tags=friendly%2Csmall'); // URL encoded comma + expect(serializedUrl).toContain('tags=friendly,small'); expect(serializedUrl).toContain('includePetDetails=true'); expect(serializedUrl).toContain('format=json'); - const deserialized = FindPetsByStatusAndCategoryParameters.fromUrl(serializedUrl, basePath); - - // Path parameters (extracted from URL) + const deserialized = parseFindPetsByStatusAndCategoryParametersFromUrl(serializedUrl, basePath); + expect(deserialized.status).toEqual(original.status); expect(deserialized.categoryId).toEqual(original.categoryId); - - // Query parameters (extracted from URL) expect(deserialized.limit).toEqual(original.limit); expect(deserialized.offset).toEqual(original.offset); expect(deserialized.sortBy).toEqual(original.sortBy); @@ -77,58 +71,24 @@ describe('parameters', () => { expect(deserialized.format).toEqual(original.format); }); - test('should handle path parameter serialization separately from query parameters', () => { - const params = new FindPetsByStatusAndCategoryParameters({ - status: 'sold', - categoryId: 789, - limit: 25 - }); - - // Test path parameter serialization - const pathParams = params.serializePathParameters(); - expect(pathParams).toEqual({ - status: 'sold', - categoryId: '789' - }); - - // Test query parameter serialization - const queryParams = params.serializeQueryParameters(); - expect(queryParams.get('limit')).toEqual('25'); - expect(queryParams.has('status')).toBe(false); // status is path param, not query - expect(queryParams.has('categoryId')).toBe(false); // categoryId is path param, not query - }); - test('should handle empty tags array correctly', () => { - const original = new FindPetsByStatusAndCategoryParameters({ + const original: FindPetsByStatusAndCategoryParameters = { status: 'available', categoryId: 1, tags: [] - }); + }; const basePath = '/pet/findByStatus/{status}/{categoryId}'; - const serializedUrl = original.serializeUrl(basePath); - - // Empty array should serialize as empty string + const serializedUrl = serializeFindPetsByStatusAndCategoryParametersUrl(original, basePath); + expect(serializedUrl).toContain('tags='); - const deserialized = FindPetsByStatusAndCategoryParameters.fromUrl(serializedUrl, basePath); + const deserialized = parseFindPetsByStatusAndCategoryParametersFromUrl(serializedUrl, basePath); expect(deserialized.tags).toEqual([]); }); - test('should handle special characters in path parameters', () => { - // Note: In real scenarios, enum values wouldn't have special chars, but testing encoding - const params = new FindPetsByStatusAndCategoryParameters({ - status: 'available', - categoryId: 999 - }); - - const pathParams = params.serializePathParameters(); - expect(pathParams.status).toEqual('available'); // No encoding needed for this value - expect(pathParams.categoryId).toEqual('999'); - }); - test('should handle undefined optional query parameters', () => { - const original = new FindPetsByStatusAndCategoryParameters({ + const original: FindPetsByStatusAndCategoryParameters = { status: 'available', categoryId: 1, limit: undefined, @@ -138,15 +98,14 @@ describe('parameters', () => { tags: undefined, includePetDetails: undefined, format: undefined - }); + }; const basePath = '/pet/findByStatus/{status}/{categoryId}'; - const serializedUrl = original.serializeUrl(basePath); - - // Should only have path parameters, no query string + const serializedUrl = serializeFindPetsByStatusAndCategoryParametersUrl(original, basePath); + expect(serializedUrl).toEqual('/pet/findByStatus/available/1'); - const deserialized = FindPetsByStatusAndCategoryParameters.fromUrl(serializedUrl, basePath); + const deserialized = parseFindPetsByStatusAndCategoryParametersFromUrl(serializedUrl, basePath); expect(deserialized.limit).toBeUndefined(); expect(deserialized.offset).toBeUndefined(); expect(deserialized.sortBy).toBeUndefined(); @@ -157,21 +116,71 @@ describe('parameters', () => { }); test('should handle boolean parameter serialization correctly', () => { - const original = new FindPetsByStatusAndCategoryParameters({ + const original: FindPetsByStatusAndCategoryParameters = { status: 'available', categoryId: 1, includePetDetails: false - }); - - const queryParams = original.serializeQueryParameters(); - expect(queryParams.get('includePetDetails')).toEqual('false'); + }; const basePath = '/pet/findByStatus/{status}/{categoryId}'; - const serializedUrl = original.serializeUrl(basePath); - const deserialized = FindPetsByStatusAndCategoryParameters.fromUrl(serializedUrl, basePath); - + const serializedUrl = serializeFindPetsByStatusAndCategoryParametersUrl(original, basePath); + expect(serializedUrl).toContain('includePetDetails=false'); + + const deserialized = parseFindPetsByStatusAndCategoryParametersFromUrl(serializedUrl, basePath); expect(deserialized.includePetDetails).toBe(false); }); + + test('should encode and decode special characters in query params without double-encoding', () => { + const original: FindPetsByStatusAndCategoryParameters = { + status: 'available', + categoryId: 1, + tags: ['a&b', 'c=d', 'e f'] + }; + + const basePath = '/pet/findByStatus/{status}/{categoryId}'; + const serializedUrl = serializeFindPetsByStatusAndCategoryParametersUrl(original, basePath); + + const deserialized = parseFindPetsByStatusAndCategoryParametersFromUrl(serializedUrl, basePath); + expect(deserialized.tags).toEqual(original.tags); + }); + + test('should coerce numeric and boolean types on parse', () => { + const original: FindPetsByStatusAndCategoryParameters = { + status: 'available', + categoryId: 42, + limit: 10, + offset: 5, + includePetDetails: true + }; + + const basePath = '/pet/findByStatus/{status}/{categoryId}'; + const serializedUrl = serializeFindPetsByStatusAndCategoryParametersUrl(original, basePath); + const deserialized = parseFindPetsByStatusAndCategoryParametersFromUrl(serializedUrl, basePath); + + expect(typeof deserialized.categoryId).toBe('number'); + expect(typeof deserialized.limit).toBe('number'); + expect(typeof deserialized.offset).toBe('number'); + expect(typeof deserialized.includePetDetails).toBe('boolean'); + }); + + test('should throw when required parameter missing from URL', () => { + const basePath = '/pet/findByStatus/{status}/{categoryId}'; + expect(() => { + parseFindPetsByStatusAndCategoryParametersFromUrl('/pet/findByStatus/available', basePath); + }).toThrow(); + }); + + test('form non-explode array should serialize as comma-joined', () => { + const original: FindPetsByStatusAndCategoryParameters = { + status: 'available', + categoryId: 1, + tags: ['3', '4', '5'] + }; + + const basePath = '/pet/findByStatus/{status}/{categoryId}'; + const serializedUrl = serializeFindPetsByStatusAndCategoryParametersUrl(original, basePath); + expect(serializedUrl).toContain('tags=3,4,5'); + }); }); }) });