From dad0a09438b1c1c4079dda2036809bb6cbf18963 Mon Sep 17 00:00:00 2001 From: Lagoni Date: Mon, 6 Jul 2026 21:32:35 +0200 Subject: [PATCH 1/3] fix: generate clean OpenAPI operation names without duplicated HTTP verb Generated HTTP client function names for OpenAPI inputs were doubling the HTTP verb (e.g. `getGetv2connectreferenceId`, `postAddPet`) and, for specs without `operationId`s, collapsing the path into an uncasable blob because all non-alphanumeric separators were stripped before casing. Two causes: - The channel generator always prepended the HTTP method to the pascal-cased operationId, even though the operationId already encodes the verb. - The synthesized operationId (`${method}${path.replace(/[^a-zA-Z0-9]/g,'')}`) destroyed word boundaries, so `/v2/connect/{referenceId}` became `v2connectreferenceid` which can no longer be cased. Extract a shared `deriveOperationId` helper that keeps spec-provided ids verbatim and synthesizes word-boundary-preserving names from method + path segments (`GET /v2/connect/{referenceId}` -> `getV2ConnectReferenceId`). Use it across payloads, parameters, types, headers and the channel generator so the correlation key stays consistent, and stop re-prepending the method in the channel function name. Co-Authored-By: Claude Opus 4.8 --- .../generators/typescript/channels/openapi.ts | 33 ++++++---------- .../inputs/openapi/generators/headers.ts | 9 +++-- .../inputs/openapi/generators/parameters.ts | 9 +++-- .../inputs/openapi/generators/payloads.ts | 9 +++-- .../inputs/openapi/generators/types.ts | 9 +++-- src/codegen/inputs/openapi/utils.ts | 33 ++++++++++++++++ .../__snapshots__/channels.spec.ts.snap | 20 +++++----- test/codegen/inputs/openapi/utils.spec.ts | 39 +++++++++++++++++++ 8 files changed, 118 insertions(+), 43 deletions(-) create mode 100644 src/codegen/inputs/openapi/utils.ts create mode 100644 test/codegen/inputs/openapi/utils.spec.ts diff --git a/src/codegen/generators/typescript/channels/openapi.ts b/src/codegen/generators/typescript/channels/openapi.ts index 73f801a2..d38f2e96 100644 --- a/src/codegen/generators/typescript/channels/openapi.ts +++ b/src/codegen/generators/typescript/channels/openapi.ts @@ -15,10 +15,11 @@ import {ConstrainedObjectModel} from '@asyncapi/modelina'; import {collectProtocolDependencies} from './utils'; import {renderHttpFetchClient, renderHttpCommonTypes} from './protocols/http'; import {getMessageTypeAndModule} from './utils'; -import {pascalCase} from '../utils'; +import {camelCase} from '../utils'; import {createMissingInputDocumentError} from '../../../errors'; import {resolveImportExtension} from '../../../utils'; import {extractSecuritySchemes} from '../../../inputs/openapi/security'; +import {deriveOperationId} from '../../../inputs/openapi/utils'; type OpenAPIDocument = | OpenAPIV3.Document @@ -172,7 +173,11 @@ function processOperation( return undefined; } - const operationId = getOperationId(operation, method, path); + const operationId = deriveOperationId({ + operationId: operation.operationId, + method, + path + }); const hasBody = METHODS_WITH_BODY.includes(method); // Look up payloads @@ -221,9 +226,12 @@ function processOperation( const description = operation.description ?? operation.summary; const deprecated = operation.deprecated === true; - // Generate the HTTP client function + // Generate the HTTP client function. + // Use the operationId directly as the function name; the HTTP method is + // already encoded in synthesized ids (and meaningful in spec-provided ones), + // so prepending the method here would double the verb (e.g. getGetUser). return renderHttpFetchClient({ - subName: pascalCase(operationId), + functionName: camelCase(operationId), requestMessageModule: hasBody ? requestMessageModule : undefined, requestMessageType: hasBody ? requestMessageType : undefined, replyMessageModule, @@ -267,20 +275,3 @@ function validateOpenAPIContext(context: TypeScriptChannelsContext): { } return {openapiDocument}; } - -/** - * Gets the operation ID from an OpenAPI operation. - * Falls back to generating one from method+path if not present. - */ -function getOperationId( - operation: OpenAPIOperation, - method: string, - path: string -): string { - if (operation.operationId) { - return operation.operationId; - } - // Generate from method + path - const sanitizedPath = path.replace(/[^a-zA-Z0-9]/g, ''); - return `${method}${sanitizedPath}`; -} diff --git a/src/codegen/inputs/openapi/generators/headers.ts b/src/codegen/inputs/openapi/generators/headers.ts index b6febf55..e87e0d84 100644 --- a/src/codegen/inputs/openapi/generators/headers.ts +++ b/src/codegen/inputs/openapi/generators/headers.ts @@ -2,6 +2,7 @@ import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; import {ProcessedHeadersData} from '../../../generators/typescript/headers'; import {pascalCase} from '../../../generators/typescript/utils'; +import {deriveOperationId} from '../utils'; // Helper function to convert OpenAPI header schema to JSON Schema function convertHeaderSchemaToJsonSchema(header: any): any { @@ -54,9 +55,11 @@ function extractHeadersFromOperations( }); if (allParameters.length > 0) { - const operationId = - operationObj.operationId ?? - `${method}${pathKey.replace(/[^a-zA-Z0-9]/g, '')}`; + const operationId = deriveOperationId({ + operationId: operationObj.operationId, + method, + path: pathKey + }); operationHeaders[operationId] = headerParams; } } diff --git a/src/codegen/inputs/openapi/generators/parameters.ts b/src/codegen/inputs/openapi/generators/parameters.ts index 7a9cb3bc..a810b8f3 100644 --- a/src/codegen/inputs/openapi/generators/parameters.ts +++ b/src/codegen/inputs/openapi/generators/parameters.ts @@ -5,6 +5,7 @@ import { pascalCase } from '../../../generators/typescript/utils'; import {ProcessedParameterSchemaData} from '../../asyncapi/generators/parameters'; +import {deriveOperationId} from '../utils'; import { ConstrainedObjectModel, TS_DESCRIPTION_PRESET, @@ -78,9 +79,11 @@ export function processOpenAPIParameters( }); if (filteredParams.length > 0) { - const operationId = - operationObj.operationId ?? - `${method}${pathKey.replace(/[^a-zA-Z0-9]/g, '')}`; + const operationId = deriveOperationId({ + operationId: operationObj.operationId, + method, + path: pathKey + }); // Create schema for the parameters const parameterSchema = createParameterSchema( operationId, diff --git a/src/codegen/inputs/openapi/generators/payloads.ts b/src/codegen/inputs/openapi/generators/payloads.ts index cfcc7fb4..1e834c56 100644 --- a/src/codegen/inputs/openapi/generators/payloads.ts +++ b/src/codegen/inputs/openapi/generators/payloads.ts @@ -4,6 +4,7 @@ import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; import {ProcessedPayloadSchemaData} from '../../asyncapi/generators/payloads'; import {pascalCase} from '../../../generators/typescript/utils'; import {onlyUnique} from '../../../utils'; +import {deriveOperationId} from '../utils'; // Constants const JSON_SCHEMA_DRAFT_07 = 'http://json-schema.org/draft-07/schema'; @@ -145,9 +146,11 @@ function extractPayloadsFromOperations( | OpenAPIV2.OperationObject | OpenAPIV3_1.OperationObject; - const operationId = - operationObj.operationId ?? - `${method}${pathKey.replace(/[^a-zA-Z0-9]/g, '')}`; + const operationId = deriveOperationId({ + operationId: operationObj.operationId, + method, + path: pathKey + }); // Extract request payload schema let requestSchema: any = null; diff --git a/src/codegen/inputs/openapi/generators/types.ts b/src/codegen/inputs/openapi/generators/types.ts index 11083679..d2be662a 100644 --- a/src/codegen/inputs/openapi/generators/types.ts +++ b/src/codegen/inputs/openapi/generators/types.ts @@ -2,6 +2,7 @@ import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; import {TypescriptTypesGeneratorInternal} from '../../../generators/typescript/types'; import {GeneratedFile} from '../../../types'; +import {deriveOperationId} from '../utils'; export interface TypesGeneratorResult { result: string; @@ -47,9 +48,11 @@ export async function generateOpenAPITypes( typeof operationObj === 'object' && method !== 'parameters' ) { - const operationId = - operationObj.operationId ?? - `${method}${pathStr.replace(/[^a-zA-Z0-9]/g, '')}`; + const operationId = deriveOperationId({ + operationId: operationObj.operationId, + method, + path: pathStr + }); operationIds.push(operationId); operationIdToPathMap[operationId] = pathStr; pathOperationIds.push(operationId); diff --git a/src/codegen/inputs/openapi/utils.ts b/src/codegen/inputs/openapi/utils.ts new file mode 100644 index 00000000..a369db11 --- /dev/null +++ b/src/codegen/inputs/openapi/utils.ts @@ -0,0 +1,33 @@ +/** + * Shared helpers for deriving stable identifiers from OpenAPI operations. + */ +import {FormatHelpers} from '@asyncapi/modelina'; + +/** + * Derive the operation identifier used to correlate payloads, parameters, + * headers and channel functions for a single OpenAPI operation. + * + * When the spec provides an `operationId` it is used verbatim so generated + * names stay predictable. Otherwise a name is synthesized from the HTTP method + * and path segments, preserving word boundaries so it can be cased cleanly + * (e.g. `GET /v2/connect/{referenceId}` -> `getV2ConnectReferenceId`) instead + * of collapsing into an uncasable blob (`getv2connectreferenceId`). + */ +export function deriveOperationId({ + operationId, + method, + path +}: { + operationId?: string; + method: string; + path: string; +}): string { + if (operationId) { + return operationId; + } + const segments = path + .split('/') + .map((segment) => segment.replace(/[{}]/g, '')) + .filter((segment) => segment.length > 0); + return FormatHelpers.toCamelCase([method, ...segments].join(' ')); +} diff --git a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap index 4d19322f..1a81c84c 100644 --- a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap @@ -998,7 +998,7 @@ async function handleTokenRefresh( // Generated HTTP Client Functions // ============================================================================ -export interface PostAddPetContext extends HttpClientContext { +export interface AddPetContext extends HttpClientContext { payload: Pet; requestHeaders?: { marshal: () => string }; } @@ -1006,7 +1006,7 @@ export interface PostAddPetContext extends HttpClientContext { /** * HTTP POST request to /pet */ -async function postAddPet(context: PostAddPetContext): Promise> { +async function addPet(context: AddPetContext): Promise> { // Apply defaults const config = { path: '/pet', @@ -1107,7 +1107,7 @@ async function postAddPet(context: PostAddPetContext): Promise string }; } @@ -1129,7 +1129,7 @@ export interface PutUpdatePetContext extends HttpClientContext { /** * HTTP PUT request to /pet */ -async function putUpdatePet(context: PutUpdatePetContext): Promise> { +async function updatePet(context: UpdatePetContext): Promise> { // Apply defaults const config = { path: '/pet', @@ -1230,7 +1230,7 @@ async function putUpdatePet(context: PutUpdatePetContext): Promise string }; requestHeaders?: { marshal: () => string }; } @@ -1252,7 +1252,7 @@ export interface GetFindPetsByStatusAndCategoryContext extends HttpClientContext /** * Find pets by status and category with additional filtering options */ -async function getFindPetsByStatusAndCategory(context: GetFindPetsByStatusAndCategoryContext): Promise> { +async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryContext): Promise> { // Apply defaults const config = { path: '/pet/findByStatus/{status}/{categoryId}', @@ -1353,7 +1353,7 @@ async function getFindPetsByStatusAndCategory(context: GetFindPetsByStatusAndCat headers: responseHeaders, rawData, pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, getFindPetsByStatusAndCategory), + ...createPaginationHelpers(config, paginationInfo, findPetsByStatusAndCategory), }; return result; @@ -1367,7 +1367,7 @@ async function getFindPetsByStatusAndCategory(context: GetFindPetsByStatusAndCat } } -export { postAddPet, putUpdatePet, getFindPetsByStatusAndCategory }; +export { addPet, updatePet, findPetsByStatusAndCategory }; " `; diff --git a/test/codegen/inputs/openapi/utils.spec.ts b/test/codegen/inputs/openapi/utils.spec.ts new file mode 100644 index 00000000..b4adb767 --- /dev/null +++ b/test/codegen/inputs/openapi/utils.spec.ts @@ -0,0 +1,39 @@ +import {deriveOperationId} from '../../../../src/codegen/inputs/openapi/utils'; + +describe('OpenAPI operation id derivation', () => { + describe('deriveOperationId', () => { + it('uses a spec-provided operationId verbatim', () => { + expect( + deriveOperationId({ + operationId: 'findPetsByStatus', + method: 'get', + path: '/pet/findByStatus' + }) + ).toEqual('findPetsByStatus'); + }); + + it('synthesizes a clean camelCase name from method and path', () => { + expect( + deriveOperationId({ + method: 'get', + path: '/v2/connect/{referenceId}' + }) + ).toEqual('getV2ConnectReferenceId'); + }); + + it('preserves word boundaries across kebab-case path segments', () => { + expect( + deriveOperationId({ + method: 'post', + path: '/v2/users/{safepayAccountId}/bank-accounts/add' + }) + ).toEqual('postV2UsersSafepayAccountIdBankAccountsAdd'); + }); + + it('does not double the HTTP verb when synthesizing', () => { + const id = deriveOperationId({method: 'get', path: '/v2/users'}); + expect(id).toEqual('getV2Users'); + expect(id).not.toMatch(/getGet/i); + }); + }); +}); From bb4ef96206a3f908e2fbb06bf9658e8c75916078 Mon Sep 17 00:00:00 2001 From: Lagoni Date: Mon, 6 Jul 2026 21:42:35 +0200 Subject: [PATCH 2/3] fix: send request body for OpenAPI 3.x operations that declare parameters The request-body extractor treated the presence of an operation `parameters` array as an OpenAPI 2.0 signal and looked for a `in: 'body'` parameter, returning nothing for 3.x operations. But 3.x operations routinely carry `parameters` (path/query/header) alongside a `requestBody`, so any such operation (e.g. a POST with an `X-Correlation-Id` header) had its body silently dropped: the generated context had no `payload` field and the client always sent `body = undefined`. Check `requestBody` first (OpenAPI 3.x) and only fall back to the `in: 'body'` parameter form when no `requestBody` is present (OpenAPI 2.0). Co-Authored-By: Claude Opus 4.8 --- .../inputs/openapi/generators/payloads.ts | 20 ++++--- .../generators/typescript/payload.spec.ts | 8 +++ test/codegen/inputs/openapi/payloads.spec.ts | 60 +++++++++++++++++++ 3 files changed, 80 insertions(+), 8 deletions(-) create mode 100644 test/codegen/inputs/openapi/payloads.spec.ts diff --git a/src/codegen/inputs/openapi/generators/payloads.ts b/src/codegen/inputs/openapi/generators/payloads.ts index cfcc7fb4..0a3cefa7 100644 --- a/src/codegen/inputs/openapi/generators/payloads.ts +++ b/src/codegen/inputs/openapi/generators/payloads.ts @@ -149,22 +149,26 @@ function extractPayloadsFromOperations( operationObj.operationId ?? `${method}${pathKey.replace(/[^a-zA-Z0-9]/g, '')}`; - // Extract request payload schema + // Extract request payload schema. + // Prefer the 3.x `requestBody`: 3.x operations also carry a `parameters` + // array (path/query/header), so presence of `parameters` cannot be used + // to detect 2.0 - doing so would drop the body on any 3.x operation that + // declares parameters. The 2.0 body lives in a `parameters` entry with + // `in: 'body'`, so fall back to that only when there is no `requestBody`. let requestSchema: any = null; - // Check if this is OpenAPI 2.0 vs 3.x based on the structure - if ('parameters' in operationObj && operationObj.parameters) { - // OpenAPI 2.0 style - requestSchema = extractOpenAPI2RequestSchema( - operationObj.parameters as OpenAPIV2.ParameterObject[] - ); - } else if ('requestBody' in operationObj && operationObj.requestBody) { + if ('requestBody' in operationObj && operationObj.requestBody) { // OpenAPI 3.x style requestSchema = extractOpenAPI3RequestSchema( operationObj.requestBody as | OpenAPIV3.RequestBodyObject | OpenAPIV3_1.RequestBodyObject ); + } else if ('parameters' in operationObj && operationObj.parameters) { + // OpenAPI 2.0 style (body carried as a `in: 'body'` parameter) + requestSchema = extractOpenAPI2RequestSchema( + operationObj.parameters as OpenAPIV2.ParameterObject[] + ); } if (requestSchema) { diff --git a/test/codegen/generators/typescript/payload.spec.ts b/test/codegen/generators/typescript/payload.spec.ts index 3201b91e..078c9df4 100644 --- a/test/codegen/generators/typescript/payload.spec.ts +++ b/test/codegen/generators/typescript/payload.spec.ts @@ -193,6 +193,10 @@ describe('payloads', () => { key: "createUsersWithListInput", value: "CreateUsersWithListInputRequest", }, + { + key: "updateUser", + value: "AUser", + }, { key: "addPet_Response", value: "APet", @@ -277,6 +281,10 @@ describe('payloads', () => { key: "createUsersWithListInput", value: "CreateUsersWithListInputRequest", }, + { + key: "updateUser", + value: "AUser", + }, { key: "addPet_Response", value: "APet", diff --git a/test/codegen/inputs/openapi/payloads.spec.ts b/test/codegen/inputs/openapi/payloads.spec.ts new file mode 100644 index 00000000..0696791d --- /dev/null +++ b/test/codegen/inputs/openapi/payloads.spec.ts @@ -0,0 +1,60 @@ +import {OpenAPIV3} from 'openapi-types'; +import {processOpenAPIPayloads} from '../../../../src/codegen/inputs/openapi/generators/payloads'; + +describe('OpenAPI payload extraction', () => { + describe('processOpenAPIPayloads', () => { + it('extracts the request body of a 3.x operation that also declares parameters', () => { + // Regression: a 3.x operation carrying both `parameters` (header/path/query) + // and a `requestBody` must not be mistaken for OpenAPI 2.0 - the body was + // previously dropped whenever any parameter was present. + const document: OpenAPIV3.Document = { + openapi: '3.0.0', + info: {title: 'Test API', version: '1.0.0'}, + paths: { + '/things': { + post: { + parameters: [ + { + name: 'X-Correlation-Id', + in: 'header', + schema: {type: 'string'} + } + ], + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + properties: {name: {type: 'string'}} + } + } + } + }, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: {type: 'object', properties: {id: {type: 'string'}}} + } + } + } + } + } + } + } + }; + + const {operationPayloads} = processOpenAPIPayloads(document); + + // The request body surfaces as an operation payload whose schema id ends + // in `Request`; before the fix only the `Response` payload was present. + const schemaIds = Object.values(operationPayloads).map( + (payload) => payload.schemaId + ); + expect(schemaIds.some((id) => id.endsWith('Request'))).toBe(true); + expect(schemaIds.some((id) => id.endsWith('Response'))).toBe(true); + }); + }); +}); From 119347e889acf7637783221bed115e39c16cbaeb Mon Sep 17 00:00:00 2001 From: Lagoni Date: Mon, 6 Jul 2026 21:49:04 +0200 Subject: [PATCH 3/3] docs: add OpenAPI http_client consumer example and usage docs There was no OpenAPI example and no "how do I consume this" entry point for the generated HTTP client, which made the payload/parameter/http_client folder split confusing for API consumers. Add a minimal, self-contained `examples/openapi-http-client` project: a trimmed OpenAPI 3.1 document (operations intentionally without operationIds), the codegen config, the committed generated output, and a runnable demo that builds a request body, supplies a path parameter, and reads a typed response. Document the OpenAPI path and consumer usage in docs/protocols/http_client.md and list the example in the examples index. Co-Authored-By: Claude Opus 4.8 --- docs/protocols/http_client.md | 32 +- examples/README.md | 3 + examples/openapi-http-client/README.md | 62 + .../openapi-http-client/codegen.config.js | 12 + .../openapi-http-client/package-lock.json | 960 ++++++++++++ examples/openapi-http-client/package.json | 34 + .../safepay-nordic-sample.json | 139 ++ .../generated/headers/PostV2ConnectHeaders.ts | 61 + .../src/generated/http_client.ts | 1386 +++++++++++++++++ .../src/generated/index.ts | 3 + .../GetV2ConnectReferenceIdParameters.ts | 120 ++ ...sSafepayAccountIdBankAccountsParameters.ts | 120 ++ .../src/generated/payload/BankAccount.ts | 98 ++ .../src/generated/payload/BankAccountsItem.ts | 98 ++ .../src/generated/payload/GetConnectModel.ts | 99 ++ .../GetV2ConnectReferenceIdResponse_200.ts | 99 ++ ...afepayAccountIdBankAccountsResponse_200.ts | 81 + .../src/generated/payload/InitializeModel.ts | 86 + .../generated/payload/InitializeRequest.ts | 86 + .../generated/payload/PostV2ConnectRequest.ts | 86 + .../payload/PostV2ConnectResponse_200.ts | 86 + .../src/generated/payload/Status.ts | 7 + examples/openapi-http-client/src/index.ts | 63 + 23 files changed, 3820 insertions(+), 1 deletion(-) create mode 100644 examples/openapi-http-client/README.md create mode 100644 examples/openapi-http-client/codegen.config.js create mode 100644 examples/openapi-http-client/package-lock.json create mode 100644 examples/openapi-http-client/package.json create mode 100644 examples/openapi-http-client/safepay-nordic-sample.json create mode 100644 examples/openapi-http-client/src/generated/headers/PostV2ConnectHeaders.ts create mode 100644 examples/openapi-http-client/src/generated/http_client.ts create mode 100644 examples/openapi-http-client/src/generated/index.ts create mode 100644 examples/openapi-http-client/src/generated/parameter/GetV2ConnectReferenceIdParameters.ts create mode 100644 examples/openapi-http-client/src/generated/parameter/GetV2UsersSafepayAccountIdBankAccountsParameters.ts create mode 100644 examples/openapi-http-client/src/generated/payload/BankAccount.ts create mode 100644 examples/openapi-http-client/src/generated/payload/BankAccountsItem.ts create mode 100644 examples/openapi-http-client/src/generated/payload/GetConnectModel.ts create mode 100644 examples/openapi-http-client/src/generated/payload/GetV2ConnectReferenceIdResponse_200.ts create mode 100644 examples/openapi-http-client/src/generated/payload/GetV2UsersSafepayAccountIdBankAccountsResponse_200.ts create mode 100644 examples/openapi-http-client/src/generated/payload/InitializeModel.ts create mode 100644 examples/openapi-http-client/src/generated/payload/InitializeRequest.ts create mode 100644 examples/openapi-http-client/src/generated/payload/PostV2ConnectRequest.ts create mode 100644 examples/openapi-http-client/src/generated/payload/PostV2ConnectResponse_200.ts create mode 100644 examples/openapi-http-client/src/generated/payload/Status.ts create mode 100644 examples/openapi-http-client/src/index.ts diff --git a/docs/protocols/http_client.md b/docs/protocols/http_client.md index ae749a71..4b942efc 100644 --- a/docs/protocols/http_client.md +++ b/docs/protocols/http_client.md @@ -8,7 +8,7 @@ HTTP client generator creates type-safe functions for making HTTP requests based It is currently available through the generators ([channels](../generators/channels.md)): -All of this is available through [AsyncAPI](../inputs/asyncapi.md). [Requires HTTP `method` binding for operation and `statusCode` for messages](../inputs/asyncapi.md#http-client). +This is available through [AsyncAPI](../inputs/asyncapi.md) ([requires the HTTP `method` binding for operations and `statusCode` for messages](../inputs/asyncapi.md#http-client)) and directly from [OpenAPI](../inputs/openapi.md) documents (see [From OpenAPI](#from-openapi) below). ## TypeScript @@ -132,6 +132,36 @@ console.log(response.rawData); // Raw JSON response +### From OpenAPI + +The `http_client` protocol is also generated directly from an OpenAPI document (2.0/3.0/3.1). Each path + method becomes one function. Configure the `channels` generator with `inputType: 'openapi'` and `protocols: ['http_client']`. + +Function names come from each operation's `operationId` (camel-cased). When an operation has **no** `operationId`, a name is synthesized from the method and path, e.g. `GET /v2/connect/{referenceId}` → `getV2ConnectReferenceId`. Give your operations `operationId`s for the cleanest client. + +As a consumer you work with three generated pieces: the **call functions** (`http_client.ts`), the **request/response body models** (`payload/`), and the **path/query parameter models** (`parameter/`): + +```ts +import { http_client } from './__gen__/channels'; +import { PostV2ConnectRequest } from './__gen__/channels/payload/PostV2ConnectRequest'; +import { GetV2ConnectReferenceIdParameters } from './__gen__/channels/parameter/GetV2ConnectReferenceIdParameters'; + +// Request with a body: build the model, pass it as `payload`. +const created = await http_client.postV2Connect({ + server: 'https://api.example.com', + payload: new PostV2ConnectRequest({ returnUrl: 'https://shop.example/return' }) +}); +console.log(created.data.connectUrl); // typed response model + +// Request with a path parameter: supply it through the parameter model. +const connect = await http_client.getV2ConnectReferenceId({ + server: 'https://api.example.com', + parameters: new GetV2ConnectReferenceIdParameters({ referenceId: 'ref_123' }) +}); +console.log(connect.data.safepayAccountId); +``` + +See the runnable [`openapi-http-client` example](https://github.com/the-codegen-project/cli/tree/main/examples/openapi-http-client) for a complete, self-contained setup. + ## Authentication The HTTP client uses a discriminated union for authentication, providing excellent TypeScript autocomplete support. diff --git a/examples/README.md b/examples/README.md index 14f15714..8fd3d96d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -28,6 +28,9 @@ An example demonstrating how to generate complete, type-safe client SDKs for NAT ### [E-commerce AsyncAPI Types](./ecommerce-asyncapi-types/) A comprehensive example showing how to generate TypeScript types from AsyncAPI specifications for an e-commerce messaging system. +### [OpenAPI HTTP Client](./openapi-http-client/) +A minimal, self-contained example of generating a type-safe HTTP client from an OpenAPI document — and how to consume it: building request bodies, supplying path parameters, and reading typed responses. + ## Getting Started 1. Choose an example that matches your use case diff --git a/examples/openapi-http-client/README.md b/examples/openapi-http-client/README.md new file mode 100644 index 00000000..0124c384 --- /dev/null +++ b/examples/openapi-http-client/README.md @@ -0,0 +1,62 @@ +# OpenAPI HTTP Client + +A minimal, self-contained example of generating a **type-safe HTTP client** from an OpenAPI document with the `channels` generator and the `http_client` protocol — and, importantly, how to *consume* it as an API client. + +**Files:** +- `safepay-nordic-sample.json` — a trimmed OpenAPI 3.1 document (a payments-style API). None of its operations declare an `operationId`, so this also demonstrates how function names are synthesized. +- `codegen.config.js` — configuration selecting the `channels` generator with the `http_client` protocol. +- `src/index.ts` — a runnable demo consuming the generated client. +- `src/generated` — the generated output (committed so you can browse it without running anything). + +## Usage + +```bash +npm install +npm run generate # regenerate src/generated from the OpenAPI document +npm run demo # run the consumer demo (set SAFEPAY_SERVER to hit a real server) +``` + +## What gets generated, and what you actually use + +The generator emits a few folders. As a **consumer** you mostly care about one file, and touch the others only through it: + +| Folder / file | What it is | You use it to… | +|---|---|---| +| `generated/http_client.ts` | **The call functions** — one per operation, plus the shared auth/pagination/retry machinery (private). Only the operation functions are exported. | This is your entry point. Import functions from here. | +| `generated/payload/` | Request & response **body** models with `marshal()`/`unmarshal()`. | Build request bodies; read strongly-typed responses off `response.data`. | +| `generated/parameter/` | Path & query **parameter** models. | Supply path/query parameters to operations that need them. | +| `generated/index.ts` | Re-exports the protocol namespace (`http_client`). | `import {http_client} from './generated'`. | + +You rarely call `marshal()`/`unmarshal()` yourself — the client does it. You construct a request-body model, pass it as `payload`, and read the typed result from `response.data`. + +## The three moving parts (see `src/index.ts`) + +```ts +import {http_client} from './generated'; +import {PostV2ConnectRequest} from './generated/payload/PostV2ConnectRequest'; +import {GetV2ConnectReferenceIdParameters} from './generated/parameter/GetV2ConnectReferenceIdParameters'; + +// 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}); +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}); +connect.data.safepayAccountId; // typed +``` + +Every function returns `HttpClientResponse` where `data` is the unmarshalled, typed response model, alongside `status`, `headers`, and pagination helpers. + +## 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: + +| Operation | Generated function | +|---|---| +| `POST /v2/connect` | `postV2Connect` | +| `GET /v2/connect/{referenceId}` | `getV2ConnectReferenceId` | +| `GET /v2/users/{safepayAccountId}/bank-accounts` | `getV2UsersSafepayAccountIdBankAccounts` | + +If your document declares `operationId`s, those are used verbatim (camel-cased) instead — which is almost always what you want. Give your operations meaningful `operationId`s for the cleanest client. diff --git a/examples/openapi-http-client/codegen.config.js b/examples/openapi-http-client/codegen.config.js new file mode 100644 index 00000000..5d03ff00 --- /dev/null +++ b/examples/openapi-http-client/codegen.config.js @@ -0,0 +1,12 @@ +export default { + inputType: 'openapi', + inputPath: './safepay-nordic-sample.json', + generators: [ + { + preset: 'channels', + outputPath: './src/generated', + language: 'typescript', + protocols: ['http_client'] + } + ] +}; diff --git a/examples/openapi-http-client/package-lock.json b/examples/openapi-http-client/package-lock.json new file mode 100644 index 00000000..dfcab781 --- /dev/null +++ b/examples/openapi-http-client/package-lock.json @@ -0,0 +1,960 @@ +{ + "name": "openapi-http-client", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "openapi-http-client", + "version": "1.0.0", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "node-fetch": "^2.6.7" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/node-fetch": "^2.6.11", + "tsx": "^4.0.0", + "typescript": "^5.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz", + "integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tsx": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.0.tgz", + "integrity": "sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } +} diff --git a/examples/openapi-http-client/package.json b/examples/openapi-http-client/package.json new file mode 100644 index 00000000..86ba45c7 --- /dev/null +++ b/examples/openapi-http-client/package.json @@ -0,0 +1,34 @@ +{ + "name": "openapi-http-client", + "version": "1.0.0", + "description": "Example showing the OpenAPI http_client generator: a type-safe HTTP client generated from an OpenAPI document", + "main": "src/index.ts", + "type": "module", + "scripts": { + "generate": "node ../../bin/run.mjs generate codegen.config.js", + "demo": "tsx src/index.ts" + }, + "keywords": [ + "openapi", + "codegen", + "channels", + "http", + "http-client", + "rest", + "api-client" + ], + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "node-fetch": "^2.6.7" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/node-fetch": "^2.6.11", + "tsx": "^4.0.0", + "typescript": "^5.0.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/examples/openapi-http-client/safepay-nordic-sample.json b/examples/openapi-http-client/safepay-nordic-sample.json new file mode 100644 index 00000000..24cba34c --- /dev/null +++ b/examples/openapi-http-client/safepay-nordic-sample.json @@ -0,0 +1,139 @@ +{ + "openapi": "3.1.1", + "info": { + "title": "Safepay API V2 (sample)", + "description": "A trimmed, self-contained slice of a real payments API used to demonstrate the OpenAPI http_client generator. None of the operations declare an operationId, so function names are synthesized from the HTTP method and path.", + "version": "v2" + }, + "servers": [{"url": "https://api.example-safepay.com"}], + "paths": { + "/v2/connect": { + "post": { + "tags": ["Connect"], + "summary": "Start a connect flow", + "description": "Generates a ConnectUrl where the user can be validated and connected.", + "parameters": [ + { + "name": "X-Correlation-Id", + "in": "header", + "description": "Correlation ID used for logging.", + "schema": {"type": "string", "minLength": 20, "maxLength": 64} + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/InitializeRequest"} + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/InitializeModel"} + } + } + }, + "400": {"description": "Bad Request"} + } + } + }, + "/v2/connect/{referenceId}": { + "get": { + "tags": ["Connect"], + "summary": "Get connect information", + "description": "Translate a ReferenceId into a SafepayAccountId.", + "parameters": [ + { + "name": "referenceId", + "in": "path", + "required": true, + "schema": {"type": "string"} + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/GetConnectModel"} + } + } + }, + "404": {"description": "Not Found"} + } + } + }, + "/v2/users/{safepayAccountId}/bank-accounts": { + "get": { + "tags": ["Users"], + "summary": "List bank accounts", + "description": "Returns the bank accounts registered for a Safepay account.", + "parameters": [ + { + "name": "safepayAccountId", + "in": "path", + "required": true, + "schema": {"type": "string"} + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "bankAccounts": { + "type": "array", + "items": {"$ref": "#/components/schemas/BankAccount"} + } + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "InitializeRequest": { + "type": "object", + "required": ["returnUrl"], + "properties": { + "returnUrl": {"type": "string", "format": "uri"}, + "skipKyc": {"type": "boolean"} + } + }, + "InitializeModel": { + "type": "object", + "properties": { + "referenceId": {"type": "string"}, + "connectUrl": {"type": "string", "format": "uri"} + } + }, + "GetConnectModel": { + "type": "object", + "properties": { + "referenceId": {"type": "string"}, + "safepayAccountId": {"type": "string"}, + "status": {"type": "string", "enum": ["pending", "connected", "expired"]} + } + }, + "BankAccount": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "iban": {"type": "string"}, + "isDefault": {"type": "boolean"} + } + } + } + } +} diff --git a/examples/openapi-http-client/src/generated/headers/PostV2ConnectHeaders.ts b/examples/openapi-http-client/src/generated/headers/PostV2ConnectHeaders.ts new file mode 100644 index 00000000..3b43ba90 --- /dev/null +++ b/examples/openapi-http-client/src/generated/headers/PostV2ConnectHeaders.ts @@ -0,0 +1,61 @@ +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; + } + + /** + * 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; + } + +} +export { PostV2ConnectHeaders }; \ 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 new file mode 100644 index 00000000..f4c3c156 --- /dev/null +++ b/examples/openapi-http-client/src/generated/http_client.ts @@ -0,0 +1,1386 @@ +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 {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 {GetV2ConnectReferenceIdParameters} from './parameter/GetV2ConnectReferenceIdParameters'; +import {GetV2UsersSafepayAccountIdBankAccountsParameters} from './parameter/GetV2UsersSafepayAccountIdBankAccountsParameters'; +import {PostV2ConnectHeaders} from './headers/PostV2ConnectHeaders'; +import { URLSearchParams, URL } from 'url'; +import * as NodeFetch from 'node-fetch'; + +// ============================================================================ +// Common Types - Shared across all HTTP client functions +// ============================================================================ + +/** + * Standard HTTP response interface that wraps fetch-like responses + */ +export interface HttpResponse { + ok: boolean; + status: number; + statusText: string; + headers?: Headers | Record; + 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 + */ +export interface HttpClientResponse { + /** The deserialized response payload */ + data: T; + /** HTTP status code */ + status: number; + /** HTTP status text */ + statusText: string; + /** Response headers */ + 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; +} + +/** + * HTTP request parameters passed to the request hook + */ +export interface HttpRequestParams { + url: string; + headers?: Record; + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'; + credentials?: RequestCredentials; + body?: any; +} + +/** + * Token response structure for OAuth2 flows + */ +export interface TokenResponse { + accessToken: string; + refreshToken?: string; + expiresIn?: number; +} + +// ============================================================================ +// Security Configuration Types - Grouped for better autocomplete +// ============================================================================ + +/** + * Bearer token authentication configuration + */ +export interface BearerAuth { + type: 'bearer'; + token: string; +} + +/** + * Basic authentication configuration (username/password) + */ +export interface BasicAuth { + type: 'basic'; + username: string; + password: string; +} + +/** + * API key authentication configuration + */ +export interface ApiKeyAuth { + type: 'apiKey'; + key: string; + name?: string; // Name of the API key parameter (default: 'X-API-Key') + in?: 'header' | 'query'; // Where to place the API key (default: 'header') +} + +/** + * OAuth2 authentication configuration + * + * Supports server-side flows only: + * - client_credentials: Server-to-server authentication + * - password: Resource owner password credentials (legacy, not recommended) + * - Pre-obtained accessToken: For tokens obtained via browser-based flows + * + * For browser-based flows (implicit, authorization_code), obtain the token + * separately and pass it as accessToken. + */ +export interface OAuth2Auth { + type: 'oauth2'; + /** Pre-obtained access token (required if not using a server-side flow) */ + accessToken?: string; + /** Refresh token for automatic token renewal on 401 */ + refreshToken?: string; + /** Token endpoint URL (required for client_credentials/password flows and token refresh) */ + tokenUrl?: string; + /** Client ID (required for flows and token refresh) */ + clientId?: string; + /** Client secret (optional, depends on OAuth provider) */ + clientSecret?: string; + /** Requested scopes */ + scopes?: string[]; + /** Server-side flow type */ + flow?: 'password' | 'client_credentials'; + /** Username for password flow */ + username?: string; + /** Password for password flow */ + password?: string; + /** Callback when tokens are refreshed (for caching/persistence) */ + onTokenRefresh?: (newTokens: TokenResponse) => void; +} + +/** + * Union type for all authentication methods - provides autocomplete support + */ +export type AuthConfig = BearerAuth | BasicAuth | ApiKeyAuth | OAuth2Auth; + +/** + * Feature flags indicating which auth types are available. + * Used internally to conditionally call auth-specific helpers. + */ +const AUTH_FEATURES = { + oauth2: true +} as const; + +/** + * Default values for API key authentication derived from the spec. + * These match the defaults documented in the ApiKeyAuth interface. + */ +const API_KEY_DEFAULTS = { + name: 'X-API-Key', + 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 +// ============================================================================ + +/** + * Retry policy configuration for failed requests + */ +export interface RetryConfig { + maxRetries?: number; // Maximum number of retry attempts (default: 3) + initialDelayMs?: number; // Initial delay before first retry (default: 1000) + maxDelayMs?: number; // Maximum delay between retries (default: 30000) + backoffMultiplier?: number; // Multiplier for exponential backoff (default: 2) + retryableStatusCodes?: number[]; // Status codes to retry (default: [408, 429, 500, 502, 503, 504]) + retryOnNetworkError?: boolean; // Retry on network errors (default: true) + onRetry?: (attempt: number, delay: number, error: Error) => void; // Callback on each retry +} + +// ============================================================================ +// Hooks Configuration - Extensible callback system +// ============================================================================ + +/** + * Hooks for customizing HTTP client behavior + */ +export interface HttpHooks { + /** + * Called before each request to transform/modify the request parameters + * Return modified params or undefined to use original + */ + beforeRequest?: (params: HttpRequestParams) => HttpRequestParams | Promise; + + /** + * The actual request implementation - allows swapping fetch for axios, etc. + * Default: uses node-fetch + */ + makeRequest?: (params: HttpRequestParams) => Promise; + + /** + * Called after each response for logging, metrics, etc. + * Can transform the response before it's processed + */ + afterResponse?: (response: HttpResponse, params: HttpRequestParams) => HttpResponse | Promise; + + /** + * Called on request error for logging, error transformation, etc. + */ + onError?: (error: Error, params: HttpRequestParams) => Error | Promise; +} + +// ============================================================================ +// Common Request Context +// ============================================================================ + +/** + * Base context shared by all HTTP client functions + */ +export interface HttpClientContext { + server?: string; + path?: string; + + // Authentication - grouped for better autocomplete + auth?: AuthConfig; + + // Pagination configuration + pagination?: PaginationConfig; + + // Retry configuration + retry?: RetryConfig; + + // Hooks for extensibility + hooks?: HttpHooks; + + // Additional options + additionalHeaders?: Record; + + // Query parameters + queryParams?: Record; +} + +// ============================================================================ +// Helper Functions - Shared logic extracted for reuse +// ============================================================================ + +/** + * Default retry configuration + */ +const DEFAULT_RETRY_CONFIG: Required = { + maxRetries: 3, + initialDelayMs: 1000, + maxDelayMs: 30000, + backoffMultiplier: 2, + retryableStatusCodes: [408, 429, 500, 502, 503, 504], + retryOnNetworkError: true, + onRetry: () => {}, +}; + +/** + * Default request hook implementation using node-fetch + */ +const defaultMakeRequest = async (params: HttpRequestParams): Promise => { + return NodeFetch.default(params.url, { + body: params.body, + method: params.method, + headers: params.headers + }) as unknown as HttpResponse; +}; + +/** + * Apply authentication to headers and URL based on auth config + */ +function applyAuth( + auth: AuthConfig | undefined, + headers: Record, + url: string +): { headers: Record; url: string } { + if (!auth) return { headers, url }; + + switch (auth.type) { + case 'bearer': + headers['Authorization'] = `Bearer ${auth.token}`; + break; + + case 'basic': { + const credentials = Buffer.from(`${auth.username}:${auth.password}`).toString('base64'); + headers['Authorization'] = `Basic ${credentials}`; + break; + } + + case 'apiKey': { + const keyName = auth.name ?? API_KEY_DEFAULTS.name; + const keyIn = auth.in ?? API_KEY_DEFAULTS.in; + + if (keyIn === 'header') { + headers[keyName] = auth.key; + } else if (keyIn === 'query') { + const separator = url.includes('?') ? '&' : '?'; + url = `${url}${separator}${keyName}=${encodeURIComponent(auth.key)}`; + } else if (keyIn === 'cookie') { + headers['Cookie'] = `${keyName}=${auth.key}`; + } + break; + } + + case 'oauth2': { + // If we have an access token, use it directly + // Token flows (client_credentials, password) are handled separately + if (auth.accessToken) { + headers['Authorization'] = `Bearer ${auth.accessToken}`; + } + break; + } + } + + 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 + */ +function applyQueryParams(queryParams: Record | undefined, url: string): string { + if (!queryParams) return url; + + const params = new URLSearchParams(); + for (const [key, value] of Object.entries(queryParams)) { + if (value !== undefined) { + params.append(key, String(value)); + } + } + + const paramString = params.toString(); + if (!paramString) return url; + + const separator = url.includes('?') ? '&' : '?'; + return `${url}${separator}${paramString}`; +} + +/** + * Sleep for a specified number of milliseconds + */ +function sleep(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +/** + * Calculate delay for exponential backoff + */ +function calculateBackoffDelay( + attempt: number, + config: Required +): number { + const delay = config.initialDelayMs * Math.pow(config.backoffMultiplier, attempt - 1); + return Math.min(delay, config.maxDelayMs); +} + +/** + * Determine if a request should be retried based on error/response + */ +function shouldRetry( + error: Error | null, + response: HttpResponse | null, + config: Required, + attempt: number +): boolean { + if (attempt >= config.maxRetries) return false; + + if (error && config.retryOnNetworkError) return true; + + if (response && config.retryableStatusCodes.includes(response.status)) return true; + + return false; +} + +/** + * Execute request with retry logic + */ +async function executeWithRetry( + params: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + const config = { ...DEFAULT_RETRY_CONFIG, ...retryConfig }; + let lastError: Error | null = null; + let lastResponse: HttpResponse | null = null; + + for (let attempt = 0; attempt <= config.maxRetries; attempt++) { + try { + if (attempt > 0) { + const delay = calculateBackoffDelay(attempt, config); + config.onRetry(attempt, delay, lastError ?? new Error('Retry attempt')); + await sleep(delay); + } + + const response = await makeRequest(params); + + // Check if we should retry this response + if (!shouldRetry(null, response, config, attempt + 1)) { + return response; + } + + lastResponse = response; + lastError = new Error(`HTTP Error: ${response.status} ${response.statusText}`); + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + + if (!shouldRetry(lastError, null, config, attempt + 1)) { + throw lastError; + } + } + } + + // All retries exhausted + if (lastResponse) { + return lastResponse; + } + throw lastError ?? new Error('Request failed after retries'); +} + +/** + * Handle HTTP error status codes with standardized messages + */ +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}`); + } +} + +/** + * Extract headers from response into a plain object + */ +function extractHeaders(response: HttpResponse): Record { + const headers: Record = {}; + + if (response.headers) { + if (typeof (response.headers as any).forEach === 'function') { + // Headers object (fetch API) + (response.headers as Headers).forEach((value, key) => { + headers[key.toLowerCase()] = value; + }); + } else { + // Plain object + for (const [key, value] of Object.entries(response.headers)) { + headers[key.toLowerCase()] = value; + } + } + } + + 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 + * @param pathTemplate - Path template with {param} placeholders + * @param parameters - Parameter object with getChannelWithParameters method + */ +function buildUrlWithParameters string }>( + server: string, + pathTemplate: string, + parameters: T +): string { + const path = parameters.getChannelWithParameters(pathTemplate); + return `${server}${path}`; +} + +/** + * Extracts headers from a typed headers object and merges with additional headers + */ +function applyTypedHeaders( + typedHeaders: { marshal: () => string } | undefined, + additionalHeaders: Record | undefined +): Record { + const headers: Record = { + 'Content-Type': 'application/json', + ...additionalHeaders + }; + + if (typedHeaders) { + // Parse the marshalled headers and merge them + const marshalledHeaders = JSON.parse(typedHeaders.marshal()); + for (const [key, value] of Object.entries(marshalledHeaders)) { + headers[key] = value as string; + } + } + + return headers; +} + +/** + * Validate OAuth2 configuration based on flow type + */ +function validateOAuth2Config(auth: OAuth2Auth): void { + // If using a flow, validate required fields + switch (auth.flow) { + case 'client_credentials': + if (!auth.tokenUrl) throw new Error('OAuth2 Client Credentials flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Client Credentials flow requires clientId'); + break; + + case 'password': + if (!auth.tokenUrl) throw new Error('OAuth2 Password flow requires tokenUrl'); + if (!auth.clientId) throw new Error('OAuth2 Password flow requires clientId'); + if (!auth.username) throw new Error('OAuth2 Password flow requires username'); + if (!auth.password) throw new Error('OAuth2 Password flow requires password'); + break; + + default: + // No flow specified - must have accessToken for OAuth2 to work + if (!auth.accessToken && !auth.flow) { + // This is fine - token refresh can still work if refreshToken is provided + // Or the request will just be made without auth + } + break; + } +} + +/** + * Handle OAuth2 token flows (client_credentials, password) + */ +async function handleOAuth2TokenFlow( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.flow || !auth.tokenUrl) return null; + + const params = new URLSearchParams(); + + if (auth.flow === 'client_credentials') { + params.append('grant_type', 'client_credentials'); + params.append('client_id', auth.clientId!); + } else if (auth.flow === 'password') { + params.append('grant_type', 'password'); + params.append('username', auth.username || ''); + params.append('password', auth.password || ''); + params.append('client_id', auth.clientId!); + } else { + return null; + } + + if (auth.clientSecret) { + params.append('client_secret', auth.clientSecret); + } + if (auth.scopes && auth.scopes.length > 0) { + params.append('scope', auth.scopes.join(' ')); + } + + const authHeaders: Record = { + 'Content-Type': 'application/x-www-form-urlencoded' + }; + + // Use basic auth for client credentials if both client ID and secret are provided + if (auth.flow === 'client_credentials' && auth.clientId && auth.clientSecret) { + const credentials = Buffer.from(`${auth.clientId}:${auth.clientSecret}`).toString('base64'); + authHeaders['Authorization'] = `Basic ${credentials}`; + params.delete('client_id'); + params.delete('client_secret'); + } + + const tokenResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: authHeaders, + body: params.toString() + }); + + if (!tokenResponse.ok) { + throw new Error(`OAuth2 token request failed: ${tokenResponse.statusText}`); + } + + const tokenData = await tokenResponse.json(); + const tokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(tokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = `Bearer ${tokens.accessToken}`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} + +/** + * Handle OAuth2 token refresh on 401 response + */ +async function handleTokenRefresh( + auth: OAuth2Auth, + originalParams: HttpRequestParams, + makeRequest: (params: HttpRequestParams) => Promise, + retryConfig?: RetryConfig +): Promise { + if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; + + const refreshResponse = await NodeFetch.default(auth.tokenUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body: new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: auth.refreshToken, + client_id: auth.clientId, + ...(auth.clientSecret ? { client_secret: auth.clientSecret } : {}) + }).toString() + }); + + if (!refreshResponse.ok) { + throw new Error('Unauthorized'); + } + + const tokenData = await refreshResponse.json(); + const newTokens: TokenResponse = { + accessToken: tokenData.access_token, + refreshToken: tokenData.refresh_token || auth.refreshToken, + expiresIn: tokenData.expires_in + }; + + // Notify the client about the refreshed tokens + if (auth.onTokenRefresh) { + auth.onTokenRefresh(newTokens); + } + + // Retry the original request with the new token + const updatedHeaders = { ...originalParams.headers }; + updatedHeaders['Authorization'] = `Bearer ${newTokens.accessToken}`; + + return executeWithRetry({ ...originalParams, headers: updatedHeaders }, makeRequest, retryConfig); +} +// ============================================================================ +// Generated HTTP Client Functions +// ============================================================================ + +export interface PostV2ConnectContext extends HttpClientContext { + payload: PostV2ConnectRequest; + requestHeaders?: { marshal: () => string }; +} + +/** + * Generates a ConnectUrl where the user can be validated and connected. + */ +async function postV2Connect(context: PostV2ConnectContext): Promise> { + // Apply defaults + const config = { + path: '/v2/connect', + server: 'localhost:3000', + ...context, + }; + + // Validate OAuth2 config if present + if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { + validateOAuth2Config(config.auth); + } + + // Build headers + let headers = context.requestHeaders + ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) + : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + + // Build URL + let url = `${config.server}${config.path}`; + url = applyQueryParams(config.queryParams, url); + + // Apply pagination (can affect URL and/or headers) + const paginationResult = applyPagination(config.pagination, url, headers); + url = paginationResult.url; + headers = paginationResult.headers; + + // Apply authentication + const authResult = applyAuth(config.auth, headers, url); + headers = authResult.headers; + url = authResult.url; + + // Prepare body + const body = context.payload?.marshal(); + + // Determine request function + const makeRequest = config.hooks?.makeRequest ?? defaultMakeRequest; + + // Build request params + let requestParams: HttpRequestParams = { + url, + method: 'POST', + headers, + body + }; + + // Apply beforeRequest hook + if (config.hooks?.beforeRequest) { + requestParams = await config.hooks.beforeRequest(requestParams); + } + + try { + // Execute request with retry logic + let response = await executeWithRetry(requestParams, makeRequest, config.retry); + + // Apply afterResponse hook + if (config.hooks?.afterResponse) { + response = await config.hooks.afterResponse(response, requestParams); + } + + // Handle OAuth2 token flows that require getting a token first + if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { + const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + if (tokenFlowResponse) { + response = tokenFlowResponse; + } + } + + // Handle 401 with token refresh + if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { + try { + const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + if (refreshResponse) { + response = refreshResponse; + } + } catch { + throw new Error('Unauthorized'); + } + } + + // Handle error responses + if (!response.ok) { + handleHttpError(response.status, response.statusText); + } + + // Parse response + const rawData = await response.json(); + const responseData = PostV2ConnectResponse_200.unmarshal(rawData); + + // 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, postV2Connect), + }; + + return result; + + } catch (error) { + // Apply onError hook if present + if (config.hooks?.onError && error instanceof Error) { + throw await config.hooks.onError(error, requestParams); + } + throw error; + } +} + +export interface GetV2ConnectReferenceIdContext extends HttpClientContext { + parameters: { getChannelWithParameters: (path: string) => string }; + requestHeaders?: { marshal: () => string }; +} + +/** + * Translate a ReferenceId into a SafepayAccountId. + */ +async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): Promise> { + // Apply defaults + const config = { + path: '/v2/connect/{referenceId}', + server: 'localhost:3000', + ...context, + }; + + // Validate OAuth2 config if present + if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { + validateOAuth2Config(config.auth); + } + + // Build headers + let headers = context.requestHeaders + ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) + : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + + // Build URL + let url = buildUrlWithParameters(config.server, '/v2/connect/{referenceId}', context.parameters); + url = applyQueryParams(config.queryParams, url); + + // Apply pagination (can affect URL and/or headers) + const paginationResult = applyPagination(config.pagination, url, headers); + url = paginationResult.url; + headers = paginationResult.headers; + + // Apply authentication + const authResult = applyAuth(config.auth, headers, url); + headers = authResult.headers; + url = authResult.url; + + // Prepare body + const body = undefined; + + // Determine request function + const makeRequest = config.hooks?.makeRequest ?? defaultMakeRequest; + + // Build request params + let requestParams: HttpRequestParams = { + url, + method: 'GET', + headers, + body + }; + + // Apply beforeRequest hook + if (config.hooks?.beforeRequest) { + requestParams = await config.hooks.beforeRequest(requestParams); + } + + try { + // Execute request with retry logic + let response = await executeWithRetry(requestParams, makeRequest, config.retry); + + // Apply afterResponse hook + if (config.hooks?.afterResponse) { + response = await config.hooks.afterResponse(response, requestParams); + } + + // Handle OAuth2 token flows that require getting a token first + if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { + const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + if (tokenFlowResponse) { + response = tokenFlowResponse; + } + } + + // Handle 401 with token refresh + if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { + try { + const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + if (refreshResponse) { + response = refreshResponse; + } + } catch { + throw new Error('Unauthorized'); + } + } + + // Handle error responses + if (!response.ok) { + handleHttpError(response.status, response.statusText); + } + + // Parse response + const rawData = await response.json(); + const responseData = GetV2ConnectReferenceIdResponse_200.unmarshal(rawData); + + // 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; + + } catch (error) { + // Apply onError hook if present + if (config.hooks?.onError && error instanceof Error) { + throw await config.hooks.onError(error, requestParams); + } + throw error; + } +} + +export interface GetV2UsersSafepayAccountIdBankAccountsContext extends HttpClientContext { + parameters: { getChannelWithParameters: (path: string) => string }; + requestHeaders?: { marshal: () => string }; +} + +/** + * Returns the bank accounts registered for a Safepay account. + */ +async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepayAccountIdBankAccountsContext): Promise> { + // Apply defaults + const config = { + path: '/v2/users/{safepayAccountId}/bank-accounts', + server: 'localhost:3000', + ...context, + }; + + // Validate OAuth2 config if present + if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { + validateOAuth2Config(config.auth); + } + + // Build headers + let headers = context.requestHeaders + ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) + : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + + // Build URL + let url = buildUrlWithParameters(config.server, '/v2/users/{safepayAccountId}/bank-accounts', context.parameters); + url = applyQueryParams(config.queryParams, url); + + // Apply pagination (can affect URL and/or headers) + const paginationResult = applyPagination(config.pagination, url, headers); + url = paginationResult.url; + headers = paginationResult.headers; + + // Apply authentication + const authResult = applyAuth(config.auth, headers, url); + headers = authResult.headers; + url = authResult.url; + + // Prepare body + const body = undefined; + + // Determine request function + const makeRequest = config.hooks?.makeRequest ?? defaultMakeRequest; + + // Build request params + let requestParams: HttpRequestParams = { + url, + method: 'GET', + headers, + body + }; + + // Apply beforeRequest hook + if (config.hooks?.beforeRequest) { + requestParams = await config.hooks.beforeRequest(requestParams); + } + + try { + // Execute request with retry logic + let response = await executeWithRetry(requestParams, makeRequest, config.retry); + + // Apply afterResponse hook + if (config.hooks?.afterResponse) { + response = await config.hooks.afterResponse(response, requestParams); + } + + // Handle OAuth2 token flows that require getting a token first + if (config.auth?.type === 'oauth2' && !config.auth.accessToken && AUTH_FEATURES.oauth2) { + const tokenFlowResponse = await handleOAuth2TokenFlow(config.auth, requestParams, makeRequest, config.retry); + if (tokenFlowResponse) { + response = tokenFlowResponse; + } + } + + // Handle 401 with token refresh + if (response.status === 401 && config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { + try { + const refreshResponse = await handleTokenRefresh(config.auth, requestParams, makeRequest, config.retry); + if (refreshResponse) { + response = refreshResponse; + } + } catch { + throw new Error('Unauthorized'); + } + } + + // Handle error responses + if (!response.ok) { + handleHttpError(response.status, response.statusText); + } + + // Parse response + const rawData = await response.json(); + const responseData = GetV2UsersSafepayAccountIdBankAccountsResponse_200.unmarshal(rawData); + + // 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; + + } catch (error) { + // Apply onError hook if present + if (config.hooks?.onError && error instanceof Error) { + throw await config.hooks.onError(error, requestParams); + } + throw error; + } +} + +export { postV2Connect, getV2ConnectReferenceId, getV2UsersSafepayAccountIdBankAccounts }; diff --git a/examples/openapi-http-client/src/generated/index.ts b/examples/openapi-http-client/src/generated/index.ts new file mode 100644 index 00000000..5727531d --- /dev/null +++ b/examples/openapi-http-client/src/generated/index.ts @@ -0,0 +1,3 @@ +import * as http_client from './http_client'; + +export {http_client}; diff --git a/examples/openapi-http-client/src/generated/parameter/GetV2ConnectReferenceIdParameters.ts b/examples/openapi-http-client/src/generated/parameter/GetV2ConnectReferenceIdParameters.ts new file mode 100644 index 00000000..d750f956 --- /dev/null +++ b/examples/openapi-http-client/src/generated/parameter/GetV2ConnectReferenceIdParameters.ts @@ -0,0 +1,120 @@ + +class GetV2ConnectReferenceIdParameters { + private _referenceId: string; + + constructor(input: { + referenceId: string, + }) { + this._referenceId = input.referenceId; + } + + get referenceId(): string { return this._referenceId; } + set referenceId(referenceId: string) { this._referenceId = referenceId; } + + + + /** + * Serialize path parameters according to OpenAPI 2.0/3.x specification + * @returns Record of parameter names to their serialized values for path substitution + */ + serializePathParameters(): Record { + const result: Record = {}; + + // Serialize path parameter: referenceId (style: simple, explode: false) + if (this.referenceId !== undefined && this.referenceId !== null) { + const value = this.referenceId; + if (Array.isArray(value)) { + result['referenceId'] = value.map(val => encodeURIComponent(String(val))).join(','); + } else if (typeof value === 'object' && value !== null) { + result['referenceId'] = Object.entries(value).map(([key, val]) => `${encodeURIComponent(key)},${encodeURIComponent(String(val))}`).join(','); + } else { + result['referenceId'] = encodeURIComponent(String(value)); + } + } + + return result; + } + /** + * Get the complete serialized URL with path and query parameters + * @param basePath The base path template (e.g., '/users/{id}') + * @returns The complete URL with serialized parameters + */ + serializeUrl(basePath: string): string { + let url = basePath; + + // Replace path parameters + + const pathParams = this.serializePathParameters(); + for (const [name, value] of Object.entries(pathParams)) { + url = url.replace(new RegExp(`{${name}}`, 'g'), value); + } + + // Add query parameters + + + return url; + } + + /** + * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) + * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') + * @returns The path with parameters replaced + */ + getChannelWithParameters(basePath: string): string { + return this.serializeUrl(basePath); + } + + /** + * Static method to create a new instance from a URL + * @param url The URL to parse + * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') + + * @returns A new GetV2ConnectReferenceIdParameters instance + */ + static fromUrl(url: string, basePath: string): GetV2ConnectReferenceIdParameters { + // Extract path parameters from URL + const pathParams = this.extractPathParameters(url, basePath); + const instance = new GetV2ConnectReferenceIdParameters({ referenceId: pathParams.referenceId }); + return instance; + } + + /** + * Extract path parameters from a URL using a base path template + * @param url The URL to extract parameters from + * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') + * @returns Object containing extracted path parameter values + */ + private static extractPathParameters(url: string, basePath: string): { referenceId: string } { + // Remove query string from URL for path matching + const urlPath = url.split('?')[0]; + + // Create regex pattern from base path template + const regexPattern = basePath.replace(/\{([^}]+)\}/g, '([^/]+)'); + const regex = new RegExp('^' + regexPattern + '$'); + + const match = urlPath.match(regex); + if (!match) { + throw new Error(`URL path '${urlPath}' does not match base path template '${basePath}'`); + } + + // Extract parameter names from base path template + const paramNames = basePath.match(/\{([^}]+)\}/g)?.map(p => p.slice(1, -1)) || []; + + // Map matched values to parameter names + const result: any = {}; + paramNames.forEach((paramName, index) => { + const rawValue = match[index + 1]; + const decodeValue = decodeURIComponent(rawValue); + switch (paramName) { + case 'referenceId': + result.referenceId = decodeValue; + break; + default: + result[paramName] = decodeValue; + } + }); + + return result; + } +} +export { GetV2ConnectReferenceIdParameters }; \ No newline at end of file diff --git a/examples/openapi-http-client/src/generated/parameter/GetV2UsersSafepayAccountIdBankAccountsParameters.ts b/examples/openapi-http-client/src/generated/parameter/GetV2UsersSafepayAccountIdBankAccountsParameters.ts new file mode 100644 index 00000000..67a20ba1 --- /dev/null +++ b/examples/openapi-http-client/src/generated/parameter/GetV2UsersSafepayAccountIdBankAccountsParameters.ts @@ -0,0 +1,120 @@ + +class GetV2UsersSafepayAccountIdBankAccountsParameters { + private _safepayAccountId: string; + + constructor(input: { + safepayAccountId: string, + }) { + this._safepayAccountId = input.safepayAccountId; + } + + get safepayAccountId(): string { return this._safepayAccountId; } + set safepayAccountId(safepayAccountId: string) { this._safepayAccountId = safepayAccountId; } + + + + /** + * Serialize path parameters according to OpenAPI 2.0/3.x specification + * @returns Record of parameter names to their serialized values for path substitution + */ + serializePathParameters(): Record { + const result: Record = {}; + + // Serialize path parameter: safepayAccountId (style: simple, explode: false) + if (this.safepayAccountId !== undefined && this.safepayAccountId !== null) { + const value = this.safepayAccountId; + if (Array.isArray(value)) { + result['safepayAccountId'] = value.map(val => encodeURIComponent(String(val))).join(','); + } else if (typeof value === 'object' && value !== null) { + result['safepayAccountId'] = Object.entries(value).map(([key, val]) => `${encodeURIComponent(key)},${encodeURIComponent(String(val))}`).join(','); + } else { + result['safepayAccountId'] = encodeURIComponent(String(value)); + } + } + + return result; + } + /** + * Get the complete serialized URL with path and query parameters + * @param basePath The base path template (e.g., '/users/{id}') + * @returns The complete URL with serialized parameters + */ + serializeUrl(basePath: string): string { + let url = basePath; + + // Replace path parameters + + const pathParams = this.serializePathParameters(); + for (const [name, value] of Object.entries(pathParams)) { + url = url.replace(new RegExp(`{${name}}`, 'g'), value); + } + + // Add query parameters + + + return url; + } + + /** + * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) + * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') + * @returns The path with parameters replaced + */ + getChannelWithParameters(basePath: string): string { + return this.serializeUrl(basePath); + } + + /** + * Static method to create a new instance from a URL + * @param url The URL to parse + * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') + + * @returns A new GetV2UsersSafepayAccountIdBankAccountsParameters instance + */ + static fromUrl(url: string, basePath: string): GetV2UsersSafepayAccountIdBankAccountsParameters { + // Extract path parameters from URL + const pathParams = this.extractPathParameters(url, basePath); + const instance = new GetV2UsersSafepayAccountIdBankAccountsParameters({ safepayAccountId: pathParams.safepayAccountId }); + return instance; + } + + /** + * Extract path parameters from a URL using a base path template + * @param url The URL to extract parameters from + * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') + * @returns Object containing extracted path parameter values + */ + private static extractPathParameters(url: string, basePath: string): { safepayAccountId: string } { + // Remove query string from URL for path matching + const urlPath = url.split('?')[0]; + + // Create regex pattern from base path template + const regexPattern = basePath.replace(/\{([^}]+)\}/g, '([^/]+)'); + const regex = new RegExp('^' + regexPattern + '$'); + + const match = urlPath.match(regex); + if (!match) { + throw new Error(`URL path '${urlPath}' does not match base path template '${basePath}'`); + } + + // Extract parameter names from base path template + const paramNames = basePath.match(/\{([^}]+)\}/g)?.map(p => p.slice(1, -1)) || []; + + // Map matched values to parameter names + const result: any = {}; + paramNames.forEach((paramName, index) => { + const rawValue = match[index + 1]; + const decodeValue = decodeURIComponent(rawValue); + switch (paramName) { + case 'safepayAccountId': + result.safepayAccountId = decodeValue; + break; + default: + result[paramName] = decodeValue; + } + }); + + return result; + } +} +export { GetV2UsersSafepayAccountIdBankAccountsParameters }; \ No newline at end of file diff --git a/examples/openapi-http-client/src/generated/payload/BankAccount.ts b/examples/openapi-http-client/src/generated/payload/BankAccount.ts new file mode 100644 index 00000000..b56919e9 --- /dev/null +++ b/examples/openapi-http-client/src/generated/payload/BankAccount.ts @@ -0,0 +1,98 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import {default as addFormats} from 'ajv-formats'; +class BankAccount { + private _id?: string; + private _iban?: string; + private _isDefault?: boolean; + private _additionalProperties?: Record; + + constructor(input: { + id?: string, + iban?: string, + isDefault?: boolean, + additionalProperties?: Record, + }) { + this._id = input.id; + this._iban = input.iban; + this._isDefault = input.isDefault; + this._additionalProperties = input.additionalProperties; + } + + get id(): string | undefined { return this._id; } + set id(id: string | undefined) { this._id = id; } + + get iban(): string | undefined { return this._iban; } + set iban(iban: string | undefined) { this._iban = iban; } + + get isDefault(): boolean | undefined { return this._isDefault; } + set isDefault(isDefault: boolean | undefined) { this._isDefault = isDefault; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.id !== undefined) { + json += `"id": ${typeof this.id === 'number' || typeof this.id === 'boolean' ? this.id : JSON.stringify(this.id)},`; + } + if(this.iban !== undefined) { + json += `"iban": ${typeof this.iban === 'number' || typeof this.iban === 'boolean' ? this.iban : JSON.stringify(this.iban)},`; + } + if(this.isDefault !== undefined) { + json += `"isDefault": ${typeof this.isDefault === 'number' || typeof this.isDefault === 'boolean' ? this.isDefault : JSON.stringify(this.isDefault)},`; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //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)},`; + } + } + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): BankAccount { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new BankAccount({} as any); + + if (obj["id"] !== undefined) { + instance.id = obj["id"]; + } + if (obj["iban"] !== undefined) { + instance.iban = obj["iban"]; + } + if (obj["isDefault"] !== undefined) { + instance.isDefault = obj["isDefault"]; + } + + instance.additionalProperties = new Map(); + 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); + } + 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 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; + } + +} +export { BankAccount }; \ No newline at end of file diff --git a/examples/openapi-http-client/src/generated/payload/BankAccountsItem.ts b/examples/openapi-http-client/src/generated/payload/BankAccountsItem.ts new file mode 100644 index 00000000..2f1d8485 --- /dev/null +++ b/examples/openapi-http-client/src/generated/payload/BankAccountsItem.ts @@ -0,0 +1,98 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import {default as addFormats} from 'ajv-formats'; +class BankAccountsItem { + private _id?: string; + private _iban?: string; + private _isDefault?: boolean; + private _additionalProperties?: Record; + + constructor(input: { + id?: string, + iban?: string, + isDefault?: boolean, + additionalProperties?: Record, + }) { + this._id = input.id; + this._iban = input.iban; + this._isDefault = input.isDefault; + this._additionalProperties = input.additionalProperties; + } + + get id(): string | undefined { return this._id; } + set id(id: string | undefined) { this._id = id; } + + get iban(): string | undefined { return this._iban; } + set iban(iban: string | undefined) { this._iban = iban; } + + get isDefault(): boolean | undefined { return this._isDefault; } + set isDefault(isDefault: boolean | undefined) { this._isDefault = isDefault; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.id !== undefined) { + json += `"id": ${typeof this.id === 'number' || typeof this.id === 'boolean' ? this.id : JSON.stringify(this.id)},`; + } + if(this.iban !== undefined) { + json += `"iban": ${typeof this.iban === 'number' || typeof this.iban === 'boolean' ? this.iban : JSON.stringify(this.iban)},`; + } + if(this.isDefault !== undefined) { + json += `"isDefault": ${typeof this.isDefault === 'number' || typeof this.isDefault === 'boolean' ? this.isDefault : JSON.stringify(this.isDefault)},`; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //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)},`; + } + } + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): BankAccountsItem { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new BankAccountsItem({} as any); + + if (obj["id"] !== undefined) { + instance.id = obj["id"]; + } + if (obj["iban"] !== undefined) { + instance.iban = obj["iban"]; + } + if (obj["isDefault"] !== undefined) { + instance.isDefault = obj["isDefault"]; + } + + instance.additionalProperties = new Map(); + 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); + } + return instance; + } + 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. + // 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; + } + +} +export { BankAccountsItem }; \ 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 new file mode 100644 index 00000000..392f53fa --- /dev/null +++ b/examples/openapi-http-client/src/generated/payload/GetConnectModel.ts @@ -0,0 +1,99 @@ +import {Status} from './Status'; +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import {default as addFormats} from 'ajv-formats'; +class GetConnectModel { + private _referenceId?: string; + private _safepayAccountId?: string; + private _status?: Status; + private _additionalProperties?: Record; + + constructor(input: { + referenceId?: string, + safepayAccountId?: string, + status?: Status, + additionalProperties?: Record, + }) { + this._referenceId = input.referenceId; + this._safepayAccountId = input.safepayAccountId; + this._status = input.status; + this._additionalProperties = input.additionalProperties; + } + + get referenceId(): string | undefined { return this._referenceId; } + set referenceId(referenceId: string | undefined) { this._referenceId = referenceId; } + + get safepayAccountId(): string | undefined { return this._safepayAccountId; } + set safepayAccountId(safepayAccountId: string | undefined) { this._safepayAccountId = safepayAccountId; } + + get status(): Status | undefined { return this._status; } + set status(status: Status | undefined) { this._status = status; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.referenceId !== undefined) { + json += `"referenceId": ${typeof this.referenceId === 'number' || typeof this.referenceId === 'boolean' ? this.referenceId : JSON.stringify(this.referenceId)},`; + } + if(this.safepayAccountId !== undefined) { + json += `"safepayAccountId": ${typeof this.safepayAccountId === 'number' || typeof this.safepayAccountId === 'boolean' ? this.safepayAccountId : JSON.stringify(this.safepayAccountId)},`; + } + if(this.status !== undefined) { + json += `"status": ${typeof this.status === 'number' || typeof this.status === 'boolean' ? this.status : JSON.stringify(this.status)},`; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //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)},`; + } + } + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): GetConnectModel { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new GetConnectModel({} as any); + + if (obj["referenceId"] !== undefined) { + instance.referenceId = obj["referenceId"]; + } + if (obj["safepayAccountId"] !== undefined) { + instance.safepayAccountId = obj["safepayAccountId"]; + } + if (obj["status"] !== undefined) { + instance.status = obj["status"]; + } + + instance.additionalProperties = new Map(); + 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); + } + return instance; + } + 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 ?? {}; + // 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; + } + +} +export { GetConnectModel }; \ 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 new file mode 100644 index 00000000..01d4e656 --- /dev/null +++ b/examples/openapi-http-client/src/generated/payload/GetV2ConnectReferenceIdResponse_200.ts @@ -0,0 +1,99 @@ +import {Status} from './Status'; +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import {default as addFormats} from 'ajv-formats'; +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, + }) { + this._referenceId = input.referenceId; + this._safepayAccountId = input.safepayAccountId; + this._status = input.status; + this._additionalProperties = input.additionalProperties; + } + + get referenceId(): string | undefined { return this._referenceId; } + set referenceId(referenceId: string | undefined) { this._referenceId = referenceId; } + + get safepayAccountId(): string | undefined { return this._safepayAccountId; } + set safepayAccountId(safepayAccountId: string | undefined) { this._safepayAccountId = safepayAccountId; } + + get status(): Status | undefined { return this._status; } + set status(status: Status | undefined) { this._status = status; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.referenceId !== undefined) { + json += `"referenceId": ${typeof this.referenceId === 'number' || typeof this.referenceId === 'boolean' ? this.referenceId : JSON.stringify(this.referenceId)},`; + } + if(this.safepayAccountId !== undefined) { + json += `"safepayAccountId": ${typeof this.safepayAccountId === 'number' || typeof this.safepayAccountId === 'boolean' ? this.safepayAccountId : JSON.stringify(this.safepayAccountId)},`; + } + if(this.status !== undefined) { + json += `"status": ${typeof this.status === 'number' || typeof this.status === 'boolean' ? this.status : JSON.stringify(this.status)},`; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //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)},`; + } + } + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): GetV2ConnectReferenceIdResponse_200 { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new GetV2ConnectReferenceIdResponse_200({} as any); + + if (obj["referenceId"] !== undefined) { + instance.referenceId = obj["referenceId"]; + } + if (obj["safepayAccountId"] !== undefined) { + instance.safepayAccountId = obj["safepayAccountId"]; + } + if (obj["status"] !== undefined) { + instance.status = obj["status"]; + } + + instance.additionalProperties = new Map(); + 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); + } + return instance; + } + 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 ?? {}; + // 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; + } + +} +export { GetV2ConnectReferenceIdResponse_200 }; \ 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 new file mode 100644 index 00000000..39d1abf3 --- /dev/null +++ b/examples/openapi-http-client/src/generated/payload/GetV2UsersSafepayAccountIdBankAccountsResponse_200.ts @@ -0,0 +1,81 @@ +import {BankAccountsItem} from './BankAccountsItem'; +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import {default as addFormats} from 'ajv-formats'; +class GetV2UsersSafepayAccountIdBankAccountsResponse_200 { + private _bankAccounts?: BankAccountsItem[]; + private _additionalProperties?: Record; + + constructor(input: { + bankAccounts?: BankAccountsItem[], + additionalProperties?: Record, + }) { + this._bankAccounts = input.bankAccounts; + this._additionalProperties = input.additionalProperties; + } + + get bankAccounts(): BankAccountsItem[] | undefined { return this._bankAccounts; } + set bankAccounts(bankAccounts: BankAccountsItem[] | undefined) { this._bankAccounts = bankAccounts; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + 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(',')}],`; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //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)},`; + } + } + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): GetV2UsersSafepayAccountIdBankAccountsResponse_200 { + const obj = typeof json === "object" ? json : JSON.parse(json); + 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)); + } + + instance.additionalProperties = new Map(); + 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); + } + return instance; + } + 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 ?? {}; + // 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; + } + +} +export { GetV2UsersSafepayAccountIdBankAccountsResponse_200 }; \ 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 new file mode 100644 index 00000000..9d12907a --- /dev/null +++ b/examples/openapi-http-client/src/generated/payload/InitializeModel.ts @@ -0,0 +1,86 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import {default as addFormats} from 'ajv-formats'; +class InitializeModel { + private _referenceId?: string; + private _connectUrl?: string; + private _additionalProperties?: Record; + + constructor(input: { + referenceId?: string, + connectUrl?: string, + additionalProperties?: Record, + }) { + this._referenceId = input.referenceId; + this._connectUrl = input.connectUrl; + this._additionalProperties = input.additionalProperties; + } + + get referenceId(): string | undefined { return this._referenceId; } + set referenceId(referenceId: string | undefined) { this._referenceId = referenceId; } + + get connectUrl(): string | undefined { return this._connectUrl; } + set connectUrl(connectUrl: string | undefined) { this._connectUrl = connectUrl; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.referenceId !== undefined) { + json += `"referenceId": ${typeof this.referenceId === 'number' || typeof this.referenceId === 'boolean' ? this.referenceId : JSON.stringify(this.referenceId)},`; + } + if(this.connectUrl !== undefined) { + json += `"connectUrl": ${typeof this.connectUrl === 'number' || typeof this.connectUrl === 'boolean' ? this.connectUrl : JSON.stringify(this.connectUrl)},`; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //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)},`; + } + } + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): InitializeModel { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new InitializeModel({} as any); + + if (obj["referenceId"] !== undefined) { + instance.referenceId = obj["referenceId"]; + } + if (obj["connectUrl"] !== undefined) { + instance.connectUrl = obj["connectUrl"]; + } + + instance.additionalProperties = new Map(); + 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); + } + return instance; + } + 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 ?? {}; + // 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; + } + +} +export { InitializeModel }; \ 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 new file mode 100644 index 00000000..d4a50a50 --- /dev/null +++ b/examples/openapi-http-client/src/generated/payload/InitializeRequest.ts @@ -0,0 +1,86 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import {default as addFormats} from 'ajv-formats'; +class InitializeRequest { + private _returnUrl: string; + private _skipKyc?: boolean; + private _additionalProperties?: Record; + + constructor(input: { + returnUrl: string, + skipKyc?: boolean, + additionalProperties?: Record, + }) { + this._returnUrl = input.returnUrl; + this._skipKyc = input.skipKyc; + this._additionalProperties = input.additionalProperties; + } + + get returnUrl(): string { return this._returnUrl; } + set returnUrl(returnUrl: string) { this._returnUrl = returnUrl; } + + get skipKyc(): boolean | undefined { return this._skipKyc; } + set skipKyc(skipKyc: boolean | undefined) { this._skipKyc = skipKyc; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.returnUrl !== undefined) { + json += `"returnUrl": ${typeof this.returnUrl === 'number' || typeof this.returnUrl === 'boolean' ? this.returnUrl : JSON.stringify(this.returnUrl)},`; + } + if(this.skipKyc !== undefined) { + json += `"skipKyc": ${typeof this.skipKyc === 'number' || typeof this.skipKyc === 'boolean' ? this.skipKyc : JSON.stringify(this.skipKyc)},`; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //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)},`; + } + } + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): InitializeRequest { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new InitializeRequest({} as any); + + if (obj["returnUrl"] !== undefined) { + instance.returnUrl = obj["returnUrl"]; + } + if (obj["skipKyc"] !== undefined) { + instance.skipKyc = obj["skipKyc"]; + } + + instance.additionalProperties = new Map(); + 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); + } + return instance; + } + 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 ?? {}; + // 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; + } + +} +export { InitializeRequest }; \ 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 new file mode 100644 index 00000000..b7d2c2bb --- /dev/null +++ b/examples/openapi-http-client/src/generated/payload/PostV2ConnectRequest.ts @@ -0,0 +1,86 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import {default as addFormats} from 'ajv-formats'; +class PostV2ConnectRequest { + private _returnUrl: string; + private _skipKyc?: boolean; + private _additionalProperties?: Record; + + constructor(input: { + returnUrl: string, + skipKyc?: boolean, + additionalProperties?: Record, + }) { + this._returnUrl = input.returnUrl; + this._skipKyc = input.skipKyc; + this._additionalProperties = input.additionalProperties; + } + + get returnUrl(): string { return this._returnUrl; } + set returnUrl(returnUrl: string) { this._returnUrl = returnUrl; } + + get skipKyc(): boolean | undefined { return this._skipKyc; } + set skipKyc(skipKyc: boolean | undefined) { this._skipKyc = skipKyc; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.returnUrl !== undefined) { + json += `"returnUrl": ${typeof this.returnUrl === 'number' || typeof this.returnUrl === 'boolean' ? this.returnUrl : JSON.stringify(this.returnUrl)},`; + } + if(this.skipKyc !== undefined) { + json += `"skipKyc": ${typeof this.skipKyc === 'number' || typeof this.skipKyc === 'boolean' ? this.skipKyc : JSON.stringify(this.skipKyc)},`; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //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)},`; + } + } + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): PostV2ConnectRequest { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new PostV2ConnectRequest({} as any); + + if (obj["returnUrl"] !== undefined) { + instance.returnUrl = obj["returnUrl"]; + } + if (obj["skipKyc"] !== undefined) { + instance.skipKyc = obj["skipKyc"]; + } + + instance.additionalProperties = new Map(); + 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); + } + return instance; + } + 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 ?? {}; + // 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; + } + +} +export { PostV2ConnectRequest }; \ 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 new file mode 100644 index 00000000..5b9942a4 --- /dev/null +++ b/examples/openapi-http-client/src/generated/payload/PostV2ConnectResponse_200.ts @@ -0,0 +1,86 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import {default as addFormats} from 'ajv-formats'; +class PostV2ConnectResponse_200 { + private _referenceId?: string; + private _connectUrl?: string; + private _additionalProperties?: Record; + + constructor(input: { + referenceId?: string, + connectUrl?: string, + additionalProperties?: Record, + }) { + this._referenceId = input.referenceId; + this._connectUrl = input.connectUrl; + this._additionalProperties = input.additionalProperties; + } + + get referenceId(): string | undefined { return this._referenceId; } + set referenceId(referenceId: string | undefined) { this._referenceId = referenceId; } + + get connectUrl(): string | undefined { return this._connectUrl; } + set connectUrl(connectUrl: string | undefined) { this._connectUrl = connectUrl; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.referenceId !== undefined) { + json += `"referenceId": ${typeof this.referenceId === 'number' || typeof this.referenceId === 'boolean' ? this.referenceId : JSON.stringify(this.referenceId)},`; + } + if(this.connectUrl !== undefined) { + json += `"connectUrl": ${typeof this.connectUrl === 'number' || typeof this.connectUrl === 'boolean' ? this.connectUrl : JSON.stringify(this.connectUrl)},`; + } + if(this.additionalProperties !== undefined) { + for (const [key, value] of this.additionalProperties.entries()) { + //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)},`; + } + } + //Remove potential last comma + return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; + } + + public static unmarshal(json: string | object): PostV2ConnectResponse_200 { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new PostV2ConnectResponse_200({} as any); + + if (obj["referenceId"] !== undefined) { + instance.referenceId = obj["referenceId"]; + } + if (obj["connectUrl"] !== undefined) { + instance.connectUrl = obj["connectUrl"]; + } + + instance.additionalProperties = new Map(); + 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); + } + return instance; + } + 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 ?? {}; + // 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; + } + +} +export { PostV2ConnectResponse_200 }; \ No newline at end of file diff --git a/examples/openapi-http-client/src/generated/payload/Status.ts b/examples/openapi-http-client/src/generated/payload/Status.ts new file mode 100644 index 00000000..f1a0a8e4 --- /dev/null +++ b/examples/openapi-http-client/src/generated/payload/Status.ts @@ -0,0 +1,7 @@ + +enum Status { + PENDING = "pending", + CONNECTED = "connected", + EXPIRED = "expired", +} +export { Status }; \ No newline at end of file diff --git a/examples/openapi-http-client/src/index.ts b/examples/openapi-http-client/src/index.ts new file mode 100644 index 00000000..9a4eded0 --- /dev/null +++ b/examples/openapi-http-client/src/index.ts @@ -0,0 +1,63 @@ +/** + * Demo: consuming the generated Safepay sample HTTP client. + * + * It shows how the three generated pieces fit together: + * - `payload/` request/response body models (build bodies, read responses) + * - `parameter/` path & query parameter models + * - `http_client.ts` the call functions you actually invoke + * + * Run with `npm run demo`. Without a server it just prints what would be sent; + * set SAFEPAY_SERVER= to perform the requests for real. + */ +import {http_client} from './generated/index'; +import {PostV2ConnectRequest} from './generated/payload/PostV2ConnectRequest'; +import {GetV2ConnectReferenceIdParameters} from './generated/parameter/GetV2ConnectReferenceIdParameters'; + +const server = process.env.SAFEPAY_SERVER; + +async function main() { + // 1. POST /v2/connect - a request WITH a body. + // Build the body with the generated request model and pass it as `payload`; + // the client calls `.marshal()` for you. + const connectBody = new PostV2ConnectRequest({ + returnUrl: 'https://my-shop.example/return', + skipKyc: false + }); + console.log('POST /v2/connect body ->', connectBody.marshal()); + + // 2. GET /v2/connect/{referenceId} - a request WITH a path parameter. + // Path/query params are supplied through the generated parameter model. + const params = new GetV2ConnectReferenceIdParameters({referenceId: 'ref_123'}); + console.log( + 'GET path ->', + params.getChannelWithParameters('/v2/connect/{referenceId}') + ); + + if (!server) { + console.log( + '\nSet SAFEPAY_SERVER= to actually perform the requests.' + ); + return; + } + + // Every call returns an HttpClientResponse where `data` is the unmarshalled, + // fully typed response model. + const created = await http_client.postV2Connect({server, payload: connectBody}); + console.log('connectUrl:', created.data.connectUrl); + + const connect = await http_client.getV2ConnectReferenceId({ + server, + parameters: params + }); + console.log( + 'safepayAccountId:', + connect.data.safepayAccountId, + 'status:', + connect.data.status + ); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +});