Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 43 additions & 6 deletions docs/generators/client.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -112,4 +112,41 @@ export class NatsClient {
publishToChannel
subscribeToChannel
}
```
```

### 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<T>` as the underlying channel function, so response metadata (status, headers, pagination helpers) is preserved.
7 changes: 7 additions & 0 deletions examples/openapi-http-client/codegen.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
]
};
Original file line number Diff line number Diff line change
@@ -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<Awaited<ReturnType<typeof http_client.postV2Connect>>> {
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<Awaited<ReturnType<typeof http_client.getV2ConnectReferenceId>>> {
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<Awaited<ReturnType<typeof http_client.getV2UsersSafepayAccountIdBankAccounts>>> {
return http_client.getV2UsersSafepayAccountIdBankAccounts({...this.config, ...context});
}
}
32 changes: 23 additions & 9 deletions examples/openapi-http-client/src/index.ts
Original file line number Diff line number Diff line change
@@ -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=<base-url> 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';

Expand Down Expand Up @@ -40,21 +47,28 @@ async function main() {
return;
}

// Every call returns an HttpClientResponse<T> 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<T> 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) => {
Expand Down
12 changes: 10 additions & 2 deletions schemas/configuration-schema-0-with-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down Expand Up @@ -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",
Expand Down
12 changes: 10 additions & 2 deletions schemas/configuration-schema-0.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down Expand Up @@ -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",
Expand Down
48 changes: 45 additions & 3 deletions src/codegen/generators/typescript/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<TypeScriptClientRenderType> {
Expand All @@ -40,13 +56,20 @@ export async function generateTypeScriptClient(
}

const renderedProtocols: Record<SupportedProtocols, string> = {
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,
Expand All @@ -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;
}
Expand All @@ -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);
Expand Down
Loading
Loading