diff --git a/docs/protocols/http_client.md b/docs/protocols/http_client.md index 236c67e3..2a5d76f5 100644 --- a/docs/protocols/http_client.md +++ b/docs/protocols/http_client.md @@ -310,6 +310,35 @@ const response = await postPingPostRequest({ }); ``` +## Error Handling + +Non-OK HTTP responses **throw** a typed `HttpError` instead of returning. `HttpError` extends the built-in `Error` and carries the HTTP `status`, `statusText`, and the parsed response `body`: + +```typescript +export class HttpError extends Error { + status: number; + statusText: string; + body?: unknown; // the parsed JSON error body, when present +} +``` + +Consume it with an `instanceof` check: + +```typescript +import { getGetUser, HttpError } from './__gen__/channels/http_client'; + +try { + const response = await getGetUser({ baseUrl: 'https://api.example.com' }); + // response.data is the typed, unmarshalled success payload +} catch (error) { + if (error instanceof HttpError) { + console.error(error.status); // e.g. 404 + console.error(error.statusText); // e.g. 'Not Found' + console.error(error.body); // parsed error body (unknown) + } +} +``` + ## Request/Response Hooks Customize request behavior with hooks: @@ -451,4 +480,4 @@ if (response.status === 200) { } else if (response.status === 404) { console.log('Not found:', response.data); // NotFoundError } -``` \ No newline at end of file +``` diff --git a/examples/openapi-http-client/README.md b/examples/openapi-http-client/README.md index df79dba4..fca69b49 100644 --- a/examples/openapi-http-client/README.md +++ b/examples/openapi-http-client/README.md @@ -38,22 +38,42 @@ import {GetV2ConnectReferenceIdParameters} from './generated/parameter/GetV2Conn // Body: build it with the generated request model, pass as `payload`. const body = new PostV2ConnectRequest({returnUrl: 'https://shop.example/return'}); -const created = await http_client.postV2Connect({server, payload: body}); +const created = await http_client.postV2Connect({baseUrl: server, payload: body}); created.data.connectUrl; // typed // Path parameters: supply them through the generated parameter model. const params = new GetV2ConnectReferenceIdParameters({referenceId: 'ref_123'}); -const connect = await http_client.getV2ConnectReferenceId({server, parameters: params}); +const connect = await http_client.getV2ConnectReferenceId({baseUrl: server, parameters: params}); connect.data.safepayAccountId; // typed // ...or, more ergonomically, pass a plain object — every parameter model also // exports a companion interface, and channels accept `Interface | Class` and // normalize internally. No need to construct the model yourself: -await http_client.getV2ConnectReferenceId({server, parameters: {referenceId: 'ref_123'}}); +await http_client.getV2ConnectReferenceId({baseUrl: server, parameters: {referenceId: 'ref_123'}}); ``` Every function returns `HttpClientResponse` where `data` is the unmarshalled, typed response model, alongside `status`, `headers`, and pagination helpers. +## Error handling + +Non-OK responses **throw** a typed `HttpError` (exported from `generated/http_client.ts`) carrying the `status`, `statusText`, and the parsed response `body`: + +```ts +import {http_client} from './generated'; + +try { + await http_client.getV2ConnectReferenceId({baseUrl: server, parameters: {referenceId: 'missing'}}); +} catch (error) { + if (error instanceof http_client.HttpError) { + error.status; // e.g. 404 + error.statusText; // e.g. 'Not Found' + error.body; // parsed JSON error body (unknown) + } +} +``` + +The explicit `handleHttpError` cases are generated from the **error status codes the OpenAPI document declares** — `safepay-nordic-sample.json` declares `400` and `404`, so those throw with the standard reason-phrase message (`'Bad Request'`, `'Not Found'`). Any status code the document does not declare falls through to a generic `HTTP Error: ` message. `HttpError` still flows through the `onError` hook and the retry logic unchanged. + ## About the generated function names The operations in this document have **no `operationId`**, so names are synthesized from the HTTP method and the path, preserving word boundaries: @@ -84,13 +104,13 @@ Call sites, before and after: ```ts // organization: 'flat' (default) -await http_client.postV2Connect({server, payload}); +await http_client.postV2Connect({baseUrl: server, payload}); // organization: 'tag' — grouped under the operation's OpenAPI tag (tag & leaf verbatim; here the tag is "Connect") -await http_client.Connect.postV2Connect({server, payload}); +await http_client.Connect.postV2Connect({baseUrl: server, payload}); // organization: 'path' — nested by URL path segments, HTTP method as the leaf -await http_client.v2.connect.post({server, payload}); +await http_client.v2.connect.post({baseUrl: server, payload}); ``` See the [channels generator docs](https://the-codegen-project.org/docs/generators/channels) for the full description of each style. diff --git a/examples/openapi-http-client/src/generated/client/SafepayApiV2SampleClient.ts b/examples/openapi-http-client/src/generated/client/SafepayApiV2SampleClient.ts index 02eeaa0f..84b015ca 100644 --- a/examples/openapi-http-client/src/generated/client/SafepayApiV2SampleClient.ts +++ b/examples/openapi-http-client/src/generated/client/SafepayApiV2SampleClient.ts @@ -1,23 +1,21 @@ -import {PostV2ConnectRequest} from './../payload/PostV2ConnectRequest'; -import {PostV2ConnectResponse_200} from './../payload/PostV2ConnectResponse_200'; -import {GetV2ConnectReferenceIdResponse_200} from './../payload/GetV2ConnectReferenceIdResponse_200'; -import {GetV2UsersSafepayAccountIdBankAccountsResponse_200} from './../payload/GetV2UsersSafepayAccountIdBankAccountsResponse_200'; +import {PostV2ConnectRequest, PostV2ConnectRequestInterface} from './../payload/PostV2ConnectRequest'; +import {PostV2ConnectResponse_200, PostV2ConnectResponse_200Interface} from './../payload/PostV2ConnectResponse_200'; +import {GetV2ConnectReferenceIdResponse_200, GetV2ConnectReferenceIdResponse_200Interface} from './../payload/GetV2ConnectReferenceIdResponse_200'; +import {GetV2UsersSafepayAccountIdBankAccountsResponse_200, GetV2UsersSafepayAccountIdBankAccountsResponse_200Interface} from './../payload/GetV2UsersSafepayAccountIdBankAccountsResponse_200'; import {Status} from './../payload/Status'; -import {BankAccountsItem} from './../payload/BankAccountsItem'; -import {InitializeRequest} from './../payload/InitializeRequest'; -import {InitializeModel} from './../payload/InitializeModel'; -import {GetConnectModel} from './../payload/GetConnectModel'; -import {BankAccount} from './../payload/BankAccount'; +import {BankAccount, BankAccountInterface} from './../payload/BankAccount'; +import {InitializeRequest, InitializeRequestInterface} from './../payload/InitializeRequest'; +import {InitializeModel, InitializeModelInterface} from './../payload/InitializeModel'; +import {GetConnectModel, GetConnectModelInterface} from './../payload/GetConnectModel'; export {PostV2ConnectRequest}; export {PostV2ConnectResponse_200}; export {GetV2ConnectReferenceIdResponse_200}; export {GetV2UsersSafepayAccountIdBankAccountsResponse_200}; export {Status}; -export {BankAccountsItem}; +export {BankAccount}; export {InitializeRequest}; export {InitializeModel}; export {GetConnectModel}; -export {BankAccount}; import {GetV2ConnectReferenceIdParameters, GetV2ConnectReferenceIdParametersInterface} from './../parameter/GetV2ConnectReferenceIdParameters'; import {GetV2UsersSafepayAccountIdBankAccountsParameters, GetV2UsersSafepayAccountIdBankAccountsParametersInterface} from './../parameter/GetV2UsersSafepayAccountIdBankAccountsParameters'; export {GetV2ConnectReferenceIdParameters}; @@ -30,7 +28,7 @@ import * as http_client from './../http_client'; * @class SafepayApiV2SampleClient * * A fully-typed HTTP client for the Safepay API V2 (sample). 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 SafepayApiV2SampleClient { diff --git a/examples/openapi-http-client/src/generated/headers/PostV2ConnectHeaders.ts b/examples/openapi-http-client/src/generated/headers/PostV2ConnectHeaders.ts index 3b43ba90..6b5a2ddd 100644 --- a/examples/openapi-http-client/src/generated/headers/PostV2ConnectHeaders.ts +++ b/examples/openapi-http-client/src/generated/headers/PostV2ConnectHeaders.ts @@ -1,61 +1,14 @@ -import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import {default as addFormats} from 'ajv-formats'; -class PostV2ConnectHeaders { - private _xCorrelationId?: string; - - constructor(input: { - xCorrelationId?: string, - }) { - this._xCorrelationId = input.xCorrelationId; - } +interface PostV2ConnectHeaders { /** * Correlation ID used for logging. */ - get xCorrelationId(): string | undefined { return this._xCorrelationId; } - set xCorrelationId(xCorrelationId: string | undefined) { this._xCorrelationId = xCorrelationId; } - - public marshal() : string { - let json = '{' - if(this.xCorrelationId !== undefined) { - json += `"X-Correlation-Id": ${typeof this.xCorrelationId === 'number' || typeof this.xCorrelationId === 'boolean' ? this.xCorrelationId : JSON.stringify(this.xCorrelationId)},`; - } - - //Remove potential last comma - return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; - } - - public static unmarshal(json: string | object): PostV2ConnectHeaders { - const obj = typeof json === "object" ? json : JSON.parse(json); - const instance = new PostV2ConnectHeaders({} as any); - - if (obj["X-Correlation-Id"] !== undefined) { - instance.xCorrelationId = obj["X-Correlation-Id"]; - } - - - return instance; - } - public static theCodeGenSchema = {"type":"object","additionalProperties":false,"properties":{"X-Correlation-Id":{"type":"string","minLength":20,"maxLength":64,"description":"Correlation ID used for logging."}},"$id":"PostV2ConnectHeaders","$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; - } - + xMinusCorrelationMinusId?: string; } -export { PostV2ConnectHeaders }; \ No newline at end of file +export { PostV2ConnectHeaders }; + +export function serializePostV2ConnectHeadersHeaders(headers: PostV2ConnectHeaders): Record { + const result: Record = {}; + if (headers.xMinusCorrelationMinusId !== undefined) { result['X-Correlation-Id'] = String(headers.xMinusCorrelationMinusId); } + return result; +} \ No newline at end of file diff --git a/examples/openapi-http-client/src/generated/http_client.ts b/examples/openapi-http-client/src/generated/http_client.ts index c007a463..cde7f0b1 100644 --- a/examples/openapi-http-client/src/generated/http_client.ts +++ b/examples/openapi-http-client/src/generated/http_client.ts @@ -1,16 +1,15 @@ -import {PostV2ConnectRequest} from './payload/PostV2ConnectRequest'; -import {PostV2ConnectResponse_200} from './payload/PostV2ConnectResponse_200'; -import {GetV2ConnectReferenceIdResponse_200} from './payload/GetV2ConnectReferenceIdResponse_200'; -import {GetV2UsersSafepayAccountIdBankAccountsResponse_200} from './payload/GetV2UsersSafepayAccountIdBankAccountsResponse_200'; +import {PostV2ConnectRequest, PostV2ConnectRequestInterface} from './payload/PostV2ConnectRequest'; +import {PostV2ConnectResponse_200, PostV2ConnectResponse_200Interface} from './payload/PostV2ConnectResponse_200'; +import {GetV2ConnectReferenceIdResponse_200, GetV2ConnectReferenceIdResponse_200Interface} from './payload/GetV2ConnectReferenceIdResponse_200'; +import {GetV2UsersSafepayAccountIdBankAccountsResponse_200, GetV2UsersSafepayAccountIdBankAccountsResponse_200Interface} from './payload/GetV2UsersSafepayAccountIdBankAccountsResponse_200'; import {Status} from './payload/Status'; -import {BankAccountsItem} from './payload/BankAccountsItem'; -import {InitializeRequest} from './payload/InitializeRequest'; -import {InitializeModel} from './payload/InitializeModel'; -import {GetConnectModel} from './payload/GetConnectModel'; -import {BankAccount} from './payload/BankAccount'; +import {BankAccount, BankAccountInterface} from './payload/BankAccount'; +import {InitializeRequest, InitializeRequestInterface} from './payload/InitializeRequest'; +import {InitializeModel, InitializeModelInterface} from './payload/InitializeModel'; +import {GetConnectModel, GetConnectModelInterface} from './payload/GetConnectModel'; import {GetV2ConnectReferenceIdParameters, GetV2ConnectReferenceIdParametersInterface} from './parameter/GetV2ConnectReferenceIdParameters'; import {GetV2UsersSafepayAccountIdBankAccountsParameters, GetV2UsersSafepayAccountIdBankAccountsParametersInterface} from './parameter/GetV2UsersSafepayAccountIdBankAccountsParameters'; -import {PostV2ConnectHeaders} from './headers/PostV2ConnectHeaders'; +import {PostV2ConnectHeaders, serializePostV2ConnectHeadersHeaders} from './headers/PostV2ConnectHeaders'; // ============================================================================ // Common Types - Shared across all HTTP client functions @@ -27,26 +26,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 +40,27 @@ 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; +} + +/** + * Error thrown for non-OK HTTP responses. + * + * Carries the HTTP `status`, `statusText`, and the parsed response `body` + * (when the error response had a JSON body). Thrown by `handleHttpError` and + * routed through the `onError` hook / retry logic unchanged. + */ +export class HttpError extends Error { + status: number; + statusText: string; + body?: unknown; + + constructor(message: string, status: number, statusText: string, body?: unknown) { + super(message); + this.name = 'HttpError'; + this.status = status; + this.statusText = statusText; + this.body = body; + } } /** @@ -181,69 +171,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 // ============================================================================ @@ -301,15 +228,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; @@ -319,8 +242,8 @@ export interface HttpClientContext { // Additional options additionalHeaders?: Record; - // Query parameters - queryParams?: Record; + // Extra query parameters not covered by the typed parameters interface + additionalQueryParams?: Record; } // ============================================================================ @@ -412,94 +335,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 */ @@ -602,20 +437,18 @@ async function executeWithRetry( } /** - * Handle HTTP error status codes with standardized messages + * Handle HTTP error status codes by throwing a typed HttpError. + * Explicit cases are generated from the error status codes declared by the + * input document; undeclared codes fall through to the default handler. */ -function handleHttpError(status: number, statusText: string): never { +function handleHttpError(status: number, statusText: string, body?: unknown): never { switch (status) { - case 401: - throw new Error('Unauthorized'); - case 403: - throw new Error('Forbidden'); + case 400: + throw new HttpError("Bad Request", status, statusText, body); case 404: - throw new Error('Not Found'); - case 500: - throw new Error('Internal Server Error'); + throw new HttpError("Not Found", status, statusText, body); default: - throw new Error(`HTTP Error: ${status} ${statusText}`); + throw new HttpError(`HTTP Error: ${status} ${statusText}`, status, statusText, body); } } @@ -642,199 +475,6 @@ function extractHeaders(response: HttpResponse): Record { return headers; } -/** - * 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 * @param server - Base server URL @@ -1025,8 +665,8 @@ async function handleTokenRefresh( // ============================================================================ export interface PostV2ConnectContext extends HttpClientContext { - payload: PostV2ConnectRequest; - requestHeaders?: { marshal: () => string }; + payload: PostV2ConnectRequestInterface | PostV2ConnectRequest; + requestHeaders?: PostV2ConnectHeaders; } /** @@ -1035,8 +675,7 @@ export interface PostV2ConnectContext extends HttpClientContext { async function postV2Connect(context: PostV2ConnectContext): Promise> { // Apply defaults const config = { - path: '/v2/connect', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1046,18 +685,11 @@ async function postV2Connect(context: PostV2ConnectContext): Promise; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders, ...(context.requestHeaders ? serializePostV2ConnectHeadersHeaders(context.requestHeaders) : {}) } 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}/v2/connect`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1065,7 +697,8 @@ async function postV2Connect(context: PostV2ConnectContext): Promise undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response @@ -1123,17 +757,13 @@ async function postV2Connect(context: PostV2ConnectContext): Promise = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, postV2Connect), }; return result; @@ -1149,7 +779,6 @@ async function postV2Connect(context: PostV2ConnectContext): Promise string }; } /** @@ -1158,8 +787,7 @@ export interface GetV2ConnectReferenceIdContext extends HttpClientContext { async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): Promise> { // Apply defaults const config = { - path: '/v2/connect/{referenceId}', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1171,18 +799,11 @@ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): } // 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 = buildUrlWithParameters(config.server, '/v2/connect/{referenceId}', 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, '/v2/connect/{referenceId}', parameters); + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1239,7 +860,8 @@ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): // Handle error responses if (!response.ok) { - handleHttpError(response.status, response.statusText); + const errorBody = await response.json().catch(() => undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response @@ -1248,17 +870,13 @@ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): // 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, getV2ConnectReferenceId), }; return result; @@ -1274,7 +892,6 @@ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): export interface GetV2UsersSafepayAccountIdBankAccountsContext extends HttpClientContext { parameters: GetV2UsersSafepayAccountIdBankAccountsParametersInterface | GetV2UsersSafepayAccountIdBankAccountsParameters; - requestHeaders?: { marshal: () => string }; } /** @@ -1283,8 +900,7 @@ export interface GetV2UsersSafepayAccountIdBankAccountsContext extends HttpClien async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepayAccountIdBankAccountsContext): Promise> { // Apply defaults const config = { - path: '/v2/users/{safepayAccountId}/bank-accounts', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1296,18 +912,11 @@ async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepay } // 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 = buildUrlWithParameters(config.server, '/v2/users/{safepayAccountId}/bank-accounts', 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, '/v2/users/{safepayAccountId}/bank-accounts', parameters); + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1364,7 +973,8 @@ async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepay // Handle error responses if (!response.ok) { - handleHttpError(response.status, response.statusText); + const errorBody = await response.json().catch(() => undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response @@ -1373,17 +983,13 @@ async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepay // 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, getV2UsersSafepayAccountIdBankAccounts), }; return result; diff --git a/examples/openapi-http-client/src/generated/payload/BankAccount.ts b/examples/openapi-http-client/src/generated/payload/BankAccount.ts index b56919e9..7a5a699f 100644 --- a/examples/openapi-http-client/src/generated/payload/BankAccount.ts +++ b/examples/openapi-http-client/src/generated/payload/BankAccount.ts @@ -1,17 +1,18 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface BankAccountInterface { + id?: string + iban?: string + isDefault?: boolean + additionalProperties?: Record +} class BankAccount { private _id?: string; private _iban?: string; private _isDefault?: boolean; private _additionalProperties?: Record; - constructor(input: { - id?: string, - iban?: string, - isDefault?: boolean, - additionalProperties?: Record, - }) { + constructor(input: BankAccountInterface) { this._id = input.id; this._iban = input.iban; this._isDefault = input.isDefault; @@ -30,50 +31,57 @@ class BankAccount { get additionalProperties(): Record | undefined { return this._additionalProperties; } set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } - public marshal() : string { - let json = '{' + public toJson(): Record { + const json: Record = {}; if(this.id !== undefined) { - json += `"id": ${typeof this.id === 'number' || typeof this.id === 'boolean' ? this.id : JSON.stringify(this.id)},`; + json["id"] = this.id; } if(this.iban !== undefined) { - json += `"iban": ${typeof this.iban === 'number' || typeof this.iban === 'boolean' ? this.iban : JSON.stringify(this.iban)},`; + json["iban"] = this.iban; } if(this.isDefault !== undefined) { - json += `"isDefault": ${typeof this.isDefault === 'number' || typeof this.isDefault === 'boolean' ? this.isDefault : JSON.stringify(this.isDefault)},`; + json["isDefault"] = this.isDefault; } - if(this.additionalProperties !== undefined) { - for (const [key, value] of this.additionalProperties.entries()) { + if(this.additionalProperties !== undefined) { + for (const [key, value] of Object.entries(this.additionalProperties)) { //Only unwrap those that are not already a property in the JSON object if(["id","iban","isDefault","additionalProperties"].includes(String(key))) continue; - json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + json[key] = value; } } - //Remove potential last comma - return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + return json; } - public static unmarshal(json: string | object): BankAccount { - const obj = typeof json === "object" ? json : JSON.parse(json); + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): BankAccount { const instance = new BankAccount({} as any); if (obj["id"] !== undefined) { - instance.id = obj["id"]; + instance.id = obj["id"] as string; } if (obj["iban"] !== undefined) { - instance.iban = obj["iban"]; + instance.iban = obj["iban"] as string; } if (obj["isDefault"] !== undefined) { - instance.isDefault = obj["isDefault"]; + instance.isDefault = obj["isDefault"] as boolean; } - - instance.additionalProperties = new Map(); + + instance.additionalProperties = {}; const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["id","iban","isDefault","additionalProperties"].includes(key);})); for (const [key, value] of propsToCheck) { - instance.additionalProperties.set(key, value as any); + instance.additionalProperties[key] = value as any; } return instance; } - public static theCodeGenSchema = {"type":"object","properties":{"id":{"type":"string"},"iban":{"type":"string"},"isDefault":{"type":"boolean"}},"$id":"BankAccount","$schema":"http://json-schema.org/draft-07/schema"}; + + public static unmarshal(json: string | object): BankAccount { + const obj = typeof json === "object" ? json : JSON.parse(json); + return BankAccount.fromJson(obj as Record); + } + public static theCodeGenSchema = {"type":"object","properties":{"id":{"type":"string"},"iban":{"type":"string"},"isDefault":{"type":"boolean"}}}; 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. @@ -95,4 +103,4 @@ class BankAccount { } } -export { BankAccount }; \ No newline at end of file +export { BankAccount, BankAccountInterface }; \ No newline at end of file diff --git a/examples/openapi-http-client/src/generated/payload/GetConnectModel.ts b/examples/openapi-http-client/src/generated/payload/GetConnectModel.ts index 392f53fa..4b973dd0 100644 --- a/examples/openapi-http-client/src/generated/payload/GetConnectModel.ts +++ b/examples/openapi-http-client/src/generated/payload/GetConnectModel.ts @@ -1,18 +1,19 @@ import {Status} from './Status'; import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface GetConnectModelInterface { + referenceId?: string + safepayAccountId?: string + status?: Status + additionalProperties?: Record +} class GetConnectModel { private _referenceId?: string; private _safepayAccountId?: string; private _status?: Status; private _additionalProperties?: Record; - constructor(input: { - referenceId?: string, - safepayAccountId?: string, - status?: Status, - additionalProperties?: Record, - }) { + constructor(input: GetConnectModelInterface) { this._referenceId = input.referenceId; this._safepayAccountId = input.safepayAccountId; this._status = input.status; @@ -31,49 +32,56 @@ class GetConnectModel { get additionalProperties(): Record | undefined { return this._additionalProperties; } set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } - public marshal() : string { - let json = '{' + public toJson(): Record { + const json: Record = {}; if(this.referenceId !== undefined) { - json += `"referenceId": ${typeof this.referenceId === 'number' || typeof this.referenceId === 'boolean' ? this.referenceId : JSON.stringify(this.referenceId)},`; + json["referenceId"] = this.referenceId; } if(this.safepayAccountId !== undefined) { - json += `"safepayAccountId": ${typeof this.safepayAccountId === 'number' || typeof this.safepayAccountId === 'boolean' ? this.safepayAccountId : JSON.stringify(this.safepayAccountId)},`; + json["safepayAccountId"] = this.safepayAccountId; } if(this.status !== undefined) { - json += `"status": ${typeof this.status === 'number' || typeof this.status === 'boolean' ? this.status : JSON.stringify(this.status)},`; + json["status"] = this.status; } - if(this.additionalProperties !== undefined) { - for (const [key, value] of this.additionalProperties.entries()) { + if(this.additionalProperties !== undefined) { + for (const [key, value] of Object.entries(this.additionalProperties)) { //Only unwrap those that are not already a property in the JSON object if(["referenceId","safepayAccountId","status","additionalProperties"].includes(String(key))) continue; - json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + json[key] = value; } } - //Remove potential last comma - return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + return json; } - public static unmarshal(json: string | object): GetConnectModel { - const obj = typeof json === "object" ? json : JSON.parse(json); + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): GetConnectModel { const instance = new GetConnectModel({} as any); if (obj["referenceId"] !== undefined) { - instance.referenceId = obj["referenceId"]; + instance.referenceId = obj["referenceId"] as string; } if (obj["safepayAccountId"] !== undefined) { - instance.safepayAccountId = obj["safepayAccountId"]; + instance.safepayAccountId = obj["safepayAccountId"] as string; } if (obj["status"] !== undefined) { - instance.status = obj["status"]; + instance.status = obj["status"] as Status; } - - instance.additionalProperties = new Map(); + + instance.additionalProperties = {}; const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["referenceId","safepayAccountId","status","additionalProperties"].includes(key);})); for (const [key, value] of propsToCheck) { - instance.additionalProperties.set(key, value as any); + instance.additionalProperties[key] = value as any; } return instance; } + + public static unmarshal(json: string | object): GetConnectModel { + const obj = typeof json === "object" ? json : JSON.parse(json); + return GetConnectModel.fromJson(obj as Record); + } public static theCodeGenSchema = {"type":"object","properties":{"referenceId":{"type":"string"},"safepayAccountId":{"type":"string"},"status":{"type":"string","enum":["pending","connected","expired"]}},"$id":"GetConnectModel","$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 ?? {}; @@ -96,4 +104,4 @@ class GetConnectModel { } } -export { GetConnectModel }; \ No newline at end of file +export { GetConnectModel, GetConnectModelInterface }; \ No newline at end of file diff --git a/examples/openapi-http-client/src/generated/payload/GetV2ConnectReferenceIdResponse_200.ts b/examples/openapi-http-client/src/generated/payload/GetV2ConnectReferenceIdResponse_200.ts index 01d4e656..f1b9bf12 100644 --- a/examples/openapi-http-client/src/generated/payload/GetV2ConnectReferenceIdResponse_200.ts +++ b/examples/openapi-http-client/src/generated/payload/GetV2ConnectReferenceIdResponse_200.ts @@ -1,18 +1,19 @@ import {Status} from './Status'; import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface GetV2ConnectReferenceIdResponse_200Interface { + referenceId?: string + safepayAccountId?: string + status?: Status + additionalProperties?: Record +} class GetV2ConnectReferenceIdResponse_200 { private _referenceId?: string; private _safepayAccountId?: string; private _status?: Status; private _additionalProperties?: Record; - constructor(input: { - referenceId?: string, - safepayAccountId?: string, - status?: Status, - additionalProperties?: Record, - }) { + constructor(input: GetV2ConnectReferenceIdResponse_200Interface) { this._referenceId = input.referenceId; this._safepayAccountId = input.safepayAccountId; this._status = input.status; @@ -31,49 +32,56 @@ class GetV2ConnectReferenceIdResponse_200 { get additionalProperties(): Record | undefined { return this._additionalProperties; } set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } - public marshal() : string { - let json = '{' + public toJson(): Record { + const json: Record = {}; if(this.referenceId !== undefined) { - json += `"referenceId": ${typeof this.referenceId === 'number' || typeof this.referenceId === 'boolean' ? this.referenceId : JSON.stringify(this.referenceId)},`; + json["referenceId"] = this.referenceId; } if(this.safepayAccountId !== undefined) { - json += `"safepayAccountId": ${typeof this.safepayAccountId === 'number' || typeof this.safepayAccountId === 'boolean' ? this.safepayAccountId : JSON.stringify(this.safepayAccountId)},`; + json["safepayAccountId"] = this.safepayAccountId; } if(this.status !== undefined) { - json += `"status": ${typeof this.status === 'number' || typeof this.status === 'boolean' ? this.status : JSON.stringify(this.status)},`; + json["status"] = this.status; } - if(this.additionalProperties !== undefined) { - for (const [key, value] of this.additionalProperties.entries()) { + if(this.additionalProperties !== undefined) { + for (const [key, value] of Object.entries(this.additionalProperties)) { //Only unwrap those that are not already a property in the JSON object if(["referenceId","safepayAccountId","status","additionalProperties"].includes(String(key))) continue; - json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + json[key] = value; } } - //Remove potential last comma - return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + return json; } - public static unmarshal(json: string | object): GetV2ConnectReferenceIdResponse_200 { - const obj = typeof json === "object" ? json : JSON.parse(json); + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): GetV2ConnectReferenceIdResponse_200 { const instance = new GetV2ConnectReferenceIdResponse_200({} as any); if (obj["referenceId"] !== undefined) { - instance.referenceId = obj["referenceId"]; + instance.referenceId = obj["referenceId"] as string; } if (obj["safepayAccountId"] !== undefined) { - instance.safepayAccountId = obj["safepayAccountId"]; + instance.safepayAccountId = obj["safepayAccountId"] as string; } if (obj["status"] !== undefined) { - instance.status = obj["status"]; + instance.status = obj["status"] as Status; } - - instance.additionalProperties = new Map(); + + instance.additionalProperties = {}; const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["referenceId","safepayAccountId","status","additionalProperties"].includes(key);})); for (const [key, value] of propsToCheck) { - instance.additionalProperties.set(key, value as any); + instance.additionalProperties[key] = value as any; } return instance; } + + public static unmarshal(json: string | object): GetV2ConnectReferenceIdResponse_200 { + const obj = typeof json === "object" ? json : JSON.parse(json); + return GetV2ConnectReferenceIdResponse_200.fromJson(obj as Record); + } public static theCodeGenSchema = {"type":"object","properties":{"referenceId":{"type":"string"},"safepayAccountId":{"type":"string"},"status":{"type":"string","enum":["pending","connected","expired"]}},"$id":"getV2ConnectReferenceId_Response_200","$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 ?? {}; @@ -96,4 +104,4 @@ class GetV2ConnectReferenceIdResponse_200 { } } -export { GetV2ConnectReferenceIdResponse_200 }; \ No newline at end of file +export { GetV2ConnectReferenceIdResponse_200, GetV2ConnectReferenceIdResponse_200Interface }; \ No newline at end of file diff --git a/examples/openapi-http-client/src/generated/payload/GetV2UsersSafepayAccountIdBankAccountsResponse_200.ts b/examples/openapi-http-client/src/generated/payload/GetV2UsersSafepayAccountIdBankAccountsResponse_200.ts index 39d1abf3..80cb4534 100644 --- a/examples/openapi-http-client/src/generated/payload/GetV2UsersSafepayAccountIdBankAccountsResponse_200.ts +++ b/examples/openapi-http-client/src/generated/payload/GetV2UsersSafepayAccountIdBankAccountsResponse_200.ts @@ -1,61 +1,69 @@ -import {BankAccountsItem} from './BankAccountsItem'; +import {BankAccount} from './BankAccount'; import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface GetV2UsersSafepayAccountIdBankAccountsResponse_200Interface { + bankAccounts?: BankAccount[] + additionalProperties?: Record +} class GetV2UsersSafepayAccountIdBankAccountsResponse_200 { - private _bankAccounts?: BankAccountsItem[]; + private _bankAccounts?: BankAccount[]; private _additionalProperties?: Record; - constructor(input: { - bankAccounts?: BankAccountsItem[], - additionalProperties?: Record, - }) { + constructor(input: GetV2UsersSafepayAccountIdBankAccountsResponse_200Interface) { this._bankAccounts = input.bankAccounts; this._additionalProperties = input.additionalProperties; } - get bankAccounts(): BankAccountsItem[] | undefined { return this._bankAccounts; } - set bankAccounts(bankAccounts: BankAccountsItem[] | undefined) { this._bankAccounts = bankAccounts; } + get bankAccounts(): BankAccount[] | undefined { return this._bankAccounts; } + set bankAccounts(bankAccounts: BankAccount[] | undefined) { this._bankAccounts = bankAccounts; } get additionalProperties(): Record | undefined { return this._additionalProperties; } set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } - public marshal() : string { - let json = '{' + public toJson(): Record { + const json: Record = {}; if(this.bankAccounts !== undefined) { - let bankAccountsJsonValues: any[] = []; - for (const unionItem of this.bankAccounts) { - bankAccountsJsonValues.push(`${unionItem && typeof unionItem === 'object' && 'marshal' in unionItem && typeof unionItem.marshal === 'function' ? unionItem.marshal() : JSON.stringify(unionItem)}`); - } - json += `"bankAccounts": [${bankAccountsJsonValues.join(',')}],`; + json["bankAccounts"] = this.bankAccounts.map((item: any) => + item && typeof item === 'object' && 'toJson' in item && typeof item.toJson === 'function' + ? item.toJson() + : item + ); } - if(this.additionalProperties !== undefined) { - for (const [key, value] of this.additionalProperties.entries()) { + if(this.additionalProperties !== undefined) { + for (const [key, value] of Object.entries(this.additionalProperties)) { //Only unwrap those that are not already a property in the JSON object if(["bankAccounts","additionalProperties"].includes(String(key))) continue; - json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + json[key] = value; } } - //Remove potential last comma - return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + return json; } - public static unmarshal(json: string | object): GetV2UsersSafepayAccountIdBankAccountsResponse_200 { - const obj = typeof json === "object" ? json : JSON.parse(json); + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): GetV2UsersSafepayAccountIdBankAccountsResponse_200 { const instance = new GetV2UsersSafepayAccountIdBankAccountsResponse_200({} as any); if (obj["bankAccounts"] !== undefined) { instance.bankAccounts = obj["bankAccounts"] == null ? undefined - : obj["bankAccounts"].map((item: any) => BankAccountsItem.unmarshal(item)); + : (obj["bankAccounts"] as Record[]).map((item: Record) => BankAccount.fromJson(item)); } - - instance.additionalProperties = new Map(); + + instance.additionalProperties = {}; const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["bankAccounts","additionalProperties"].includes(key);})); for (const [key, value] of propsToCheck) { - instance.additionalProperties.set(key, value as any); + instance.additionalProperties[key] = value as any; } return instance; } + + public static unmarshal(json: string | object): GetV2UsersSafepayAccountIdBankAccountsResponse_200 { + const obj = typeof json === "object" ? json : JSON.parse(json); + return GetV2UsersSafepayAccountIdBankAccountsResponse_200.fromJson(obj as Record); + } public static theCodeGenSchema = {"type":"object","properties":{"bankAccounts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"iban":{"type":"string"},"isDefault":{"type":"boolean"}}}}},"$id":"getV2UsersSafepayAccountIdBankAccounts_Response_200","$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 ?? {}; @@ -78,4 +86,4 @@ class GetV2UsersSafepayAccountIdBankAccountsResponse_200 { } } -export { GetV2UsersSafepayAccountIdBankAccountsResponse_200 }; \ No newline at end of file +export { GetV2UsersSafepayAccountIdBankAccountsResponse_200, GetV2UsersSafepayAccountIdBankAccountsResponse_200Interface }; \ No newline at end of file diff --git a/examples/openapi-http-client/src/generated/payload/InitializeModel.ts b/examples/openapi-http-client/src/generated/payload/InitializeModel.ts index 9d12907a..dce7b9c9 100644 --- a/examples/openapi-http-client/src/generated/payload/InitializeModel.ts +++ b/examples/openapi-http-client/src/generated/payload/InitializeModel.ts @@ -1,15 +1,16 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface InitializeModelInterface { + referenceId?: string + connectUrl?: string + additionalProperties?: Record +} class InitializeModel { private _referenceId?: string; private _connectUrl?: string; private _additionalProperties?: Record; - constructor(input: { - referenceId?: string, - connectUrl?: string, - additionalProperties?: Record, - }) { + constructor(input: InitializeModelInterface) { this._referenceId = input.referenceId; this._connectUrl = input.connectUrl; this._additionalProperties = input.additionalProperties; @@ -24,43 +25,50 @@ class InitializeModel { get additionalProperties(): Record | undefined { return this._additionalProperties; } set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } - public marshal() : string { - let json = '{' + public toJson(): Record { + const json: Record = {}; if(this.referenceId !== undefined) { - json += `"referenceId": ${typeof this.referenceId === 'number' || typeof this.referenceId === 'boolean' ? this.referenceId : JSON.stringify(this.referenceId)},`; + json["referenceId"] = this.referenceId; } if(this.connectUrl !== undefined) { - json += `"connectUrl": ${typeof this.connectUrl === 'number' || typeof this.connectUrl === 'boolean' ? this.connectUrl : JSON.stringify(this.connectUrl)},`; + json["connectUrl"] = this.connectUrl; } - if(this.additionalProperties !== undefined) { - for (const [key, value] of this.additionalProperties.entries()) { + if(this.additionalProperties !== undefined) { + for (const [key, value] of Object.entries(this.additionalProperties)) { //Only unwrap those that are not already a property in the JSON object if(["referenceId","connectUrl","additionalProperties"].includes(String(key))) continue; - json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + json[key] = value; } } - //Remove potential last comma - return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + return json; } - public static unmarshal(json: string | object): InitializeModel { - const obj = typeof json === "object" ? json : JSON.parse(json); + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): InitializeModel { const instance = new InitializeModel({} as any); if (obj["referenceId"] !== undefined) { - instance.referenceId = obj["referenceId"]; + instance.referenceId = obj["referenceId"] as string; } if (obj["connectUrl"] !== undefined) { - instance.connectUrl = obj["connectUrl"]; + instance.connectUrl = obj["connectUrl"] as string; } - - instance.additionalProperties = new Map(); + + instance.additionalProperties = {}; const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["referenceId","connectUrl","additionalProperties"].includes(key);})); for (const [key, value] of propsToCheck) { - instance.additionalProperties.set(key, value as any); + instance.additionalProperties[key] = value as any; } return instance; } + + public static unmarshal(json: string | object): InitializeModel { + const obj = typeof json === "object" ? json : JSON.parse(json); + return InitializeModel.fromJson(obj as Record); + } public static theCodeGenSchema = {"type":"object","properties":{"referenceId":{"type":"string"},"connectUrl":{"type":"string","format":"uri"}},"$id":"InitializeModel","$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 ?? {}; @@ -83,4 +91,4 @@ class InitializeModel { } } -export { InitializeModel }; \ No newline at end of file +export { InitializeModel, InitializeModelInterface }; \ No newline at end of file diff --git a/examples/openapi-http-client/src/generated/payload/InitializeRequest.ts b/examples/openapi-http-client/src/generated/payload/InitializeRequest.ts index d4a50a50..af34d425 100644 --- a/examples/openapi-http-client/src/generated/payload/InitializeRequest.ts +++ b/examples/openapi-http-client/src/generated/payload/InitializeRequest.ts @@ -1,15 +1,16 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface InitializeRequestInterface { + returnUrl: string + skipKyc?: boolean + additionalProperties?: Record +} class InitializeRequest { private _returnUrl: string; private _skipKyc?: boolean; private _additionalProperties?: Record; - constructor(input: { - returnUrl: string, - skipKyc?: boolean, - additionalProperties?: Record, - }) { + constructor(input: InitializeRequestInterface) { this._returnUrl = input.returnUrl; this._skipKyc = input.skipKyc; this._additionalProperties = input.additionalProperties; @@ -24,43 +25,50 @@ class InitializeRequest { get additionalProperties(): Record | undefined { return this._additionalProperties; } set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } - public marshal() : string { - let json = '{' + public toJson(): Record { + const json: Record = {}; if(this.returnUrl !== undefined) { - json += `"returnUrl": ${typeof this.returnUrl === 'number' || typeof this.returnUrl === 'boolean' ? this.returnUrl : JSON.stringify(this.returnUrl)},`; + json["returnUrl"] = this.returnUrl; } if(this.skipKyc !== undefined) { - json += `"skipKyc": ${typeof this.skipKyc === 'number' || typeof this.skipKyc === 'boolean' ? this.skipKyc : JSON.stringify(this.skipKyc)},`; + json["skipKyc"] = this.skipKyc; } - if(this.additionalProperties !== undefined) { - for (const [key, value] of this.additionalProperties.entries()) { + if(this.additionalProperties !== undefined) { + for (const [key, value] of Object.entries(this.additionalProperties)) { //Only unwrap those that are not already a property in the JSON object if(["returnUrl","skipKyc","additionalProperties"].includes(String(key))) continue; - json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + json[key] = value; } } - //Remove potential last comma - return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + return json; } - public static unmarshal(json: string | object): InitializeRequest { - const obj = typeof json === "object" ? json : JSON.parse(json); + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): InitializeRequest { const instance = new InitializeRequest({} as any); if (obj["returnUrl"] !== undefined) { - instance.returnUrl = obj["returnUrl"]; + instance.returnUrl = obj["returnUrl"] as string; } if (obj["skipKyc"] !== undefined) { - instance.skipKyc = obj["skipKyc"]; + instance.skipKyc = obj["skipKyc"] as boolean; } - - instance.additionalProperties = new Map(); + + instance.additionalProperties = {}; const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["returnUrl","skipKyc","additionalProperties"].includes(key);})); for (const [key, value] of propsToCheck) { - instance.additionalProperties.set(key, value as any); + instance.additionalProperties[key] = value as any; } return instance; } + + public static unmarshal(json: string | object): InitializeRequest { + const obj = typeof json === "object" ? json : JSON.parse(json); + return InitializeRequest.fromJson(obj as Record); + } public static theCodeGenSchema = {"type":"object","required":["returnUrl"],"properties":{"returnUrl":{"type":"string","format":"uri"},"skipKyc":{"type":"boolean"}},"$id":"InitializeRequest","$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 ?? {}; @@ -83,4 +91,4 @@ class InitializeRequest { } } -export { InitializeRequest }; \ No newline at end of file +export { InitializeRequest, InitializeRequestInterface }; \ No newline at end of file diff --git a/examples/openapi-http-client/src/generated/payload/PostV2ConnectRequest.ts b/examples/openapi-http-client/src/generated/payload/PostV2ConnectRequest.ts index b7d2c2bb..e2caea1b 100644 --- a/examples/openapi-http-client/src/generated/payload/PostV2ConnectRequest.ts +++ b/examples/openapi-http-client/src/generated/payload/PostV2ConnectRequest.ts @@ -1,15 +1,16 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface PostV2ConnectRequestInterface { + returnUrl: string + skipKyc?: boolean + additionalProperties?: Record +} class PostV2ConnectRequest { private _returnUrl: string; private _skipKyc?: boolean; private _additionalProperties?: Record; - constructor(input: { - returnUrl: string, - skipKyc?: boolean, - additionalProperties?: Record, - }) { + constructor(input: PostV2ConnectRequestInterface) { this._returnUrl = input.returnUrl; this._skipKyc = input.skipKyc; this._additionalProperties = input.additionalProperties; @@ -24,43 +25,50 @@ class PostV2ConnectRequest { get additionalProperties(): Record | undefined { return this._additionalProperties; } set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } - public marshal() : string { - let json = '{' + public toJson(): Record { + const json: Record = {}; if(this.returnUrl !== undefined) { - json += `"returnUrl": ${typeof this.returnUrl === 'number' || typeof this.returnUrl === 'boolean' ? this.returnUrl : JSON.stringify(this.returnUrl)},`; + json["returnUrl"] = this.returnUrl; } if(this.skipKyc !== undefined) { - json += `"skipKyc": ${typeof this.skipKyc === 'number' || typeof this.skipKyc === 'boolean' ? this.skipKyc : JSON.stringify(this.skipKyc)},`; + json["skipKyc"] = this.skipKyc; } - if(this.additionalProperties !== undefined) { - for (const [key, value] of this.additionalProperties.entries()) { + if(this.additionalProperties !== undefined) { + for (const [key, value] of Object.entries(this.additionalProperties)) { //Only unwrap those that are not already a property in the JSON object if(["returnUrl","skipKyc","additionalProperties"].includes(String(key))) continue; - json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + json[key] = value; } } - //Remove potential last comma - return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + return json; } - public static unmarshal(json: string | object): PostV2ConnectRequest { - const obj = typeof json === "object" ? json : JSON.parse(json); + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): PostV2ConnectRequest { const instance = new PostV2ConnectRequest({} as any); if (obj["returnUrl"] !== undefined) { - instance.returnUrl = obj["returnUrl"]; + instance.returnUrl = obj["returnUrl"] as string; } if (obj["skipKyc"] !== undefined) { - instance.skipKyc = obj["skipKyc"]; + instance.skipKyc = obj["skipKyc"] as boolean; } - - instance.additionalProperties = new Map(); + + instance.additionalProperties = {}; const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["returnUrl","skipKyc","additionalProperties"].includes(key);})); for (const [key, value] of propsToCheck) { - instance.additionalProperties.set(key, value as any); + instance.additionalProperties[key] = value as any; } return instance; } + + public static unmarshal(json: string | object): PostV2ConnectRequest { + const obj = typeof json === "object" ? json : JSON.parse(json); + return PostV2ConnectRequest.fromJson(obj as Record); + } public static theCodeGenSchema = {"type":"object","required":["returnUrl"],"properties":{"returnUrl":{"type":"string","format":"uri"},"skipKyc":{"type":"boolean"}},"$id":"PostV2ConnectRequest","$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 ?? {}; @@ -83,4 +91,4 @@ class PostV2ConnectRequest { } } -export { PostV2ConnectRequest }; \ No newline at end of file +export { PostV2ConnectRequest, PostV2ConnectRequestInterface }; \ No newline at end of file diff --git a/examples/openapi-http-client/src/generated/payload/PostV2ConnectResponse_200.ts b/examples/openapi-http-client/src/generated/payload/PostV2ConnectResponse_200.ts index 5b9942a4..a2e05fbc 100644 --- a/examples/openapi-http-client/src/generated/payload/PostV2ConnectResponse_200.ts +++ b/examples/openapi-http-client/src/generated/payload/PostV2ConnectResponse_200.ts @@ -1,15 +1,16 @@ import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; import {default as addFormats} from 'ajv-formats'; +interface PostV2ConnectResponse_200Interface { + referenceId?: string + connectUrl?: string + additionalProperties?: Record +} class PostV2ConnectResponse_200 { private _referenceId?: string; private _connectUrl?: string; private _additionalProperties?: Record; - constructor(input: { - referenceId?: string, - connectUrl?: string, - additionalProperties?: Record, - }) { + constructor(input: PostV2ConnectResponse_200Interface) { this._referenceId = input.referenceId; this._connectUrl = input.connectUrl; this._additionalProperties = input.additionalProperties; @@ -24,43 +25,50 @@ class PostV2ConnectResponse_200 { get additionalProperties(): Record | undefined { return this._additionalProperties; } set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } - public marshal() : string { - let json = '{' + public toJson(): Record { + const json: Record = {}; if(this.referenceId !== undefined) { - json += `"referenceId": ${typeof this.referenceId === 'number' || typeof this.referenceId === 'boolean' ? this.referenceId : JSON.stringify(this.referenceId)},`; + json["referenceId"] = this.referenceId; } if(this.connectUrl !== undefined) { - json += `"connectUrl": ${typeof this.connectUrl === 'number' || typeof this.connectUrl === 'boolean' ? this.connectUrl : JSON.stringify(this.connectUrl)},`; + json["connectUrl"] = this.connectUrl; } - if(this.additionalProperties !== undefined) { - for (const [key, value] of this.additionalProperties.entries()) { + if(this.additionalProperties !== undefined) { + for (const [key, value] of Object.entries(this.additionalProperties)) { //Only unwrap those that are not already a property in the JSON object if(["referenceId","connectUrl","additionalProperties"].includes(String(key))) continue; - json += `"${key}": ${typeof value === 'number' || typeof value === 'boolean' ? value : JSON.stringify(value)},`; + json[key] = value; } } - //Remove potential last comma - return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + return json; } - public static unmarshal(json: string | object): PostV2ConnectResponse_200 { - const obj = typeof json === "object" ? json : JSON.parse(json); + public marshal(): string { + return JSON.stringify(this.toJson()); + } + + public static fromJson(obj: Record): PostV2ConnectResponse_200 { const instance = new PostV2ConnectResponse_200({} as any); if (obj["referenceId"] !== undefined) { - instance.referenceId = obj["referenceId"]; + instance.referenceId = obj["referenceId"] as string; } if (obj["connectUrl"] !== undefined) { - instance.connectUrl = obj["connectUrl"]; + instance.connectUrl = obj["connectUrl"] as string; } - - instance.additionalProperties = new Map(); + + instance.additionalProperties = {}; const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["referenceId","connectUrl","additionalProperties"].includes(key);})); for (const [key, value] of propsToCheck) { - instance.additionalProperties.set(key, value as any); + instance.additionalProperties[key] = value as any; } return instance; } + + public static unmarshal(json: string | object): PostV2ConnectResponse_200 { + const obj = typeof json === "object" ? json : JSON.parse(json); + return PostV2ConnectResponse_200.fromJson(obj as Record); + } public static theCodeGenSchema = {"type":"object","properties":{"referenceId":{"type":"string"},"connectUrl":{"type":"string","format":"uri"}},"$id":"postV2Connect_Response_200","$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 ?? {}; @@ -83,4 +91,4 @@ class PostV2ConnectResponse_200 { } } -export { PostV2ConnectResponse_200 }; \ No newline at end of file +export { PostV2ConnectResponse_200, PostV2ConnectResponse_200Interface }; \ No newline at end of file diff --git a/examples/openapi-http-client/src/index.ts b/examples/openapi-http-client/src/index.ts index 1de8caaf..21bf836d 100644 --- a/examples/openapi-http-client/src/index.ts +++ b/examples/openapi-http-client/src/index.ts @@ -51,7 +51,7 @@ async function main() { // reuses `server`/`auth` and returns an HttpClientResponse whose `data` is // the unmarshalled, fully typed response model. const safepay = new SafepayApiV2SampleClient({ - server, + baseUrl: server, auth: {type: 'bearer', token: process.env.SAFEPAY_TOKEN ?? ''} }); @@ -76,7 +76,26 @@ async function main() { // The standalone channel functions remain available for one-off calls where // constructing a client is unnecessary. - await http_client.postV2Connect({server, payload: connectBody}); + await http_client.postV2Connect({baseUrl: server, payload: connectBody}); + + // Error handling: non-OK responses throw a typed `HttpError`. Because the + // OpenAPI document declares 400/404 responses, those codes get explicit cases + // with standard reason-phrase messages ('Bad Request', 'Not Found'); any + // undeclared code uses the generic `HTTP Error: ` form. + try { + await safepay.getV2ConnectReferenceId({parameters: {referenceId: 'does-not-exist'}}); + } catch (error) { + if (error instanceof http_client.HttpError) { + console.log('Request failed with HttpError:', { + status: error.status, + statusText: error.statusText, + message: error.message, + body: error.body + }); + } else { + throw error; + } + } } main().catch((error) => { diff --git a/src/codegen/generators/typescript/channels/openapi.ts b/src/codegen/generators/typescript/channels/openapi.ts index cf8b6caa..f1d0abce 100644 --- a/src/codegen/generators/typescript/channels/openapi.ts +++ b/src/codegen/generators/typescript/channels/openapi.ts @@ -113,7 +113,11 @@ export async function generateTypeScriptChannelsForOpenAPI( // Generate common types once (stateless check) // Pass security schemes to generate only relevant auth types if (protocolCodeFunctions['http_client'].length === 0 && renders.length > 0) { - const commonTypesCode = renderHttpCommonTypes(securitySchemes); + const errorStatusCodes = collectErrorStatusCodes(openapiDocument); + const commonTypesCode = renderHttpCommonTypes({ + securitySchemes, + errorStatusCodes + }); protocolCodeFunctions['http_client'].unshift(commonTypesCode); } @@ -130,6 +134,58 @@ export async function generateTypeScriptChannelsForOpenAPI( }); } +/** + * Collect the error status codes declared across every operation in the + * document. Numeric response keys >= 400 and the literal 'default' are gathered + * and deduped; 2xx/3xx keys are ignored. These drive the explicit cases in the + * shared, document-wide `handleHttpError`. Handles OpenAPI 2.0/3.0/3.1 + * uniformly (response keys are strings in all three). + */ +function collectResponseErrorCodes( + responses: Record | undefined, + codes: Set +): void { + if (!responses) { + return; + } + + for (const statusCode of Object.keys(responses)) { + if (statusCode === 'default') { + codes.add('default'); + continue; + } + const numericCode = Number(statusCode); + if (Number.isInteger(numericCode) && numericCode >= 400) { + codes.add(numericCode); + } + } +} + +function collectErrorStatusCodes( + openapiDocument: OpenAPIDocument +): (number | 'default')[] { + const codes = new Set(); + + for (const pathItem of Object.values(openapiDocument.paths ?? {})) { + if (!pathItem) { + continue; + } + + for (const method of HTTP_METHODS) { + // eslint-disable-next-line security/detect-object-injection + const operation = (pathItem as Record)[method] as + | OpenAPIOperation + | undefined; + collectResponseErrorCodes( + operation?.responses as Record | undefined, + codes + ); + } + } + + return [...codes]; +} + /** * Process all OpenAPI operations and generate HTTP client functions. */ diff --git a/src/codegen/generators/typescript/channels/protocols/http/client.ts b/src/codegen/generators/typescript/channels/protocols/http/client.ts index 8a5a02ed..457c4767 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/client.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/client.ts @@ -367,7 +367,8 @@ ${parameterNormalization}${oauth2ValidateBlock} // Build headers ${oauth2TokenBlock} // Handle error responses if (!response.ok) { - handleHttpError(response.status, response.statusText); + const errorBody = await response.json().catch(() => undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response diff --git a/src/codegen/generators/typescript/channels/protocols/http/common-types.ts b/src/codegen/generators/typescript/channels/protocols/http/common-types.ts index 8dedf8bd..c52b1acd 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/common-types.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/common-types.ts @@ -12,6 +12,94 @@ import { renderSecurityTypes } from './security'; +/** + * Standard HTTP reason phrases used to bake a message into each generated + * `handleHttpError` case. Keyed by status code; deterministic and + * collision-free across operations that declare the same code. + */ +const HTTP_REASON_PHRASES: Record = { + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Payload Too Large', + 414: 'URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Range Not Satisfiable', + 417: 'Expectation Failed', + 418: "I'm a Teapot", + 421: 'Misdirected Request', + 422: 'Unprocessable Entity', + 423: 'Locked', + 424: 'Failed Dependency', + 425: 'Too Early', + 426: 'Upgrade Required', + 428: 'Precondition Required', + 429: 'Too Many Requests', + 431: 'Request Header Fields Too Large', + 451: 'Unavailable For Legal Reasons', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported', + 506: 'Variant Also Negotiates', + 507: 'Insufficient Storage', + 508: 'Loop Detected', + 510: 'Not Extended', + 511: 'Network Authentication Required' +}; + +/** + * Build the body of the generated `handleHttpError` function from the set of + * error status codes the input document declares. Numeric codes become explicit + * `case` statements throwing an `HttpError` with the standard reason phrase; a + * `default` case throws the generic `HTTP Error: ` form. + * When no numeric codes are declared (e.g. the AsyncAPI path) only the default + * throw is emitted. + */ +function renderHandleHttpErrorBody( + errorStatusCodes: (number | 'default')[] +): string { + const defaultThrow = + 'throw new HttpError(`HTTP Error: ${status} ${statusText}`, status, statusText, body);'; + + const numericCodes = errorStatusCodes + .filter((code): code is number => typeof code === 'number') + .filter((code, index, all) => all.indexOf(code) === index) + .sort((first, second) => first - second); + + if (numericCodes.length === 0) { + return ` ${defaultThrow}`; + } + + const cases = numericCodes + .map((code) => { + // eslint-disable-next-line security/detect-object-injection + const phrase = HTTP_REASON_PHRASES[code] ?? `HTTP Error: ${code}`; + return ` case ${code}:\n throw new HttpError(${JSON.stringify( + phrase + )}, status, statusText, body);`; + }) + .join('\n'); + + return ` switch (status) { +${cases} + default: + ${defaultThrow} + }`; +} + /** * Generates common types and helper functions shared across all HTTP client functions. * This should be called once per protocol generation to avoid code duplication. @@ -19,10 +107,17 @@ import { * @param securitySchemes - Optional security schemes extracted from OpenAPI. * When provided, only relevant auth types are generated. * When undefined/empty, all auth types are generated for backward compatibility. + * @param errorStatusCodes - Error status codes declared by the input document. + * Each numeric code becomes an explicit `handleHttpError` + * case; when empty only the default handler is emitted. */ -export function renderHttpCommonTypes( - securitySchemes?: SecuritySchemeOptions[] -): string { +export function renderHttpCommonTypes({ + securitySchemes, + errorStatusCodes = [] +}: { + securitySchemes?: SecuritySchemeOptions[]; + errorStatusCodes?: (number | 'default')[]; +} = {}): string { const requirements = analyzeSecuritySchemes(securitySchemes); const securityTypes = renderSecurityTypes(securitySchemes, requirements); const applyAuthCases = renderApplyAuthCases(requirements); @@ -86,6 +181,27 @@ export interface HttpClientResponse { rawData: Record; } +/** + * Error thrown for non-OK HTTP responses. + * + * Carries the HTTP \`status\`, \`statusText\`, and the parsed response \`body\` + * (when the error response had a JSON body). Thrown by \`handleHttpError\` and + * routed through the \`onError\` hook / retry logic unchanged. + */ +export class HttpError extends Error { + status: number; + statusText: string; + body?: unknown; + + constructor(message: string, status: number, statusText: string, body?: unknown) { + super(message); + this.name = 'HttpError'; + this.status = status; + this.statusText = statusText; + this.body = body; + } +} + /** * HTTP request parameters passed to the request hook */ @@ -342,21 +458,12 @@ async function executeWithRetry( } /** - * Handle HTTP error status codes with standardized messages + * Handle HTTP error status codes by throwing a typed HttpError. + * Explicit cases are generated from the error status codes declared by the + * input document; undeclared codes fall through to the default handler. */ -function handleHttpError(status: number, statusText: string): never { - switch (status) { - case 401: - throw new Error('Unauthorized'); - case 403: - throw new Error('Forbidden'); - case 404: - throw new Error('Not Found'); - case 500: - throw new Error('Internal Server Error'); - default: - throw new Error(\`HTTP Error: \${status} \${statusText}\`); - } +function handleHttpError(status: number, statusText: string, body?: unknown): never { +${renderHandleHttpErrorBody(errorStatusCodes)} } /** diff --git a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap index 99e26dd5..3f0096a5 100644 --- a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap @@ -44,6 +44,27 @@ export interface HttpClientResponse { rawData: Record; } +/** + * Error thrown for non-OK HTTP responses. + * + * Carries the HTTP \`status\`, \`statusText\`, and the parsed response \`body\` + * (when the error response had a JSON body). Thrown by \`handleHttpError\` and + * routed through the \`onError\` hook / retry logic unchanged. + */ +export class HttpError extends Error { + status: number; + statusText: string; + body?: unknown; + + constructor(message: string, status: number, statusText: string, body?: unknown) { + super(message); + this.name = 'HttpError'; + this.status = status; + this.statusText = statusText; + this.body = body; + } +} + /** * HTTP request parameters passed to the request hook */ @@ -392,20 +413,20 @@ async function executeWithRetry( } /** - * Handle HTTP error status codes with standardized messages + * Handle HTTP error status codes by throwing a typed HttpError. + * Explicit cases are generated from the error status codes declared by the + * input document; undeclared codes fall through to the default handler. */ -function handleHttpError(status: number, statusText: string): never { +function handleHttpError(status: number, statusText: string, body?: unknown): never { switch (status) { - case 401: - throw new Error('Unauthorized'); - case 403: - throw new Error('Forbidden'); + case 400: + throw new HttpError("Bad Request", status, statusText, body); case 404: - throw new Error('Not Found'); - case 500: - throw new Error('Internal Server Error'); + throw new HttpError("Not Found", status, statusText, body); + case 405: + throw new HttpError("Method Not Allowed", status, statusText, body); default: - throw new Error(\`HTTP Error: \${status} \${statusText}\`); + throw new HttpError(\`HTTP Error: \${status} \${statusText}\`, status, statusText, body); } } @@ -703,7 +724,8 @@ async function addPet(context: AddPetContext): Promise> // Handle error responses if (!response.ok) { - handleHttpError(response.status, response.statusText); + const errorBody = await response.json().catch(() => undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response @@ -814,7 +836,8 @@ async function updatePet(context: UpdatePetContext): Promise undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response @@ -927,7 +950,8 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Handle error responses if (!response.ok) { - handleHttpError(response.status, response.statusText); + const errorBody = await response.json().catch(() => undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response @@ -1580,6 +1604,27 @@ export interface HttpClientResponse { rawData: Record; } +/** + * Error thrown for non-OK HTTP responses. + * + * Carries the HTTP \`status\`, \`statusText\`, and the parsed response \`body\` + * (when the error response had a JSON body). Thrown by \`handleHttpError\` and + * routed through the \`onError\` hook / retry logic unchanged. + */ +export class HttpError extends Error { + status: number; + statusText: string; + body?: unknown; + + constructor(message: string, status: number, statusText: string, body?: unknown) { + super(message); + this.name = 'HttpError'; + this.status = status; + this.statusText = statusText; + this.body = body; + } +} + /** * HTTP request parameters passed to the request hook */ @@ -1954,21 +1999,12 @@ async function executeWithRetry( } /** - * Handle HTTP error status codes with standardized messages + * Handle HTTP error status codes by throwing a typed HttpError. + * Explicit cases are generated from the error status codes declared by the + * input document; undeclared codes fall through to the default handler. */ -function handleHttpError(status: number, statusText: string): never { - switch (status) { - case 401: - throw new Error('Unauthorized'); - case 403: - throw new Error('Forbidden'); - case 404: - throw new Error('Not Found'); - case 500: - throw new Error('Internal Server Error'); - default: - throw new Error(\`HTTP Error: \${status} \${statusText}\`); - } +function handleHttpError(status: number, statusText: string, body?: unknown): never { + throw new HttpError(\`HTTP Error: \${status} \${statusText}\`, status, statusText, body); } /** @@ -2262,7 +2298,8 @@ async function getPingRequest(context: GetPingRequestContext = {}): Promise undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response diff --git a/test/codegen/generators/typescript/channels.spec.ts b/test/codegen/generators/typescript/channels.spec.ts index e1f2f0da..b690b94d 100644 --- a/test/codegen/generators/typescript/channels.spec.ts +++ b/test/codegen/generators/typescript/channels.spec.ts @@ -511,6 +511,16 @@ describe('channels', () => { expect(generatedChannels.files.length).toBeGreaterThan(0); expect(generatedChannels.result).toMatchSnapshot('http_client-index'); expect(generatedChannels.protocolFiles['http_client']).toMatchSnapshot('http_client-protocol-code'); + + // AsyncAPI declares no error responses, so handleHttpError is + // default-only (no explicit numeric cases / switch), but the typed + // HttpError class is still exported. + const httpProtocolCode = generatedChannels.protocolFiles['http_client']; + expect(httpProtocolCode).toContain('export class HttpError extends Error'); + expect(httpProtocolCode).toContain( + 'function handleHttpError(status: number, statusText: string, body?: unknown): never {\n throw new HttpError(`HTTP Error: ${status} ${statusText}`, status, statusText, body);\n}' + ); + expect(httpProtocolCode).not.toMatch(/case \d+:/); }); }); @@ -791,6 +801,29 @@ describe('channels', () => { expect(httpProtocolCode).toContain('unmarshal(JSON.stringify(rawData))'); expect(httpProtocolCode).not.toMatch(/unmarshal\(rawData\)/); + // Error handling is input-driven: openapi-3.json declares 400/404/405 + // across its operations, so handleHttpError emits an explicit case per + // code (aggregated document-wide) throwing a typed HttpError with the + // standard reason phrase. The typed HttpError class is always exported. + expect(httpProtocolCode).toContain('export class HttpError extends Error'); + expect(httpProtocolCode).toContain( + 'case 400:\n throw new HttpError("Bad Request", status, statusText, body);' + ); + expect(httpProtocolCode).toContain( + 'case 404:\n throw new HttpError("Not Found", status, statusText, body);' + ); + expect(httpProtocolCode).toContain( + 'case 405:\n throw new HttpError("Method Not Allowed", status, statusText, body);' + ); + // Undeclared codes fall through to the generic default handler. + expect(httpProtocolCode).toContain( + 'throw new HttpError(`HTTP Error: ${status} ${statusText}`, status, statusText, body);' + ); + // The per-operation error branch parses and forwards the error body. + expect(httpProtocolCode).toContain( + 'const errorBody = await response.json().catch(() => undefined);' + ); + // Operations with path parameters must expose their parameter model name via // parameterType so downstream consumers (e.g. README generation) know the // operation requires a `parameters` argument. Operations without parameters diff --git a/test/codegen/generators/typescript/channels/protocols/http/fetch-security.spec.ts b/test/codegen/generators/typescript/channels/protocols/http/fetch-security.spec.ts index b3d7778e..5c19c5d8 100644 --- a/test/codegen/generators/typescript/channels/protocols/http/fetch-security.spec.ts +++ b/test/codegen/generators/typescript/channels/protocols/http/fetch-security.spec.ts @@ -16,7 +16,7 @@ describe('HTTP Fetch Generator - Security Types', () => { } ]; - const result = renderHttpCommonTypes(securitySchemes); + const result = renderHttpCommonTypes({securitySchemes}); // Should include ApiKeyAuth interface expect(result).toContain('export interface ApiKeyAuth'); @@ -44,7 +44,7 @@ describe('HTTP Fetch Generator - Security Types', () => { } ]; - const result = renderHttpCommonTypes(securitySchemes); + const result = renderHttpCommonTypes({securitySchemes}); expect(result).toContain('export interface BearerAuth'); expect(result).toContain("type: 'bearer'"); @@ -63,7 +63,7 @@ describe('HTTP Fetch Generator - Security Types', () => { } ]; - const result = renderHttpCommonTypes(securitySchemes); + const result = renderHttpCommonTypes({securitySchemes}); expect(result).toContain('export interface BasicAuth'); expect(result).toContain("type: 'basic'"); @@ -87,7 +87,7 @@ describe('HTTP Fetch Generator - Security Types', () => { } ]; - const result = renderHttpCommonTypes(securitySchemes); + const result = renderHttpCommonTypes({securitySchemes}); expect(result).toContain('export interface OAuth2Auth'); expect(result).toContain("type: 'oauth2'"); @@ -116,7 +116,7 @@ describe('HTTP Fetch Generator - Security Types', () => { } ]; - const result = renderHttpCommonTypes(securitySchemes); + const result = renderHttpCommonTypes({securitySchemes}); expect(result).toContain('export interface ApiKeyAuth'); expect(result).toContain('export interface BearerAuth'); @@ -142,7 +142,7 @@ describe('HTTP Fetch Generator - Security Types', () => { }); it('should generate all auth types when empty security schemes array provided', () => { - const result = renderHttpCommonTypes([]); + const result = renderHttpCommonTypes({securitySchemes: []}); expect(result).toContain('export interface BearerAuth'); expect(result).toContain('export interface BasicAuth'); @@ -160,7 +160,7 @@ describe('HTTP Fetch Generator - Security Types', () => { } ]; - const result = renderHttpCommonTypes(securitySchemes); + const result = renderHttpCommonTypes({securitySchemes}); // The generated interface should have name and in pre-set expect(result).toContain('X-Petstore-Key'); @@ -181,7 +181,7 @@ describe('HTTP Fetch Generator - Security Types', () => { } ]; - const result = renderHttpCommonTypes(securitySchemes); + const result = renderHttpCommonTypes({securitySchemes}); expect(result).toContain('export interface OAuth2Auth'); expect(result).toContain('https://example.com/authorize'); @@ -197,7 +197,7 @@ describe('HTTP Fetch Generator - Security Types', () => { } ]; - const result = renderHttpCommonTypes(securitySchemes); + const result = renderHttpCommonTypes({securitySchemes}); // OpenID Connect should be treated similar to OAuth2 expect(result).toContain('export interface OAuth2Auth'); @@ -210,9 +210,9 @@ describe('HTTP Fetch Generator - Security Types', () => { // A bearer-only spec narrows AuthConfig to BearerAuth. The generated // helpers must not reference the other auth types, otherwise the output // fails to type-check (fields/discriminants that no longer exist). - const result = renderHttpCommonTypes([ + const result = renderHttpCommonTypes({securitySchemes: [ {name: 'bearerAuth', type: 'http', httpScheme: 'bearer'} - ]); + ]}); expect(result).toContain("case 'bearer':"); expect(result).not.toContain("case 'basic'"); @@ -255,7 +255,7 @@ describe('HTTP Fetch Generator - Security Types', () => { } ]; - const result = renderHttpCommonTypes(securitySchemes); + const result = renderHttpCommonTypes({securitySchemes}); // Should only have one ApiKeyAuth interface const apiKeyInterfaceMatches = result.match( diff --git a/test/runtime/typescript/src/openapi/channels/http_client.ts b/test/runtime/typescript/src/openapi/channels/http_client.ts index b356f6c5..5a2dc4ed 100644 --- a/test/runtime/typescript/src/openapi/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi/channels/http_client.ts @@ -41,6 +41,27 @@ export interface HttpClientResponse { rawData: Record; } +/** + * Error thrown for non-OK HTTP responses. + * + * Carries the HTTP `status`, `statusText`, and the parsed response `body` + * (when the error response had a JSON body). Thrown by `handleHttpError` and + * routed through the `onError` hook / retry logic unchanged. + */ +export class HttpError extends Error { + status: number; + statusText: string; + body?: unknown; + + constructor(message: string, status: number, statusText: string, body?: unknown) { + super(message); + this.name = 'HttpError'; + this.status = status; + this.statusText = statusText; + this.body = body; + } +} + /** * HTTP request parameters passed to the request hook */ @@ -389,20 +410,20 @@ async function executeWithRetry( } /** - * Handle HTTP error status codes with standardized messages + * Handle HTTP error status codes by throwing a typed HttpError. + * Explicit cases are generated from the error status codes declared by the + * input document; undeclared codes fall through to the default handler. */ -function handleHttpError(status: number, statusText: string): never { +function handleHttpError(status: number, statusText: string, body?: unknown): never { switch (status) { - case 401: - throw new Error('Unauthorized'); - case 403: - throw new Error('Forbidden'); + case 400: + throw new HttpError("Bad Request", status, statusText, body); case 404: - throw new Error('Not Found'); - case 500: - throw new Error('Internal Server Error'); + throw new HttpError("Not Found", status, statusText, body); + case 405: + throw new HttpError("Method Not Allowed", status, statusText, body); default: - throw new Error(`HTTP Error: ${status} ${statusText}`); + throw new HttpError(`HTTP Error: ${status} ${statusText}`, status, statusText, body); } } @@ -700,7 +721,8 @@ async function addPet(context: AddPetContext): Promise> // Handle error responses if (!response.ok) { - handleHttpError(response.status, response.statusText); + const errorBody = await response.json().catch(() => undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response @@ -811,7 +833,8 @@ async function updatePet(context: UpdatePetContext): Promise undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response @@ -924,7 +947,8 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Handle error responses if (!response.ok) { - handleHttpError(response.status, response.statusText); + const errorBody = await response.json().catch(() => undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response diff --git a/test/runtime/typescript/src/request-reply/channels/http_client.ts b/test/runtime/typescript/src/request-reply/channels/http_client.ts index 17b02755..10189682 100644 --- a/test/runtime/typescript/src/request-reply/channels/http_client.ts +++ b/test/runtime/typescript/src/request-reply/channels/http_client.ts @@ -42,6 +42,27 @@ export interface HttpClientResponse { rawData: Record; } +/** + * Error thrown for non-OK HTTP responses. + * + * Carries the HTTP `status`, `statusText`, and the parsed response `body` + * (when the error response had a JSON body). Thrown by `handleHttpError` and + * routed through the `onError` hook / retry logic unchanged. + */ +export class HttpError extends Error { + status: number; + statusText: string; + body?: unknown; + + constructor(message: string, status: number, statusText: string, body?: unknown) { + super(message); + this.name = 'HttpError'; + this.status = status; + this.statusText = statusText; + this.body = body; + } +} + /** * HTTP request parameters passed to the request hook */ @@ -416,21 +437,12 @@ async function executeWithRetry( } /** - * Handle HTTP error status codes with standardized messages + * Handle HTTP error status codes by throwing a typed HttpError. + * Explicit cases are generated from the error status codes declared by the + * input document; undeclared codes fall through to the default handler. */ -function handleHttpError(status: number, statusText: string): never { - switch (status) { - case 401: - throw new Error('Unauthorized'); - case 403: - throw new Error('Forbidden'); - case 404: - throw new Error('Not Found'); - case 500: - throw new Error('Internal Server Error'); - default: - throw new Error(`HTTP Error: ${status} ${statusText}`); - } +function handleHttpError(status: number, statusText: string, body?: unknown): never { + throw new HttpError(`HTTP Error: ${status} ${statusText}`, status, statusText, body); } /** @@ -727,7 +739,8 @@ async function postPingPostRequest(context: PostPingPostRequestContext): Promise // Handle error responses if (!response.ok) { - handleHttpError(response.status, response.statusText); + const errorBody = await response.json().catch(() => undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response @@ -835,7 +848,8 @@ async function getPingGetRequest(context: GetPingGetRequestContext = {}): Promis // Handle error responses if (!response.ok) { - handleHttpError(response.status, response.statusText); + const errorBody = await response.json().catch(() => undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response @@ -946,7 +960,8 @@ async function putPingPutRequest(context: PutPingPutRequestContext): Promise undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response @@ -1054,7 +1069,8 @@ async function deletePingDeleteRequest(context: DeletePingDeleteRequestContext = // Handle error responses if (!response.ok) { - handleHttpError(response.status, response.statusText); + const errorBody = await response.json().catch(() => undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response @@ -1165,7 +1181,8 @@ async function patchPingPatchRequest(context: PatchPingPatchRequestContext): Pro // Handle error responses if (!response.ok) { - handleHttpError(response.status, response.statusText); + const errorBody = await response.json().catch(() => undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response @@ -1273,7 +1290,8 @@ async function headPingHeadRequest(context: HeadPingHeadRequestContext = {}): Pr // Handle error responses if (!response.ok) { - handleHttpError(response.status, response.statusText); + const errorBody = await response.json().catch(() => undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response @@ -1381,7 +1399,8 @@ async function optionsPingOptionsRequest(context: OptionsPingOptionsRequestConte // Handle error responses if (!response.ok) { - handleHttpError(response.status, response.statusText); + const errorBody = await response.json().catch(() => undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response @@ -1489,7 +1508,8 @@ async function getMultiStatusResponse(context: GetMultiStatusResponseContext = { // Handle error responses if (!response.ok) { - handleHttpError(response.status, response.statusText); + const errorBody = await response.json().catch(() => undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response @@ -1604,7 +1624,8 @@ async function getGetUserItem(context: GetGetUserItemContext): Promise undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response @@ -1721,7 +1742,8 @@ async function putUpdateUserItem(context: PutUpdateUserItemContext): Promise undefined); + handleHttpError(response.status, response.statusText, errorBody); } // Parse response 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 97a8143c..194fb18d 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 @@ -186,7 +186,7 @@ describe('HTTP Client - API Key and Basic Authentication', () => { }); throw new Error('Expected request to fail with 401 status'); } catch (error) { - expect(error.message).toBe('Unauthorized'); + expect(error.message).toBe('HTTP Error: 401 Unauthorized'); } }); }); 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 3864aad5..cffc0671 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 @@ -251,7 +251,9 @@ describe('HTTP Client - Hooks', () => { } expect(onErrorCalled).toBe(true); - expect(capturedError?.message).toBe('Not Found'); + // AsyncAPI-generated client declares no error responses, so the + // default-only handleHttpError uses the generic message form. + expect(capturedError?.message).toBe('HTTP Error: 404 Not Found'); }); }); 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 9a061a9a..6fc26250 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 @@ -10,6 +10,7 @@ import { patchPingPatchRequest, headPingHeadRequest, optionsPingOptionsRequest, + HttpError, } from '../../../../src/request-reply/channels/http_client'; jest.setTimeout(15000); @@ -319,6 +320,10 @@ describe('HTTP Client - HTTP Methods', () => { }); describe('error handling across methods', () => { + // The request-reply client is AsyncAPI-generated, which declares no error + // responses, so handleHttpError is default-only: every error throws a typed + // HttpError carrying status/statusText/body, with the default message form + // `HTTP Error: `. it('should handle 404 for PUT', async () => { const { app, router, port } = createTestServer(); @@ -329,10 +334,16 @@ describe('HTTP Client - HTTP Methods', () => { }); return runWithServer(app, port, async (_server, actualPort) => { - await expect(putPingPutRequest({ + const error = await putPingPutRequest({ baseUrl: `http://localhost:${actualPort}`, payload: requestMessage - })).rejects.toThrow('Not Found'); + }).catch((e) => e); + + expect(error).toBeInstanceOf(HttpError); + expect(error.status).toBe(404); + expect(error.statusText).toBeDefined(); + expect(error.body).toEqual({ error: 'Not Found' }); + expect(error.message).toBe('HTTP Error: 404 Not Found'); }); }); @@ -344,9 +355,15 @@ describe('HTTP Client - HTTP Methods', () => { }); return runWithServer(app, port, async (_server, actualPort) => { - await expect(deletePingDeleteRequest({ + const error = await deletePingDeleteRequest({ baseUrl: `http://localhost:${actualPort}` - })).rejects.toThrow('Internal Server Error'); + }).catch((e) => e); + + expect(error).toBeInstanceOf(HttpError); + expect(error.status).toBe(500); + expect(error.statusText).toBeDefined(); + expect(error.body).toEqual({ error: 'Internal Server Error' }); + expect(error.message).toBe('HTTP Error: 500 Internal Server Error'); }); }); @@ -360,10 +377,16 @@ describe('HTTP Client - HTTP Methods', () => { }); return runWithServer(app, port, async (_server, actualPort) => { - await expect(patchPingPatchRequest({ + const error = await patchPingPatchRequest({ baseUrl: `http://localhost:${actualPort}`, payload: requestMessage - })).rejects.toThrow('Forbidden'); + }).catch((e) => e); + + expect(error).toBeInstanceOf(HttpError); + expect(error.status).toBe(403); + expect(error.statusText).toBeDefined(); + expect(error.body).toEqual({ error: 'Forbidden' }); + expect(error.message).toBe('HTTP Error: 403 Forbidden'); }); }); }); 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 287cbe16..1f587fac 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 @@ -202,7 +202,7 @@ describe('HTTP Client - OAuth2 Pre-obtained Access Token', () => { }); throw new Error('Expected to fail'); } catch (error) { - expect(error.message).toBe('Unauthorized'); + expect(error.message).toBe('HTTP Error: 401 Unauthorized'); } }); }); 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 ee2b9183..f0259961 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 @@ -153,7 +153,9 @@ describe('HTTP Client - OAuth2 Refresh Token Flow', () => { }); throw new Error('Expected request to fail with 401 status'); } catch (error) { - // Request should fail with the original 401 error since refresh failed + // Refresh was attempted (all credentials present) but failed, so the + // OAuth2 refresh branch throws its own 'Unauthorized' error before + // handleHttpError is reached. expect(error.message).toBe('Unauthorized'); } }); @@ -188,7 +190,7 @@ describe('HTTP Client - OAuth2 Refresh Token Flow', () => { } catch (error) { // The request should fail with the original 401 error // since refresh can't be performed without clientId - expect(error.message).toBe('Unauthorized'); + expect(error.message).toBe('HTTP Error: 401 Unauthorized'); } }); }); 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 2316c9d3..4e6fa404 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 @@ -4,6 +4,7 @@ import { addPet, updatePet, findPetsByStatusAndCategory, + HttpError, } from '../../../../src/openapi/channels/http_client'; import { APet } from '../../../../src/openapi/payloads/APet'; import { FindPetsByStatusAndCategoryParameters } from '../../../../src/openapi/parameters/FindPetsByStatusAndCategoryParameters'; @@ -101,27 +102,37 @@ describe('HTTP Client - OpenAPI Generated', () => { }); describe('error handling', () => { - it('should throw error for 400 response', async () => { + // The OpenAPI client is input-driven: error status codes declared anywhere + // in openapi-3.json (400, 404, 405) become explicit cases in the shared + // handleHttpError, each throwing a typed HttpError whose message is the + // standard HTTP reason phrase for that code. + it('should throw a typed HttpError with the reason phrase for a declared 400', async () => { const { app, router, port } = createTestServer(); router.post('/pet', (req, res) => { - res.status(400).json({ error: 'Bad Request' }); + res.status(400).json({ error: 'invalid pet' }); }); return runWithServer(app, port, async (_server, actualPort) => { const pet = new APet({ name: 'Test', photoUrls: [] }); - await expect(addPet({ + const error = await addPet({ payload: pet, baseUrl: `http://localhost:${actualPort}` - })).rejects.toThrow(); + }).catch((e) => e); + + expect(error).toBeInstanceOf(HttpError); + expect(error.status).toBe(400); + // 400 is declared in the input -> explicit case with reason phrase + expect(error.message).toBe('Bad Request'); + expect(error.body).toEqual({ error: 'invalid pet' }); }); }); - it('should throw error for 404 response', async () => { + it('should throw a typed HttpError with the reason phrase for a declared 404', async () => { const { app, router, port } = createTestServer(); router.get('/pet/findByStatus/:status/:categoryId', (req, res) => { - res.status(404).json({ error: 'Not Found' }); + res.status(404).json({ error: 'no pets' }); }); return runWithServer(app, port, async (_server, actualPort) => { @@ -130,10 +141,16 @@ describe('HTTP Client - OpenAPI Generated', () => { categoryId: 999 }); - await expect(findPetsByStatusAndCategory({ + const error = await findPetsByStatusAndCategory({ parameters: params, baseUrl: `http://localhost:${actualPort}` - })).rejects.toThrow('Not Found'); + }).catch((e) => e); + + expect(error).toBeInstanceOf(HttpError); + expect(error.status).toBe(404); + // 404 is declared in the input -> explicit case with reason phrase + expect(error.message).toBe('Not Found'); + expect(error.body).toEqual({ error: 'no pets' }); }); }); }); 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 083d0c8b..b6974e74 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 @@ -4,6 +4,7 @@ import { createTestServer, runWithServer } from './test-utils'; import { getPingGetRequest, RetryConfig, + HttpError, } from '../../../../src/request-reply/channels/http_client'; jest.setTimeout(15000); @@ -142,11 +143,13 @@ describe('HTTP Client - Retry Logic', () => { retryableStatusCodes: [500] }; - await expect(getPingGetRequest({ + const error = await getPingGetRequest({ baseUrl: `http://localhost:${actualPort}`, retry - })).rejects.toThrow('Internal Server Error'); + }).catch((e) => e); + expect(error).toBeInstanceOf(HttpError); + expect(error.status).toBe(500); expect(requestCount).toBe(3); }); });