diff --git a/docs/generators/client.md b/docs/generators/client.md index c3458a05..2401cc57 100644 --- a/docs/generators/client.md +++ b/docs/generators/client.md @@ -18,21 +18,21 @@ export default { }; ``` -`client` preset with `asyncapi` input generates for each protocol a class (`{Protocol}Client`) that makes it easier to interact with the protocol. +`client` preset generates a class that makes it easier to interact with a protocol. For message brokers (`nats`) this is a `{Protocol}Client` that manages the connection; for `http` it is a single API client class whose name is derived from the input document title (see [HTTP](#http)). It will generate; -- Support function for connecting to the protocol +- Support function for connecting to the protocol (message brokers) or holding shared request configuration (HTTP) - Simpler functions then those generated by [`channels`](./channels.md) to interact with the given protocols - Exporting all generated [`parameters`](./parameters.md) - Exporting all generated [`payloads`](./payloads.md) -This generator uses `channels` generators, in case you dont have any defined, it will automatically include them with default values and dependencies. +This generator uses `channels` generators, in case you dont have any defined, it will automatically include them with default values and dependencies. When a configured protocol has no matching channel functions in the input document, no client is emitted for it. -This is supported through the following inputs: [`asyncapi`](../inputs/asyncapi.md) +This is supported through the following inputs: [`asyncapi`](../inputs/asyncapi.md), [`openapi`](../inputs/openapi.md) (HTTP only) It supports the following languages; [`typescript`](#typescript) -It supports the following protocols; [`nats`](../protocols/nats.md) +It supports the following protocols; [`nats`](../protocols/nats.md), [`http`](../protocols/http_client.md) ## TypeScript @@ -112,4 +112,41 @@ export class NatsClient { publishToChannel subscribeToChannel } -``` \ No newline at end of file +``` + +### HTTP + +For `http` a single API client class is generated that wraps the standalone [`http_client`](../protocols/http_client.md) channel functions. You construct it once with the shared request configuration (`server`, `auth`, `hooks`, retry, pagination, ...) and every operation becomes a method that reuses that configuration; any field can still be overridden per call. + +```js +export default { + inputType: 'openapi', + inputPath: './my-api.json', + generators: [ + { + preset: 'client', + outputPath: './src/__gen__/client', + language: 'typescript', + protocols: ['http'] + } + ] +}; +``` + +The class name is derived from the input document title (a `"Safepay Nordic API"` title yields `SafepayNordicClient`), falling back to `HttpClient`. Override it explicitly with the `clientName` option. + +Example generated usage: +```ts +import {SafepayNordicClient} from './__gen__/client/SafepayNordicClient'; + +const safepay = new SafepayNordicClient({ + server: 'https://api.example.com', + auth: {type: 'bearer', token: process.env.API_TOKEN ?? ''} +}); + +// Shared server/auth are reused; the call only supplies what is operation-specific. +const response = await safepay.getV2Documents(); +console.log(response.status, response.data); +``` + +Each method returns the same `HttpClientResponse` as the underlying channel function, so response metadata (status, headers, pagination helpers) is preserved. \ No newline at end of file diff --git a/examples/openapi-http-client/codegen.config.js b/examples/openapi-http-client/codegen.config.js index 5d03ff00..44100e01 100644 --- a/examples/openapi-http-client/codegen.config.js +++ b/examples/openapi-http-client/codegen.config.js @@ -7,6 +7,13 @@ export default { outputPath: './src/generated', language: 'typescript', protocols: ['http_client'] + }, + { + preset: 'client', + outputPath: './src/generated/client', + language: 'typescript', + protocols: ['http'], + channelsGeneratorId: 'channels-typescript' } ] }; diff --git a/examples/openapi-http-client/src/generated/client/SafepayApiV2SampleClient.ts b/examples/openapi-http-client/src/generated/client/SafepayApiV2SampleClient.ts new file mode 100644 index 00000000..c543e99f --- /dev/null +++ b/examples/openapi-http-client/src/generated/client/SafepayApiV2SampleClient.ts @@ -0,0 +1,69 @@ +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'; +export {PostV2ConnectRequest}; +export {PostV2ConnectResponse_200}; +export {GetV2ConnectReferenceIdResponse_200}; +export {GetV2UsersSafepayAccountIdBankAccountsResponse_200}; +export {Status}; +export {BankAccountsItem}; +export {InitializeRequest}; +export {InitializeModel}; +export {GetConnectModel}; +export {BankAccount}; +import {GetV2ConnectReferenceIdParameters} from './../parameter/GetV2ConnectReferenceIdParameters'; +import {GetV2UsersSafepayAccountIdBankAccountsParameters} from './../parameter/GetV2UsersSafepayAccountIdBankAccountsParameters'; +export {GetV2ConnectReferenceIdParameters}; +export {GetV2UsersSafepayAccountIdBankAccountsParameters}; + +//Import channel functions +import * as http_client from './../http_client'; + +/** + * @class SafepayApiV2SampleClient + * + * A fully-typed HTTP client for the Safepay API V2 (sample). Construct it once with the shared request configuration + * (server, auth, hooks, ...) and call the operation methods; every method + * forwards to the underlying channel function with that configuration applied. + */ +export class SafepayApiV2SampleClient { + /** + * @param config shared HTTP configuration applied to every request. Any field + * can be overridden per call through the method's context argument. + */ + constructor(private readonly config: http_client.HttpClientContext = {}) {} + + /** + * Invokes the `postV2Connect` operation using this client's shared configuration. + * + * @param context per-call request context; overrides any field set on the client. + */ + public async postV2Connect(context: http_client.PostV2ConnectContext): Promise>> { + return http_client.postV2Connect({...this.config, ...context}); + } + + /** + * Invokes the `getV2ConnectReferenceId` operation using this client's shared configuration. + * + * @param context per-call request context; overrides any field set on the client. + */ + public async getV2ConnectReferenceId(context: http_client.GetV2ConnectReferenceIdContext): Promise>> { + return http_client.getV2ConnectReferenceId({...this.config, ...context}); + } + + /** + * Invokes the `getV2UsersSafepayAccountIdBankAccounts` operation using this client's shared configuration. + * + * @param context per-call request context; overrides any field set on the client. + */ + public async getV2UsersSafepayAccountIdBankAccounts(context: http_client.GetV2UsersSafepayAccountIdBankAccountsContext): Promise>> { + return http_client.getV2UsersSafepayAccountIdBankAccounts({...this.config, ...context}); + } +} \ No newline at end of file diff --git a/examples/openapi-http-client/src/index.ts b/examples/openapi-http-client/src/index.ts index 9a4eded0..fe62030e 100644 --- a/examples/openapi-http-client/src/index.ts +++ b/examples/openapi-http-client/src/index.ts @@ -1,15 +1,22 @@ /** * Demo: consuming the generated Safepay sample HTTP client. * - * It shows how the three generated pieces fit together: + * Two ways to call the API are shown: + * - The `client` preset: a single class (`SafepayApiV2SampleClient`) + * constructed once with the shared config (server, auth, ...); every + * operation is a method that reuses that config. + * - The `channels` preset: standalone per-operation functions where each + * call carries its own context. The class simply forwards to these. + * + * The supporting generated pieces: * - `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 {SafepayApiV2SampleClient} from './generated/client/SafepayApiV2SampleClient'; import {PostV2ConnectRequest} from './generated/payload/PostV2ConnectRequest'; import {GetV2ConnectReferenceIdParameters} from './generated/parameter/GetV2ConnectReferenceIdParameters'; @@ -40,21 +47,28 @@ async function main() { 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({ + // Construct the client once with the shared configuration. Every method call + // reuses `server`/`auth` and returns an HttpClientResponse whose `data` is + // the unmarshalled, fully typed response model. + const safepay = new SafepayApiV2SampleClient({ server, - parameters: params + auth: {type: 'bearer', token: process.env.SAFEPAY_TOKEN ?? ''} }); + + const created = await safepay.postV2Connect({payload: connectBody}); + console.log('connectUrl:', created.data.connectUrl); + + const connect = await safepay.getV2ConnectReferenceId({parameters: params}); console.log( 'safepayAccountId:', connect.data.safepayAccountId, 'status:', connect.data.status ); + + // The standalone channel functions remain available for one-off calls where + // constructing a client is unnecessary. + await http_client.postV2Connect({server, payload: connectBody}); } main().catch((error) => { diff --git a/schemas/configuration-schema-0-with-docs.json b/schemas/configuration-schema-0-with-docs.json index 67d272f6..e482912a 100644 --- a/schemas/configuration-schema-0-with-docs.json +++ b/schemas/configuration-schema-0-with-docs.json @@ -47,6 +47,9 @@ { "$ref": "#/definitions/AsyncAPICodegenConfiguration/properties/generators/items/anyOf/2" }, + { + "$ref": "#/definitions/AsyncAPICodegenConfiguration/properties/generators/items/anyOf/3" + }, { "$ref": "#/definitions/AsyncAPICodegenConfiguration/properties/generators/items/anyOf/6" } @@ -499,13 +502,18 @@ "items": { "type": "string", "enum": [ - "nats" + "nats", + "http" ] }, "default": [ "nats" ], - "markdownDescription": "The protocols to generate clients for. The client wraps the protocol channel functions and manages the underlying connection. [Read more about supported protocols here](https://the-codegen-project.org/docs/getting-started/protocols)" + "markdownDescription": "The protocols to generate clients for. The client wraps the protocol channel functions and manages the underlying connection (NATS) or shared request configuration (HTTP). [Read more about supported protocols here](https://the-codegen-project.org/docs/getting-started/protocols)" + }, + "clientName": { + "type": "string", + "markdownDescription": "Overrides the generated HTTP client class name. When omitted, the name is derived from the input document title (e.g. a \"Safepay Nordic API\" title yields \"SafepayNordicClient\"), falling back to \"HttpClient\". Only affects the HTTP protocol client. [Read more about the client generator here](https://the-codegen-project.org/docs/generators/client)" }, "language": { "type": "string", diff --git a/schemas/configuration-schema-0.json b/schemas/configuration-schema-0.json index 5c09fa6b..596307ce 100644 --- a/schemas/configuration-schema-0.json +++ b/schemas/configuration-schema-0.json @@ -47,6 +47,9 @@ { "$ref": "#/definitions/AsyncAPICodegenConfiguration/properties/generators/items/anyOf/2" }, + { + "$ref": "#/definitions/AsyncAPICodegenConfiguration/properties/generators/items/anyOf/3" + }, { "$ref": "#/definitions/AsyncAPICodegenConfiguration/properties/generators/items/anyOf/6" } @@ -499,13 +502,18 @@ "items": { "type": "string", "enum": [ - "nats" + "nats", + "http" ] }, "default": [ "nats" ], - "description": "The protocols to generate clients for. The client wraps the protocol channel functions and manages the underlying connection. Read more about supported protocols here" + "description": "The protocols to generate clients for. The client wraps the protocol channel functions and manages the underlying connection (NATS) or shared request configuration (HTTP). Read more about supported protocols here" + }, + "clientName": { + "type": "string", + "description": "Overrides the generated HTTP client class name. When omitted, the name is derived from the input document title (e.g. a \"Safepay Nordic API\" title yields \"SafepayNordicClient\"), falling back to \"HttpClient\". Only affects the HTTP protocol client. Read more about the client generator here" }, "language": { "type": "string", diff --git a/src/codegen/generators/typescript/client/index.ts b/src/codegen/generators/typescript/client/index.ts index 44628d34..bd4c5182 100644 --- a/src/codegen/generators/typescript/client/index.ts +++ b/src/codegen/generators/typescript/client/index.ts @@ -2,9 +2,11 @@ /* eslint-disable sonarjs/no-duplicate-string */ import { defaultTypeScriptChannelsGenerator, - TypeScriptChannelsGenerator + TypeScriptChannelsGenerator, + TypeScriptChannelRenderType } from '../channels'; import {generateNatsClient} from './protocols/nats'; +import {generateHttpClient} from './protocols/http'; import {TheCodegenConfiguration, GeneratedFile} from '../../../types'; import {joinPath} from '../../../utils'; import { @@ -28,6 +30,20 @@ export { TypeScriptClientGeneratorInternal }; +/** + * Whether the depended-on channels generator rendered any functions for the + * given channel protocol key ('nats', 'http_client', ...). + */ +function hasRenderedFunctions( + context: TypeScriptClientContext, + channelProtocol: string +): boolean { + const channels = context.dependencyOutputs?.[ + context.generator.channelsGeneratorId + ] as TypeScriptChannelRenderType | undefined; + return (channels?.renderedFunctions?.[channelProtocol]?.length ?? 0) > 0; +} + export async function generateTypeScriptClient( context: TypeScriptClientContext ): Promise { @@ -40,13 +56,20 @@ export async function generateTypeScriptClient( } const renderedProtocols: Record = { - nats: '' + nats: '', + http: '' }; const files: GeneratedFile[] = []; for (const protocol of generator.protocols) { switch (protocol) { case 'nats': { + // Skip when the channels generator produced no NATS functions. The + // config merge unions the default protocols with the user's, so an + // HTTP-only client would otherwise emit an empty NatsClient. + if (!hasRenderedFunctions(context, 'nats')) { + break; + } const renderedResult = await generateNatsClient(context); const filePath = joinPath( context.generator.outputPath, @@ -56,6 +79,19 @@ export async function generateTypeScriptClient( renderedProtocols[protocol] = renderedResult; break; } + case 'http': { + if (!hasRenderedFunctions(context, 'http_client')) { + break; + } + const {content, className} = await generateHttpClient(context); + const filePath = joinPath( + context.generator.outputPath, + `${className}.ts` + ); + files.push({path: filePath, content}); + renderedProtocols[protocol] = content; + break; + } default: break; } @@ -80,9 +116,15 @@ export function includeTypeScriptClientDependencies( (generatorSearch) => generatorSearch.id === channelsGeneratorId ) !== undefined; if (!hasChannelsGenerator) { + // The client protocol names map 1:1 to channel protocols except for HTTP, + // where the client exposes 'http' but the channels generator expects + // 'http_client'. + const channelProtocols = generator.protocols?.map((protocol) => + protocol === 'http' ? 'http_client' : protocol + ); const defaultChannelPayloadGenerator: TypeScriptChannelsGenerator = { ...defaultTypeScriptChannelsGenerator, - protocols: generator.protocols, + protocols: channelProtocols, outputPath: joinPath(generator.outputPath ?? '', './channels') }; newGenerators.push(defaultChannelPayloadGenerator); diff --git a/src/codegen/generators/typescript/client/protocols/http.ts b/src/codegen/generators/typescript/client/protocols/http.ts new file mode 100644 index 00000000..0eedb80c --- /dev/null +++ b/src/codegen/generators/typescript/client/protocols/http.ts @@ -0,0 +1,194 @@ +/* eslint-disable security/detect-object-injection */ +/* eslint-disable sonarjs/no-duplicate-string */ +import { + TypeScriptChannelRenderType, + TypeScriptChannelRenderedFunctionType +} from '../../channels'; +import { + ensureRelativePath, + appendImportExtension, + resolveImportExtension, + joinPath, + relativePath +} from '../../../../utils'; +import {pascalCase} from '../../utils'; +import {TypeScriptClientContext} from '..'; +import { + addParametersToDependencies, + addParametersToExports, + addPayloadsToDependencies, + addPayloadsToExports +} from '../../channels/utils'; +import { + createMissingInputDocumentError, + createMissingDependencyOutputError +} from '../../../../errors'; + +const VALID_IDENTIFIER = /^[A-Za-z_$][\w$]*$/; + +/** + * Derives the HTTP client class name. An explicit `clientName` wins; otherwise + * the name is built from the input document title so a "Safepay Nordic API" + * becomes `SafepayNordicClient`, falling back to `HttpClient`. + */ +export function deriveHttpClientName(context: TypeScriptClientContext): string { + const explicit = context.generator.clientName?.trim(); + if (explicit) { + return VALID_IDENTIFIER.test(explicit) ? explicit : pascalCase(explicit); + } + + const title = getDocumentTitle(context); + if (!title) { + return 'HttpClient'; + } + // Drop a trailing "Api" token so titles ending in "API" don't yield the + // awkward "...ApiClient" suffix. + const base = pascalCase(title).replace(/Api$/, ''); + if (base.length === 0) { + return 'HttpClient'; + } + return base.endsWith('Client') ? base : `${base}Client`; +} + +function getDocumentTitle( + context: TypeScriptClientContext +): string | undefined { + if (context.inputType === 'asyncapi' && context.asyncapiDocument) { + return context.asyncapiDocument.info()?.title(); + } + if (context.openapiDocument) { + return context.openapiDocument.info?.title; + } + return undefined; +} + +/** + * Renders a single class method that forwards to the standalone channel + * function, merging the client's shared configuration with the per-call + * context (per-call values win). + */ +function renderHttpClientMethod( + func: TypeScriptChannelRenderedFunctionType +): string { + const {functionName} = func; + const contextType = `http_client.${pascalCase(functionName)}Context`; + // Operations without a request body and without path parameters accept an + // entirely optional context, so the method call can be argument-free. + const needsContext = Boolean(func.messageType) || Boolean(func.parameterType); + const contextDefault = needsContext ? '' : ' = {}'; + return ` + /** + * Invokes the \`${functionName}\` operation using this client's shared configuration. + * + * @param context per-call request context; overrides any field set on the client. + */ + public async ${functionName}(context: ${contextType}${contextDefault}): Promise>> { + return http_client.${functionName}({...this.config, ...context}); + }`; +} + +export async function generateHttpClient( + context: TypeScriptClientContext +): Promise<{content: string; className: string}> { + const {asyncapiDocument, openapiDocument, generator, inputType} = context; + if (inputType === 'asyncapi' && asyncapiDocument === undefined) { + throw createMissingInputDocumentError({ + expectedType: 'asyncapi', + generatorPreset: 'client' + }); + } + if (inputType === 'openapi' && openapiDocument === undefined) { + throw createMissingInputDocumentError({ + expectedType: 'openapi', + generatorPreset: 'client' + }); + } + if (!context.dependencyOutputs) { + throw createMissingDependencyOutputError({ + generatorPreset: 'client', + dependencyName: 'dependencyOutputs' + }); + } + const channels = context.dependencyOutputs[ + generator.channelsGeneratorId + ] as TypeScriptChannelRenderType; + if (!channels) { + throw createMissingDependencyOutputError({ + generatorPreset: 'client', + dependencyName: 'channels' + }); + } + + const renderedFunctions = channels.renderedFunctions; + const renderedHttpFunctions = renderedFunctions['http_client'] ?? []; + + const payloads = channels.payloadRender; + const parameters = channels.parameterRender; + + const dependencies: string[] = []; + const importExtension = resolveImportExtension( + context.generator, + context.config + ); + const modelPayloads = [ + ...Object.values(payloads.operationModels), + ...Object.values(payloads.channelModels), + ...Object.values(payloads.otherModels) + ]; + addPayloadsToDependencies( + modelPayloads, + payloads.generator, + context.generator, + dependencies, + importExtension + ); + addPayloadsToExports(modelPayloads, dependencies); + addParametersToDependencies( + parameters.channelModels, + parameters.generator, + context.generator, + dependencies, + importExtension + ); + addParametersToExports(parameters.channelModels, dependencies); + + const methods = renderedHttpFunctions.map(renderHttpClientMethod); + + const httpChannelsImportPath = relativePath( + context.generator.outputPath, + joinPath(channels.generator.outputPath, 'http_client') + ); + const channelImportPath = appendImportExtension( + `./${ensureRelativePath(httpChannelsImportPath)}`, + importExtension + ); + + const className = deriveHttpClientName(context); + const title = getDocumentTitle(context); + const classDescription = title + ? `A fully-typed HTTP client for the ${title}.` + : 'A fully-typed HTTP client.'; + + const content = `${[...new Set(dependencies)].join('\n')} + +//Import channel functions +import * as http_client from '${channelImportPath}'; + +/** + * @class ${className} + * + * ${classDescription} Construct it once with the shared request configuration + * (server, auth, hooks, ...) and call the operation methods; every method + * forwards to the underlying channel function with that configuration applied. + */ +export class ${className} { + /** + * @param config shared HTTP configuration applied to every request. Any field + * can be overridden per call through the method's context argument. + */ + constructor(private readonly config: http_client.HttpClientContext = {}) {} + ${methods.join('\n')} +}`; + + return {content, className}; +} diff --git a/src/codegen/generators/typescript/client/types.ts b/src/codegen/generators/typescript/client/types.ts index b6ea5e3c..48ad63a8 100644 --- a/src/codegen/generators/typescript/client/types.ts +++ b/src/codegen/generators/typescript/client/types.ts @@ -4,7 +4,7 @@ import {GenericCodegenContext} from '../../../types'; import {zodImportExtension} from '../../../utils'; import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; -export type SupportedProtocols = 'nats'; +export type SupportedProtocols = 'nats' | 'http'; export const zodTypescriptClientGenerator = z.object({ id: z @@ -34,10 +34,16 @@ export const zodTypescriptClientGenerator = z.object({ 'The directory path where the generated client code will be written. [Read more about the client generator here](https://the-codegen-project.org/docs/generators/client)' ), protocols: z - .array(z.enum(['nats'])) + .array(z.enum(['nats', 'http'])) .default(['nats']) .describe( - 'The protocols to generate clients for. The client wraps the protocol channel functions and manages the underlying connection. [Read more about supported protocols here](https://the-codegen-project.org/docs/getting-started/protocols)' + 'The protocols to generate clients for. The client wraps the protocol channel functions and manages the underlying connection (NATS) or shared request configuration (HTTP). [Read more about supported protocols here](https://the-codegen-project.org/docs/getting-started/protocols)' + ), + clientName: z + .string() + .optional() + .describe( + 'Overrides the generated HTTP client class name. When omitted, the name is derived from the input document title (e.g. a "Safepay Nordic API" title yields "SafepayNordicClient"), falling back to "HttpClient". Only affects the HTTP protocol client. [Read more about the client generator here](https://the-codegen-project.org/docs/generators/client)' ), language: z.literal('typescript').optional().default('typescript'), channelsGeneratorId: z diff --git a/src/codegen/types.ts b/src/codegen/types.ts index c6b9c437..bc6edbba 100644 --- a/src/codegen/types.ts +++ b/src/codegen/types.ts @@ -104,6 +104,7 @@ export const zodOpenAPITypeScriptGenerators = z.discriminatedUnion('preset', [ zodTypescriptHeadersGenerator, zodTypescriptTypesGenerator, zodTypescriptChannelsGenerator, + zodTypescriptClientGenerator, zodTypescriptModelsGenerator, zodCustomGenerator ]); diff --git a/test/codegen/generators/typescript/client.spec.ts b/test/codegen/generators/typescript/client.spec.ts index 2d77cc23..63c3b7a1 100644 --- a/test/codegen/generators/typescript/client.spec.ts +++ b/test/codegen/generators/typescript/client.spec.ts @@ -85,5 +85,94 @@ describe('client', () => { }); expect(result.files.length).toBeGreaterThan(0); }); + + it('should generate an HTTP client class from OpenAPI inputs', async () => { + const payloadModel = new OutputModel('', new ConstrainedAnyModel('', undefined, {}, 'Payload'), '', {models: {}, originalInput: undefined}, []); + const parametersDependency: TypeScriptParameterRenderType = { + channelModels: {}, + generator: {outputPath: './test'} as any, + files: [] + }; + const payloadsDependency: TypeScriptPayloadRenderType = { + channelModels: {}, + operationModels: { + getV2Documents: { + messageModel: payloadModel, + messageType: 'GetV2DocumentsResponse_200', + } + }, + otherModels: [], + generator: {outputPath: './test'} as any, + files: [] + }; + const channelsDependency: TypeScriptChannelRenderType = { + payloadRender: payloadsDependency, + result: '', + parameterRender: parametersDependency, + renderedFunctions: { + http_client: [ + { + functionName: 'getV2Documents', + functionType: ChannelFunctionTypes.HTTP_CLIENT, + messageType: '', + replyType: 'GetV2DocumentsResponse_200' + } + ] + }, + generator: defaultTypeScriptChannelsGenerator, + protocolFiles: {}, + files: [] + }; + const result = await generateTypeScriptClient({ + generator: {...defaultTypeScriptClientGenerator, protocols: ['http']}, + inputType: 'openapi', + openapiDocument: {info: {title: 'Safepay API'}} as any, + dependencyOutputs: { + 'parameters-typescript': parametersDependency, + 'payloads-typescript': payloadsDependency, + 'channels-typescript': channelsDependency + } + }); + expect(result.files.length).toBe(1); + expect(result.files[0].path).toContain('SafepayClient.ts'); + expect(result.files[0].content).toContain('export class SafepayClient'); + expect(result.files[0].content).toContain('public async getV2Documents('); + expect(result.files[0].content).toContain('http_client.getV2Documents({...this.config, ...context})'); + }); + + it('does not emit a client for a protocol with no rendered functions', async () => { + const parametersDependency: TypeScriptParameterRenderType = { + channelModels: {}, + generator: {outputPath: './test'} as any, + files: [] + }; + const payloadsDependency: TypeScriptPayloadRenderType = { + channelModels: {}, + operationModels: {}, + otherModels: [], + generator: {outputPath: './test'} as any, + files: [] + }; + const channelsDependency: TypeScriptChannelRenderType = { + payloadRender: payloadsDependency, + result: '', + parameterRender: parametersDependency, + renderedFunctions: {http_client: []}, + generator: defaultTypeScriptChannelsGenerator, + protocolFiles: {}, + files: [] + }; + const result = await generateTypeScriptClient({ + generator: {...defaultTypeScriptClientGenerator, protocols: ['nats', 'http']}, + inputType: 'openapi', + openapiDocument: {info: {title: 'Empty API'}} as any, + dependencyOutputs: { + 'parameters-typescript': parametersDependency, + 'payloads-typescript': payloadsDependency, + 'channels-typescript': channelsDependency + } + }); + expect(result.files.length).toBe(0); + }); }); });