diff --git a/docs/generators/parameters.md b/docs/generators/parameters.md index a58ba0f7..263c5e02 100644 --- a/docs/generators/parameters.md +++ b/docs/generators/parameters.md @@ -24,6 +24,35 @@ This is supported through the following inputs: [`asyncapi`](#inputs), [`openapi It supports the following languages; `typescript` +## Companion Interface + +Every generated parameter model file exports **two** symbols: the parameter +class (`Parameters`) and a plain-data companion interface +(`ParametersInterface`) declared above it. The class constructor takes the +interface (`constructor(input: ParametersInterface)`), so the two always +stay in sync. + +```typescript +export { FindPetsByStatusParameters, FindPetsByStatusParametersInterface }; +``` + +This lets you pass a **plain object** wherever a channel expects parameters — +you do not have to construct the class yourself: + +```typescript +// Both of these are accepted by every generated channel helper: +await publishToUserSignedup({ message, parameters: { myParameter: 'test', enumParameter: 'openapi' }, nc }); +await publishToUserSignedup({ message, parameters: new UserSignedupParameters({ myParameter: 'test', enumParameter: 'openapi' }), nc }); +``` + +Channel consumers type their parameter argument as the union +`ParametersInterface | Parameters` and normalize it to a class +instance internally (via an `instanceof` guard) before using the rich class +behavior (`getChannelWithParameters`, serialization, etc.). The plain-object +form is purely an ergonomic convenience; the generated code always operates on a +class instance. See the [protocols documentation](../protocols) for how each +channel accepts parameters. + ## Inputs ### `asyncapi` diff --git a/examples/openapi-http-client/README.md b/examples/openapi-http-client/README.md index df7b3d1c..df79dba4 100644 --- a/examples/openapi-http-client/README.md +++ b/examples/openapi-http-client/README.md @@ -45,6 +45,11 @@ created.data.connectUrl; // typed const params = new GetV2ConnectReferenceIdParameters({referenceId: 'ref_123'}); const connect = await http_client.getV2ConnectReferenceId({server, parameters: params}); connect.data.safepayAccountId; // typed + +// ...or, more ergonomically, pass a plain object — every parameter model also +// exports a companion interface, and channels accept `Interface | Class` and +// normalize internally. No need to construct the model yourself: +await http_client.getV2ConnectReferenceId({server, parameters: {referenceId: 'ref_123'}}); ``` Every function returns `HttpClientResponse` where `data` is the unmarshalled, typed response model, alongside `status`, `headers`, and pagination helpers. diff --git a/examples/openapi-http-client/src/generated/client/SafepayApiV2SampleClient.ts b/examples/openapi-http-client/src/generated/client/SafepayApiV2SampleClient.ts index c543e99f..02eeaa0f 100644 --- a/examples/openapi-http-client/src/generated/client/SafepayApiV2SampleClient.ts +++ b/examples/openapi-http-client/src/generated/client/SafepayApiV2SampleClient.ts @@ -18,8 +18,8 @@ export {InitializeRequest}; export {InitializeModel}; export {GetConnectModel}; export {BankAccount}; -import {GetV2ConnectReferenceIdParameters} from './../parameter/GetV2ConnectReferenceIdParameters'; -import {GetV2UsersSafepayAccountIdBankAccountsParameters} from './../parameter/GetV2UsersSafepayAccountIdBankAccountsParameters'; +import {GetV2ConnectReferenceIdParameters, GetV2ConnectReferenceIdParametersInterface} from './../parameter/GetV2ConnectReferenceIdParameters'; +import {GetV2UsersSafepayAccountIdBankAccountsParameters, GetV2UsersSafepayAccountIdBankAccountsParametersInterface} from './../parameter/GetV2UsersSafepayAccountIdBankAccountsParameters'; export {GetV2ConnectReferenceIdParameters}; export {GetV2UsersSafepayAccountIdBankAccountsParameters}; diff --git a/examples/openapi-http-client/src/generated/http_client.ts b/examples/openapi-http-client/src/generated/http_client.ts index bc1ad414..c007a463 100644 --- a/examples/openapi-http-client/src/generated/http_client.ts +++ b/examples/openapi-http-client/src/generated/http_client.ts @@ -8,8 +8,8 @@ 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 {GetV2ConnectReferenceIdParameters, GetV2ConnectReferenceIdParametersInterface} from './parameter/GetV2ConnectReferenceIdParameters'; +import {GetV2UsersSafepayAccountIdBankAccountsParameters, GetV2UsersSafepayAccountIdBankAccountsParametersInterface} from './parameter/GetV2UsersSafepayAccountIdBankAccountsParameters'; import {PostV2ConnectHeaders} from './headers/PostV2ConnectHeaders'; // ============================================================================ @@ -1119,7 +1119,7 @@ async function postV2Connect(context: PostV2ConnectContext): Promise string }; + parameters: GetV2ConnectReferenceIdParametersInterface | GetV2ConnectReferenceIdParameters; requestHeaders?: { marshal: () => string }; } @@ -1163,6 +1163,8 @@ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): ...context, }; + const parameters = context.parameters instanceof GetV2ConnectReferenceIdParameters ? context.parameters : new GetV2ConnectReferenceIdParameters(context.parameters); + // Validate OAuth2 config if present if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { validateOAuth2Config(config.auth); @@ -1174,7 +1176,7 @@ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = buildUrlWithParameters(config.server, '/v2/connect/{referenceId}', context.parameters); + let url = buildUrlWithParameters(config.server, '/v2/connect/{referenceId}', parameters); url = applyQueryParams(config.queryParams, url); // Apply pagination (can affect URL and/or headers) @@ -1242,7 +1244,7 @@ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): // Parse response const rawData = await response.json(); - const responseData = GetV2ConnectReferenceIdResponse_200.unmarshal(rawData); + const responseData = GetV2ConnectReferenceIdResponse_200.unmarshal(JSON.stringify(rawData)); // Extract response metadata const responseHeaders = extractHeaders(response); @@ -1271,7 +1273,7 @@ async function getV2ConnectReferenceId(context: GetV2ConnectReferenceIdContext): } export interface GetV2UsersSafepayAccountIdBankAccountsContext extends HttpClientContext { - parameters: { getChannelWithParameters: (path: string) => string }; + parameters: GetV2UsersSafepayAccountIdBankAccountsParametersInterface | GetV2UsersSafepayAccountIdBankAccountsParameters; requestHeaders?: { marshal: () => string }; } @@ -1286,6 +1288,8 @@ async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepay ...context, }; + const parameters = context.parameters instanceof GetV2UsersSafepayAccountIdBankAccountsParameters ? context.parameters : new GetV2UsersSafepayAccountIdBankAccountsParameters(context.parameters); + // Validate OAuth2 config if present if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { validateOAuth2Config(config.auth); @@ -1297,7 +1301,7 @@ async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepay : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = buildUrlWithParameters(config.server, '/v2/users/{safepayAccountId}/bank-accounts', context.parameters); + let url = buildUrlWithParameters(config.server, '/v2/users/{safepayAccountId}/bank-accounts', parameters); url = applyQueryParams(config.queryParams, url); // Apply pagination (can affect URL and/or headers) @@ -1365,7 +1369,7 @@ async function getV2UsersSafepayAccountIdBankAccounts(context: GetV2UsersSafepay // Parse response const rawData = await response.json(); - const responseData = GetV2UsersSafepayAccountIdBankAccountsResponse_200.unmarshal(rawData); + const responseData = GetV2UsersSafepayAccountIdBankAccountsResponse_200.unmarshal(JSON.stringify(rawData)); // Extract response metadata const responseHeaders = extractHeaders(response); diff --git a/examples/openapi-http-client/src/generated/parameter/GetV2ConnectReferenceIdParameters.ts b/examples/openapi-http-client/src/generated/parameter/GetV2ConnectReferenceIdParameters.ts index d750f956..7a73074a 100644 --- a/examples/openapi-http-client/src/generated/parameter/GetV2ConnectReferenceIdParameters.ts +++ b/examples/openapi-http-client/src/generated/parameter/GetV2ConnectReferenceIdParameters.ts @@ -1,10 +1,11 @@ +interface GetV2ConnectReferenceIdParametersInterface { + referenceId: string +} class GetV2ConnectReferenceIdParameters { private _referenceId: string; - constructor(input: { - referenceId: string, - }) { + constructor(input: GetV2ConnectReferenceIdParametersInterface) { this._referenceId = input.referenceId; } @@ -117,4 +118,4 @@ class GetV2ConnectReferenceIdParameters { return result; } } -export { GetV2ConnectReferenceIdParameters }; \ No newline at end of file +export { GetV2ConnectReferenceIdParameters, GetV2ConnectReferenceIdParametersInterface }; \ 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 index 67a20ba1..971de2cb 100644 --- a/examples/openapi-http-client/src/generated/parameter/GetV2UsersSafepayAccountIdBankAccountsParameters.ts +++ b/examples/openapi-http-client/src/generated/parameter/GetV2UsersSafepayAccountIdBankAccountsParameters.ts @@ -1,10 +1,11 @@ +interface GetV2UsersSafepayAccountIdBankAccountsParametersInterface { + safepayAccountId: string +} class GetV2UsersSafepayAccountIdBankAccountsParameters { private _safepayAccountId: string; - constructor(input: { - safepayAccountId: string, - }) { + constructor(input: GetV2UsersSafepayAccountIdBankAccountsParametersInterface) { this._safepayAccountId = input.safepayAccountId; } @@ -117,4 +118,4 @@ class GetV2UsersSafepayAccountIdBankAccountsParameters { return result; } } -export { GetV2UsersSafepayAccountIdBankAccountsParameters }; \ No newline at end of file +export { GetV2UsersSafepayAccountIdBankAccountsParameters, GetV2UsersSafepayAccountIdBankAccountsParametersInterface }; \ 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 fe62030e..1de8caaf 100644 --- a/examples/openapi-http-client/src/index.ts +++ b/examples/openapi-http-client/src/index.ts @@ -66,6 +66,14 @@ async function main() { connect.data.status ); + // Ergonomic alternative: pass a plain object satisfying the parameter + // interface. The channel normalizes it to a class instance internally, so + // you get identical behavior without constructing the model yourself. + const connectPlain = await safepay.getV2ConnectReferenceId({ + parameters: {referenceId: 'ref_123'} + }); + console.log('safepayAccountId (plain object):', connectPlain.data.safepayAccountId); + // The standalone channel functions remain available for one-off calls where // constructing a client is unnecessary. await http_client.postV2Connect({server, payload: connectBody}); diff --git a/src/codegen/generators/typescript/channels/protocols/amqp/publishExchange.ts b/src/codegen/generators/typescript/channels/protocols/amqp/publishExchange.ts index 217fdaf1..ca629ae4 100644 --- a/src/codegen/generators/typescript/channels/protocols/amqp/publishExchange.ts +++ b/src/codegen/generators/typescript/channels/protocols/amqp/publishExchange.ts @@ -3,7 +3,11 @@ import {ChannelFunctionTypes} from '../..'; import {SingleFunctionRenderType} from '../../../../../types'; import {pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; -import {renderChannelJSDoc} from '../../utils'; +import { + parameterInstanceExpression, + parameterUnionType, + renderChannelJSDoc +} from '../../utils'; export function renderPublishExchange({ topic, @@ -20,7 +24,7 @@ export function renderPublishExchange({ exchange: string | undefined; }>): SingleFunctionRenderType { const addressToUse = channelParameters - ? `parameters.getChannelWithParameters('${topic}')` + ? `${parameterInstanceExpression({modelName: channelParameters.type, source: 'parameters'})}.getChannelWithParameters('${topic}')` : `'${topic}'`; let messageMarshalling = 'message.marshal()'; if (messageModule) { @@ -59,7 +63,7 @@ channel.publish(exchange, routingKey, Buffer.from(dataToSend), publishOptions);` ? [ { parameter: `parameters`, - parameterType: `parameters: ${channelParameters.type}`, + parameterType: `parameters: ${parameterUnionType(channelParameters.type)}`, jsDoc: ' * @param parameters for topic substitution' } ] diff --git a/src/codegen/generators/typescript/channels/protocols/amqp/publishQueue.ts b/src/codegen/generators/typescript/channels/protocols/amqp/publishQueue.ts index c9509e52..ba8dfdca 100644 --- a/src/codegen/generators/typescript/channels/protocols/amqp/publishQueue.ts +++ b/src/codegen/generators/typescript/channels/protocols/amqp/publishQueue.ts @@ -3,7 +3,11 @@ import {ChannelFunctionTypes} from '../..'; import {SingleFunctionRenderType} from '../../../../../types'; import {pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; -import {renderChannelJSDoc} from '../../utils'; +import { + parameterInstanceExpression, + parameterUnionType, + renderChannelJSDoc +} from '../../utils'; export function renderPublishQueue({ topic, @@ -17,7 +21,7 @@ export function renderPublishQueue({ deprecated }: RenderRegularParameters): SingleFunctionRenderType { const addressToUse = channelParameters - ? `parameters.getChannelWithParameters('${topic}')` + ? `${parameterInstanceExpression({modelName: channelParameters.type, source: 'parameters'})}.getChannelWithParameters('${topic}')` : `'${topic}'`; let messageMarshalling = 'message.marshal()'; if (messageModule) { @@ -56,7 +60,7 @@ channel.sendToQueue(queue, Buffer.from(dataToSend), publishOptions);`; ? [ { parameter: `parameters`, - parameterType: `parameters: ${channelParameters.type}`, + parameterType: `parameters: ${parameterUnionType(channelParameters.type)}`, jsDoc: ' * @param parameters for topic substitution' } ] diff --git a/src/codegen/generators/typescript/channels/protocols/amqp/subscribeQueue.ts b/src/codegen/generators/typescript/channels/protocols/amqp/subscribeQueue.ts index 22f1256e..6ed2e6ed 100644 --- a/src/codegen/generators/typescript/channels/protocols/amqp/subscribeQueue.ts +++ b/src/codegen/generators/typescript/channels/protocols/amqp/subscribeQueue.ts @@ -2,7 +2,12 @@ import {ChannelFunctionTypes} from '../..'; import {SingleFunctionRenderType} from '../../../../../types'; import {pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; -import {getValidationFunctions, renderChannelJSDoc} from '../../utils'; +import { + getValidationFunctions, + parameterInstanceExpression, + parameterUnionType, + renderChannelJSDoc +} from '../../utils'; export function renderSubscribeQueue({ topic, @@ -18,7 +23,7 @@ export function renderSubscribeQueue({ }: RenderRegularParameters): SingleFunctionRenderType { const includeValidation = payloadGenerator.generator.includeValidation; const addressToUse = channelParameters - ? `parameters.getChannelWithParameters('${topic}')` + ? `${parameterInstanceExpression({modelName: channelParameters.type, source: 'parameters'})}.getChannelWithParameters('${topic}')` : `'${topic}'`; const messageUnmarshalling = `${messageModule ?? messageType}.unmarshal(receivedData)`; messageType = messageModule ? `${messageModule}.${messageType}` : messageType; @@ -95,7 +100,7 @@ channel.consume(queue, (msg) => { ? [ { parameter: `parameters`, - parameterType: `parameters: ${channelParameters.type}`, + parameterType: `parameters: ${parameterUnionType(channelParameters.type)}`, jsDoc: ' * @param parameters for topic substitution' } ] diff --git a/src/codegen/generators/typescript/channels/protocols/eventsource/fetch.ts b/src/codegen/generators/typescript/channels/protocols/eventsource/fetch.ts index 998cbec1..8c5b4e8b 100644 --- a/src/codegen/generators/typescript/channels/protocols/eventsource/fetch.ts +++ b/src/codegen/generators/typescript/channels/protocols/eventsource/fetch.ts @@ -6,7 +6,12 @@ import { defaultTypeScriptChannelsGenerator, RenderRegularParameters } from '../../types'; -import {getValidationFunctions, renderChannelJSDoc} from '../../utils'; +import { + getValidationFunctions, + parameterInstanceExpression, + parameterUnionType, + renderChannelJSDoc +} from '../../utils'; export function renderFetch({ topic, @@ -27,7 +32,7 @@ export function renderFetch({ }>): SingleFunctionRenderType { const includeValidation = payloadGenerator.generator.includeValidation; const addressToUse = channelParameters - ? `parameters.getChannelWithParameters('${topic}')` + ? `${parameterInstanceExpression({modelName: channelParameters.type, source: 'parameters'})}.getChannelWithParameters('${topic}')` : `'${topic}'`; const messageUnmarshalling = `${messageModule ?? messageType}.unmarshal(receivedData)`; messageType = messageModule ? `${messageModule}.${messageType}` : messageType; @@ -49,7 +54,7 @@ export function renderFetch({ ? [ { parameter: `parameters`, - parameterType: `parameters: ${channelParameters.type}`, + parameterType: `parameters: ${parameterUnionType(channelParameters.type)}`, jsDoc: ' * @param parameters for listening' } ] diff --git a/src/codegen/generators/typescript/channels/protocols/http/client.ts b/src/codegen/generators/typescript/channels/protocols/http/client.ts index ffc56eb3..b0e8ba49 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/client.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/client.ts @@ -5,7 +5,11 @@ import {HttpRenderType} from '../../../../../types'; import {pascalCase} from '../../../utils'; import {ChannelFunctionTypes, RenderHttpParameters} from '../../types'; -import {renderChannelJSDoc} from '../../utils'; +import { + parameterUnionType, + renderParameterNormalization, + renderChannelJSDoc +} from '../../utils'; /** * Renders an HTTP fetch client function for a specific API operation. @@ -68,6 +72,7 @@ export function renderHttpFetchClient({ messageType, requestTopic, hasParameters, + parameterModelName: channelParameters?.name, hasHeaders, headersType: channelHeaders?.type, hasSerializeHeaders, @@ -110,11 +115,13 @@ function generateContextInterface( fields.push(` payload: ${messageType};`); } - // Reference the concrete generated parameter class so consumers get typed - // query/path parameters. It still exposes getChannelWithParameters, so the - // buildUrlWithParameters call in the function body keeps working. + // Add parameters field if the operation has path parameters. The field + // accepts either a plain object satisfying the parameter interface + // (ergonomic) or a concrete parameter class instance (rich behavior); the + // function body normalizes it to an instance before use — the normalized + // instance still exposes getChannelWithParameters for buildUrlWithParameters. if (parametersType) { - fields.push(` parameters: ${parametersType};`); + fields.push(` parameters: ${parameterUnionType(parametersType)};`); } // Emit requestHeaders only when the spec defines operation headers so the @@ -128,6 +135,29 @@ function generateContextInterface( return `export interface ${interfaceName} extends HttpClientContext {${fieldsStr}}`; } +/** + * Generate the `let headers = ...` initializer. Operations without spec-defined + * headers get a plain default; operations with headers either serialize a typed + * header model or merge the raw typed headers via applyTypedHeaders. + */ +function generateHeadersInit(params: { + hasHeaders: boolean; + headersType: string | undefined; + hasSerializeHeaders: boolean; +}): string { + const {hasHeaders, headersType, hasSerializeHeaders} = params; + + if (!hasHeaders) { + return `let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record;`; + } + if (hasSerializeHeaders) { + return `let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders, ...(context.requestHeaders ? serialize${headersType}Headers(context.requestHeaders) : {}) } as Record;`; + } + return `let headers = context.requestHeaders + ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) + : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record;`; +} + /** * Generate the function implementation */ @@ -140,6 +170,7 @@ function generateFunctionImplementation(params: { messageType: string | undefined; requestTopic: string; hasParameters: boolean; + parameterModelName: string | undefined; hasHeaders: boolean; headersType: string | undefined; hasSerializeHeaders: boolean; @@ -158,6 +189,7 @@ function generateFunctionImplementation(params: { messageType, requestTopic, hasParameters, + parameterModelName, hasHeaders, headersType, hasSerializeHeaders, @@ -172,22 +204,28 @@ function generateFunctionImplementation(params: { const hasBody = messageType && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase()); + // Normalize the user-provided parameters (interface object or class instance) + // to a concrete class instance so the URL builder gets the rich behavior. + const parameterNormalization = + hasParameters && parameterModelName + ? ` ${renderParameterNormalization({ + modelName: parameterModelName, + source: 'context.parameters', + target: 'parameters' + })}\n\n` + : ''; + // Generate URL building code const urlBuildCode = hasParameters - ? `let url = buildUrlWithParameters(config.baseUrl, '${requestTopic}', context.parameters);` + ? `let url = buildUrlWithParameters(config.baseUrl, '${requestTopic}', parameters);` : `let url = \`\${config.baseUrl}${requestTopic}\`;`; // Generate headers initialization - let headersInit: string; - if (!hasHeaders) { - headersInit = `let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record;`; - } else if (hasSerializeHeaders) { - headersInit = `let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders, ...(context.requestHeaders ? serialize${headersType}Headers(context.requestHeaders) : {}) } as Record;`; - } else { - headersInit = `let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) - : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record;`; - } + const headersInit = generateHeadersInit({ + hasHeaders, + headersType, + hasSerializeHeaders + }); // Generate body preparation const bodyPrep = hasBody @@ -257,7 +295,7 @@ async function ${functionName}(context: ${contextInterfaceName}${contextDefault} ...context, }; -${oauth2ValidateBlock} // Build headers +${parameterNormalization}${oauth2ValidateBlock} // Build headers ${headersInit} // Build URL diff --git a/src/codegen/generators/typescript/channels/protocols/kafka/publish.ts b/src/codegen/generators/typescript/channels/protocols/kafka/publish.ts index d059bdb5..5c70f9c8 100644 --- a/src/codegen/generators/typescript/channels/protocols/kafka/publish.ts +++ b/src/codegen/generators/typescript/channels/protocols/kafka/publish.ts @@ -3,7 +3,11 @@ import {ChannelFunctionTypes} from '../..'; import {SingleFunctionRenderType} from '../../../../../types'; import {pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; -import {renderChannelJSDoc} from '../../utils'; +import { + parameterInstanceExpression, + parameterUnionType, + renderChannelJSDoc +} from '../../utils'; export function renderPublish({ topic, @@ -17,7 +21,7 @@ export function renderPublish({ deprecated }: RenderRegularParameters): SingleFunctionRenderType { const addressToUse = channelParameters - ? `parameters.getChannelWithParameters('${topic}')` + ? `${parameterInstanceExpression({modelName: channelParameters.type, source: 'parameters'})}.getChannelWithParameters('${topic}')` : `'${topic}'`; let messageMarshalling = 'message.marshal()'; if (messageModule) { @@ -39,7 +43,7 @@ export function renderPublish({ ? [ { parameter: `parameters`, - parameterType: `parameters: ${channelParameters.type}`, + parameterType: `parameters: ${parameterUnionType(channelParameters.type)}`, jsDoc: ' * @param parameters for topic substitution' } ] diff --git a/src/codegen/generators/typescript/channels/protocols/kafka/subscribe.ts b/src/codegen/generators/typescript/channels/protocols/kafka/subscribe.ts index 679679e9..2e75b2a3 100644 --- a/src/codegen/generators/typescript/channels/protocols/kafka/subscribe.ts +++ b/src/codegen/generators/typescript/channels/protocols/kafka/subscribe.ts @@ -4,7 +4,12 @@ import {ChannelFunctionTypes} from '../..'; import {SingleFunctionRenderType} from '../../../../../types'; import {findRegexFromChannel, pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; -import {getValidationFunctions, renderChannelJSDoc} from '../../utils'; +import { + getValidationFunctions, + parameterInstanceExpression, + parameterUnionType, + renderChannelJSDoc +} from '../../utils'; import {generateKafkaMessageReceivingCode} from './utils'; export function renderSubscribe({ @@ -21,7 +26,7 @@ export function renderSubscribe({ }: RenderRegularParameters): SingleFunctionRenderType { const includeValidation = payloadGenerator.generator.includeValidation; const addressToUse = channelParameters - ? `parameters.getChannelWithParameters('${topic}')` + ? `${parameterInstanceExpression({modelName: channelParameters.type, source: 'parameters'})}.getChannelWithParameters('${topic}')` : `'${topic}'`; const messageUnmarshalling = `${messageModule ?? messageType}.unmarshal(receivedData)`; messageType = messageModule ? `${messageModule}.${messageType}` : messageType; @@ -82,7 +87,7 @@ export function renderSubscribe({ ? [ { parameter: `parameters`, - parameterType: `parameters: ${channelParameters.type}`, + parameterType: `parameters: ${parameterUnionType(channelParameters.type)}`, jsDoc: ' * @param parameters for topic substitution' } ] diff --git a/src/codegen/generators/typescript/channels/protocols/mqtt/publish.ts b/src/codegen/generators/typescript/channels/protocols/mqtt/publish.ts index 517468a0..7c204c8f 100644 --- a/src/codegen/generators/typescript/channels/protocols/mqtt/publish.ts +++ b/src/codegen/generators/typescript/channels/protocols/mqtt/publish.ts @@ -3,7 +3,11 @@ import {ChannelFunctionTypes} from '../..'; import {SingleFunctionRenderType} from '../../../../../types'; import {pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; -import {renderChannelJSDoc} from '../../utils'; +import { + parameterInstanceExpression, + parameterUnionType, + renderChannelJSDoc +} from '../../utils'; export function renderPublish({ topic, @@ -17,7 +21,7 @@ export function renderPublish({ deprecated }: RenderRegularParameters): SingleFunctionRenderType { const addressToUse = channelParameters - ? `parameters.getChannelWithParameters('${topic}')` + ? `${parameterInstanceExpression({modelName: channelParameters.type, source: 'parameters'})}.getChannelWithParameters('${topic}')` : `'${topic}'`; let messageMarshalling = 'message.marshal()'; if (messageModule) { @@ -59,7 +63,7 @@ export function renderPublish({ ? [ { parameter: `parameters`, - parameterType: `parameters: ${channelParameters.type}`, + parameterType: `parameters: ${parameterUnionType(channelParameters.type)}`, jsDoc: ' * @param parameters for topic substitution' } ] diff --git a/src/codegen/generators/typescript/channels/protocols/mqtt/subscribe.ts b/src/codegen/generators/typescript/channels/protocols/mqtt/subscribe.ts index ca1ca724..6923230d 100644 --- a/src/codegen/generators/typescript/channels/protocols/mqtt/subscribe.ts +++ b/src/codegen/generators/typescript/channels/protocols/mqtt/subscribe.ts @@ -4,7 +4,12 @@ import {ChannelFunctionTypes} from '../..'; import {SingleFunctionRenderType} from '../../../../../types'; import {findRegexFromChannel, pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; -import {getValidationFunctions, renderChannelJSDoc} from '../../utils'; +import { + getValidationFunctions, + parameterInstanceExpression, + parameterUnionType, + renderChannelJSDoc +} from '../../utils'; /* eslint-disable sonarjs/cognitive-complexity */ export function renderSubscribe({ @@ -21,7 +26,7 @@ export function renderSubscribe({ }: RenderRegularParameters): SingleFunctionRenderType { const includeValidation = payloadGenerator.generator.includeValidation; const addressToUse = channelParameters - ? `parameters.getChannelWithParameters('${topic}')` + ? `${parameterInstanceExpression({modelName: channelParameters.type, source: 'parameters'})}.getChannelWithParameters('${topic}')` : `'${topic}'`; let messageUnmarshalling = `${messageType}.unmarshal(receivedData)`; if (messageModule) { @@ -103,7 +108,7 @@ export function renderSubscribe({ ? [ { parameter: `parameters`, - parameterType: `parameters: ${channelParameters.type}`, + parameterType: `parameters: ${parameterUnionType(channelParameters.type)}`, jsDoc: ' * @param parameters for topic substitution' } ] diff --git a/src/codegen/generators/typescript/channels/protocols/nats/corePublish.ts b/src/codegen/generators/typescript/channels/protocols/nats/corePublish.ts index a69c5e56..b9287be6 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/corePublish.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/corePublish.ts @@ -4,7 +4,11 @@ import {SingleFunctionRenderType} from '../../../../../types'; import {pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; import {generateHeaderSetupCode, generateHeaderParameter} from './utils'; -import {renderChannelJSDoc} from '../../utils'; +import { + parameterInstanceExpression, + parameterUnionType, + renderChannelJSDoc +} from '../../utils'; export function renderCorePublish({ topic, @@ -18,7 +22,10 @@ export function renderCorePublish({ deprecated }: RenderRegularParameters): SingleFunctionRenderType { const addressToUse = channelParameters - ? `parameters.getChannelWithParameters('${topic}')` + ? `${parameterInstanceExpression({ + modelName: channelParameters.type, + source: 'parameters' + })}.getChannelWithParameters('${topic}')` : `'${topic}'`; let messageMarshalling = 'message.marshal()'; if (messageModule) { @@ -47,7 +54,7 @@ nc.publish(${addressToUse}, dataToSend, options);`; ? [ { parameter: `parameters`, - parameterType: `parameters: ${channelParameters.type}`, + parameterType: `parameters: ${parameterUnionType(channelParameters.type)}`, jsDoc: ' * @param parameters for topic substitution' } ] diff --git a/src/codegen/generators/typescript/channels/protocols/nats/coreReply.ts b/src/codegen/generators/typescript/channels/protocols/nats/coreReply.ts index 971e1247..8c5c0ef3 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/coreReply.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/coreReply.ts @@ -3,7 +3,12 @@ import {ChannelFunctionTypes, RenderRequestReplyParameters} from '../../types'; import {SingleFunctionRenderType} from '../../../../../types'; import {findRegexFromChannel, pascalCase} from '../../../utils'; -import {getValidationFunctions, renderChannelJSDoc} from '../../utils'; +import { + getValidationFunctions, + parameterInstanceExpression, + parameterUnionType, + renderChannelJSDoc +} from '../../utils'; export function renderCoreReply({ requestTopic, @@ -20,7 +25,7 @@ export function renderCoreReply({ }: RenderRequestReplyParameters): SingleFunctionRenderType { const includeValidation = payloadGenerator.generator.includeValidation; const addressToUse = channelParameters - ? `parameters.getChannelWithParameters('${requestTopic}')` + ? `${parameterInstanceExpression({modelName: channelParameters.type, source: 'parameters'})}.getChannelWithParameters('${requestTopic}')` : `'${requestTopic}'`; const messageType = requestMessageModule @@ -67,7 +72,7 @@ export function renderCoreReply({ ? [ { parameter: `parameters`, - parameterType: `parameters: ${channelParameters.type}`, + parameterType: `parameters: ${parameterUnionType(channelParameters.type)}`, jsDoc: ' * @param parameters for topic substitution' } ] diff --git a/src/codegen/generators/typescript/channels/protocols/nats/coreRequest.ts b/src/codegen/generators/typescript/channels/protocols/nats/coreRequest.ts index 1080a325..dccc7905 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/coreRequest.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/coreRequest.ts @@ -3,7 +3,12 @@ import {ChannelFunctionTypes, RenderRequestReplyParameters} from '../../types'; import {SingleFunctionRenderType} from '../../../../../types'; import {pascalCase} from '../../../utils'; -import {getValidationFunctions, renderChannelJSDoc} from '../../utils'; +import { + getValidationFunctions, + parameterInstanceExpression, + parameterUnionType, + renderChannelJSDoc +} from '../../utils'; export function renderCoreRequest({ requestTopic, @@ -20,7 +25,7 @@ export function renderCoreRequest({ }: RenderRequestReplyParameters): SingleFunctionRenderType { const includeValidation = payloadGenerator.generator.includeValidation; const addressToUse = channelParameters - ? `parameters.getChannelWithParameters('${requestTopic}')` + ? `${parameterInstanceExpression({modelName: channelParameters.type, source: 'parameters'})}.getChannelWithParameters('${requestTopic}')` : `'${requestTopic}'`; const requestType = requestMessageModule @@ -48,7 +53,7 @@ export function renderCoreRequest({ ? [ { parameter: `parameters`, - parameterType: `parameters: ${channelParameters.type}`, + parameterType: `parameters: ${parameterUnionType(channelParameters.type)}`, jsDoc: ' * @param parameters for topic substitution' } ] diff --git a/src/codegen/generators/typescript/channels/protocols/nats/coreSubscribe.ts b/src/codegen/generators/typescript/channels/protocols/nats/coreSubscribe.ts index a267268d..948bcaea 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/coreSubscribe.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/coreSubscribe.ts @@ -4,7 +4,12 @@ import {ChannelFunctionTypes} from '../..'; import {SingleFunctionRenderType} from '../../../../../types'; import {findRegexFromChannel, pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; -import {getValidationFunctions, renderChannelJSDoc} from '../../utils'; +import { + getValidationFunctions, + parameterInstanceExpression, + parameterUnionType, + renderChannelJSDoc +} from '../../utils'; import { generateHeaderExtractionCode, generateHeaderCallbackParameter, @@ -25,7 +30,10 @@ export function renderCoreSubscribe({ }: RenderRegularParameters): SingleFunctionRenderType { const includeValidation = payloadGenerator.generator.includeValidation; const addressToUse = channelParameters - ? `parameters.getChannelWithParameters('${topic}')` + ? `${parameterInstanceExpression({ + modelName: channelParameters.type, + source: 'parameters' + })}.getChannelWithParameters('${topic}')` : `'${topic}'`; let messageUnmarshalling = `${messageType}.unmarshal(receivedData)`; if (messageModule) { @@ -77,7 +85,7 @@ export function renderCoreSubscribe({ ? [ { parameter: `parameters`, - parameterType: `parameters: ${channelParameters.type}`, + parameterType: `parameters: ${parameterUnionType(channelParameters.type)}`, jsDoc: ' * @param parameters for topic substitution' } ] diff --git a/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPublish.ts b/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPublish.ts index cca66e79..6ca91131 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPublish.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPublish.ts @@ -4,7 +4,11 @@ import {pascalCase} from '../../../utils'; import {ChannelFunctionTypes} from '../../index'; import {RenderRegularParameters} from '../../types'; import {generateHeaderSetupCode, generateHeaderParameter} from './utils'; -import {renderChannelJSDoc} from '../../utils'; +import { + parameterInstanceExpression, + parameterUnionType, + renderChannelJSDoc +} from '../../utils'; export function renderJetstreamPublish({ topic, messageType, @@ -17,7 +21,7 @@ export function renderJetstreamPublish({ deprecated }: RenderRegularParameters): SingleFunctionRenderType { const addressToUse = channelParameters - ? `parameters.getChannelWithParameters('${topic}')` + ? `${parameterInstanceExpression({modelName: channelParameters.type, source: 'parameters'})}.getChannelWithParameters('${topic}')` : `'${topic}'`; let messageMarshalling = 'message.marshal()'; if (messageModule) { @@ -47,7 +51,7 @@ await js.publish(${addressToUse}, dataToSend, options);`; ? [ { parameter: `parameters`, - parameterType: `parameters: ${channelParameters.type}`, + parameterType: `parameters: ${parameterUnionType(channelParameters.type)}`, jsDoc: ' * @param parameters for topic substitution' } ] diff --git a/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPullSubscribe.ts b/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPullSubscribe.ts index 7b18ae13..3e2944e7 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPullSubscribe.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPullSubscribe.ts @@ -4,7 +4,12 @@ import {ChannelFunctionTypes} from '../..'; import {SingleFunctionRenderType} from '../../../../../types'; import {findRegexFromChannel, pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; -import {getValidationFunctions, renderChannelJSDoc} from '../../utils'; +import { + getValidationFunctions, + parameterInstanceExpression, + parameterUnionType, + renderChannelJSDoc +} from '../../utils'; import { generateHeaderExtractionCode, generateMessageReceivingCode @@ -24,7 +29,7 @@ export function renderJetstreamPullSubscribe({ }: RenderRegularParameters): SingleFunctionRenderType { const includeValidation = payloadGenerator.generator.includeValidation; const addressToUse = channelParameters - ? `parameters.getChannelWithParameters('${topic}')` + ? `${parameterInstanceExpression({modelName: channelParameters.type, source: 'parameters'})}.getChannelWithParameters('${topic}')` : `'${topic}'`; let messageUnmarshalling = `${messageType}.unmarshal(receivedData)`; if (messageModule) { @@ -78,7 +83,7 @@ export function renderJetstreamPullSubscribe({ ? [ { parameter: `parameters`, - parameterType: `parameters: ${channelParameters.type}`, + parameterType: `parameters: ${parameterUnionType(channelParameters.type)}`, jsDoc: ' * @param parameters for topic substitution' } ] diff --git a/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPushSubscription.ts b/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPushSubscription.ts index a7724b15..7f8e5472 100644 --- a/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPushSubscription.ts +++ b/src/codegen/generators/typescript/channels/protocols/nats/jetstreamPushSubscription.ts @@ -4,7 +4,12 @@ import {ChannelFunctionTypes} from '../..'; import {SingleFunctionRenderType} from '../../../../../types'; import {findRegexFromChannel, pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; -import {getValidationFunctions, renderChannelJSDoc} from '../../utils'; +import { + getValidationFunctions, + parameterInstanceExpression, + parameterUnionType, + renderChannelJSDoc +} from '../../utils'; import { generateHeaderExtractionCode, generateMessageReceivingCode @@ -24,7 +29,7 @@ export function renderJetstreamPushSubscription({ }: RenderRegularParameters): SingleFunctionRenderType { const includeValidation = payloadGenerator.generator.includeValidation; const addressToUse = channelParameters - ? `parameters.getChannelWithParameters('${topic}')` + ? `${parameterInstanceExpression({modelName: channelParameters.type, source: 'parameters'})}.getChannelWithParameters('${topic}')` : `'${topic}'`; let messageUnmarshalling = `${messageType}.unmarshal(receivedData)`; if (messageModule) { @@ -81,7 +86,7 @@ export function renderJetstreamPushSubscription({ ? [ { parameter: `parameters`, - parameterType: `parameters: ${channelParameters.type}`, + parameterType: `parameters: ${parameterUnionType(channelParameters.type)}`, jsDoc: ' * @param parameters for topic substitution' } ] diff --git a/src/codegen/generators/typescript/channels/protocols/websocket/subscribe.ts b/src/codegen/generators/typescript/channels/protocols/websocket/subscribe.ts index ef33eb3d..bfca5d68 100644 --- a/src/codegen/generators/typescript/channels/protocols/websocket/subscribe.ts +++ b/src/codegen/generators/typescript/channels/protocols/websocket/subscribe.ts @@ -4,7 +4,11 @@ import {ChannelFunctionTypes} from '../..'; import {SingleFunctionRenderType} from '../../../../../types'; import {pascalCase} from '../../../utils'; import {RenderRegularParameters} from '../../types'; -import {renderChannelJSDoc} from '../../utils'; +import { + parameterUnionType, + renderParameterNormalization, + renderChannelJSDoc +} from '../../utils'; export function renderWebSocketSubscribe({ topic, @@ -22,6 +26,16 @@ export function renderWebSocketSubscribe({ } messageType = messageModule ? `${messageModule}.${messageType}` : messageType; + // Normalize the user-provided parameters (interface object or class instance) + // to a concrete class instance so callbacks always receive a class. + const parameterNormalization = channelParameters + ? ` ${renderParameterNormalization({ + modelName: channelParameters.type, + source: 'parameters', + target: 'parameterInstance' + })}\n` + : ''; + const callbackParameters = [ 'err?: Error', `msg?: ${messageType}`, @@ -32,14 +46,14 @@ export function renderWebSocketSubscribe({ const callbackArguments = [ 'err: undefined', 'msg: parsedMessage', - ...(channelParameters ? ['parameters'] : []), + ...(channelParameters ? ['parameters: parameterInstance'] : []), 'ws' ]; const errorCallbackArguments = [ 'err: new Error(`Failed to parse message: ${error.message}`)', 'msg: undefined', - ...(channelParameters ? ['parameters'] : []), + ...(channelParameters ? ['parameters: parameterInstance'] : []), 'ws' ]; @@ -53,7 +67,7 @@ export function renderWebSocketSubscribe({ ? [ { parameter: `parameters`, - parameterType: `parameters: ${channelParameters.type}`, + parameterType: `parameters: ${parameterUnionType(channelParameters.type)}`, jsDoc: ' * @param parameters for URL path substitution' } ] @@ -87,12 +101,12 @@ function ${functionName}({ }: { ${functionParameters.map((param) => param.parameterType).join(',\n ')} }): void { - try { +${parameterNormalization} try { // Check if WebSocket is open if (ws.readyState !== WebSocket.WebSocket.OPEN) { onDataCallback({ err: new Error('WebSocket is not open'), - msg: undefined,${channelParameters ? '\n parameters,' : ''} + msg: undefined,${channelParameters ? '\n parameters: parameterInstance,' : ''} ws }); return; @@ -112,7 +126,7 @@ function ${functionName}({ if (!valid) { onDataCallback({ err: new Error(\`Invalid message payload received; \${JSON.stringify({cause: errors})}\`), - msg: undefined,${channelParameters ? '\n parameters,' : ''} + msg: undefined,${channelParameters ? '\n parameters: parameterInstance,' : ''} ws }); return; @@ -133,7 +147,7 @@ function ${functionName}({ ws.on('error', (error: Error) => { onDataCallback({ err: new Error(\`WebSocket error: \${error.message}\`), - msg: undefined,${channelParameters ? '\n parameters,' : ''} + msg: undefined,${channelParameters ? '\n parameters: parameterInstance,' : ''} ws }); }); @@ -143,7 +157,7 @@ function ${functionName}({ if (code !== 1000 && code !== 1001 && code !== 1005) { // 1005 is no status received onDataCallback({ err: new Error(\`WebSocket closed unexpectedly: \${code} \${reason.toString()}\`), - msg: undefined,${channelParameters ? '\n parameters,' : ''} + msg: undefined,${channelParameters ? '\n parameters: parameterInstance,' : ''} ws }); } @@ -152,7 +166,7 @@ function ${functionName}({ } catch (error: any) { onDataCallback({ err: new Error(\`Failed to set up WebSocket subscription: \${error.message}\`), - msg: undefined,${channelParameters ? '\n parameters,' : ''} + msg: undefined,${channelParameters ? '\n parameters: parameterInstance,' : ''} ws }); } diff --git a/src/codegen/generators/typescript/channels/utils.ts b/src/codegen/generators/typescript/channels/utils.ts index 187008a1..2f30af16 100644 --- a/src/codegen/generators/typescript/channels/utils.ts +++ b/src/codegen/generators/typescript/channels/utils.ts @@ -94,10 +94,53 @@ export function addParametersToDependencies( ); dependencies.push( - `import {${parameter.modelName}} from '${importPath}';` + `import {${parameter.modelName}, ${parameter.modelName}Interface} from '${importPath}';` ); }); } +/** + * Widen a parameter model name to the `Interface | Class` union that channel + * consumers accept for user-provided parameters — the interface makes a plain + * object literal ergonomic, the class keeps the rich behavior. + */ +export function parameterUnionType(modelName: string): string { + return `${modelName}Interface | ${modelName}`; +} + +/** + * Emit an `instanceof`-guarded expression that resolves a user-provided + * `Interface | Class` value to a concrete class instance at a single use site + * (e.g. `( instanceof ? : new ())`). + * Used by the message-broker protocols where the parameter is only consumed + * once to build the topic/subject, so a local statement would be overkill. + */ +export function parameterInstanceExpression({ + modelName, + source +}: { + modelName: string; + source: string; +}): string { + return `(${source} instanceof ${modelName} ? ${source} : new ${modelName}(${source}))`; +} + +/** + * Emit the `instanceof`-guarded normalization statement that turns a + * user-provided `Interface | Class` value into a concrete class instance before + * the channel uses it (for `getChannelWithParameters`, serialization, etc.). + */ +export function renderParameterNormalization({ + modelName, + source, + target +}: { + modelName: string; + source: string; + target: string; +}): string { + return `const ${target} = ${source} instanceof ${modelName} ? ${source} : new ${modelName}(${source});`; +} + export function addParametersToExports( parameters: Record, dependencies: string[] diff --git a/src/codegen/generators/typescript/parameters.ts b/src/codegen/generators/typescript/parameters.ts index ebacba39..6cb9dd89 100644 --- a/src/codegen/generators/typescript/parameters.ts +++ b/src/codegen/generators/typescript/parameters.ts @@ -80,6 +80,36 @@ export interface TypescriptParametersContext extends GenericCodegenContext { export type TypeScriptParameterRenderType = ParameterRenderType; +/** + * Rewrite a generated parameter model's trailing `export { };` to also + * export the companion `Interface` emitted by the class preset. Modelina + * appends the export outside the preset chain from the single class model, so + * the interface (raw text injected by the `self` hook) would otherwise not be + * exported. Only rewrites when the companion interface is actually present, so + * models without the interface treatment are left untouched. + */ +function withParameterInterfaceExport(model: OutputModel): OutputModel { + const interfaceName = `${model.modelName}Interface`; + const originalExport = `export { ${model.modelName} };`; + if ( + !model.result.includes(`interface ${interfaceName}`) || + !model.result.includes(originalExport) + ) { + return model; + } + const rewritten = model.result.replace( + originalExport, + `export { ${model.modelName}, ${interfaceName} };` + ); + return OutputModel.toOutputModel({ + result: rewritten, + model: model.model, + modelName: model.modelName, + inputModel: model.inputModel, + dependencies: model.dependencies + }); +} + // Main generator function that orchestrates input processing and generation export async function generateTypescriptParameters( context: TypescriptParametersContext @@ -131,9 +161,20 @@ export async function generateTypescriptParameters( input: schemaData.schema, outputPath: generator.outputPath }); - channelModels[channelId] = - result.models.length > 0 ? result.models[0] : undefined; - files.push(...result.files); + const mainModel = + result.models.length > 0 + ? withParameterInterfaceExport(result.models[0]) + : undefined; + channelModels[channelId] = mainModel; + for (const file of result.files) { + if (mainModel && file.path.endsWith(`/${mainModel.modelName}.ts`)) { + // The parameter model's own file carries the companion interface, + // so re-export both symbols from its (rewritten) content. + files.push({path: file.path, content: mainModel.result}); + } else { + files.push(file); + } + } } else { channelModels[channelId] = undefined; } diff --git a/src/codegen/generators/typescript/utils.ts b/src/codegen/generators/typescript/utils.ts index f3fa8937..6623dc4f 100644 --- a/src/codegen/generators/typescript/utils.ts +++ b/src/codegen/generators/typescript/utils.ts @@ -5,7 +5,8 @@ import { NO_RESERVED_KEYWORDS, typeScriptDefaultModelNameConstraints, typeScriptDefaultPropertyKeyConstraints, - TypeScriptOptions + TypeScriptOptions, + TypeScriptPreset } from '@asyncapi/modelina'; import {DeepPartial} from '../../utils'; @@ -37,61 +38,58 @@ export function castToTsType(jsonSchemaType: string, variableToCast: string) { } /** - * Realize parameters without using types and without trailing comma - * - * @param {Object.} parameters - * @returns + * Build the body of a parameter `interface` declaration from a constrained + * object model. Emits one 2-space-indented `propertyName: type` line per + * property (newline-separated, no trailing separator); `?` is added when the + * property is not required. Used by the parameter generators to prepend a + * plain-data companion interface above each generated parameter class. */ -export function realizeParametersForChannelWithoutType( - parameters: ConstrainedObjectModel -) { - let returnString = ''; - for (const paramName in Object.keys(parameters.properties)) { - returnString += `${paramName},`; - } - - if (returnString.length > 0) { - returnString = returnString.slice(0, -1); - } - - return returnString; +export function buildParametersInterfaceBody( + model: ConstrainedObjectModel +): string { + return Object.values(model.properties) + .filter((parameter) => !parameter.property.options.const) + .map((parameter) => { + const requiredType = parameter.required ? '' : '?'; + return ` ${parameter.propertyName}${requiredType}: ${parameter.property.type}`; + }) + .join('\n'); } /** - * Realize parameters using types without trailing comma - * @param {Object.} channelParameters parameters to realize - * @param {boolean} required optional or required + * Shared class preset for parameter models. Prepends a plain-data companion + * `interface Interface` above the class (`self`), rewrites the + * constructor to accept `input: Interface` (`ctor`), and appends the + * input-specific helper methods (`additionalContent`). Reused by both the + * OpenAPI and AsyncAPI parameter generators so the interface + ctor logic can + * never diverge between them. */ -export function realizeParametersForChannel( - channelParameters: ConstrainedObjectModel, - required = true -) { - let returnString = ''; - for (const parameter of Object.values(channelParameters.properties)) { - returnString += `${realizeParameterForChannelWithType(parameter.propertyName, parameter.property.type, required)},`; - } - - if (returnString.length > 0) { - returnString = returnString.slice(0, -1); - } - - return returnString; +export function parameterClassPreset( + generateAdditionalMethods: (model: ConstrainedObjectModel) => string +): TypeScriptPreset { + return { + class: { + self: ({content, model}) => + `interface ${model.name}Interface { +${buildParametersInterfaceBody(model)} } - -/** - * Realize a single parameter with its type - * - * @param {string} parameterName parameter name to use as - * @param {ChannelParameter} parameter which contains the schema - * @param {boolean} required should it be optional or required - */ -function realizeParameterForChannelWithType( - parameterName: string, - parameterType: string, - required = true -) { - const requiredType = required ? '' : '?'; - return `${parameterName}${requiredType}: ${parameterType})}`; +${content}`, + ctor: ({renderer, model}) => { + const assignments = Object.values(model.properties) + .filter((property) => !property.property.options.const) + .map( + (property) => + `this._${property.propertyName} = input.${property.propertyName};` + ); + return `constructor(input: ${model.name}Interface) { +${renderer.indent(renderer.renderBlock(assignments))} +}`; + }, + additionalContent: ({content, model}) => + `${content} +${generateAdditionalMethods(model)}` + } + }; } /** diff --git a/src/codegen/inputs/asyncapi/generators/parameters.ts b/src/codegen/inputs/asyncapi/generators/parameters.ts index 10b03c87..8c432606 100644 --- a/src/codegen/inputs/asyncapi/generators/parameters.ts +++ b/src/codegen/inputs/asyncapi/generators/parameters.ts @@ -2,6 +2,7 @@ import {AsyncAPIDocumentInterface} from '@asyncapi/parser'; import { defaultCodegenTypescriptModelinaOptions, + parameterClassPreset, pascalCase } from '../../../generators/typescript/utils'; import {findNameFromChannel} from '../../../utils'; @@ -138,15 +139,7 @@ export function createAsyncAPIGenerator() { useJavascriptReservedKeywords: false, presets: [ TS_DESCRIPTION_PRESET, - { - class: { - additionalContent: ({content, model}) => { - const additionalMethods = generateAsyncAPIParameterMethods(model); - return `${content} -${additionalMethods}`; - } - } - } + parameterClassPreset(generateAsyncAPIParameterMethods) ] }); } diff --git a/src/codegen/inputs/openapi/generators/parameters.ts b/src/codegen/inputs/openapi/generators/parameters.ts index a810b8f3..94242cb3 100644 --- a/src/codegen/inputs/openapi/generators/parameters.ts +++ b/src/codegen/inputs/openapi/generators/parameters.ts @@ -2,6 +2,7 @@ import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types'; import { defaultCodegenTypescriptModelinaOptions, + parameterClassPreset, pascalCase } from '../../../generators/typescript/utils'; import {ProcessedParameterSchemaData} from '../../asyncapi/generators/parameters'; @@ -1261,15 +1262,7 @@ export function createOpenAPIGenerator() { useJavascriptReservedKeywords: false, presets: [ TS_DESCRIPTION_PRESET, - { - class: { - additionalContent: ({content, model}) => { - const additionalMethods = generateOpenAPIParameterMethods(model); - return `${content} -${additionalMethods}`; - } - } - } + parameterClassPreset(generateOpenAPIParameterMethods) ] }); } diff --git a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap index ce2c4a3d..84508252 100644 --- a/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/channels.spec.ts.snap @@ -10,7 +10,7 @@ export {http_client}; exports[`channels typescript OpenAPI input should generate HTTP client protocol code for OpenAPI spec: openapi-http_client-protocol-code 1`] = ` "import {Pet} from './../../../../../payloads/Pet'; import * as FindPetsByStatusAndCategoryResponseModule from './../../../../../payloads/FindPetsByStatusAndCategoryResponse'; -import {FindPetsByStatusAndCategoryParameters} from './../../../../../parameters/FindPetsByStatusAndCategoryParameters'; +import {FindPetsByStatusAndCategoryParameters, FindPetsByStatusAndCategoryParametersInterface} from './../../../../../parameters/FindPetsByStatusAndCategoryParameters'; import {FindPetsByStatusAndCategoryHeaders, serializeFindPetsByStatusAndCategoryHeadersHeaders} from './../../../../../headers/FindPetsByStatusAndCategoryHeaders'; // ============================================================================ @@ -842,7 +842,7 @@ async function updatePet(context: UpdatePetContext): Promise; // Build URL - let url = buildUrlWithParameters(config.baseUrl, '/pet/findByStatus/{status}/{categoryId}', context.parameters); + let url = buildUrlWithParameters(config.baseUrl, '/pet/findByStatus/{status}/{categoryId}', parameters); url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication @@ -993,7 +995,7 @@ export {amqp}; exports[`channels typescript protocol-specific code generation should generate AMQP protocol code with parameters and headers: amqp-protocol-code 1`] = ` "import * as TestPayloadModelModule from './../../../../../payloads/TestPayloadModel'; -import {TestParameter} from './../../../../../parameters/TestParameter'; +import {TestParameter, TestParameterInterface} from './../../../../../parameters/TestParameter'; import {TestHeaders} from './../../../../../headers/TestHeaders'; import * as Amqp from 'amqplib'; @@ -1014,7 +1016,7 @@ function publishToUserSignedupExchange({ options }: { message: TestPayloadModelModule.UserSignedUpPayload, - parameters: Parameter, + parameters: ParameterInterface | Parameter, headers?: Headers, amqp: Amqp.Connection, options?: {exchange: string | undefined} & Amqp.Options.Publish @@ -1027,7 +1029,7 @@ function publishToUserSignedupExchange({ try { let dataToSend: any = TestPayloadModelModule.marshal(message); const channel = await amqp.createChannel(); -const routingKey = parameters.getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'); +const routingKey = (parameters instanceof Parameter ? parameters : new Parameter(parameters)).getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'); // Set up message properties (headers) if provided let publishOptions = { ...options }; if (headers) { @@ -1065,7 +1067,7 @@ function publishToUserSignedupQueue({ options }: { message: TestPayloadModelModule.UserSignedUpPayload, - parameters: Parameter, + parameters: ParameterInterface | Parameter, headers?: Headers, amqp: Amqp.Connection, options?: Amqp.Options.Publish @@ -1074,7 +1076,7 @@ function publishToUserSignedupQueue({ try { let dataToSend: any = TestPayloadModelModule.marshal(message); const channel = await amqp.createChannel(); -const queue = parameters.getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'); +const queue = (parameters instanceof Parameter ? parameters : new Parameter(parameters)).getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'); // Set up message properties (headers) if provided let publishOptions = { ...options }; if (headers) { @@ -1112,7 +1114,7 @@ function subscribeToUserSignedupQueue({ skipMessageValidation = false }: { onDataCallback: (params: {err?: Error, msg?: TestPayloadModelModule.UserSignedUpPayload, headers?: Headers, amqpMsg?: Amqp.ConsumeMessage}) => void, - parameters: Parameter, + parameters: ParameterInterface | Parameter, amqp: Amqp.Connection, options?: Amqp.Options.Consume, skipMessageValidation?: boolean @@ -1120,7 +1122,7 @@ function subscribeToUserSignedupQueue({ return new Promise(async (resolve, reject) => { try { const channel = await amqp.createChannel(); -const queue = parameters.getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'); +const queue = (parameters instanceof Parameter ? parameters : new Parameter(parameters)).getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'); await channel.assertQueue(queue, { durable: true }); channel.consume(queue, (msg) => { @@ -1305,7 +1307,7 @@ export {event_source}; exports[`channels typescript protocol-specific code generation should generate EventSource protocol code with parameters and headers: event_source-protocol-code 1`] = ` "import * as TestPayloadModelModule from './../../../../../payloads/TestPayloadModel'; -import {TestParameter} from './../../../../../parameters/TestParameter'; +import {TestParameter, TestParameterInterface} from './../../../../../parameters/TestParameter'; import {TestHeaders} from './../../../../../headers/TestHeaders'; import { fetchEventSource, EventStreamContentType, EventSourceMessage } from '@microsoft/fetch-event-source'; import { NextFunction, Request, Response, Router } from 'express'; @@ -1328,13 +1330,13 @@ function listenForUserSignedup({ skipMessageValidation = false }: { callback: (params: {error?: Error, messageEvent?: TestPayloadModelModule.UserSignedUpPayload}) => void, - parameters: Parameter, + parameters: ParameterInterface | Parameter, headers?: Headers, options: {authorization?: string, onClose?: (err?: string) => void, baseUrl: string, headers?: Record}, skipMessageValidation?: boolean }): (() => void) { const controller = new AbortController(); - let eventsUrl: string = parameters.getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'); + let eventsUrl: string = (parameters instanceof Parameter ? parameters : new Parameter(parameters)).getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'); const url = \`\${options.baseUrl}/\${eventsUrl}\` const requestHeaders: Record = { ...options.headers ?? {}, @@ -2300,7 +2302,7 @@ export {kafka}; exports[`channels typescript protocol-specific code generation should generate Kafka protocol code with parameters and headers: kafka-protocol-code 1`] = ` "import * as TestPayloadModelModule from './../../../../../payloads/TestPayloadModel'; -import {TestParameter} from './../../../../../parameters/TestParameter'; +import {TestParameter, TestParameterInterface} from './../../../../../parameters/TestParameter'; import {TestHeaders} from './../../../../../headers/TestHeaders'; import * as Kafka from 'kafkajs'; @@ -2319,7 +2321,7 @@ function produceToUserSignedup({ kafka }: { message: TestPayloadModelModule.UserSignedUpPayload, - parameters: Parameter, + parameters: ParameterInterface | Parameter, headers?: Headers, kafka: Kafka.Kafka }): Promise { @@ -2342,7 +2344,7 @@ function produceToUserSignedup({ } await producer.send({ - topic: parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), + topic: (parameters instanceof Parameter ? parameters : new Parameter(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), messages: [ { value: dataToSend, @@ -2385,7 +2387,7 @@ function consumeFromUserSignedup({ skipMessageValidation = false }: { onDataCallback: (err?: Error, msg?: TestPayloadModelModule.UserSignedUpPayload, parameters?: Parameter, headers?: Headers, kafkaMsg?: Kafka.EachMessagePayload) => void, - parameters: Parameter, + parameters: ParameterInterface | Parameter, kafka: Kafka.Kafka, options: {fromBeginning: boolean, groupId: string}, skipMessageValidation?: boolean @@ -2399,7 +2401,7 @@ function consumeFromUserSignedup({ await consumer.connect(); - await consumer.subscribe({ topic: parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), fromBeginning: options.fromBeginning }); + await consumer.subscribe({ topic: (parameters instanceof Parameter ? parameters : new Parameter(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), fromBeginning: options.fromBeginning }); await consumer.run({ eachMessage: async (kafkaMessage: Kafka.EachMessagePayload) => { const { topic, message } = kafkaMessage; @@ -2560,7 +2562,7 @@ export {mqtt}; exports[`channels typescript protocol-specific code generation should generate MQTT protocol code with parameters and headers: mqtt-protocol-code 1`] = ` "import * as TestPayloadModelModule from './../../../../../payloads/TestPayloadModel'; -import {TestParameter} from './../../../../../parameters/TestParameter'; +import {TestParameter, TestParameterInterface} from './../../../../../parameters/TestParameter'; import {TestHeaders} from './../../../../../headers/TestHeaders'; import * as Mqtt from 'mqtt'; @@ -2579,7 +2581,7 @@ function publishToUserSignedup({ mqtt }: { message: TestPayloadModelModule.UserSignedUpPayload, - parameters: Parameter, + parameters: ParameterInterface | Parameter, headers?: Headers, mqtt: Mqtt.MqttClient }): Promise { @@ -2599,7 +2601,7 @@ function publishToUserSignedup({ } publishOptions.properties = { userProperties }; } - mqtt.publish(parameters.getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'), dataToSend, publishOptions); + mqtt.publish((parameters instanceof Parameter ? parameters : new Parameter(parameters)).getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'), dataToSend, publishOptions); resolve(); } catch (e: any) { reject(e); @@ -2633,7 +2635,7 @@ function subscribeToUserSignedup({ skipMessageValidation = false }: { onDataCallback: (params: {err?: Error, msg?: TestPayloadModelModule.UserSignedUpPayload, parameters?: Parameter, headers?: Headers, mqttMsg?: Mqtt.IPublishPacket}) => void, - parameters: Parameter, + parameters: ParameterInterface | Parameter, mqtt: Mqtt.MqttClient, skipMessageValidation?: boolean }): Promise { @@ -2676,7 +2678,7 @@ function subscribeToUserSignedup({ mqtt.on('message', messageHandler); // Subscribe to the topic - await mqtt.subscribeAsync(parameters.getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}')); + await mqtt.subscribeAsync((parameters instanceof Parameter ? parameters : new Parameter(parameters)).getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}')); resolve(); } catch (e: any) { @@ -2812,7 +2814,7 @@ export {nats}; exports[`channels typescript protocol-specific code generation should generate NATS protocol code with parameters and headers: nats-protocol-code 1`] = ` "import * as TestPayloadModelModule from './../../../../../payloads/TestPayloadModel'; -import {TestParameter} from './../../../../../parameters/TestParameter'; +import {TestParameter, TestParameterInterface} from './../../../../../parameters/TestParameter'; import {TestHeaders} from './../../../../../headers/TestHeaders'; import * as Nats from 'nats'; @@ -2835,7 +2837,7 @@ function publishToUserSignedup({ options }: { message: TestPayloadModelModule.UserSignedUpPayload, - parameters: Parameter, + parameters: ParameterInterface | Parameter, headers?: Headers, nc: Nats.NatsConnection, codec?: Nats.Codec, @@ -2857,7 +2859,7 @@ function publishToUserSignedup({ options = { ...options, headers: natsHeaders }; } dataToSend = codec.encode(dataToSend); -nc.publish(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), dataToSend, options); +nc.publish((parameters instanceof Parameter ? parameters : new Parameter(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), dataToSend, options); resolve(); } catch (e: any) { reject(e); @@ -2895,7 +2897,7 @@ function subscribeToUserSignedup({ skipMessageValidation = false }: { onDataCallback: (err?: Error, msg?: TestPayloadModelModule.UserSignedUpPayload, parameters?: Parameter, headers?: Headers, natsMsg?: Nats.Msg) => void, - parameters: Parameter, + parameters: ParameterInterface | Parameter, nc: Nats.NatsConnection, codec?: Nats.Codec, options?: Nats.SubscriptionOptions, @@ -2903,7 +2905,7 @@ function subscribeToUserSignedup({ }): Promise { return new Promise(async (resolve, reject) => { try { - const subscription = nc.subscribe(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); + const subscription = nc.subscribe((parameters instanceof Parameter ? parameters : new Parameter(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); (async () => { for await (const msg of subscription) { @@ -2967,7 +2969,7 @@ function jetStreamPullSubscribeToUserSignedup({ skipMessageValidation = false }: { onDataCallback: (err?: Error, msg?: TestPayloadModelModule.UserSignedUpPayload, parameters?: Parameter, headers?: Headers, jetstreamMsg?: Nats.JsMsg) => void, - parameters: Parameter, + parameters: ParameterInterface | Parameter, js: Nats.JetStreamClient, options: Nats.ConsumerOptsBuilder | Partial, codec?: Nats.Codec, @@ -2975,7 +2977,7 @@ function jetStreamPullSubscribeToUserSignedup({ }): Promise { return new Promise(async (resolve, reject) => { try { - const subscription = await js.pullSubscribe(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); + const subscription = await js.pullSubscribe((parameters instanceof Parameter ? parameters : new Parameter(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); (async () => { for await (const msg of subscription) { @@ -3039,7 +3041,7 @@ function jetStreamPushSubscriptionFromUserSignedup({ skipMessageValidation = false }: { onDataCallback: (err?: Error, msg?: TestPayloadModelModule.UserSignedUpPayload, parameters?: Parameter, headers?: Headers, jetstreamMsg?: Nats.JsMsg) => void, - parameters: Parameter, + parameters: ParameterInterface | Parameter, js: Nats.JetStreamClient, options: Nats.ConsumerOptsBuilder | Partial, codec?: Nats.Codec, @@ -3047,7 +3049,7 @@ function jetStreamPushSubscriptionFromUserSignedup({ }): Promise { return new Promise(async (resolve, reject) => { try { - const subscription = await js.subscribe(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); + const subscription = await js.subscribe((parameters instanceof Parameter ? parameters : new Parameter(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); (async () => { for await (const msg of subscription) { @@ -3100,7 +3102,7 @@ function jetStreamPublishToUserSignedup({ options = {} }: { message: TestPayloadModelModule.UserSignedUpPayload, - parameters: Parameter, + parameters: ParameterInterface | Parameter, headers?: Headers, js: Nats.JetStreamClient, codec?: Nats.Codec, @@ -3122,7 +3124,7 @@ function jetStreamPublishToUserSignedup({ options = { ...options, headers: natsHeaders }; } dataToSend = codec.encode(dataToSend); -await js.publish(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), dataToSend, options); +await js.publish((parameters instanceof Parameter ? parameters : new Parameter(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), dataToSend, options); resolve(); } catch (e: any) { reject(e); @@ -3439,7 +3441,7 @@ export {websocket}; exports[`channels typescript protocol-specific code generation should generate WebSocket protocol code with parameters and headers: websocket-protocol-code 1`] = ` "import * as TestPayloadModelModule from './../../../../../payloads/TestPayloadModel'; -import {TestParameter} from './../../../../../parameters/TestParameter'; +import {TestParameter, TestParameterInterface} from './../../../../../parameters/TestParameter'; import {TestHeaders} from './../../../../../headers/TestHeaders'; import * as WebSocket from 'ws'; import { IncomingMessage } from 'http'; @@ -3489,17 +3491,18 @@ function subscribeToUserSignedup({ skipMessageValidation = false }: { onDataCallback: (params: {err?: Error, msg?: TestPayloadModelModule.UserSignedUpPayload, parameters?: Parameter, ws?: WebSocket.WebSocket}) => void, - parameters: Parameter, + parameters: ParameterInterface | Parameter, ws: WebSocket.WebSocket, skipMessageValidation?: boolean }): void { + const parameterInstance = parameters instanceof Parameter ? parameters : new Parameter(parameters); try { // Check if WebSocket is open if (ws.readyState !== WebSocket.WebSocket.OPEN) { onDataCallback({ err: new Error('WebSocket is not open'), msg: undefined, - parameters, + parameters: parameterInstance, ws }); return; @@ -3520,7 +3523,7 @@ function subscribeToUserSignedup({ onDataCallback({ err: new Error(\`Invalid message payload received; \${JSON.stringify({cause: errors})}\`), msg: undefined, - parameters, + parameters: parameterInstance, ws }); return; @@ -3530,7 +3533,7 @@ function subscribeToUserSignedup({ onDataCallback({ err: undefined, msg: parsedMessage, - parameters, + parameters: parameterInstance, ws }); @@ -3538,7 +3541,7 @@ function subscribeToUserSignedup({ onDataCallback({ err: new Error(\`Failed to parse message: \${error.message}\`), msg: undefined, - parameters, + parameters: parameterInstance, ws }); } @@ -3548,7 +3551,7 @@ function subscribeToUserSignedup({ onDataCallback({ err: new Error(\`WebSocket error: \${error.message}\`), msg: undefined, - parameters, + parameters: parameterInstance, ws }); }); @@ -3559,7 +3562,7 @@ function subscribeToUserSignedup({ onDataCallback({ err: new Error(\`WebSocket closed unexpectedly: \${code} \${reason.toString()}\`), msg: undefined, - parameters, + parameters: parameterInstance, ws }); } @@ -3569,7 +3572,7 @@ function subscribeToUserSignedup({ onDataCallback({ err: new Error(\`Failed to set up WebSocket subscription: \${error.message}\`), msg: undefined, - parameters, + parameters: parameterInstance, ws }); } diff --git a/test/codegen/generators/typescript/__snapshots__/parameters.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/parameters.spec.ts.snap index 11d024f6..d638d4c2 100644 --- a/test/codegen/generators/typescript/__snapshots__/parameters.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/parameters.spec.ts.snap @@ -2,12 +2,13 @@ exports[`parameters typescript asyncapi should work with AsyncAPI that contains parameters 1`] = ` " +interface SingleParameterParametersInterface { + firstParameter: string +} class SingleParameterParameters { private _firstParameter: string; - constructor(input: { - firstParameter: string, - }) { + constructor(input: SingleParameterParametersInterface) { this._firstParameter = input.firstParameter; } @@ -41,19 +42,20 @@ class SingleParameterParameters { return parameters; } } -export { SingleParameterParameters };" +export { SingleParameterParameters, SingleParameterParametersInterface };" `; exports[`parameters typescript asyncapi should work with AsyncAPI that contains parameters 2`] = ` "import {EnumParameter} from './EnumParameter'; +interface MultipleParameterParametersInterface { + enumParameter: EnumParameter + constParameter: string +} class MultipleParameterParameters { private _enumParameter: EnumParameter; private _constParameter: string; - constructor(input: { - enumParameter: EnumParameter, - constParameter: string, - }) { + constructor(input: MultipleParameterParametersInterface) { this._enumParameter = input.enumParameter; this._constParameter = input.constParameter; } @@ -98,17 +100,18 @@ class MultipleParameterParameters { return parameters; } } -export { MultipleParameterParameters };" +export { MultipleParameterParameters, MultipleParameterParametersInterface };" `; exports[`parameters typescript asyncapi should work with AsyncAPI v2 that contains parameters and const parameters 1`] = ` " +interface SingleParameterParametersInterface { + firstParameter: string +} class SingleParameterParameters { private _firstParameter: string; - constructor(input: { - firstParameter: string, - }) { + constructor(input: SingleParameterParametersInterface) { this._firstParameter = input.firstParameter; } @@ -142,18 +145,19 @@ class SingleParameterParameters { return parameters; } } -export { SingleParameterParameters };" +export { SingleParameterParameters, SingleParameterParametersInterface };" `; exports[`parameters typescript asyncapi should work with AsyncAPI v2 that contains parameters and const parameters 2`] = ` "import {EnumParameter} from './EnumParameter'; +interface MultipleParameterParametersInterface { + enumParameter: EnumParameter +} class MultipleParameterParameters { private _enumParameter: EnumParameter; private _constParameter: 'test' = 'test'; - constructor(input: { - enumParameter: EnumParameter, - }) { + constructor(input: MultipleParameterParametersInterface) { this._enumParameter = input.enumParameter; } @@ -196,17 +200,18 @@ class MultipleParameterParameters { return parameters; } } -export { MultipleParameterParameters };" +export { MultipleParameterParameters, MultipleParameterParametersInterface };" `; exports[`parameters typescript openapi should use constrained property names and skip deserializeUrl without query parameters 1`] = ` " +interface GetDocumentParametersInterface { + documentId: string +} class GetDocumentParameters { private _documentId: string; - constructor(input: { - documentId: string, - }) { + constructor(input: GetDocumentParametersInterface) { this._documentId = input.documentId; } @@ -319,19 +324,20 @@ class GetDocumentParameters { return result; } } -export { GetDocumentParameters };" +export { GetDocumentParameters, GetDocumentParametersInterface };" `; exports[`parameters typescript openapi should use constrained property names and skip deserializeUrl without query parameters 2`] = ` " +interface GetTransactionsParametersInterface { + skip?: number + cvr?: string +} class GetTransactionsParameters { private _skip?: number; private _cvr?: string; - constructor(input: { - skip?: number, - cvr?: string, - }) { + constructor(input: GetTransactionsParametersInterface) { this._skip = input.skip; this._cvr = input.cvr; } @@ -461,17 +467,18 @@ class GetTransactionsParameters { return instance; } } -export { GetTransactionsParameters };" +export { GetTransactionsParameters, GetTransactionsParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 1`] = ` " +interface DeleteOrderParametersInterface { + orderId: number +} class DeleteOrderParameters { private _orderId: number; - constructor(input: { - orderId: number, - }) { + constructor(input: DeleteOrderParametersInterface) { this._orderId = input.orderId; } @@ -587,17 +594,18 @@ class DeleteOrderParameters { return result; } } -export { DeleteOrderParameters };" +export { DeleteOrderParameters, DeleteOrderParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 2`] = ` " +interface DeletePetParametersInterface { + petId: number +} class DeletePetParameters { private _petId: number; - constructor(input: { - petId: number, - }) { + constructor(input: DeletePetParametersInterface) { this._petId = input.petId; } @@ -713,17 +721,18 @@ class DeletePetParameters { return result; } } -export { DeletePetParameters };" +export { DeletePetParameters, DeletePetParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 3`] = ` " +interface DeleteUserParametersInterface { + username: string +} class DeleteUserParameters { private _username: string; - constructor(input: { - username: string, - }) { + constructor(input: DeleteUserParametersInterface) { this._username = input.username; } @@ -839,17 +848,18 @@ class DeleteUserParameters { return result; } } -export { DeleteUserParameters };" +export { DeleteUserParameters, DeleteUserParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 4`] = ` " +interface FindPetsByStatusParametersInterface { + status: any[] +} class FindPetsByStatusParameters { private _status: any[]; - constructor(input: { - status: any[], - }) { + constructor(input: FindPetsByStatusParametersInterface) { this._status = input.status; } @@ -957,17 +967,18 @@ class FindPetsByStatusParameters { return instance; } } -export { FindPetsByStatusParameters };" +export { FindPetsByStatusParameters, FindPetsByStatusParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 5`] = ` " +interface FindPetsByTagsParametersInterface { + tags: any[] +} class FindPetsByTagsParameters { private _tags: any[]; - constructor(input: { - tags: any[], - }) { + constructor(input: FindPetsByTagsParametersInterface) { this._tags = input.tags; } @@ -1075,17 +1086,18 @@ class FindPetsByTagsParameters { return instance; } } -export { FindPetsByTagsParameters };" +export { FindPetsByTagsParameters, FindPetsByTagsParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 6`] = ` " +interface GetOrderByIdParametersInterface { + orderId: number +} class GetOrderByIdParameters { private _orderId: number; - constructor(input: { - orderId: number, - }) { + constructor(input: GetOrderByIdParametersInterface) { this._orderId = input.orderId; } @@ -1201,17 +1213,18 @@ class GetOrderByIdParameters { return result; } } -export { GetOrderByIdParameters };" +export { GetOrderByIdParameters, GetOrderByIdParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 7`] = ` " +interface GetPetByIdParametersInterface { + petId: number +} class GetPetByIdParameters { private _petId: number; - constructor(input: { - petId: number, - }) { + constructor(input: GetPetByIdParametersInterface) { this._petId = input.petId; } @@ -1327,17 +1340,18 @@ class GetPetByIdParameters { return result; } } -export { GetPetByIdParameters };" +export { GetPetByIdParameters, GetPetByIdParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 8`] = ` " +interface GetUserByNameParametersInterface { + username: string +} class GetUserByNameParameters { private _username: string; - constructor(input: { - username: string, - }) { + constructor(input: GetUserByNameParametersInterface) { this._username = input.username; } @@ -1453,19 +1467,20 @@ class GetUserByNameParameters { return result; } } -export { GetUserByNameParameters };" +export { GetUserByNameParameters, GetUserByNameParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 9`] = ` " +interface LoginUserParametersInterface { + username: string + password: string +} class LoginUserParameters { private _username: string; private _password: string; - constructor(input: { - username: string, - password: string, - }) { + constructor(input: LoginUserParametersInterface) { this._username = input.username; this._password = input.password; } @@ -1599,17 +1614,18 @@ class LoginUserParameters { return instance; } } -export { LoginUserParameters };" +export { LoginUserParameters, LoginUserParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 10`] = ` " +interface UpdatePetWithFormParametersInterface { + petId: number +} class UpdatePetWithFormParameters { private _petId: number; - constructor(input: { - petId: number, - }) { + constructor(input: UpdatePetWithFormParametersInterface) { this._petId = input.petId; } @@ -1725,17 +1741,18 @@ class UpdatePetWithFormParameters { return result; } } -export { UpdatePetWithFormParameters };" +export { UpdatePetWithFormParameters, UpdatePetWithFormParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 11`] = ` " +interface UpdateUserParametersInterface { + username: string +} class UpdateUserParameters { private _username: string; - constructor(input: { - username: string, - }) { + constructor(input: UpdateUserParametersInterface) { this._username = input.username; } @@ -1851,17 +1868,18 @@ class UpdateUserParameters { return result; } } -export { UpdateUserParameters };" +export { UpdateUserParameters, UpdateUserParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 12`] = ` " +interface UploadFileParametersInterface { + petId: number +} class UploadFileParameters { private _petId: number; - constructor(input: { - petId: number, - }) { + constructor(input: UploadFileParametersInterface) { this._petId = input.petId; } @@ -1977,17 +1995,18 @@ class UploadFileParameters { return result; } } -export { UploadFileParameters };" +export { UploadFileParameters, UploadFileParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 1`] = ` " +interface DeleteOrderParametersInterface { + orderId: string +} class DeleteOrderParameters { private _orderId: string; - constructor(input: { - orderId: string, - }) { + constructor(input: DeleteOrderParametersInterface) { this._orderId = input.orderId; } @@ -2103,17 +2122,18 @@ class DeleteOrderParameters { return result; } } -export { DeleteOrderParameters };" +export { DeleteOrderParameters, DeleteOrderParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 2`] = ` " +interface DeletePetParametersInterface { + petId: number +} class DeletePetParameters { private _petId: number; - constructor(input: { - petId: number, - }) { + constructor(input: DeletePetParametersInterface) { this._petId = input.petId; } @@ -2229,17 +2249,18 @@ class DeletePetParameters { return result; } } -export { DeletePetParameters };" +export { DeletePetParameters, DeletePetParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 3`] = ` " +interface DeleteUserParametersInterface { + username: string +} class DeleteUserParameters { private _username: string; - constructor(input: { - username: string, - }) { + constructor(input: DeleteUserParametersInterface) { this._username = input.username; } @@ -2355,17 +2376,18 @@ class DeleteUserParameters { return result; } } -export { DeleteUserParameters };" +export { DeleteUserParameters, DeleteUserParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 4`] = ` "import {StatusItem} from './StatusItem'; +interface FindPetsByStatusParametersInterface { + status: StatusItem[] +} class FindPetsByStatusParameters { private _status: StatusItem[]; - constructor(input: { - status: StatusItem[], - }) { + constructor(input: FindPetsByStatusParametersInterface) { this._status = input.status; } @@ -2475,17 +2497,18 @@ class FindPetsByStatusParameters { return instance; } } -export { FindPetsByStatusParameters };" +export { FindPetsByStatusParameters, FindPetsByStatusParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 5`] = ` " +interface FindPetsByTagsParametersInterface { + tags: string[] +} class FindPetsByTagsParameters { private _tags: string[]; - constructor(input: { - tags: string[], - }) { + constructor(input: FindPetsByTagsParametersInterface) { this._tags = input.tags; } @@ -2595,17 +2618,18 @@ class FindPetsByTagsParameters { return instance; } } -export { FindPetsByTagsParameters };" +export { FindPetsByTagsParameters, FindPetsByTagsParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 6`] = ` " +interface GetOrderByIdParametersInterface { + orderId: number +} class GetOrderByIdParameters { private _orderId: number; - constructor(input: { - orderId: number, - }) { + constructor(input: GetOrderByIdParametersInterface) { this._orderId = input.orderId; } @@ -2721,17 +2745,18 @@ class GetOrderByIdParameters { return result; } } -export { GetOrderByIdParameters };" +export { GetOrderByIdParameters, GetOrderByIdParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 7`] = ` " +interface GetPetByIdParametersInterface { + petId: number +} class GetPetByIdParameters { private _petId: number; - constructor(input: { - petId: number, - }) { + constructor(input: GetPetByIdParametersInterface) { this._petId = input.petId; } @@ -2847,17 +2872,18 @@ class GetPetByIdParameters { return result; } } -export { GetPetByIdParameters };" +export { GetPetByIdParameters, GetPetByIdParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 8`] = ` " +interface GetUserByNameParametersInterface { + username: string +} class GetUserByNameParameters { private _username: string; - constructor(input: { - username: string, - }) { + constructor(input: GetUserByNameParametersInterface) { this._username = input.username; } @@ -2973,19 +2999,20 @@ class GetUserByNameParameters { return result; } } -export { GetUserByNameParameters };" +export { GetUserByNameParameters, GetUserByNameParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 9`] = ` " +interface LoginUserParametersInterface { + username: string + password: string +} class LoginUserParameters { private _username: string; private _password: string; - constructor(input: { - username: string, - password: string, - }) { + constructor(input: LoginUserParametersInterface) { this._username = input.username; this._password = input.password; } @@ -3119,17 +3146,18 @@ class LoginUserParameters { return instance; } } -export { LoginUserParameters };" +export { LoginUserParameters, LoginUserParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 10`] = ` " +interface UpdatePetWithFormParametersInterface { + petId: number +} class UpdatePetWithFormParameters { private _petId: number; - constructor(input: { - petId: number, - }) { + constructor(input: UpdatePetWithFormParametersInterface) { this._petId = input.petId; } @@ -3245,17 +3273,18 @@ class UpdatePetWithFormParameters { return result; } } -export { UpdatePetWithFormParameters };" +export { UpdatePetWithFormParameters, UpdatePetWithFormParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 11`] = ` " +interface UpdateUserParametersInterface { + username: string +} class UpdateUserParameters { private _username: string; - constructor(input: { - username: string, - }) { + constructor(input: UpdateUserParametersInterface) { this._username = input.username; } @@ -3371,17 +3400,18 @@ class UpdateUserParameters { return result; } } -export { UpdateUserParameters };" +export { UpdateUserParameters, UpdateUserParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.0 that contains parameters 12`] = ` " +interface UploadFileParametersInterface { + petId: number +} class UploadFileParameters { private _petId: number; - constructor(input: { - petId: number, - }) { + constructor(input: UploadFileParametersInterface) { this._petId = input.petId; } @@ -3497,17 +3527,18 @@ class UploadFileParameters { return result; } } -export { UploadFileParameters };" +export { UploadFileParameters, UploadFileParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 1`] = ` " +interface DeleteOrderParametersInterface { + orderId: string +} class DeleteOrderParameters { private _orderId: string; - constructor(input: { - orderId: string, - }) { + constructor(input: DeleteOrderParametersInterface) { this._orderId = input.orderId; } @@ -3623,17 +3654,18 @@ class DeleteOrderParameters { return result; } } -export { DeleteOrderParameters };" +export { DeleteOrderParameters, DeleteOrderParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 2`] = ` " +interface DeletePetParametersInterface { + petId: number +} class DeletePetParameters { private _petId: number; - constructor(input: { - petId: number, - }) { + constructor(input: DeletePetParametersInterface) { this._petId = input.petId; } @@ -3749,17 +3781,18 @@ class DeletePetParameters { return result; } } -export { DeletePetParameters };" +export { DeletePetParameters, DeletePetParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 3`] = ` " +interface DeleteUserParametersInterface { + username: string +} class DeleteUserParameters { private _username: string; - constructor(input: { - username: string, - }) { + constructor(input: DeleteUserParametersInterface) { this._username = input.username; } @@ -3875,17 +3908,18 @@ class DeleteUserParameters { return result; } } -export { DeleteUserParameters };" +export { DeleteUserParameters, DeleteUserParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 4`] = ` "import {StatusItem} from './StatusItem'; +interface FindPetsByStatusParametersInterface { + status: StatusItem[] +} class FindPetsByStatusParameters { private _status: StatusItem[]; - constructor(input: { - status: StatusItem[], - }) { + constructor(input: FindPetsByStatusParametersInterface) { this._status = input.status; } @@ -3995,17 +4029,18 @@ class FindPetsByStatusParameters { return instance; } } -export { FindPetsByStatusParameters };" +export { FindPetsByStatusParameters, FindPetsByStatusParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 5`] = ` " +interface FindPetsByTagsParametersInterface { + tags: string[] +} class FindPetsByTagsParameters { private _tags: string[]; - constructor(input: { - tags: string[], - }) { + constructor(input: FindPetsByTagsParametersInterface) { this._tags = input.tags; } @@ -4115,17 +4150,18 @@ class FindPetsByTagsParameters { return instance; } } -export { FindPetsByTagsParameters };" +export { FindPetsByTagsParameters, FindPetsByTagsParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 6`] = ` " +interface GetOrderByIdParametersInterface { + orderId: number +} class GetOrderByIdParameters { private _orderId: number; - constructor(input: { - orderId: number, - }) { + constructor(input: GetOrderByIdParametersInterface) { this._orderId = input.orderId; } @@ -4241,17 +4277,18 @@ class GetOrderByIdParameters { return result; } } -export { GetOrderByIdParameters };" +export { GetOrderByIdParameters, GetOrderByIdParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 7`] = ` " +interface GetPetByIdParametersInterface { + petId: number +} class GetPetByIdParameters { private _petId: number; - constructor(input: { - petId: number, - }) { + constructor(input: GetPetByIdParametersInterface) { this._petId = input.petId; } @@ -4367,17 +4404,18 @@ class GetPetByIdParameters { return result; } } -export { GetPetByIdParameters };" +export { GetPetByIdParameters, GetPetByIdParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 8`] = ` " +interface GetUserByNameParametersInterface { + username: string +} class GetUserByNameParameters { private _username: string; - constructor(input: { - username: string, - }) { + constructor(input: GetUserByNameParametersInterface) { this._username = input.username; } @@ -4493,19 +4531,20 @@ class GetUserByNameParameters { return result; } } -export { GetUserByNameParameters };" +export { GetUserByNameParameters, GetUserByNameParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 9`] = ` " +interface LoginUserParametersInterface { + username: string + password: string +} class LoginUserParameters { private _username: string; private _password: string; - constructor(input: { - username: string, - password: string, - }) { + constructor(input: LoginUserParametersInterface) { this._username = input.username; this._password = input.password; } @@ -4639,17 +4678,18 @@ class LoginUserParameters { return instance; } } -export { LoginUserParameters };" +export { LoginUserParameters, LoginUserParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 10`] = ` " +interface UpdatePetWithFormParametersInterface { + petId: number +} class UpdatePetWithFormParameters { private _petId: number; - constructor(input: { - petId: number, - }) { + constructor(input: UpdatePetWithFormParametersInterface) { this._petId = input.petId; } @@ -4765,17 +4805,18 @@ class UpdatePetWithFormParameters { return result; } } -export { UpdatePetWithFormParameters };" +export { UpdatePetWithFormParameters, UpdatePetWithFormParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 11`] = ` " +interface UpdateUserParametersInterface { + username: string +} class UpdateUserParameters { private _username: string; - constructor(input: { - username: string, - }) { + constructor(input: UpdateUserParametersInterface) { this._username = input.username; } @@ -4891,17 +4932,18 @@ class UpdateUserParameters { return result; } } -export { UpdateUserParameters };" +export { UpdateUserParameters, UpdateUserParametersInterface };" `; exports[`parameters typescript openapi should work with OpenAPI 3.1 that contains parameters 12`] = ` " +interface UploadFileParametersInterface { + petId: number +} class UploadFileParameters { private _petId: number; - constructor(input: { - petId: number, - }) { + constructor(input: UploadFileParametersInterface) { this._petId = input.petId; } @@ -5017,5 +5059,5 @@ class UploadFileParameters { return result; } } -export { UploadFileParameters };" +export { UploadFileParameters, UploadFileParametersInterface };" `; diff --git a/test/codegen/generators/typescript/channels/utils.spec.ts b/test/codegen/generators/typescript/channels/utils.spec.ts new file mode 100644 index 00000000..a642b0ca --- /dev/null +++ b/test/codegen/generators/typescript/channels/utils.spec.ts @@ -0,0 +1,71 @@ +import { + ConstrainedObjectModel, + OutputModel +} from '@asyncapi/modelina'; +import { + addParametersToDependencies, + parameterInstanceExpression, + parameterUnionType, + renderParameterNormalization +} from '../../../../../src/codegen/generators/typescript/channels/utils'; + +describe('channels utils', () => { + describe('addParametersToDependencies', () => { + it('should import both the parameter class and its companion interface', () => { + const parameterModel = new OutputModel( + '', + new ConstrainedObjectModel('TestParameters', undefined, {}, 'Parameter', {}), + 'TestParameters', + {models: {}, originalInput: undefined} as any, + [] + ); + const dependencies: string[] = []; + addParametersToDependencies( + {channel: parameterModel}, + {outputPath: './parameters'}, + {outputPath: './channels'}, + dependencies + ); + expect(dependencies).toHaveLength(1); + expect(dependencies[0]).toContain('TestParameters, TestParametersInterface'); + expect(dependencies[0]).toMatch( + /^import \{TestParameters, TestParametersInterface\} from '.+';$/ + ); + }); + }); + + describe('parameterUnionType', () => { + it('should widen a parameter model name to an interface | class union', () => { + expect(parameterUnionType('FindPetsByStatusAndCategoryParameters')).toEqual( + 'FindPetsByStatusAndCategoryParametersInterface | FindPetsByStatusAndCategoryParameters' + ); + }); + }); + + describe('parameterInstanceExpression', () => { + it('should resolve a source value to a class instance inline', () => { + expect( + parameterInstanceExpression({ + modelName: 'UserSignedupParameters', + source: 'parameters' + }) + ).toEqual( + '(parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters))' + ); + }); + }); + + describe('renderParameterNormalization', () => { + it('should normalize a source value to a class instance via instanceof guard', () => { + expect( + renderParameterNormalization({ + modelName: 'FindPetsByStatusAndCategoryParameters', + source: 'context.parameters', + target: 'parameters' + }) + ).toEqual( + 'const parameters = context.parameters instanceof FindPetsByStatusAndCategoryParameters ? context.parameters : new FindPetsByStatusAndCategoryParameters(context.parameters);' + ); + }); + }); +}); diff --git a/test/codegen/generators/typescript/utils.spec.ts b/test/codegen/generators/typescript/utils.spec.ts new file mode 100644 index 00000000..41fa2f76 --- /dev/null +++ b/test/codegen/generators/typescript/utils.spec.ts @@ -0,0 +1,54 @@ +import { + ConstrainedObjectModel, + ConstrainedObjectPropertyModel, + ConstrainedIntegerModel, + ConstrainedReferenceModel, + ConstrainedStringModel +} from '@asyncapi/modelina'; +import {buildParametersInterfaceBody} from '../../../../src/codegen/generators/typescript/utils'; + +describe('typescript generator utils', () => { + describe('buildParametersInterfaceBody', () => { + it('should emit one indented property line per property with optional markers', () => { + const statusProperty = new ConstrainedObjectPropertyModel( + 'status', + 'status', + true, + new ConstrainedReferenceModel( + 'status', + undefined, + {}, + 'Status', + new ConstrainedStringModel('status', undefined, {}, 'string') + ) + ); + const categoryIdProperty = new ConstrainedObjectPropertyModel( + 'categoryId', + 'categoryId', + true, + new ConstrainedIntegerModel('categoryId', undefined, {}, 'number') + ); + const limitProperty = new ConstrainedObjectPropertyModel( + 'limit', + 'limit', + false, + new ConstrainedIntegerModel('limit', undefined, {}, 'number') + ); + const model = new ConstrainedObjectModel( + 'FindPetsParameters', + undefined, + {}, + 'Parameter', + { + status: statusProperty, + categoryId: categoryIdProperty, + limit: limitProperty + } + ); + + expect(buildParametersInterfaceBody(model)).toEqual( + ' status: Status\n categoryId: number\n limit?: number' + ); + }); + }); +}); diff --git a/test/runtime/typescript/src/asyncapi-path-organization/channels/nats.ts b/test/runtime/typescript/src/asyncapi-path-organization/channels/nats.ts index 620d6923..38401be4 100644 --- a/test/runtime/typescript/src/asyncapi-path-organization/channels/nats.ts +++ b/test/runtime/typescript/src/asyncapi-path-organization/channels/nats.ts @@ -5,7 +5,7 @@ import * as UnionMessageModule from './payload/UnionMessage'; import {LegacyNotification} from './payload/LegacyNotification'; import {UnionPayloadOneOfOption2} from './payload/UnionPayloadOneOfOption2'; import {LegacyNotificationPayloadLevelEnum} from './payload/LegacyNotificationPayloadLevelEnum'; -import {UserSignedupParameters} from './parameter/UserSignedupParameters'; +import {UserSignedupParameters, UserSignedupParametersInterface} from './parameter/UserSignedupParameters'; import {UserSignedUpHeaders} from './headers/UserSignedUpHeaders'; import * as Nats from 'nats'; @@ -28,7 +28,7 @@ function publishToSendUserSignedup({ options }: { message: UserSignedUp, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, headers?: UserSignedUpHeaders, nc: Nats.NatsConnection, codec?: Nats.Codec, @@ -50,7 +50,7 @@ function publishToSendUserSignedup({ options = { ...options, headers: natsHeaders }; } dataToSend = codec.encode(dataToSend); -nc.publish(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), dataToSend, options); +nc.publish((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), dataToSend, options); resolve(); } catch (e: any) { reject(e); @@ -77,7 +77,7 @@ function jetStreamPublishToSendUserSignedup({ options = {} }: { message: UserSignedUp, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, headers?: UserSignedUpHeaders, js: Nats.JetStreamClient, codec?: Nats.Codec, @@ -99,7 +99,7 @@ function jetStreamPublishToSendUserSignedup({ options = { ...options, headers: natsHeaders }; } dataToSend = codec.encode(dataToSend); -await js.publish(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), dataToSend, options); +await js.publish((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), dataToSend, options); resolve(); } catch (e: any) { reject(e); @@ -137,7 +137,7 @@ function subscribeToReceiveUserSignedup({ skipMessageValidation = false }: { onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, headers?: UserSignedUpHeaders, natsMsg?: Nats.Msg) => void, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, nc: Nats.NatsConnection, codec?: Nats.Codec, options?: Nats.SubscriptionOptions, @@ -145,7 +145,7 @@ function subscribeToReceiveUserSignedup({ }): Promise { return new Promise(async (resolve, reject) => { try { - const subscription = nc.subscribe(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); + const subscription = nc.subscribe((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); const validator = UserSignedUp.createValidator(); (async () => { for await (const msg of subscription) { @@ -215,7 +215,7 @@ function jetStreamPullSubscribeToReceiveUserSignedup({ skipMessageValidation = false }: { onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, headers?: UserSignedUpHeaders, jetstreamMsg?: Nats.JsMsg) => void, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, js: Nats.JetStreamClient, options: Nats.ConsumerOptsBuilder | Partial, codec?: Nats.Codec, @@ -223,7 +223,7 @@ function jetStreamPullSubscribeToReceiveUserSignedup({ }): Promise { return new Promise(async (resolve, reject) => { try { - const subscription = await js.pullSubscribe(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); + const subscription = await js.pullSubscribe((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); const validator = UserSignedUp.createValidator(); (async () => { for await (const msg of subscription) { @@ -293,7 +293,7 @@ function jetStreamPushSubscriptionFromReceiveUserSignedup({ skipMessageValidation = false }: { onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, headers?: UserSignedUpHeaders, jetstreamMsg?: Nats.JsMsg) => void, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, js: Nats.JetStreamClient, options: Nats.ConsumerOptsBuilder | Partial, codec?: Nats.Codec, @@ -301,7 +301,7 @@ function jetStreamPushSubscriptionFromReceiveUserSignedup({ }): Promise { return new Promise(async (resolve, reject) => { try { - const subscription = await js.subscribe(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); + const subscription = await js.subscribe((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); const validator = UserSignedUp.createValidator(); (async () => { for await (const msg of subscription) { diff --git a/test/runtime/typescript/src/asyncapi-path-organization/channels/parameter/UserSignedupParameters.ts b/test/runtime/typescript/src/asyncapi-path-organization/channels/parameter/UserSignedupParameters.ts index cf4660d5..4195c425 100644 --- a/test/runtime/typescript/src/asyncapi-path-organization/channels/parameter/UserSignedupParameters.ts +++ b/test/runtime/typescript/src/asyncapi-path-organization/channels/parameter/UserSignedupParameters.ts @@ -1,12 +1,13 @@ import {EnumParameter} from './EnumParameter'; +interface UserSignedupParametersInterface { + myParameter: string + enumParameter: EnumParameter +} class UserSignedupParameters { private _myParameter: string; private _enumParameter: EnumParameter; - constructor(input: { - myParameter: string, - enumParameter: EnumParameter, - }) { + constructor(input: UserSignedupParametersInterface) { this._myParameter = input.myParameter; this._enumParameter = input.enumParameter; } @@ -57,4 +58,4 @@ class UserSignedupParameters { return parameters; } } -export { UserSignedupParameters }; \ No newline at end of file +export { UserSignedupParameters, UserSignedupParametersInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/asyncapi-tag-organization/channels/nats.ts b/test/runtime/typescript/src/asyncapi-tag-organization/channels/nats.ts index ba5b09e6..dde54388 100644 --- a/test/runtime/typescript/src/asyncapi-tag-organization/channels/nats.ts +++ b/test/runtime/typescript/src/asyncapi-tag-organization/channels/nats.ts @@ -1,7 +1,7 @@ import {UserSignedUp} from './payload/UserSignedUp'; import {AdminAlert} from './payload/AdminAlert'; import {SystemPing} from './payload/SystemPing'; -import {UserSignedupParameters} from './parameter/UserSignedupParameters'; +import {UserSignedupParameters, UserSignedupParametersInterface} from './parameter/UserSignedupParameters'; import * as Nats from 'nats'; /** @@ -21,7 +21,7 @@ function publishToSendUserSignedup({ options }: { message: UserSignedUp, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, nc: Nats.NatsConnection, codec?: Nats.Codec, options?: Nats.PublishOptions @@ -31,7 +31,7 @@ function publishToSendUserSignedup({ let dataToSend: any = message.marshal(); dataToSend = codec.encode(dataToSend); -nc.publish(parameters.getChannelWithParameters('user.signedup.{id}'), dataToSend, options); +nc.publish((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{id}'), dataToSend, options); resolve(); } catch (e: any) { reject(e); @@ -56,7 +56,7 @@ function jetStreamPublishToSendUserSignedup({ options = {} }: { message: UserSignedUp, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, js: Nats.JetStreamClient, codec?: Nats.Codec, options?: Partial @@ -66,7 +66,7 @@ function jetStreamPublishToSendUserSignedup({ let dataToSend: any = message.marshal(); dataToSend = codec.encode(dataToSend); -await js.publish(parameters.getChannelWithParameters('user.signedup.{id}'), dataToSend, options); +await js.publish((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{id}'), dataToSend, options); resolve(); } catch (e: any) { reject(e); @@ -103,7 +103,7 @@ function subscribeToReceiveUserSignedup({ skipMessageValidation = false }: { onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, natsMsg?: Nats.Msg) => void, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, nc: Nats.NatsConnection, codec?: Nats.Codec, options?: Nats.SubscriptionOptions, @@ -111,7 +111,7 @@ function subscribeToReceiveUserSignedup({ }): Promise { return new Promise(async (resolve, reject) => { try { - const subscription = nc.subscribe(parameters.getChannelWithParameters('user.signedup.{id}'), options); + const subscription = nc.subscribe((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{id}'), options); const validator = UserSignedUp.createValidator(); (async () => { for await (const msg of subscription) { @@ -162,7 +162,7 @@ function jetStreamPullSubscribeToReceiveUserSignedup({ skipMessageValidation = false }: { onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, jetstreamMsg?: Nats.JsMsg) => void, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, js: Nats.JetStreamClient, options: Nats.ConsumerOptsBuilder | Partial, codec?: Nats.Codec, @@ -170,7 +170,7 @@ function jetStreamPullSubscribeToReceiveUserSignedup({ }): Promise { return new Promise(async (resolve, reject) => { try { - const subscription = await js.pullSubscribe(parameters.getChannelWithParameters('user.signedup.{id}'), options); + const subscription = await js.pullSubscribe((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{id}'), options); const validator = UserSignedUp.createValidator(); (async () => { for await (const msg of subscription) { @@ -221,7 +221,7 @@ function jetStreamPushSubscriptionFromReceiveUserSignedup({ skipMessageValidation = false }: { onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, jetstreamMsg?: Nats.JsMsg) => void, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, js: Nats.JetStreamClient, options: Nats.ConsumerOptsBuilder | Partial, codec?: Nats.Codec, @@ -229,7 +229,7 @@ function jetStreamPushSubscriptionFromReceiveUserSignedup({ }): Promise { return new Promise(async (resolve, reject) => { try { - const subscription = await js.subscribe(parameters.getChannelWithParameters('user.signedup.{id}'), options); + const subscription = await js.subscribe((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{id}'), options); const validator = UserSignedUp.createValidator(); (async () => { for await (const msg of subscription) { diff --git a/test/runtime/typescript/src/asyncapi-tag-organization/channels/parameter/UserSignedupParameters.ts b/test/runtime/typescript/src/asyncapi-tag-organization/channels/parameter/UserSignedupParameters.ts index 1582c932..1cd5731e 100644 --- a/test/runtime/typescript/src/asyncapi-tag-organization/channels/parameter/UserSignedupParameters.ts +++ b/test/runtime/typescript/src/asyncapi-tag-organization/channels/parameter/UserSignedupParameters.ts @@ -1,10 +1,11 @@ +interface UserSignedupParametersInterface { + id: string +} class UserSignedupParameters { private _id: string; - constructor(input: { - id: string, - }) { + constructor(input: UserSignedupParametersInterface) { this._id = input.id; } @@ -41,4 +42,4 @@ class UserSignedupParameters { return parameters; } } -export { UserSignedupParameters }; \ No newline at end of file +export { UserSignedupParameters, UserSignedupParametersInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/channels/amqp.ts b/test/runtime/typescript/src/channels/amqp.ts index 4b497b5b..c8abf775 100644 --- a/test/runtime/typescript/src/channels/amqp.ts +++ b/test/runtime/typescript/src/channels/amqp.ts @@ -5,7 +5,7 @@ import * as UnionMessageModule from './../payloads/UnionMessage'; import {LegacyNotification} from './../payloads/LegacyNotification'; import {UnionPayloadOneOfOption2} from './../payloads/UnionPayloadOneOfOption2'; import {LegacyNotificationPayloadLevelEnum} from './../payloads/LegacyNotificationPayloadLevelEnum'; -import {UserSignedupParameters} from './../parameters/UserSignedupParameters'; +import {UserSignedupParameters, UserSignedupParametersInterface} from './../parameters/UserSignedupParameters'; import {UserSignedUpHeaders} from './../headers/UserSignedUpHeaders'; import * as Amqp from 'amqplib'; @@ -26,7 +26,7 @@ function publishToSendUserSignedupExchange({ options }: { message: UserSignedUp, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, headers?: UserSignedUpHeaders, amqp: Amqp.Connection, options?: {exchange: string | undefined} & Amqp.Options.Publish @@ -39,7 +39,7 @@ function publishToSendUserSignedupExchange({ try { let dataToSend: any = message.marshal(); const channel = await amqp.createChannel(); -const routingKey = parameters.getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'); +const routingKey = (parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'); // Set up message properties (headers) if provided let publishOptions = { ...options }; if (headers) { @@ -77,7 +77,7 @@ function publishToSendUserSignedupQueue({ options }: { message: UserSignedUp, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, headers?: UserSignedUpHeaders, amqp: Amqp.Connection, options?: Amqp.Options.Publish @@ -86,7 +86,7 @@ function publishToSendUserSignedupQueue({ try { let dataToSend: any = message.marshal(); const channel = await amqp.createChannel(); -const queue = parameters.getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'); +const queue = (parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'); // Set up message properties (headers) if provided let publishOptions = { ...options }; if (headers) { @@ -124,7 +124,7 @@ function subscribeToReceiveUserSignedupQueue({ skipMessageValidation = false }: { onDataCallback: (params: {err?: Error, msg?: UserSignedUp, headers?: UserSignedUpHeaders, amqpMsg?: Amqp.ConsumeMessage}) => void, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, amqp: Amqp.Connection, options?: Amqp.Options.Consume, skipMessageValidation?: boolean @@ -132,7 +132,7 @@ function subscribeToReceiveUserSignedupQueue({ return new Promise(async (resolve, reject) => { try { const channel = await amqp.createChannel(); -const queue = parameters.getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'); +const queue = (parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'); await channel.assertQueue(queue, { durable: true }); const validator = UserSignedUp.createValidator(); channel.consume(queue, (msg) => { diff --git a/test/runtime/typescript/src/channels/event_source.ts b/test/runtime/typescript/src/channels/event_source.ts index 31a7e879..69d41912 100644 --- a/test/runtime/typescript/src/channels/event_source.ts +++ b/test/runtime/typescript/src/channels/event_source.ts @@ -5,7 +5,7 @@ import * as UnionMessageModule from './../payloads/UnionMessage'; import {LegacyNotification} from './../payloads/LegacyNotification'; import {UnionPayloadOneOfOption2} from './../payloads/UnionPayloadOneOfOption2'; import {LegacyNotificationPayloadLevelEnum} from './../payloads/LegacyNotificationPayloadLevelEnum'; -import {UserSignedupParameters} from './../parameters/UserSignedupParameters'; +import {UserSignedupParameters, UserSignedupParametersInterface} from './../parameters/UserSignedupParameters'; import {UserSignedUpHeaders} from './../headers/UserSignedUpHeaders'; import { NextFunction, Request, Response, Router } from 'express'; import { fetchEventSource, EventStreamContentType, EventSourceMessage } from '@ai-zen/node-fetch-event-source'; @@ -62,13 +62,13 @@ function listenForReceiveUserSignedup({ skipMessageValidation = false }: { callback: (params: {error?: Error, messageEvent?: UserSignedUp}) => void, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, headers?: UserSignedUpHeaders, options: {authorization?: string, onClose?: (err?: string) => void, baseUrl: string, headers?: Record}, skipMessageValidation?: boolean }): (() => void) { const controller = new AbortController(); - let eventsUrl: string = parameters.getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'); + let eventsUrl: string = (parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'); const url = `${options.baseUrl}/${eventsUrl}` const requestHeaders: Record = { ...options.headers ?? {}, diff --git a/test/runtime/typescript/src/channels/kafka.ts b/test/runtime/typescript/src/channels/kafka.ts index d1884490..ef5d6450 100644 --- a/test/runtime/typescript/src/channels/kafka.ts +++ b/test/runtime/typescript/src/channels/kafka.ts @@ -5,7 +5,7 @@ import * as UnionMessageModule from './../payloads/UnionMessage'; import {LegacyNotification} from './../payloads/LegacyNotification'; import {UnionPayloadOneOfOption2} from './../payloads/UnionPayloadOneOfOption2'; import {LegacyNotificationPayloadLevelEnum} from './../payloads/LegacyNotificationPayloadLevelEnum'; -import {UserSignedupParameters} from './../parameters/UserSignedupParameters'; +import {UserSignedupParameters, UserSignedupParametersInterface} from './../parameters/UserSignedupParameters'; import {UserSignedUpHeaders} from './../headers/UserSignedUpHeaders'; import * as Kafka from 'kafkajs'; @@ -24,7 +24,7 @@ function produceToSendUserSignedup({ kafka }: { message: UserSignedUp, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, headers?: UserSignedUpHeaders, kafka: Kafka.Kafka }): Promise { @@ -47,7 +47,7 @@ function produceToSendUserSignedup({ } await producer.send({ - topic: parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), + topic: (parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), messages: [ { value: dataToSend, @@ -90,7 +90,7 @@ function consumeFromReceiveUserSignedup({ skipMessageValidation = false }: { onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, headers?: UserSignedUpHeaders, kafkaMsg?: Kafka.EachMessagePayload) => void, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, kafka: Kafka.Kafka, options: {fromBeginning: boolean, groupId: string}, skipMessageValidation?: boolean @@ -104,7 +104,7 @@ function consumeFromReceiveUserSignedup({ const validator = UserSignedUp.createValidator(); await consumer.connect(); - await consumer.subscribe({ topic: parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), fromBeginning: options.fromBeginning }); + await consumer.subscribe({ topic: (parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), fromBeginning: options.fromBeginning }); await consumer.run({ eachMessage: async (kafkaMessage: Kafka.EachMessagePayload) => { const { topic, message } = kafkaMessage; diff --git a/test/runtime/typescript/src/channels/mqtt.ts b/test/runtime/typescript/src/channels/mqtt.ts index 31def5c7..94053e23 100644 --- a/test/runtime/typescript/src/channels/mqtt.ts +++ b/test/runtime/typescript/src/channels/mqtt.ts @@ -5,7 +5,7 @@ import * as UnionMessageModule from './../payloads/UnionMessage'; import {LegacyNotification} from './../payloads/LegacyNotification'; import {UnionPayloadOneOfOption2} from './../payloads/UnionPayloadOneOfOption2'; import {LegacyNotificationPayloadLevelEnum} from './../payloads/LegacyNotificationPayloadLevelEnum'; -import {UserSignedupParameters} from './../parameters/UserSignedupParameters'; +import {UserSignedupParameters, UserSignedupParametersInterface} from './../parameters/UserSignedupParameters'; import {UserSignedUpHeaders} from './../headers/UserSignedUpHeaders'; import * as Mqtt from 'mqtt'; @@ -24,7 +24,7 @@ function publishToSendUserSignedup({ mqtt }: { message: UserSignedUp, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, headers?: UserSignedUpHeaders, mqtt: Mqtt.MqttClient }): Promise { @@ -44,7 +44,7 @@ function publishToSendUserSignedup({ } publishOptions.properties = { userProperties }; } - mqtt.publish(parameters.getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'), dataToSend, publishOptions); + mqtt.publish((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}'), dataToSend, publishOptions); resolve(); } catch (e: any) { reject(e); @@ -78,7 +78,7 @@ function subscribeToReceiveUserSignedup({ skipMessageValidation = false }: { onDataCallback: (params: {err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, headers?: UserSignedUpHeaders, mqttMsg?: Mqtt.IPublishPacket}) => void, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, mqtt: Mqtt.MqttClient, skipMessageValidation?: boolean }): Promise { @@ -126,7 +126,7 @@ function subscribeToReceiveUserSignedup({ mqtt.on('message', messageHandler); // Subscribe to the topic - await mqtt.subscribeAsync(parameters.getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}')); + await mqtt.subscribeAsync((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user/signedup/{my_parameter}/{enum_parameter}')); resolve(); } catch (e: any) { diff --git a/test/runtime/typescript/src/channels/nats.ts b/test/runtime/typescript/src/channels/nats.ts index 3bd66868..7341aac4 100644 --- a/test/runtime/typescript/src/channels/nats.ts +++ b/test/runtime/typescript/src/channels/nats.ts @@ -5,7 +5,7 @@ import * as UnionMessageModule from './../payloads/UnionMessage'; import {LegacyNotification} from './../payloads/LegacyNotification'; import {UnionPayloadOneOfOption2} from './../payloads/UnionPayloadOneOfOption2'; import {LegacyNotificationPayloadLevelEnum} from './../payloads/LegacyNotificationPayloadLevelEnum'; -import {UserSignedupParameters} from './../parameters/UserSignedupParameters'; +import {UserSignedupParameters, UserSignedupParametersInterface} from './../parameters/UserSignedupParameters'; import {UserSignedUpHeaders} from './../headers/UserSignedUpHeaders'; import * as Nats from 'nats'; @@ -28,7 +28,7 @@ function publishToSendUserSignedup({ options }: { message: UserSignedUp, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, headers?: UserSignedUpHeaders, nc: Nats.NatsConnection, codec?: Nats.Codec, @@ -50,7 +50,7 @@ function publishToSendUserSignedup({ options = { ...options, headers: natsHeaders }; } dataToSend = codec.encode(dataToSend); -nc.publish(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), dataToSend, options); +nc.publish((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), dataToSend, options); resolve(); } catch (e: any) { reject(e); @@ -77,7 +77,7 @@ function jetStreamPublishToSendUserSignedup({ options = {} }: { message: UserSignedUp, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, headers?: UserSignedUpHeaders, js: Nats.JetStreamClient, codec?: Nats.Codec, @@ -99,7 +99,7 @@ function jetStreamPublishToSendUserSignedup({ options = { ...options, headers: natsHeaders }; } dataToSend = codec.encode(dataToSend); -await js.publish(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), dataToSend, options); +await js.publish((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), dataToSend, options); resolve(); } catch (e: any) { reject(e); @@ -137,7 +137,7 @@ function subscribeToReceiveUserSignedup({ skipMessageValidation = false }: { onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, headers?: UserSignedUpHeaders, natsMsg?: Nats.Msg) => void, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, nc: Nats.NatsConnection, codec?: Nats.Codec, options?: Nats.SubscriptionOptions, @@ -145,7 +145,7 @@ function subscribeToReceiveUserSignedup({ }): Promise { return new Promise(async (resolve, reject) => { try { - const subscription = nc.subscribe(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); + const subscription = nc.subscribe((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); const validator = UserSignedUp.createValidator(); (async () => { for await (const msg of subscription) { @@ -215,7 +215,7 @@ function jetStreamPullSubscribeToReceiveUserSignedup({ skipMessageValidation = false }: { onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, headers?: UserSignedUpHeaders, jetstreamMsg?: Nats.JsMsg) => void, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, js: Nats.JetStreamClient, options: Nats.ConsumerOptsBuilder | Partial, codec?: Nats.Codec, @@ -223,7 +223,7 @@ function jetStreamPullSubscribeToReceiveUserSignedup({ }): Promise { return new Promise(async (resolve, reject) => { try { - const subscription = await js.pullSubscribe(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); + const subscription = await js.pullSubscribe((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); const validator = UserSignedUp.createValidator(); (async () => { for await (const msg of subscription) { @@ -293,7 +293,7 @@ function jetStreamPushSubscriptionFromReceiveUserSignedup({ skipMessageValidation = false }: { onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, headers?: UserSignedUpHeaders, jetstreamMsg?: Nats.JsMsg) => void, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, js: Nats.JetStreamClient, options: Nats.ConsumerOptsBuilder | Partial, codec?: Nats.Codec, @@ -301,7 +301,7 @@ function jetStreamPushSubscriptionFromReceiveUserSignedup({ }): Promise { return new Promise(async (resolve, reject) => { try { - const subscription = await js.subscribe(parameters.getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); + const subscription = await js.subscribe((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); const validator = UserSignedUp.createValidator(); (async () => { for await (const msg of subscription) { diff --git a/test/runtime/typescript/src/channels/websocket.ts b/test/runtime/typescript/src/channels/websocket.ts index 831a59df..c9bfef02 100644 --- a/test/runtime/typescript/src/channels/websocket.ts +++ b/test/runtime/typescript/src/channels/websocket.ts @@ -5,7 +5,7 @@ import * as UnionMessageModule from './../payloads/UnionMessage'; import {LegacyNotification} from './../payloads/LegacyNotification'; import {UnionPayloadOneOfOption2} from './../payloads/UnionPayloadOneOfOption2'; import {LegacyNotificationPayloadLevelEnum} from './../payloads/LegacyNotificationPayloadLevelEnum'; -import {UserSignedupParameters} from './../parameters/UserSignedupParameters'; +import {UserSignedupParameters, UserSignedupParametersInterface} from './../parameters/UserSignedupParameters'; import {UserSignedUpHeaders} from './../headers/UserSignedUpHeaders'; import * as WebSocket from 'ws'; import { IncomingMessage } from 'http'; @@ -111,17 +111,18 @@ function subscribeToReceiveUserSignedup({ skipMessageValidation = false }: { onDataCallback: (params: {err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, ws?: WebSocket.WebSocket}) => void, - parameters: UserSignedupParameters, + parameters: UserSignedupParametersInterface | UserSignedupParameters, ws: WebSocket.WebSocket, skipMessageValidation?: boolean }): void { + const parameterInstance = parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters); try { // Check if WebSocket is open if (ws.readyState !== WebSocket.WebSocket.OPEN) { onDataCallback({ err: new Error('WebSocket is not open'), msg: undefined, - parameters, + parameters: parameterInstance, ws }); return; @@ -142,7 +143,7 @@ function subscribeToReceiveUserSignedup({ onDataCallback({ err: new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), msg: undefined, - parameters, + parameters: parameterInstance, ws }); return; @@ -152,7 +153,7 @@ function subscribeToReceiveUserSignedup({ onDataCallback({ err: undefined, msg: parsedMessage, - parameters, + parameters: parameterInstance, ws }); @@ -160,7 +161,7 @@ function subscribeToReceiveUserSignedup({ onDataCallback({ err: new Error(`Failed to parse message: ${error.message}`), msg: undefined, - parameters, + parameters: parameterInstance, ws }); } @@ -170,7 +171,7 @@ function subscribeToReceiveUserSignedup({ onDataCallback({ err: new Error(`WebSocket error: ${error.message}`), msg: undefined, - parameters, + parameters: parameterInstance, ws }); }); @@ -181,7 +182,7 @@ function subscribeToReceiveUserSignedup({ onDataCallback({ err: new Error(`WebSocket closed unexpectedly: ${code} ${reason.toString()}`), msg: undefined, - parameters, + parameters: parameterInstance, ws }); } @@ -191,7 +192,7 @@ function subscribeToReceiveUserSignedup({ onDataCallback({ err: new Error(`Failed to set up WebSocket subscription: ${error.message}`), msg: undefined, - parameters, + parameters: parameterInstance, ws }); } diff --git a/test/runtime/typescript/src/client/NatsClient.ts b/test/runtime/typescript/src/client/NatsClient.ts index 1d3e548b..3388dc9a 100644 --- a/test/runtime/typescript/src/client/NatsClient.ts +++ b/test/runtime/typescript/src/client/NatsClient.ts @@ -12,7 +12,7 @@ export {UnionMessageModule}; export {LegacyNotification}; export {UnionPayloadOneOfOption2}; export {LegacyNotificationPayloadLevelEnum}; -import {UserSignedupParameters} from './../parameters/UserSignedupParameters'; +import {UserSignedupParameters, UserSignedupParametersInterface} from './../parameters/UserSignedupParameters'; export {UserSignedupParameters}; //Import channel functions diff --git a/test/runtime/typescript/src/node16/channels/index.ts b/test/runtime/typescript/src/node16/channels/index.ts new file mode 100644 index 00000000..98c9909d --- /dev/null +++ b/test/runtime/typescript/src/node16/channels/index.ts @@ -0,0 +1,3 @@ +import * as nats from './nats.ts'; + +export {nats}; diff --git a/test/runtime/typescript/src/node16/channels/nats.ts b/test/runtime/typescript/src/node16/channels/nats.ts new file mode 100644 index 00000000..81fe632b --- /dev/null +++ b/test/runtime/typescript/src/node16/channels/nats.ts @@ -0,0 +1,1584 @@ +import {UserSignedUp} from './../payloads/UserSignedUp.ts'; +import * as StringMessageModule from './../payloads/StringMessage.ts'; +import * as ArrayMessageModule from './../payloads/ArrayMessage.ts'; +import * as UnionMessageModule from './../payloads/UnionMessage.ts'; +import {LegacyNotification} from './../payloads/LegacyNotification.ts'; +import {UnionPayloadOneOfOption2} from './../payloads/UnionPayloadOneOfOption2.ts'; +import {LegacyNotificationPayloadLevelEnum} from './../payloads/LegacyNotificationPayloadLevelEnum.ts'; +import {UserSignedupParameters, UserSignedupParametersInterface} from './../parameters/UserSignedupParameters.ts'; +import {UserSignedUpHeaders} from './../headers/UserSignedUpHeaders.ts'; +import * as Nats from 'nats'; + +/** + * Publishes a user signup event to notify other services that a new user has registered in the system. + * + * @param message to publish + * @param parameters for topic substitution + * @param headers optional headers to include with the message + * @param nc the NATS client to publish from + * @param codec the serialization codec to use while transmitting the message + * @param options to use while publishing the message + */ +function publishToSendUserSignedup({ + message, + parameters, + headers, + nc, + codec = Nats.JSONCodec(), + options +}: { + message: UserSignedUp, + parameters: UserSignedupParametersInterface | UserSignedupParameters, + headers?: UserSignedUpHeaders, + nc: Nats.NatsConnection, + codec?: Nats.Codec, + options?: Nats.PublishOptions +}): Promise { + return new Promise(async (resolve, reject) => { + try { + let dataToSend: any = message.marshal(); + // Set up headers if provided + if (headers) { + const natsHeaders = Nats.headers(); + const headerData = headers.marshal(); + const parsedHeaders = typeof headerData === 'string' ? JSON.parse(headerData) : headerData; + for (const [key, value] of Object.entries(parsedHeaders)) { + if (value !== undefined) { + natsHeaders.append(key, String(value)); + } + } + options = { ...options, headers: natsHeaders }; + } +dataToSend = codec.encode(dataToSend); +nc.publish((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), dataToSend, options); + resolve(); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Publishes a user signup event to notify other services that a new user has registered in the system. + * + * @param message to publish over jetstream + * @param parameters for topic substitution + * @param headers optional headers to include with the message + * @param js the JetStream client to publish from + * @param codec the serialization codec to use while transmitting the message + * @param options to use while publishing the message + */ +function jetStreamPublishToSendUserSignedup({ + message, + parameters, + headers, + js, + codec = Nats.JSONCodec(), + options = {} +}: { + message: UserSignedUp, + parameters: UserSignedupParametersInterface | UserSignedupParameters, + headers?: UserSignedUpHeaders, + js: Nats.JetStreamClient, + codec?: Nats.Codec, + options?: Partial +}): Promise { + return new Promise(async (resolve, reject) => { + try { + let dataToSend: any = message.marshal(); + // Set up headers if provided + if (headers) { + const natsHeaders = Nats.headers(); + const headerData = headers.marshal(); + const parsedHeaders = typeof headerData === 'string' ? JSON.parse(headerData) : headerData; + for (const [key, value] of Object.entries(parsedHeaders)) { + if (value !== undefined) { + natsHeaders.append(key, String(value)); + } + } + options = { ...options, headers: natsHeaders }; + } +dataToSend = codec.encode(dataToSend); +await js.publish((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), dataToSend, options); + resolve(); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Callback for when receiving messages + * + * @callback subscribeToReceiveUserSignedupCallback + * @param err if any error occurred this will be sat + * @param msg that was received + * @param parameters that was received in the topic + * @param headers that were received with the message + * @param natsMsg + */ + +/** + * Receives user signup events to process new user registrations. + * + * @param {subscribeToReceiveUserSignedupCallback} onDataCallback to call when messages are received + * @param parameters for topic substitution + * @param nc the nats client to setup the subscribe for + * @param codec the serialization codec to use while receiving the message + * @param options when setting up the subscription + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +function subscribeToReceiveUserSignedup({ + onDataCallback, + parameters, + nc, + codec = Nats.JSONCodec(), + options, + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, headers?: UserSignedUpHeaders, natsMsg?: Nats.Msg) => void, + parameters: UserSignedupParametersInterface | UserSignedupParameters, + nc: Nats.NatsConnection, + codec?: Nats.Codec, + options?: Nats.SubscriptionOptions, + skipMessageValidation?: boolean +}): Promise { + return new Promise(async (resolve, reject) => { + try { + const subscription = nc.subscribe((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); + const validator = UserSignedUp.createValidator(); + (async () => { + for await (const msg of subscription) { + const parameters = UserSignedupParameters.createFromChannel(msg.subject, 'user.signedup.{my_parameter}.{enum_parameter}', /^user.signedup.([^.]*).([^.]*)$/) + let receivedData: any = codec.decode(msg.data); +// Extract headers if present + let extractedHeaders: UserSignedUpHeaders | undefined = undefined; + if (msg.headers) { + const headerObj: Record = {}; + // NATS headers support both iteration and get() method + if (typeof msg.headers.keys === 'function') { + // Use keys() method if available (NATS MsgHdrs) + for (const key of msg.headers.keys()) { + headerObj[key] = msg.headers.get(key); + } + } else { + // Fallback to Object.entries for plain objects + for (const [key, value] of Object.entries(msg.headers)) { + headerObj[key] = value; + } + } + extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); + } +if(!skipMessageValidation) { + const {valid, errors} = UserSignedUp.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined,parameters, extractedHeaders, msg); continue; + } + } +onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), parameters, extractedHeaders, msg); + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Callback for when receiving messages + * + * @callback jetStreamPullSubscribeToReceiveUserSignedupCallback + * @param err if any error occurred this will be sat + * @param msg that was received + * @param parameters that was received in the topic + * @param headers that was received with the message + * @param jetstreamMsg + */ + +/** + * Receives user signup events to process new user registrations. + * + * @param {jetStreamPullSubscribeToReceiveUserSignedupCallback} onDataCallback to call when messages are received + * @param parameters for topic substitution + * @param js the JetStream client to pull subscribe through + * @param options when setting up the subscription + * @param codec the serialization codec to use while transmitting the message + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +function jetStreamPullSubscribeToReceiveUserSignedup({ + onDataCallback, + parameters, + js, + options, + codec = Nats.JSONCodec(), + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, headers?: UserSignedUpHeaders, jetstreamMsg?: Nats.JsMsg) => void, + parameters: UserSignedupParametersInterface | UserSignedupParameters, + js: Nats.JetStreamClient, + options: Nats.ConsumerOptsBuilder | Partial, + codec?: Nats.Codec, + skipMessageValidation?: boolean +}): Promise { + return new Promise(async (resolve, reject) => { + try { + const subscription = await js.pullSubscribe((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); + const validator = UserSignedUp.createValidator(); + (async () => { + for await (const msg of subscription) { + const parameters = UserSignedupParameters.createFromChannel(msg.subject, 'user.signedup.{my_parameter}.{enum_parameter}', /^user.signedup.([^.]*).([^.]*)$/) + let receivedData: any = codec.decode(msg.data); +// Extract headers if present + let extractedHeaders: UserSignedUpHeaders | undefined = undefined; + if (msg.headers) { + const headerObj: Record = {}; + // NATS headers support both iteration and get() method + if (typeof msg.headers.keys === 'function') { + // Use keys() method if available (NATS MsgHdrs) + for (const key of msg.headers.keys()) { + headerObj[key] = msg.headers.get(key); + } + } else { + // Fallback to Object.entries for plain objects + for (const [key, value] of Object.entries(msg.headers)) { + headerObj[key] = value; + } + } + extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); + } +if(!skipMessageValidation) { + const {valid, errors} = UserSignedUp.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined,parameters, extractedHeaders, msg); continue; + } + } +onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), parameters, extractedHeaders, msg); + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Callback for when receiving messages + * + * @callback jetStreamPushSubscriptionFromReceiveUserSignedupCallback + * @param err if any error occurred this will be sat + * @param msg that was received + * @param parameters that was received in the topic + * @param headers that was received with the message + * @param jetstreamMsg + */ + +/** + * Receives user signup events to process new user registrations. + * + * @param {jetStreamPushSubscriptionFromReceiveUserSignedupCallback} onDataCallback to call when messages are received + * @param parameters for topic substitution + * @param js the JetStream client to pull subscribe through + * @param options when setting up the subscription + * @param codec the serialization codec to use while transmitting the message + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +function jetStreamPushSubscriptionFromReceiveUserSignedup({ + onDataCallback, + parameters, + js, + options, + codec = Nats.JSONCodec(), + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, msg?: UserSignedUp, parameters?: UserSignedupParameters, headers?: UserSignedUpHeaders, jetstreamMsg?: Nats.JsMsg) => void, + parameters: UserSignedupParametersInterface | UserSignedupParameters, + js: Nats.JetStreamClient, + options: Nats.ConsumerOptsBuilder | Partial, + codec?: Nats.Codec, + skipMessageValidation?: boolean +}): Promise { + return new Promise(async (resolve, reject) => { + try { + const subscription = await js.subscribe((parameters instanceof UserSignedupParameters ? parameters : new UserSignedupParameters(parameters)).getChannelWithParameters('user.signedup.{my_parameter}.{enum_parameter}'), options); + const validator = UserSignedUp.createValidator(); + (async () => { + for await (const msg of subscription) { + const parameters = UserSignedupParameters.createFromChannel(msg.subject, 'user.signedup.{my_parameter}.{enum_parameter}', /^user.signedup.([^.]*).([^.]*)$/) + let receivedData: any = codec.decode(msg.data); +// Extract headers if present + let extractedHeaders: UserSignedUpHeaders | undefined = undefined; + if (msg.headers) { + const headerObj: Record = {}; + // NATS headers support both iteration and get() method + if (typeof msg.headers.keys === 'function') { + // Use keys() method if available (NATS MsgHdrs) + for (const key of msg.headers.keys()) { + headerObj[key] = msg.headers.get(key); + } + } else { + // Fallback to Object.entries for plain objects + for (const [key, value] of Object.entries(msg.headers)) { + headerObj[key] = value; + } + } + extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); + } +if(!skipMessageValidation) { + const {valid, errors} = UserSignedUp.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined,parameters, extractedHeaders, msg); continue; + } + } +onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), parameters, extractedHeaders, msg); + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * NATS publish operation for `noparameters` + * + * @param message to publish + * @param headers optional headers to include with the message + * @param nc the NATS client to publish from + * @param codec the serialization codec to use while transmitting the message + * @param options to use while publishing the message + */ +function publishToNoParameter({ + message, + headers, + nc, + codec = Nats.JSONCodec(), + options +}: { + message: UserSignedUp, + headers?: UserSignedUpHeaders, + nc: Nats.NatsConnection, + codec?: Nats.Codec, + options?: Nats.PublishOptions +}): Promise { + return new Promise(async (resolve, reject) => { + try { + let dataToSend: any = message.marshal(); + // Set up headers if provided + if (headers) { + const natsHeaders = Nats.headers(); + const headerData = headers.marshal(); + const parsedHeaders = typeof headerData === 'string' ? JSON.parse(headerData) : headerData; + for (const [key, value] of Object.entries(parsedHeaders)) { + if (value !== undefined) { + natsHeaders.append(key, String(value)); + } + } + options = { ...options, headers: natsHeaders }; + } +dataToSend = codec.encode(dataToSend); +nc.publish('noparameters', dataToSend, options); + resolve(); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Callback for when receiving messages + * + * @callback subscribeToNoParameterCallback + * @param err if any error occurred this will be sat + * @param msg that was received + * @param headers that were received with the message + * @param natsMsg + */ + +/** + * Core subscription for `noparameters` + * + * @param {subscribeToNoParameterCallback} onDataCallback to call when messages are received + * @param nc the nats client to setup the subscribe for + * @param codec the serialization codec to use while receiving the message + * @param options when setting up the subscription + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +function subscribeToNoParameter({ + onDataCallback, + nc, + codec = Nats.JSONCodec(), + options, + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, msg?: UserSignedUp, headers?: UserSignedUpHeaders, natsMsg?: Nats.Msg) => void, + nc: Nats.NatsConnection, + codec?: Nats.Codec, + options?: Nats.SubscriptionOptions, + skipMessageValidation?: boolean +}): Promise { + return new Promise(async (resolve, reject) => { + try { + const subscription = nc.subscribe('noparameters', options); + const validator = UserSignedUp.createValidator(); + (async () => { + for await (const msg of subscription) { + + let receivedData: any = codec.decode(msg.data); +// Extract headers if present + let extractedHeaders: UserSignedUpHeaders | undefined = undefined; + if (msg.headers) { + const headerObj: Record = {}; + // NATS headers support both iteration and get() method + if (typeof msg.headers.keys === 'function') { + // Use keys() method if available (NATS MsgHdrs) + for (const key of msg.headers.keys()) { + headerObj[key] = msg.headers.get(key); + } + } else { + // Fallback to Object.entries for plain objects + for (const [key, value] of Object.entries(msg.headers)) { + headerObj[key] = value; + } + } + extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); + } +if(!skipMessageValidation) { + const {valid, errors} = UserSignedUp.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined,extractedHeaders, msg); continue; + } + } +onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), extractedHeaders, msg); + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Callback for when receiving messages + * + * @callback jetStreamPullSubscribeToNoParameterCallback + * @param err if any error occurred this will be sat + * @param msg that was received + * @param headers that was received with the message + * @param jetstreamMsg + */ + +/** + * JetStream pull subscription for `noparameters` + * + * @param {jetStreamPullSubscribeToNoParameterCallback} onDataCallback to call when messages are received + * @param js the JetStream client to pull subscribe through + * @param options when setting up the subscription + * @param codec the serialization codec to use while transmitting the message + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +function jetStreamPullSubscribeToNoParameter({ + onDataCallback, + js, + options, + codec = Nats.JSONCodec(), + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, msg?: UserSignedUp, headers?: UserSignedUpHeaders, jetstreamMsg?: Nats.JsMsg) => void, + js: Nats.JetStreamClient, + options: Nats.ConsumerOptsBuilder | Partial, + codec?: Nats.Codec, + skipMessageValidation?: boolean +}): Promise { + return new Promise(async (resolve, reject) => { + try { + const subscription = await js.pullSubscribe('noparameters', options); + const validator = UserSignedUp.createValidator(); + (async () => { + for await (const msg of subscription) { + + let receivedData: any = codec.decode(msg.data); +// Extract headers if present + let extractedHeaders: UserSignedUpHeaders | undefined = undefined; + if (msg.headers) { + const headerObj: Record = {}; + // NATS headers support both iteration and get() method + if (typeof msg.headers.keys === 'function') { + // Use keys() method if available (NATS MsgHdrs) + for (const key of msg.headers.keys()) { + headerObj[key] = msg.headers.get(key); + } + } else { + // Fallback to Object.entries for plain objects + for (const [key, value] of Object.entries(msg.headers)) { + headerObj[key] = value; + } + } + extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); + } +if(!skipMessageValidation) { + const {valid, errors} = UserSignedUp.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined,extractedHeaders, msg); continue; + } + } +onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), extractedHeaders, msg); + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Callback for when receiving messages + * + * @callback jetStreamPushSubscriptionFromNoParameterCallback + * @param err if any error occurred this will be sat + * @param msg that was received + * @param headers that was received with the message + * @param jetstreamMsg + */ + +/** + * JetStream push subscription for `noparameters` + * + * @param {jetStreamPushSubscriptionFromNoParameterCallback} onDataCallback to call when messages are received + * @param js the JetStream client to pull subscribe through + * @param options when setting up the subscription + * @param codec the serialization codec to use while transmitting the message + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +function jetStreamPushSubscriptionFromNoParameter({ + onDataCallback, + js, + options, + codec = Nats.JSONCodec(), + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, msg?: UserSignedUp, headers?: UserSignedUpHeaders, jetstreamMsg?: Nats.JsMsg) => void, + js: Nats.JetStreamClient, + options: Nats.ConsumerOptsBuilder | Partial, + codec?: Nats.Codec, + skipMessageValidation?: boolean +}): Promise { + return new Promise(async (resolve, reject) => { + try { + const subscription = await js.subscribe('noparameters', options); + const validator = UserSignedUp.createValidator(); + (async () => { + for await (const msg of subscription) { + + let receivedData: any = codec.decode(msg.data); +// Extract headers if present + let extractedHeaders: UserSignedUpHeaders | undefined = undefined; + if (msg.headers) { + const headerObj: Record = {}; + // NATS headers support both iteration and get() method + if (typeof msg.headers.keys === 'function') { + // Use keys() method if available (NATS MsgHdrs) + for (const key of msg.headers.keys()) { + headerObj[key] = msg.headers.get(key); + } + } else { + // Fallback to Object.entries for plain objects + for (const [key, value] of Object.entries(msg.headers)) { + headerObj[key] = value; + } + } + extractedHeaders = UserSignedUpHeaders.unmarshal(headerObj); + } +if(!skipMessageValidation) { + const {valid, errors} = UserSignedUp.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined,extractedHeaders, msg); continue; + } + } +onDataCallback(undefined, UserSignedUp.unmarshal(receivedData), extractedHeaders, msg); + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * JetStream publish operation for `noparameters` + * + * @param message to publish over jetstream + * @param headers optional headers to include with the message + * @param js the JetStream client to publish from + * @param codec the serialization codec to use while transmitting the message + * @param options to use while publishing the message + */ +function jetStreamPublishToNoParameter({ + message, + headers, + js, + codec = Nats.JSONCodec(), + options = {} +}: { + message: UserSignedUp, + headers?: UserSignedUpHeaders, + js: Nats.JetStreamClient, + codec?: Nats.Codec, + options?: Partial +}): Promise { + return new Promise(async (resolve, reject) => { + try { + let dataToSend: any = message.marshal(); + // Set up headers if provided + if (headers) { + const natsHeaders = Nats.headers(); + const headerData = headers.marshal(); + const parsedHeaders = typeof headerData === 'string' ? JSON.parse(headerData) : headerData; + for (const [key, value] of Object.entries(parsedHeaders)) { + if (value !== undefined) { + natsHeaders.append(key, String(value)); + } + } + options = { ...options, headers: natsHeaders }; + } +dataToSend = codec.encode(dataToSend); +await js.publish('noparameters', dataToSend, options); + resolve(); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * NATS publish operation for `string.payload` + * + * @param message to publish + * @param nc the NATS client to publish from + * @param codec the serialization codec to use while transmitting the message + * @param options to use while publishing the message + */ +function publishToSendStringPayload({ + message, + nc, + codec = Nats.JSONCodec(), + options +}: { + message: StringMessageModule.StringMessage, + nc: Nats.NatsConnection, + codec?: Nats.Codec, + options?: Nats.PublishOptions +}): Promise { + return new Promise(async (resolve, reject) => { + try { + let dataToSend: any = StringMessageModule.marshal(message); + +dataToSend = codec.encode(dataToSend); +nc.publish('string.payload', dataToSend, options); + resolve(); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * JetStream publish operation for `string.payload` + * + * @param message to publish over jetstream + * @param js the JetStream client to publish from + * @param codec the serialization codec to use while transmitting the message + * @param options to use while publishing the message + */ +function jetStreamPublishToSendStringPayload({ + message, + js, + codec = Nats.JSONCodec(), + options = {} +}: { + message: StringMessageModule.StringMessage, + js: Nats.JetStreamClient, + codec?: Nats.Codec, + options?: Partial +}): Promise { + return new Promise(async (resolve, reject) => { + try { + let dataToSend: any = StringMessageModule.marshal(message); + +dataToSend = codec.encode(dataToSend); +await js.publish('string.payload', dataToSend, options); + resolve(); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Callback for when receiving messages + * + * @callback subscribeToReceiveStringPayloadCallback + * @param err if any error occurred this will be sat + * @param msg that was received + * @param natsMsg + */ + +/** + * Core subscription for `string.payload` + * + * @param {subscribeToReceiveStringPayloadCallback} onDataCallback to call when messages are received + * @param nc the nats client to setup the subscribe for + * @param codec the serialization codec to use while receiving the message + * @param options when setting up the subscription + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +function subscribeToReceiveStringPayload({ + onDataCallback, + nc, + codec = Nats.JSONCodec(), + options, + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, msg?: StringMessageModule.StringMessage, natsMsg?: Nats.Msg) => void, + nc: Nats.NatsConnection, + codec?: Nats.Codec, + options?: Nats.SubscriptionOptions, + skipMessageValidation?: boolean +}): Promise { + return new Promise(async (resolve, reject) => { + try { + const subscription = nc.subscribe('string.payload', options); + const validator = StringMessageModule.createValidator(); + (async () => { + for await (const msg of subscription) { + + let receivedData: any = codec.decode(msg.data); +if(!skipMessageValidation) { + const {valid, errors} = StringMessageModule.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, msg); continue; + } + } +onDataCallback(undefined, StringMessageModule.unmarshal(receivedData), msg); + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Callback for when receiving messages + * + * @callback jetStreamPullSubscribeToReceiveStringPayloadCallback + * @param err if any error occurred this will be sat + * @param msg that was received + * @param jetstreamMsg + */ + +/** + * JetStream pull subscription for `string.payload` + * + * @param {jetStreamPullSubscribeToReceiveStringPayloadCallback} onDataCallback to call when messages are received + * @param js the JetStream client to pull subscribe through + * @param options when setting up the subscription + * @param codec the serialization codec to use while transmitting the message + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +function jetStreamPullSubscribeToReceiveStringPayload({ + onDataCallback, + js, + options, + codec = Nats.JSONCodec(), + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, msg?: StringMessageModule.StringMessage, jetstreamMsg?: Nats.JsMsg) => void, + js: Nats.JetStreamClient, + options: Nats.ConsumerOptsBuilder | Partial, + codec?: Nats.Codec, + skipMessageValidation?: boolean +}): Promise { + return new Promise(async (resolve, reject) => { + try { + const subscription = await js.pullSubscribe('string.payload', options); + const validator = StringMessageModule.createValidator(); + (async () => { + for await (const msg of subscription) { + + let receivedData: any = codec.decode(msg.data); +if(!skipMessageValidation) { + const {valid, errors} = StringMessageModule.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, msg); continue; + } + } +onDataCallback(undefined, StringMessageModule.unmarshal(receivedData), msg); + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Callback for when receiving messages + * + * @callback jetStreamPushSubscriptionFromReceiveStringPayloadCallback + * @param err if any error occurred this will be sat + * @param msg that was received + * @param jetstreamMsg + */ + +/** + * JetStream push subscription for `string.payload` + * + * @param {jetStreamPushSubscriptionFromReceiveStringPayloadCallback} onDataCallback to call when messages are received + * @param js the JetStream client to pull subscribe through + * @param options when setting up the subscription + * @param codec the serialization codec to use while transmitting the message + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +function jetStreamPushSubscriptionFromReceiveStringPayload({ + onDataCallback, + js, + options, + codec = Nats.JSONCodec(), + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, msg?: StringMessageModule.StringMessage, jetstreamMsg?: Nats.JsMsg) => void, + js: Nats.JetStreamClient, + options: Nats.ConsumerOptsBuilder | Partial, + codec?: Nats.Codec, + skipMessageValidation?: boolean +}): Promise { + return new Promise(async (resolve, reject) => { + try { + const subscription = await js.subscribe('string.payload', options); + const validator = StringMessageModule.createValidator(); + (async () => { + for await (const msg of subscription) { + + let receivedData: any = codec.decode(msg.data); +if(!skipMessageValidation) { + const {valid, errors} = StringMessageModule.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, msg); continue; + } + } +onDataCallback(undefined, StringMessageModule.unmarshal(receivedData), msg); + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * NATS publish operation for `array.payload` + * + * @param message to publish + * @param nc the NATS client to publish from + * @param codec the serialization codec to use while transmitting the message + * @param options to use while publishing the message + */ +function publishToSendArrayPayload({ + message, + nc, + codec = Nats.JSONCodec(), + options +}: { + message: ArrayMessageModule.ArrayMessage, + nc: Nats.NatsConnection, + codec?: Nats.Codec, + options?: Nats.PublishOptions +}): Promise { + return new Promise(async (resolve, reject) => { + try { + let dataToSend: any = ArrayMessageModule.marshal(message); + +dataToSend = codec.encode(dataToSend); +nc.publish('array.payload', dataToSend, options); + resolve(); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * JetStream publish operation for `array.payload` + * + * @param message to publish over jetstream + * @param js the JetStream client to publish from + * @param codec the serialization codec to use while transmitting the message + * @param options to use while publishing the message + */ +function jetStreamPublishToSendArrayPayload({ + message, + js, + codec = Nats.JSONCodec(), + options = {} +}: { + message: ArrayMessageModule.ArrayMessage, + js: Nats.JetStreamClient, + codec?: Nats.Codec, + options?: Partial +}): Promise { + return new Promise(async (resolve, reject) => { + try { + let dataToSend: any = ArrayMessageModule.marshal(message); + +dataToSend = codec.encode(dataToSend); +await js.publish('array.payload', dataToSend, options); + resolve(); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Callback for when receiving messages + * + * @callback subscribeToReceiveArrayPayloadCallback + * @param err if any error occurred this will be sat + * @param msg that was received + * @param natsMsg + */ + +/** + * Core subscription for `array.payload` + * + * @param {subscribeToReceiveArrayPayloadCallback} onDataCallback to call when messages are received + * @param nc the nats client to setup the subscribe for + * @param codec the serialization codec to use while receiving the message + * @param options when setting up the subscription + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +function subscribeToReceiveArrayPayload({ + onDataCallback, + nc, + codec = Nats.JSONCodec(), + options, + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, msg?: ArrayMessageModule.ArrayMessage, natsMsg?: Nats.Msg) => void, + nc: Nats.NatsConnection, + codec?: Nats.Codec, + options?: Nats.SubscriptionOptions, + skipMessageValidation?: boolean +}): Promise { + return new Promise(async (resolve, reject) => { + try { + const subscription = nc.subscribe('array.payload', options); + const validator = ArrayMessageModule.createValidator(); + (async () => { + for await (const msg of subscription) { + + let receivedData: any = codec.decode(msg.data); +if(!skipMessageValidation) { + const {valid, errors} = ArrayMessageModule.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, msg); continue; + } + } +onDataCallback(undefined, ArrayMessageModule.unmarshal(receivedData), msg); + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Callback for when receiving messages + * + * @callback jetStreamPullSubscribeToReceiveArrayPayloadCallback + * @param err if any error occurred this will be sat + * @param msg that was received + * @param jetstreamMsg + */ + +/** + * JetStream pull subscription for `array.payload` + * + * @param {jetStreamPullSubscribeToReceiveArrayPayloadCallback} onDataCallback to call when messages are received + * @param js the JetStream client to pull subscribe through + * @param options when setting up the subscription + * @param codec the serialization codec to use while transmitting the message + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +function jetStreamPullSubscribeToReceiveArrayPayload({ + onDataCallback, + js, + options, + codec = Nats.JSONCodec(), + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, msg?: ArrayMessageModule.ArrayMessage, jetstreamMsg?: Nats.JsMsg) => void, + js: Nats.JetStreamClient, + options: Nats.ConsumerOptsBuilder | Partial, + codec?: Nats.Codec, + skipMessageValidation?: boolean +}): Promise { + return new Promise(async (resolve, reject) => { + try { + const subscription = await js.pullSubscribe('array.payload', options); + const validator = ArrayMessageModule.createValidator(); + (async () => { + for await (const msg of subscription) { + + let receivedData: any = codec.decode(msg.data); +if(!skipMessageValidation) { + const {valid, errors} = ArrayMessageModule.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, msg); continue; + } + } +onDataCallback(undefined, ArrayMessageModule.unmarshal(receivedData), msg); + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Callback for when receiving messages + * + * @callback jetStreamPushSubscriptionFromReceiveArrayPayloadCallback + * @param err if any error occurred this will be sat + * @param msg that was received + * @param jetstreamMsg + */ + +/** + * JetStream push subscription for `array.payload` + * + * @param {jetStreamPushSubscriptionFromReceiveArrayPayloadCallback} onDataCallback to call when messages are received + * @param js the JetStream client to pull subscribe through + * @param options when setting up the subscription + * @param codec the serialization codec to use while transmitting the message + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +function jetStreamPushSubscriptionFromReceiveArrayPayload({ + onDataCallback, + js, + options, + codec = Nats.JSONCodec(), + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, msg?: ArrayMessageModule.ArrayMessage, jetstreamMsg?: Nats.JsMsg) => void, + js: Nats.JetStreamClient, + options: Nats.ConsumerOptsBuilder | Partial, + codec?: Nats.Codec, + skipMessageValidation?: boolean +}): Promise { + return new Promise(async (resolve, reject) => { + try { + const subscription = await js.subscribe('array.payload', options); + const validator = ArrayMessageModule.createValidator(); + (async () => { + for await (const msg of subscription) { + + let receivedData: any = codec.decode(msg.data); +if(!skipMessageValidation) { + const {valid, errors} = ArrayMessageModule.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, msg); continue; + } + } +onDataCallback(undefined, ArrayMessageModule.unmarshal(receivedData), msg); + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * NATS publish operation for `union.payload` + * + * @param message to publish + * @param nc the NATS client to publish from + * @param codec the serialization codec to use while transmitting the message + * @param options to use while publishing the message + */ +function publishToSendUnionPayload({ + message, + nc, + codec = Nats.JSONCodec(), + options +}: { + message: UnionMessageModule.UnionMessage, + nc: Nats.NatsConnection, + codec?: Nats.Codec, + options?: Nats.PublishOptions +}): Promise { + return new Promise(async (resolve, reject) => { + try { + let dataToSend: any = UnionMessageModule.marshal(message); + +dataToSend = codec.encode(dataToSend); +nc.publish('union.payload', dataToSend, options); + resolve(); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * JetStream publish operation for `union.payload` + * + * @param message to publish over jetstream + * @param js the JetStream client to publish from + * @param codec the serialization codec to use while transmitting the message + * @param options to use while publishing the message + */ +function jetStreamPublishToSendUnionPayload({ + message, + js, + codec = Nats.JSONCodec(), + options = {} +}: { + message: UnionMessageModule.UnionMessage, + js: Nats.JetStreamClient, + codec?: Nats.Codec, + options?: Partial +}): Promise { + return new Promise(async (resolve, reject) => { + try { + let dataToSend: any = UnionMessageModule.marshal(message); + +dataToSend = codec.encode(dataToSend); +await js.publish('union.payload', dataToSend, options); + resolve(); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Callback for when receiving messages + * + * @callback subscribeToReceiveUnionPayloadCallback + * @param err if any error occurred this will be sat + * @param msg that was received + * @param natsMsg + */ + +/** + * Core subscription for `union.payload` + * + * @param {subscribeToReceiveUnionPayloadCallback} onDataCallback to call when messages are received + * @param nc the nats client to setup the subscribe for + * @param codec the serialization codec to use while receiving the message + * @param options when setting up the subscription + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +function subscribeToReceiveUnionPayload({ + onDataCallback, + nc, + codec = Nats.JSONCodec(), + options, + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, msg?: UnionMessageModule.UnionMessage, natsMsg?: Nats.Msg) => void, + nc: Nats.NatsConnection, + codec?: Nats.Codec, + options?: Nats.SubscriptionOptions, + skipMessageValidation?: boolean +}): Promise { + return new Promise(async (resolve, reject) => { + try { + const subscription = nc.subscribe('union.payload', options); + const validator = UnionMessageModule.createValidator(); + (async () => { + for await (const msg of subscription) { + + let receivedData: any = codec.decode(msg.data); +if(!skipMessageValidation) { + const {valid, errors} = UnionMessageModule.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, msg); continue; + } + } +onDataCallback(undefined, UnionMessageModule.unmarshal(receivedData), msg); + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Callback for when receiving messages + * + * @callback jetStreamPullSubscribeToReceiveUnionPayloadCallback + * @param err if any error occurred this will be sat + * @param msg that was received + * @param jetstreamMsg + */ + +/** + * JetStream pull subscription for `union.payload` + * + * @param {jetStreamPullSubscribeToReceiveUnionPayloadCallback} onDataCallback to call when messages are received + * @param js the JetStream client to pull subscribe through + * @param options when setting up the subscription + * @param codec the serialization codec to use while transmitting the message + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +function jetStreamPullSubscribeToReceiveUnionPayload({ + onDataCallback, + js, + options, + codec = Nats.JSONCodec(), + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, msg?: UnionMessageModule.UnionMessage, jetstreamMsg?: Nats.JsMsg) => void, + js: Nats.JetStreamClient, + options: Nats.ConsumerOptsBuilder | Partial, + codec?: Nats.Codec, + skipMessageValidation?: boolean +}): Promise { + return new Promise(async (resolve, reject) => { + try { + const subscription = await js.pullSubscribe('union.payload', options); + const validator = UnionMessageModule.createValidator(); + (async () => { + for await (const msg of subscription) { + + let receivedData: any = codec.decode(msg.data); +if(!skipMessageValidation) { + const {valid, errors} = UnionMessageModule.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, msg); continue; + } + } +onDataCallback(undefined, UnionMessageModule.unmarshal(receivedData), msg); + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Callback for when receiving messages + * + * @callback jetStreamPushSubscriptionFromReceiveUnionPayloadCallback + * @param err if any error occurred this will be sat + * @param msg that was received + * @param jetstreamMsg + */ + +/** + * JetStream push subscription for `union.payload` + * + * @param {jetStreamPushSubscriptionFromReceiveUnionPayloadCallback} onDataCallback to call when messages are received + * @param js the JetStream client to pull subscribe through + * @param options when setting up the subscription + * @param codec the serialization codec to use while transmitting the message + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +function jetStreamPushSubscriptionFromReceiveUnionPayload({ + onDataCallback, + js, + options, + codec = Nats.JSONCodec(), + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, msg?: UnionMessageModule.UnionMessage, jetstreamMsg?: Nats.JsMsg) => void, + js: Nats.JetStreamClient, + options: Nats.ConsumerOptsBuilder | Partial, + codec?: Nats.Codec, + skipMessageValidation?: boolean +}): Promise { + return new Promise(async (resolve, reject) => { + try { + const subscription = await js.subscribe('union.payload', options); + const validator = UnionMessageModule.createValidator(); + (async () => { + for await (const msg of subscription) { + + let receivedData: any = codec.decode(msg.data); +if(!skipMessageValidation) { + const {valid, errors} = UnionMessageModule.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, msg); continue; + } + } +onDataCallback(undefined, UnionMessageModule.unmarshal(receivedData), msg); + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Sends a notification using the legacy notification system. Use the new notification service instead. + * + * @deprecated + * + * @param message to publish + * @param nc the NATS client to publish from + * @param codec the serialization codec to use while transmitting the message + * @param options to use while publishing the message + */ +function publishToSendLegacyNotification({ + message, + nc, + codec = Nats.JSONCodec(), + options +}: { + message: LegacyNotification, + nc: Nats.NatsConnection, + codec?: Nats.Codec, + options?: Nats.PublishOptions +}): Promise { + return new Promise(async (resolve, reject) => { + try { + let dataToSend: any = message.marshal(); + +dataToSend = codec.encode(dataToSend); +nc.publish('legacy.notification', dataToSend, options); + resolve(); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Sends a notification using the legacy notification system. Use the new notification service instead. + * + * @deprecated + * + * @param message to publish over jetstream + * @param js the JetStream client to publish from + * @param codec the serialization codec to use while transmitting the message + * @param options to use while publishing the message + */ +function jetStreamPublishToSendLegacyNotification({ + message, + js, + codec = Nats.JSONCodec(), + options = {} +}: { + message: LegacyNotification, + js: Nats.JetStreamClient, + codec?: Nats.Codec, + options?: Partial +}): Promise { + return new Promise(async (resolve, reject) => { + try { + let dataToSend: any = message.marshal(); + +dataToSend = codec.encode(dataToSend); +await js.publish('legacy.notification', dataToSend, options); + resolve(); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Callback for when receiving messages + * + * @callback subscribeToReceiveLegacyNotificationCallback + * @param err if any error occurred this will be sat + * @param msg that was received + * @param natsMsg + */ + +/** + * Receives notifications from the legacy notification system. Use the new notification service instead. + * + * @deprecated + * + * @param {subscribeToReceiveLegacyNotificationCallback} onDataCallback to call when messages are received + * @param nc the nats client to setup the subscribe for + * @param codec the serialization codec to use while receiving the message + * @param options when setting up the subscription + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +function subscribeToReceiveLegacyNotification({ + onDataCallback, + nc, + codec = Nats.JSONCodec(), + options, + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, msg?: LegacyNotification, natsMsg?: Nats.Msg) => void, + nc: Nats.NatsConnection, + codec?: Nats.Codec, + options?: Nats.SubscriptionOptions, + skipMessageValidation?: boolean +}): Promise { + return new Promise(async (resolve, reject) => { + try { + const subscription = nc.subscribe('legacy.notification', options); + const validator = LegacyNotification.createValidator(); + (async () => { + for await (const msg of subscription) { + + let receivedData: any = codec.decode(msg.data); +if(!skipMessageValidation) { + const {valid, errors} = LegacyNotification.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, msg); continue; + } + } +onDataCallback(undefined, LegacyNotification.unmarshal(receivedData), msg); + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Callback for when receiving messages + * + * @callback jetStreamPullSubscribeToReceiveLegacyNotificationCallback + * @param err if any error occurred this will be sat + * @param msg that was received + * @param jetstreamMsg + */ + +/** + * Receives notifications from the legacy notification system. Use the new notification service instead. + * + * @deprecated + * + * @param {jetStreamPullSubscribeToReceiveLegacyNotificationCallback} onDataCallback to call when messages are received + * @param js the JetStream client to pull subscribe through + * @param options when setting up the subscription + * @param codec the serialization codec to use while transmitting the message + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +function jetStreamPullSubscribeToReceiveLegacyNotification({ + onDataCallback, + js, + options, + codec = Nats.JSONCodec(), + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, msg?: LegacyNotification, jetstreamMsg?: Nats.JsMsg) => void, + js: Nats.JetStreamClient, + options: Nats.ConsumerOptsBuilder | Partial, + codec?: Nats.Codec, + skipMessageValidation?: boolean +}): Promise { + return new Promise(async (resolve, reject) => { + try { + const subscription = await js.pullSubscribe('legacy.notification', options); + const validator = LegacyNotification.createValidator(); + (async () => { + for await (const msg of subscription) { + + let receivedData: any = codec.decode(msg.data); +if(!skipMessageValidation) { + const {valid, errors} = LegacyNotification.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, msg); continue; + } + } +onDataCallback(undefined, LegacyNotification.unmarshal(receivedData), msg); + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} + +/** + * Callback for when receiving messages + * + * @callback jetStreamPushSubscriptionFromReceiveLegacyNotificationCallback + * @param err if any error occurred this will be sat + * @param msg that was received + * @param jetstreamMsg + */ + +/** + * Receives notifications from the legacy notification system. Use the new notification service instead. + * + * @deprecated + * + * @param {jetStreamPushSubscriptionFromReceiveLegacyNotificationCallback} onDataCallback to call when messages are received + * @param js the JetStream client to pull subscribe through + * @param options when setting up the subscription + * @param codec the serialization codec to use while transmitting the message + * @param skipMessageValidation turn off runtime validation of incoming messages + */ +function jetStreamPushSubscriptionFromReceiveLegacyNotification({ + onDataCallback, + js, + options, + codec = Nats.JSONCodec(), + skipMessageValidation = false +}: { + onDataCallback: (err?: Error, msg?: LegacyNotification, jetstreamMsg?: Nats.JsMsg) => void, + js: Nats.JetStreamClient, + options: Nats.ConsumerOptsBuilder | Partial, + codec?: Nats.Codec, + skipMessageValidation?: boolean +}): Promise { + return new Promise(async (resolve, reject) => { + try { + const subscription = await js.subscribe('legacy.notification', options); + const validator = LegacyNotification.createValidator(); + (async () => { + for await (const msg of subscription) { + + let receivedData: any = codec.decode(msg.data); +if(!skipMessageValidation) { + const {valid, errors} = LegacyNotification.validate({data: receivedData, ajvValidatorFunction: validator}); + if(!valid) { + onDataCallback(new Error(`Invalid message payload received; ${JSON.stringify({cause: errors})}`), undefined, msg); continue; + } + } +onDataCallback(undefined, LegacyNotification.unmarshal(receivedData), msg); + } + })(); + resolve(subscription); + } catch (e: any) { + reject(e); + } + }); +} + +export { publishToSendUserSignedup, jetStreamPublishToSendUserSignedup, subscribeToReceiveUserSignedup, jetStreamPullSubscribeToReceiveUserSignedup, jetStreamPushSubscriptionFromReceiveUserSignedup, publishToNoParameter, subscribeToNoParameter, jetStreamPullSubscribeToNoParameter, jetStreamPushSubscriptionFromNoParameter, jetStreamPublishToNoParameter, publishToSendStringPayload, jetStreamPublishToSendStringPayload, subscribeToReceiveStringPayload, jetStreamPullSubscribeToReceiveStringPayload, jetStreamPushSubscriptionFromReceiveStringPayload, publishToSendArrayPayload, jetStreamPublishToSendArrayPayload, subscribeToReceiveArrayPayload, jetStreamPullSubscribeToReceiveArrayPayload, jetStreamPushSubscriptionFromReceiveArrayPayload, publishToSendUnionPayload, jetStreamPublishToSendUnionPayload, subscribeToReceiveUnionPayload, jetStreamPullSubscribeToReceiveUnionPayload, jetStreamPushSubscriptionFromReceiveUnionPayload, publishToSendLegacyNotification, jetStreamPublishToSendLegacyNotification, subscribeToReceiveLegacyNotification, jetStreamPullSubscribeToReceiveLegacyNotification, jetStreamPushSubscriptionFromReceiveLegacyNotification }; diff --git a/test/runtime/typescript/src/node16/headers/UserSignedUpHeaders.ts b/test/runtime/typescript/src/node16/headers/UserSignedUpHeaders.ts new file mode 100644 index 00000000..e15ff250 --- /dev/null +++ b/test/runtime/typescript/src/node16/headers/UserSignedUpHeaders.ts @@ -0,0 +1,77 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import {default as addFormats} from 'ajv-formats'; +class UserSignedUpHeaders { + private _xTestHeader?: string; + private _additionalProperties?: Map; + + constructor(input: { + xTestHeader?: string, + additionalProperties?: Map, + }) { + this._xTestHeader = input.xTestHeader; + this._additionalProperties = input.additionalProperties; + } + + /** + * Test header + */ + get xTestHeader(): string | undefined { return this._xTestHeader; } + set xTestHeader(xTestHeader: string | undefined) { this._xTestHeader = xTestHeader; } + + get additionalProperties(): Map | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Map | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.xTestHeader !== undefined) { + json += `"x-test-header": ${typeof this.xTestHeader === 'number' || typeof this.xTestHeader === 'boolean' ? this.xTestHeader : JSON.stringify(this.xTestHeader)},`; + } + 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(["x-test-header","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): UserSignedUpHeaders { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new UserSignedUpHeaders({} as any); + + if (obj["x-test-header"] !== undefined) { + instance.xTestHeader = obj["x-test-header"]; + } + + instance.additionalProperties = new Map(); + const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["x-test-header","additionalProperties"].includes(key);})); + for (const [key, value] of propsToCheck) { + instance.additionalProperties.set(key, value as any); + } + return instance; + } + public static theCodeGenSchema = {"type":"object","properties":{"x-test-header":{"type":"string","description":"Test header"}},"$id":"UserSignedUpHeaders","$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); + + const validate = ajvInstance.compile(this.theCodeGenSchema); + return validate; + } + +} +export { UserSignedUpHeaders }; \ No newline at end of file diff --git a/test/runtime/typescript/src/node16/parameters/EnumParameter.ts b/test/runtime/typescript/src/node16/parameters/EnumParameter.ts new file mode 100644 index 00000000..94ab76db --- /dev/null +++ b/test/runtime/typescript/src/node16/parameters/EnumParameter.ts @@ -0,0 +1,6 @@ + +/** + * enum parameter + */ +type EnumParameter = "openapi" | "asyncapi"; +export { EnumParameter }; \ No newline at end of file diff --git a/test/runtime/typescript/src/node16/parameters/UserSignedupParameters.ts b/test/runtime/typescript/src/node16/parameters/UserSignedupParameters.ts new file mode 100644 index 00000000..4195c425 --- /dev/null +++ b/test/runtime/typescript/src/node16/parameters/UserSignedupParameters.ts @@ -0,0 +1,61 @@ +import {EnumParameter} from './EnumParameter'; +interface UserSignedupParametersInterface { + myParameter: string + enumParameter: EnumParameter +} +class UserSignedupParameters { + private _myParameter: string; + private _enumParameter: EnumParameter; + + constructor(input: UserSignedupParametersInterface) { + this._myParameter = input.myParameter; + this._enumParameter = input.enumParameter; + } + + /** + * parameter description + */ + get myParameter(): string { return this._myParameter; } + set myParameter(myParameter: string) { this._myParameter = myParameter; } + + /** + * enum parameter + */ + get enumParameter(): EnumParameter { return this._enumParameter; } + set enumParameter(enumParameter: EnumParameter) { this._enumParameter = enumParameter; } + + + /** + * Realize the channel/topic with the parameters added to this class. + */ + public getChannelWithParameters(channel: string) { + channel = channel.replace(/\{my_parameter\}/g, this.myParameter); + channel = channel.replace(/\{enum_parameter\}/g, this.enumParameter); + return channel; + } + + public static createFromChannel(msgSubject: string, channel: string, regex: RegExp): UserSignedupParameters { + const parameters = new UserSignedupParameters({myParameter: '', enumParameter: "openapi"}); + const match = msgSubject.match(regex); + const sequentialParameters: string[] = channel.match(/\{(\w+)\}/g) || []; + + if (match) { + const myParameterMatch = match[sequentialParameters.indexOf('{my_parameter}')+1]; + if(myParameterMatch && myParameterMatch !== '') { + parameters.myParameter = myParameterMatch as any + } else { + throw new Error(`Parameter: 'myParameter' is not valid in UserSignedupParameters. Aborting parameter extracting! `) + } + const enumParameterMatch = match[sequentialParameters.indexOf('{enum_parameter}')+1]; + if(enumParameterMatch && enumParameterMatch !== '') { + parameters.enumParameter = enumParameterMatch as any + } else { + throw new Error(`Parameter: 'enumParameter' is not valid in UserSignedupParameters. Aborting parameter extracting! `) + } + } else { + throw new Error(`Unable to find parameters in channel/topic, topic was ${channel}`) + } + return parameters; + } +} +export { UserSignedupParameters, UserSignedupParametersInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/node16/payloads/ArrayMessage.ts b/test/runtime/typescript/src/node16/payloads/ArrayMessage.ts new file mode 100644 index 00000000..e3d8a41c --- /dev/null +++ b/test/runtime/typescript/src/node16/payloads/ArrayMessage.ts @@ -0,0 +1,39 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import {default as addFormats} from 'ajv-formats'; +/** + * An array of strings payload + */ +type ArrayMessage = string[]; + +export function unmarshal(json: string | any[]): ArrayMessage { + if (typeof json === 'string') { + return JSON.parse(json) as ArrayMessage; + } + return json as ArrayMessage; +} +export function marshal(payload: ArrayMessage): string { + return JSON.stringify(payload); +} +export const theCodeGenSchema = {"type":"array","$schema":"http://json-schema.org/draft-07/schema","items":{"type":"string"},"description":"An array of strings payload","$id":"ArrayMessage"}; +export function 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 ?? createValidator(context) + return { + valid: validate(parsedData), + errors: validate.errors ?? undefined, + }; +} +export function createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { + const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; + addFormats(ajvInstance); + + const validate = ajvInstance.compile(theCodeGenSchema); + return validate; +} + + +export { ArrayMessage }; \ No newline at end of file diff --git a/test/runtime/typescript/src/node16/payloads/LegacyNotification.ts b/test/runtime/typescript/src/node16/payloads/LegacyNotification.ts new file mode 100644 index 00000000..7a50dcf7 --- /dev/null +++ b/test/runtime/typescript/src/node16/payloads/LegacyNotification.ts @@ -0,0 +1,96 @@ +import {LegacyNotificationPayloadLevelEnum} from './LegacyNotificationPayloadLevelEnum'; +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import {default as addFormats} from 'ajv-formats'; +/** + * Legacy notification payload - use NewNotificationPayload instead + */ +class LegacyNotification { + private _message?: string; + private _level?: LegacyNotificationPayloadLevelEnum; + private _additionalProperties?: Record; + + constructor(input: { + message?: string, + level?: LegacyNotificationPayloadLevelEnum, + additionalProperties?: Record, + }) { + this._message = input.message; + this._level = input.level; + this._additionalProperties = input.additionalProperties; + } + + /** + * The notification message + */ + get message(): string | undefined { return this._message; } + set message(message: string | undefined) { this._message = message; } + + /** + * Notification severity level + */ + get level(): LegacyNotificationPayloadLevelEnum | undefined { return this._level; } + set level(level: LegacyNotificationPayloadLevelEnum | undefined) { this._level = level; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.message !== undefined) { + json += `"message": ${typeof this.message === 'number' || typeof this.message === 'boolean' ? this.message : JSON.stringify(this.message)},`; + } + if(this.level !== undefined) { + json += `"level": ${typeof this.level === 'number' || typeof this.level === 'boolean' ? this.level : JSON.stringify(this.level)},`; + } + 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(["message","level","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): LegacyNotification { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new LegacyNotification({} as any); + + if (obj["message"] !== undefined) { + instance.message = obj["message"]; + } + if (obj["level"] !== undefined) { + instance.level = obj["level"]; + } + + instance.additionalProperties = new Map(); + const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["message","level","additionalProperties"].includes(key);})); + for (const [key, value] of propsToCheck) { + instance.additionalProperties.set(key, value as any); + } + return instance; + } + public static theCodeGenSchema = {"type":"object","$schema":"http://json-schema.org/draft-07/schema","description":"Legacy notification payload - use NewNotificationPayload instead","deprecated":true,"properties":{"message":{"type":"string","description":"The notification message"},"level":{"type":"string","enum":["info","warning","error"],"description":"Notification severity level"}},"$id":"LegacyNotification"}; + 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); + + const validate = ajvInstance.compile(this.theCodeGenSchema); + return validate; + } + +} +export { LegacyNotification }; \ No newline at end of file diff --git a/test/runtime/typescript/src/node16/payloads/LegacyNotificationPayloadLevelEnum.ts b/test/runtime/typescript/src/node16/payloads/LegacyNotificationPayloadLevelEnum.ts new file mode 100644 index 00000000..b7e9ab91 --- /dev/null +++ b/test/runtime/typescript/src/node16/payloads/LegacyNotificationPayloadLevelEnum.ts @@ -0,0 +1,10 @@ + +/** + * Notification severity level + */ +enum LegacyNotificationPayloadLevelEnum { + INFO = "info", + WARNING = "warning", + ERROR = "error", +} +export { LegacyNotificationPayloadLevelEnum }; \ No newline at end of file diff --git a/test/runtime/typescript/src/node16/payloads/StringMessage.ts b/test/runtime/typescript/src/node16/payloads/StringMessage.ts new file mode 100644 index 00000000..b4efe734 --- /dev/null +++ b/test/runtime/typescript/src/node16/payloads/StringMessage.ts @@ -0,0 +1,36 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import {default as addFormats} from 'ajv-formats'; +/** + * A simple string payload + */ +type StringMessage = string; + +export function unmarshal(json: string): StringMessage { + return JSON.parse(json) as StringMessage; +} +export function marshal(payload: StringMessage): string { + return JSON.stringify(payload); +} +export const theCodeGenSchema = {"type":"string","$schema":"http://json-schema.org/draft-07/schema","description":"A simple string payload","$id":"StringMessage"}; +export function 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 ?? createValidator(context) + return { + valid: validate(parsedData), + errors: validate.errors ?? undefined, + }; +} +export function createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { + const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; + addFormats(ajvInstance); + + const validate = ajvInstance.compile(theCodeGenSchema); + return validate; +} + + +export { StringMessage }; \ No newline at end of file diff --git a/test/runtime/typescript/src/node16/payloads/UnionMessage.ts b/test/runtime/typescript/src/node16/payloads/UnionMessage.ts new file mode 100644 index 00000000..f2545d83 --- /dev/null +++ b/test/runtime/typescript/src/node16/payloads/UnionMessage.ts @@ -0,0 +1,44 @@ +import {UnionPayloadOneOfOption2} from './UnionPayloadOneOfOption2'; +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import {default as addFormats} from 'ajv-formats'; +/** + * A union type payload + */ +type UnionMessage = string | number | UnionPayloadOneOfOption2; + +export function unmarshal(json: any): UnionMessage { + + return JSON.parse(json); +} +export function marshal(payload: UnionMessage) { + + +if(payload instanceof UnionPayloadOneOfOption2) { +return payload.marshal(); +} + return JSON.stringify(payload); +} + +export const theCodeGenSchema = {"$schema":"http://json-schema.org/draft-07/schema","oneOf":[{"type":"string"},{"type":"number"},{"type":"object","properties":{"name":{"type":"string"}}}],"description":"A union type payload","$id":"UnionMessage"}; +export function 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 ?? createValidator(context) + return { + valid: validate(parsedData), + errors: validate.errors ?? undefined, + }; +} +export function createValidator(context?: {ajvInstance?: Ajv, ajvOptions?: AjvOptions}): ValidateFunction { + const {ajvInstance} = {...context ?? {}, ajvInstance: new Ajv(context?.ajvOptions ?? {})}; + addFormats(ajvInstance); + + const validate = ajvInstance.compile(theCodeGenSchema); + return validate; +} + + +export { UnionMessage }; \ No newline at end of file diff --git a/test/runtime/typescript/src/node16/payloads/UnionPayloadOneOfOption2.ts b/test/runtime/typescript/src/node16/payloads/UnionPayloadOneOfOption2.ts new file mode 100644 index 00000000..d166bc77 --- /dev/null +++ b/test/runtime/typescript/src/node16/payloads/UnionPayloadOneOfOption2.ts @@ -0,0 +1,74 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import {default as addFormats} from 'ajv-formats'; +class UnionPayloadOneOfOption2 { + private _name?: string; + private _additionalProperties?: Record; + + constructor(input: { + name?: string, + additionalProperties?: Record, + }) { + this._name = input.name; + this._additionalProperties = input.additionalProperties; + } + + get name(): string | undefined { return this._name; } + set name(name: string | undefined) { this._name = name; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.name !== undefined) { + json += `"name": ${typeof this.name === 'number' || typeof this.name === 'boolean' ? this.name : JSON.stringify(this.name)},`; + } + 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(["name","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): UnionPayloadOneOfOption2 { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new UnionPayloadOneOfOption2({} as any); + + if (obj["name"] !== undefined) { + instance.name = obj["name"]; + } + + instance.additionalProperties = new Map(); + const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["name","additionalProperties"].includes(key);})); + for (const [key, value] of propsToCheck) { + instance.additionalProperties.set(key, value as any); + } + return instance; + } + public static theCodeGenSchema = {"type":"object","properties":{"name":{"type":"string"}}}; + 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); + + const validate = ajvInstance.compile(this.theCodeGenSchema); + return validate; + } + +} +export { UnionPayloadOneOfOption2 }; \ No newline at end of file diff --git a/test/runtime/typescript/src/node16/payloads/UserSignedUp.ts b/test/runtime/typescript/src/node16/payloads/UserSignedUp.ts new file mode 100644 index 00000000..13f2c67a --- /dev/null +++ b/test/runtime/typescript/src/node16/payloads/UserSignedUp.ts @@ -0,0 +1,95 @@ +import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; +import {default as addFormats} from 'ajv-formats'; +/** + * Payload for user signup events containing user registration details + */ +class UserSignedUp { + private _displayName?: string; + private _email?: string; + private _additionalProperties?: Record; + + constructor(input: { + displayName?: string, + email?: string, + additionalProperties?: Record, + }) { + this._displayName = input.displayName; + this._email = input.email; + this._additionalProperties = input.additionalProperties; + } + + /** + * Name of the user + */ + get displayName(): string | undefined { return this._displayName; } + set displayName(displayName: string | undefined) { this._displayName = displayName; } + + /** + * Email of the user + */ + get email(): string | undefined { return this._email; } + set email(email: string | undefined) { this._email = email; } + + get additionalProperties(): Record | undefined { return this._additionalProperties; } + set additionalProperties(additionalProperties: Record | undefined) { this._additionalProperties = additionalProperties; } + + public marshal() : string { + let json = '{' + if(this.displayName !== undefined) { + json += `"display_name": ${typeof this.displayName === 'number' || typeof this.displayName === 'boolean' ? this.displayName : JSON.stringify(this.displayName)},`; + } + if(this.email !== undefined) { + json += `"email": ${typeof this.email === 'number' || typeof this.email === 'boolean' ? this.email : JSON.stringify(this.email)},`; + } + 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(["display_name","email","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): UserSignedUp { + const obj = typeof json === "object" ? json : JSON.parse(json); + const instance = new UserSignedUp({} as any); + + if (obj["display_name"] !== undefined) { + instance.displayName = obj["display_name"]; + } + if (obj["email"] !== undefined) { + instance.email = obj["email"]; + } + + instance.additionalProperties = new Map(); + const propsToCheck = Object.entries(obj).filter((([key,]) => {return !["display_name","email","additionalProperties"].includes(key);})); + for (const [key, value] of propsToCheck) { + instance.additionalProperties.set(key, value as any); + } + return instance; + } + public static theCodeGenSchema = {"type":"object","$schema":"http://json-schema.org/draft-07/schema","description":"Payload for user signup events containing user registration details","properties":{"display_name":{"type":"string","description":"Name of the user"},"email":{"type":"string","format":"email","description":"Email of the user"}},"$id":"UserSignedUp"}; + 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); + + const validate = ajvInstance.compile(this.theCodeGenSchema); + return validate; + } + +} +export { UserSignedUp }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi-path-organization/channels/headers/FindPetsByStatusAndCategoryHeaders.ts b/test/runtime/typescript/src/openapi-path-organization/channels/headers/FindPetsByStatusAndCategoryHeaders.ts index 9ab9e878..1c3d9915 100644 --- a/test/runtime/typescript/src/openapi-path-organization/channels/headers/FindPetsByStatusAndCategoryHeaders.ts +++ b/test/runtime/typescript/src/openapi-path-organization/channels/headers/FindPetsByStatusAndCategoryHeaders.ts @@ -1,76 +1,19 @@ -import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import {default as addFormats} from 'ajv-formats'; -class FindPetsByStatusAndCategoryHeaders { - private _xRequestId?: string; - private _acceptLanguage?: string; - - constructor(input: { - xRequestId?: string, - acceptLanguage?: string, - }) { - this._xRequestId = input.xRequestId; - this._acceptLanguage = input.acceptLanguage; - } +interface FindPetsByStatusAndCategoryHeaders { /** * Unique request identifier for tracing */ - get xRequestId(): string | undefined { return this._xRequestId; } - set xRequestId(xRequestId: string | undefined) { this._xRequestId = xRequestId; } - + xMinusRequestMinusId?: string; /** * Preferred language for response messages */ - get acceptLanguage(): string | undefined { return this._acceptLanguage; } - set acceptLanguage(acceptLanguage: string | undefined) { this._acceptLanguage = acceptLanguage; } - - public marshal() : string { - let json = '{' - if(this.xRequestId !== undefined) { - json += `"X-Request-ID": ${typeof this.xRequestId === 'number' || typeof this.xRequestId === 'boolean' ? this.xRequestId : JSON.stringify(this.xRequestId)},`; - } - if(this.acceptLanguage !== undefined) { - json += `"Accept-Language": ${typeof this.acceptLanguage === 'number' || typeof this.acceptLanguage === 'boolean' ? this.acceptLanguage : JSON.stringify(this.acceptLanguage)},`; - } - - //Remove potential last comma - return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; - } - - public static unmarshal(json: string | object): FindPetsByStatusAndCategoryHeaders { - const obj = typeof json === "object" ? json : JSON.parse(json); - const instance = new FindPetsByStatusAndCategoryHeaders({} as any); - - if (obj["X-Request-ID"] !== undefined) { - instance.xRequestId = obj["X-Request-ID"]; - } - if (obj["Accept-Language"] !== undefined) { - instance.acceptLanguage = obj["Accept-Language"]; - } - - - return instance; - } - public static theCodeGenSchema = {"type":"object","additionalProperties":false,"properties":{"X-Request-ID":{"type":"string","format":"uuid","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","description":"Unique request identifier for tracing"},"Accept-Language":{"type":"string","pattern":"^[a-z]{2}(-[A-Z]{2})?$","default":"en-US","description":"Preferred language for response messages"}},"$id":"FindPetsByStatusAndCategoryHeaders","$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; - } - + acceptMinusLanguage?: string; } -export { FindPetsByStatusAndCategoryHeaders }; \ No newline at end of file +export { FindPetsByStatusAndCategoryHeaders }; + +export function serializeFindPetsByStatusAndCategoryHeadersHeaders(headers: FindPetsByStatusAndCategoryHeaders): Record { + const result: Record = {}; + if (headers.xMinusRequestMinusId !== undefined) { result['X-Request-ID'] = String(headers.xMinusRequestMinusId); } + if (headers.acceptMinusLanguage !== undefined) { result['Accept-Language'] = String(headers.acceptMinusLanguage); } + return result; +} \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi-path-organization/channels/http_client.ts b/test/runtime/typescript/src/openapi-path-organization/channels/http_client.ts index 6dcb861d..d1a9164b 100644 --- a/test/runtime/typescript/src/openapi-path-organization/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi-path-organization/channels/http_client.ts @@ -7,8 +7,8 @@ import {ItemStatus} from './payload/ItemStatus'; import {PetOrder} from './payload/PetOrder'; import {AUser} from './payload/AUser'; import {AnUploadedResponse} from './payload/AnUploadedResponse'; -import {FindPetsByStatusAndCategoryParameters} from './parameter/FindPetsByStatusAndCategoryParameters'; -import {FindPetsByStatusAndCategoryHeaders} from './headers/FindPetsByStatusAndCategoryHeaders'; +import {FindPetsByStatusAndCategoryParameters, FindPetsByStatusAndCategoryParametersInterface} from './parameter/FindPetsByStatusAndCategoryParameters'; +import {FindPetsByStatusAndCategoryHeaders, serializeFindPetsByStatusAndCategoryHeadersHeaders} from './headers/FindPetsByStatusAndCategoryHeaders'; // ============================================================================ // Common Types - Shared across all HTTP client functions @@ -25,26 +25,6 @@ export interface HttpResponse { json: () => Record | Promise>; } -/** - * Pagination info extracted from response - */ -export interface PaginationInfo { - /** Total number of items (if available from headers like X-Total-Count) */ - totalCount?: number; - /** Total number of pages (if available) */ - totalPages?: number; - /** Current page/offset */ - currentOffset?: number; - /** Items per page */ - limit?: number; - /** Next cursor (for cursor-based pagination) */ - nextCursor?: string; - /** Previous cursor */ - prevCursor?: string; - /** Whether there are more items */ - hasMore?: boolean; -} - /** * Rich response wrapper returned by HTTP client functions */ @@ -59,16 +39,6 @@ export interface HttpClientResponse { headers: Record; /** Raw JSON response before deserialization */ rawData: Record; - /** Pagination info extracted from response (if applicable) */ - pagination?: PaginationInfo; - /** Fetch the next page (if pagination is configured and more data exists) */ - getNextPage?: () => Promise>; - /** Fetch the previous page (if pagination is configured) */ - getPrevPage?: () => Promise>; - /** Check if there's a next page */ - hasNextPage?: () => boolean; - /** Check if there's a previous page */ - hasPrevPage?: () => boolean; } /** @@ -163,69 +133,6 @@ const API_KEY_DEFAULTS = { in: 'header' as 'header' | 'query' | 'cookie' } as const; -// ============================================================================ -// Pagination Types -// ============================================================================ - -/** - * Where to place pagination parameters - */ -export type PaginationLocation = 'query' | 'header'; - -/** - * Offset-based pagination configuration - */ -export interface OffsetPagination { - type: 'offset'; - in?: PaginationLocation; // Where to place params (default: 'query') - offset: number; - limit: number; - offsetParam?: string; // Param name for offset (default: 'offset' for query, 'X-Offset' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Cursor-based pagination configuration - */ -export interface CursorPagination { - type: 'cursor'; - in?: PaginationLocation; // Where to place params (default: 'query') - cursor?: string; - limit?: number; - cursorParam?: string; // Param name for cursor (default: 'cursor' for query, 'X-Cursor' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Page-based pagination configuration - */ -export interface PagePagination { - type: 'page'; - in?: PaginationLocation; // Where to place params (default: 'query') - page: number; - pageSize: number; - pageParam?: string; // Param name for page (default: 'page' for query, 'X-Page' for header) - pageSizeParam?: string; // Param name for page size (default: 'pageSize' for query, 'X-Page-Size' for header) -} - -/** - * Range-based pagination (typically used with headers) - * Follows RFC 7233 style: Range: items=0-24 - */ -export interface RangePagination { - type: 'range'; - in?: 'header'; // Range pagination is typically header-only - start: number; - end: number; - unit?: string; // Range unit (default: 'items') - rangeHeader?: string; // Header name (default: 'Range') -} - -/** - * Union type for all pagination methods - */ -export type PaginationConfig = OffsetPagination | CursorPagination | PagePagination | RangePagination; - // ============================================================================ // Retry Configuration // ============================================================================ @@ -283,15 +190,11 @@ export interface HttpHooks { * Base context shared by all HTTP client functions */ export interface HttpClientContext { - server?: string; - path?: string; + baseUrl?: string; // Authentication - grouped for better autocomplete auth?: AuthConfig; - // Pagination configuration - pagination?: PaginationConfig; - // Retry configuration retry?: RetryConfig; @@ -301,8 +204,8 @@ export interface HttpClientContext { // Additional options additionalHeaders?: Record; - // Query parameters - queryParams?: Record; + // Extra query parameters not covered by the typed parameters interface + additionalQueryParams?: Record; } // ============================================================================ @@ -384,94 +287,6 @@ function applyAuth( return { headers, url }; } -/** - * Apply pagination parameters to URL and/or headers based on configuration - */ -function applyPagination( - pagination: PaginationConfig | undefined, - url: string, - headers: Record -): { url: string; headers: Record } { - if (!pagination) return { url, headers }; - - const location = pagination.in ?? 'query'; - const isHeader = location === 'header'; - - // Helper to get default param names based on location - const getDefaultName = (queryName: string, headerName: string) => - isHeader ? headerName : queryName; - - const queryParams = new URLSearchParams(); - const headerParams: Record = {}; - - const addParam = (name: string, value: string) => { - if (isHeader) { - headerParams[name] = value; - } else { - queryParams.append(name, value); - } - }; - - switch (pagination.type) { - case 'offset': - addParam( - pagination.offsetParam ?? getDefaultName('offset', 'X-Offset'), - String(pagination.offset) - ); - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - break; - - case 'cursor': - if (pagination.cursor) { - addParam( - pagination.cursorParam ?? getDefaultName('cursor', 'X-Cursor'), - pagination.cursor - ); - } - if (pagination.limit !== undefined) { - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - } - break; - - case 'page': - addParam( - pagination.pageParam ?? getDefaultName('page', 'X-Page'), - String(pagination.page) - ); - addParam( - pagination.pageSizeParam ?? getDefaultName('pageSize', 'X-Page-Size'), - String(pagination.pageSize) - ); - break; - - case 'range': { - // Range pagination is always header-based (RFC 7233 style) - const unit = pagination.unit ?? 'items'; - const headerName = pagination.rangeHeader ?? 'Range'; - headerParams[headerName] = `${unit}=${pagination.start}-${pagination.end}`; - break; - } - } - - // Apply query params to URL - const queryString = queryParams.toString(); - if (queryString) { - const separator = url.includes('?') ? '&' : '?'; - url = `${url}${separator}${queryString}`; - } - - // Merge header params - const updatedHeaders = { ...headers, ...headerParams }; - - return { url, headers: updatedHeaders }; -} - /** * Apply query parameters to URL */ @@ -614,199 +429,6 @@ function extractHeaders(response: HttpResponse): Record { return headers; } -/** - * Extract pagination info from response headers - */ -function extractPaginationInfo( - headers: Record, - currentPagination?: PaginationConfig -): PaginationInfo | undefined { - const info: PaginationInfo = {}; - let hasPaginationInfo = false; - - // Common total count headers - const totalCount = headers['x-total-count'] || headers['x-total'] || headers['total-count']; - if (totalCount) { - info.totalCount = parseInt(totalCount, 10); - hasPaginationInfo = true; - } - - // Total pages - const totalPages = headers['x-total-pages'] || headers['x-page-count']; - if (totalPages) { - info.totalPages = parseInt(totalPages, 10); - hasPaginationInfo = true; - } - - // Next cursor - const nextCursor = headers['x-next-cursor'] || headers['x-cursor-next']; - if (nextCursor) { - info.nextCursor = nextCursor; - info.hasMore = true; - hasPaginationInfo = true; - } - - // Previous cursor - const prevCursor = headers['x-prev-cursor'] || headers['x-cursor-prev']; - if (prevCursor) { - info.prevCursor = prevCursor; - hasPaginationInfo = true; - } - - // Has more indicator - const hasMore = headers['x-has-more'] || headers['x-has-next']; - if (hasMore) { - info.hasMore = hasMore.toLowerCase() === 'true' || hasMore === '1'; - hasPaginationInfo = true; - } - - // Parse Link header (RFC 5988) - const linkHeader = headers['link']; - if (linkHeader) { - const links = parseLinkHeader(linkHeader); - if (links.next) { - info.hasMore = true; - hasPaginationInfo = true; - } - } - - // Include current pagination state - if (currentPagination) { - switch (currentPagination.type) { - case 'offset': - info.currentOffset = currentPagination.offset; - info.limit = currentPagination.limit; - break; - case 'cursor': - info.limit = currentPagination.limit; - break; - case 'page': - info.currentOffset = (currentPagination.page - 1) * currentPagination.pageSize; - info.limit = currentPagination.pageSize; - break; - case 'range': - info.currentOffset = currentPagination.start; - info.limit = currentPagination.end - currentPagination.start + 1; - break; - } - hasPaginationInfo = true; - } - - // Calculate hasMore based on total count - if (info.hasMore === undefined && info.totalCount !== undefined && - info.currentOffset !== undefined && info.limit !== undefined) { - info.hasMore = info.currentOffset + info.limit < info.totalCount; - } - - return hasPaginationInfo ? info : undefined; -} - -/** - * Parse RFC 5988 Link header - */ -function parseLinkHeader(header: string): Record { - const links: Record = {}; - const parts = header.split(','); - - for (const part of parts) { - const match = part.match(/<([^>]+)>;\s*rel="?([^";\s]+)"?/); - if (match) { - links[match[2]] = match[1]; - } - } - - return links; -} - -/** - * Create pagination helper functions for the response - */ -function createPaginationHelpers( - currentConfig: TContext, - paginationInfo: PaginationInfo | undefined, - requestFn: (config: TContext) => Promise> -): Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> { - const helpers: Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> = {}; - - if (!currentConfig.pagination) { - return helpers; - } - - const pagination = currentConfig.pagination; - - helpers.hasNextPage = () => { - if (paginationInfo?.hasMore !== undefined) return paginationInfo.hasMore; - if (paginationInfo?.nextCursor) return true; - if (paginationInfo?.totalCount !== undefined && - paginationInfo.currentOffset !== undefined && - paginationInfo.limit !== undefined) { - return paginationInfo.currentOffset + paginationInfo.limit < paginationInfo.totalCount; - } - return false; - }; - - helpers.hasPrevPage = () => { - if (paginationInfo?.prevCursor) return true; - if (paginationInfo?.currentOffset !== undefined) { - return paginationInfo.currentOffset > 0; - } - return false; - }; - - helpers.getNextPage = async () => { - let nextPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - nextPagination = { ...pagination, offset: pagination.offset + pagination.limit }; - break; - case 'cursor': - if (!paginationInfo?.nextCursor) throw new Error('No next cursor available'); - nextPagination = { ...pagination, cursor: paginationInfo.nextCursor }; - break; - case 'page': - nextPagination = { ...pagination, page: pagination.page + 1 }; - break; - case 'range': - const rangeSize = pagination.end - pagination.start + 1; - nextPagination = { ...pagination, start: pagination.end + 1, end: pagination.end + rangeSize }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: nextPagination }); - }; - - helpers.getPrevPage = async () => { - let prevPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - prevPagination = { ...pagination, offset: Math.max(0, pagination.offset - pagination.limit) }; - break; - case 'cursor': - if (!paginationInfo?.prevCursor) throw new Error('No previous cursor available'); - prevPagination = { ...pagination, cursor: paginationInfo.prevCursor }; - break; - case 'page': - prevPagination = { ...pagination, page: Math.max(1, pagination.page - 1) }; - break; - case 'range': - const size = pagination.end - pagination.start + 1; - const newStart = Math.max(0, pagination.start - size); - prevPagination = { ...pagination, start: newStart, end: newStart + size - 1 }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: prevPagination }); - }; - - return helpers; -} - /** * Builds a URL with path parameters replaced * @param server - Base server URL @@ -998,7 +620,6 @@ async function handleTokenRefresh( export interface AddPetContext extends HttpClientContext { payload: APet; - requestHeaders?: { marshal: () => string }; } /** @@ -1007,8 +628,7 @@ export interface AddPetContext extends HttpClientContext { async function addPet(context: AddPetContext): Promise> { // Apply defaults const config = { - path: '/pet', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1018,18 +638,11 @@ async function addPet(context: AddPetContext): Promise> } // Build headers - let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) - : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = `${config.server}${config.path}`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = `${config.baseUrl}/pet`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1095,17 +708,13 @@ async function addPet(context: AddPetContext): Promise> // Extract response metadata const responseHeaders = extractHeaders(response); - const paginationInfo = extractPaginationInfo(responseHeaders, config.pagination); - // Build response wrapper with pagination helpers const result: HttpClientResponse = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, addPet), }; return result; @@ -1121,7 +730,6 @@ async function addPet(context: AddPetContext): Promise> export interface UpdatePetContext extends HttpClientContext { payload: APet; - requestHeaders?: { marshal: () => string }; } /** @@ -1130,8 +738,7 @@ export interface UpdatePetContext extends HttpClientContext { async function updatePet(context: UpdatePetContext): Promise> { // Apply defaults const config = { - path: '/pet', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1141,18 +748,11 @@ async function updatePet(context: UpdatePetContext): Promise; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = `${config.server}${config.path}`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = `${config.baseUrl}/pet`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1218,17 +818,13 @@ async function updatePet(context: UpdatePetContext): Promise = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, updatePet), }; return result; @@ -1243,8 +839,8 @@ async function updatePet(context: UpdatePetContext): Promise string }; - requestHeaders?: { marshal: () => string }; + parameters: FindPetsByStatusAndCategoryParametersInterface | FindPetsByStatusAndCategoryParameters; + requestHeaders?: FindPetsByStatusAndCategoryHeaders; } /** @@ -1253,29 +849,23 @@ export interface FindPetsByStatusAndCategoryContext extends HttpClientContext { async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryContext): Promise> { // Apply defaults const config = { - path: '/pet/findByStatus/{status}/{categoryId}', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; + const parameters = context.parameters instanceof FindPetsByStatusAndCategoryParameters ? context.parameters : new FindPetsByStatusAndCategoryParameters(context.parameters); + // 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; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders, ...(context.requestHeaders ? serializeFindPetsByStatusAndCategoryHeadersHeaders(context.requestHeaders) : {}) } as Record; // Build URL - let url = buildUrlWithParameters(config.server, '/pet/findByStatus/{status}/{categoryId}', context.parameters); - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = buildUrlWithParameters(config.baseUrl, '/pet/findByStatus/{status}/{categoryId}', parameters); + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1341,17 +931,13 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Extract response metadata const responseHeaders = extractHeaders(response); - const paginationInfo = extractPaginationInfo(responseHeaders, config.pagination); - // Build response wrapper with pagination helpers const result: HttpClientResponse = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, findPetsByStatusAndCategory), }; return result; diff --git a/test/runtime/typescript/src/openapi-path-organization/channels/parameter/FindPetsByStatusAndCategoryParameters.ts b/test/runtime/typescript/src/openapi-path-organization/channels/parameter/FindPetsByStatusAndCategoryParameters.ts index f90e6da9..d978dc08 100644 --- a/test/runtime/typescript/src/openapi-path-organization/channels/parameter/FindPetsByStatusAndCategoryParameters.ts +++ b/test/runtime/typescript/src/openapi-path-organization/channels/parameter/FindPetsByStatusAndCategoryParameters.ts @@ -2,6 +2,17 @@ import {Status} from './Status'; import {SortBy} from './SortBy'; import {SortOrder} from './SortOrder'; import {Format} from './Format'; +interface FindPetsByStatusAndCategoryParametersInterface { + status: Status + categoryId: number + limit?: number + offset?: number + sortBy?: SortBy + sortOrder?: SortOrder + tags?: string[] + includePetDetails?: boolean + format?: Format +} class FindPetsByStatusAndCategoryParameters { private _status: Status; private _categoryId: number; @@ -13,17 +24,7 @@ class FindPetsByStatusAndCategoryParameters { private _includePetDetails?: boolean; private _format?: Format; - constructor(input: { - status: Status, - categoryId: number, - limit?: number, - offset?: number, - sortBy?: SortBy, - sortOrder?: SortOrder, - tags?: string[], - includePetDetails?: boolean, - format?: Format, - }) { + constructor(input: FindPetsByStatusAndCategoryParametersInterface) { this._status = input.status; this._categoryId = input.categoryId; this._limit = input.limit; @@ -388,4 +389,4 @@ class FindPetsByStatusAndCategoryParameters { return result; } } -export { FindPetsByStatusAndCategoryParameters }; \ No newline at end of file +export { FindPetsByStatusAndCategoryParameters, FindPetsByStatusAndCategoryParametersInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi-primitive/channels/http_client.ts b/test/runtime/typescript/src/openapi-primitive/channels/http_client.ts index 355e2c5a..c2a5bf7f 100644 --- a/test/runtime/typescript/src/openapi-primitive/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi-primitive/channels/http_client.ts @@ -1,7 +1,5 @@ import * as GetEchoResponse_200Module from './../payloads/GetEchoResponse_200'; import * as GetCountResponse_200Module from './../payloads/GetCountResponse_200'; -import { URLSearchParams, URL } from 'url'; -import * as NodeFetch from 'node-fetch'; // ============================================================================ // Common Types - Shared across all HTTP client functions @@ -18,26 +16,6 @@ export interface HttpResponse { json: () => Record | Promise>; } -/** - * Pagination info extracted from response - */ -export interface PaginationInfo { - /** Total number of items (if available from headers like X-Total-Count) */ - totalCount?: number; - /** Total number of pages (if available) */ - totalPages?: number; - /** Current page/offset */ - currentOffset?: number; - /** Items per page */ - limit?: number; - /** Next cursor (for cursor-based pagination) */ - nextCursor?: string; - /** Previous cursor */ - prevCursor?: string; - /** Whether there are more items */ - hasMore?: boolean; -} - /** * Rich response wrapper returned by HTTP client functions */ @@ -52,16 +30,6 @@ export interface HttpClientResponse { headers: Record; /** Raw JSON response before deserialization */ rawData: Record; - /** Pagination info extracted from response (if applicable) */ - pagination?: PaginationInfo; - /** Fetch the next page (if pagination is configured and more data exists) */ - getNextPage?: () => Promise>; - /** Fetch the previous page (if pagination is configured) */ - getPrevPage?: () => Promise>; - /** Check if there's a next page */ - hasNextPage?: () => boolean; - /** Check if there's a previous page */ - hasPrevPage?: () => boolean; } /** @@ -71,7 +39,7 @@ export interface HttpRequestParams { url: string; headers?: Record; method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'; - credentials?: RequestCredentials; + credentials?: 'omit' | 'include' | 'same-origin'; body?: any; } @@ -172,69 +140,6 @@ const API_KEY_DEFAULTS = { in: 'header' as 'header' | 'query' | 'cookie' } as const; -// ============================================================================ -// Pagination Types -// ============================================================================ - -/** - * Where to place pagination parameters - */ -export type PaginationLocation = 'query' | 'header'; - -/** - * Offset-based pagination configuration - */ -export interface OffsetPagination { - type: 'offset'; - in?: PaginationLocation; // Where to place params (default: 'query') - offset: number; - limit: number; - offsetParam?: string; // Param name for offset (default: 'offset' for query, 'X-Offset' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Cursor-based pagination configuration - */ -export interface CursorPagination { - type: 'cursor'; - in?: PaginationLocation; // Where to place params (default: 'query') - cursor?: string; - limit?: number; - cursorParam?: string; // Param name for cursor (default: 'cursor' for query, 'X-Cursor' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Page-based pagination configuration - */ -export interface PagePagination { - type: 'page'; - in?: PaginationLocation; // Where to place params (default: 'query') - page: number; - pageSize: number; - pageParam?: string; // Param name for page (default: 'page' for query, 'X-Page' for header) - pageSizeParam?: string; // Param name for page size (default: 'pageSize' for query, 'X-Page-Size' for header) -} - -/** - * Range-based pagination (typically used with headers) - * Follows RFC 7233 style: Range: items=0-24 - */ -export interface RangePagination { - type: 'range'; - in?: 'header'; // Range pagination is typically header-only - start: number; - end: number; - unit?: string; // Range unit (default: 'items') - rangeHeader?: string; // Header name (default: 'Range') -} - -/** - * Union type for all pagination methods - */ -export type PaginationConfig = OffsetPagination | CursorPagination | PagePagination | RangePagination; - // ============================================================================ // Retry Configuration // ============================================================================ @@ -268,7 +173,7 @@ export interface HttpHooks { /** * The actual request implementation - allows swapping fetch for axios, etc. - * Default: uses node-fetch + * Default: uses the global fetch (Node.js 18+) */ makeRequest?: (params: HttpRequestParams) => Promise; @@ -292,15 +197,11 @@ export interface HttpHooks { * Base context shared by all HTTP client functions */ export interface HttpClientContext { - server?: string; - path?: string; + baseUrl?: string; // Authentication - grouped for better autocomplete auth?: AuthConfig; - // Pagination configuration - pagination?: PaginationConfig; - // Retry configuration retry?: RetryConfig; @@ -310,8 +211,8 @@ export interface HttpClientContext { // Additional options additionalHeaders?: Record; - // Query parameters - queryParams?: Record; + // Extra query parameters not covered by the typed parameters interface + additionalQueryParams?: Record; } // ============================================================================ @@ -332,13 +233,25 @@ const DEFAULT_RETRY_CONFIG: Required = { }; /** - * Default request hook implementation using node-fetch + * Default request hook implementation using the global fetch (Node.js 18+) */ const defaultMakeRequest = async (params: HttpRequestParams): Promise => { - return NodeFetch.default(params.url, { + // Build a Headers object so multi-value headers (string[]) are preserved - + // the global fetch's HeadersInit only accepts string values in a plain object. + const headers = new Headers(); + for (const [name, value] of Object.entries(params.headers ?? {})) { + if (Array.isArray(value)) { + for (const entry of value) { + headers.append(name, entry); + } + } else { + headers.set(name, value); + } + } + return fetch(params.url, { body: params.body, method: params.method, - headers: params.headers + headers }) as unknown as HttpResponse; }; @@ -391,94 +304,6 @@ function applyAuth( return { headers, url }; } -/** - * Apply pagination parameters to URL and/or headers based on configuration - */ -function applyPagination( - pagination: PaginationConfig | undefined, - url: string, - headers: Record -): { url: string; headers: Record } { - if (!pagination) return { url, headers }; - - const location = pagination.in ?? 'query'; - const isHeader = location === 'header'; - - // Helper to get default param names based on location - const getDefaultName = (queryName: string, headerName: string) => - isHeader ? headerName : queryName; - - const queryParams = new URLSearchParams(); - const headerParams: Record = {}; - - const addParam = (name: string, value: string) => { - if (isHeader) { - headerParams[name] = value; - } else { - queryParams.append(name, value); - } - }; - - switch (pagination.type) { - case 'offset': - addParam( - pagination.offsetParam ?? getDefaultName('offset', 'X-Offset'), - String(pagination.offset) - ); - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - break; - - case 'cursor': - if (pagination.cursor) { - addParam( - pagination.cursorParam ?? getDefaultName('cursor', 'X-Cursor'), - pagination.cursor - ); - } - if (pagination.limit !== undefined) { - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - } - break; - - case 'page': - addParam( - pagination.pageParam ?? getDefaultName('page', 'X-Page'), - String(pagination.page) - ); - addParam( - pagination.pageSizeParam ?? getDefaultName('pageSize', 'X-Page-Size'), - String(pagination.pageSize) - ); - break; - - case 'range': { - // Range pagination is always header-based (RFC 7233 style) - const unit = pagination.unit ?? 'items'; - const headerName = pagination.rangeHeader ?? 'Range'; - headerParams[headerName] = `${unit}=${pagination.start}-${pagination.end}`; - break; - } - } - - // Apply query params to URL - const queryString = queryParams.toString(); - if (queryString) { - const separator = url.includes('?') ? '&' : '?'; - url = `${url}${separator}${queryString}`; - } - - // Merge header params - const updatedHeaders = { ...headers, ...headerParams }; - - return { url, headers: updatedHeaders }; -} - /** * Apply query parameters to URL */ @@ -621,199 +446,6 @@ function extractHeaders(response: HttpResponse): Record { return headers; } -/** - * Extract pagination info from response headers - */ -function extractPaginationInfo( - headers: Record, - currentPagination?: PaginationConfig -): PaginationInfo | undefined { - const info: PaginationInfo = {}; - let hasPaginationInfo = false; - - // Common total count headers - const totalCount = headers['x-total-count'] || headers['x-total'] || headers['total-count']; - if (totalCount) { - info.totalCount = parseInt(totalCount, 10); - hasPaginationInfo = true; - } - - // Total pages - const totalPages = headers['x-total-pages'] || headers['x-page-count']; - if (totalPages) { - info.totalPages = parseInt(totalPages, 10); - hasPaginationInfo = true; - } - - // Next cursor - const nextCursor = headers['x-next-cursor'] || headers['x-cursor-next']; - if (nextCursor) { - info.nextCursor = nextCursor; - info.hasMore = true; - hasPaginationInfo = true; - } - - // Previous cursor - const prevCursor = headers['x-prev-cursor'] || headers['x-cursor-prev']; - if (prevCursor) { - info.prevCursor = prevCursor; - hasPaginationInfo = true; - } - - // Has more indicator - const hasMore = headers['x-has-more'] || headers['x-has-next']; - if (hasMore) { - info.hasMore = hasMore.toLowerCase() === 'true' || hasMore === '1'; - hasPaginationInfo = true; - } - - // Parse Link header (RFC 5988) - const linkHeader = headers['link']; - if (linkHeader) { - const links = parseLinkHeader(linkHeader); - if (links.next) { - info.hasMore = true; - hasPaginationInfo = true; - } - } - - // Include current pagination state - if (currentPagination) { - switch (currentPagination.type) { - case 'offset': - info.currentOffset = currentPagination.offset; - info.limit = currentPagination.limit; - break; - case 'cursor': - info.limit = currentPagination.limit; - break; - case 'page': - info.currentOffset = (currentPagination.page - 1) * currentPagination.pageSize; - info.limit = currentPagination.pageSize; - break; - case 'range': - info.currentOffset = currentPagination.start; - info.limit = currentPagination.end - currentPagination.start + 1; - break; - } - hasPaginationInfo = true; - } - - // Calculate hasMore based on total count - if (info.hasMore === undefined && info.totalCount !== undefined && - info.currentOffset !== undefined && info.limit !== undefined) { - info.hasMore = info.currentOffset + info.limit < info.totalCount; - } - - return hasPaginationInfo ? info : undefined; -} - -/** - * Parse RFC 5988 Link header - */ -function parseLinkHeader(header: string): Record { - const links: Record = {}; - const parts = header.split(','); - - for (const part of parts) { - const match = part.match(/<([^>]+)>;\s*rel="?([^";\s]+)"?/); - if (match) { - links[match[2]] = match[1]; - } - } - - return links; -} - -/** - * Create pagination helper functions for the response - */ -function createPaginationHelpers( - currentConfig: TContext, - paginationInfo: PaginationInfo | undefined, - requestFn: (config: TContext) => Promise> -): Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> { - const helpers: Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> = {}; - - if (!currentConfig.pagination) { - return helpers; - } - - const pagination = currentConfig.pagination; - - helpers.hasNextPage = () => { - if (paginationInfo?.hasMore !== undefined) return paginationInfo.hasMore; - if (paginationInfo?.nextCursor) return true; - if (paginationInfo?.totalCount !== undefined && - paginationInfo.currentOffset !== undefined && - paginationInfo.limit !== undefined) { - return paginationInfo.currentOffset + paginationInfo.limit < paginationInfo.totalCount; - } - return false; - }; - - helpers.hasPrevPage = () => { - if (paginationInfo?.prevCursor) return true; - if (paginationInfo?.currentOffset !== undefined) { - return paginationInfo.currentOffset > 0; - } - return false; - }; - - helpers.getNextPage = async () => { - let nextPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - nextPagination = { ...pagination, offset: pagination.offset + pagination.limit }; - break; - case 'cursor': - if (!paginationInfo?.nextCursor) throw new Error('No next cursor available'); - nextPagination = { ...pagination, cursor: paginationInfo.nextCursor }; - break; - case 'page': - nextPagination = { ...pagination, page: pagination.page + 1 }; - break; - case 'range': - const rangeSize = pagination.end - pagination.start + 1; - nextPagination = { ...pagination, start: pagination.end + 1, end: pagination.end + rangeSize }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: nextPagination }); - }; - - helpers.getPrevPage = async () => { - let prevPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - prevPagination = { ...pagination, offset: Math.max(0, pagination.offset - pagination.limit) }; - break; - case 'cursor': - if (!paginationInfo?.prevCursor) throw new Error('No previous cursor available'); - prevPagination = { ...pagination, cursor: paginationInfo.prevCursor }; - break; - case 'page': - prevPagination = { ...pagination, page: Math.max(1, pagination.page - 1) }; - break; - case 'range': - const size = pagination.end - pagination.start + 1; - const newStart = Math.max(0, pagination.start - size); - prevPagination = { ...pagination, start: newStart, end: newStart + size - 1 }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: prevPagination }); - }; - - return helpers; -} - /** * Builds a URL with path parameters replaced * @param server - Base server URL @@ -924,7 +556,7 @@ async function handleOAuth2TokenFlow( params.delete('client_secret'); } - const tokenResponse = await NodeFetch.default(auth.tokenUrl, { + const tokenResponse = await fetch(auth.tokenUrl, { method: 'POST', headers: authHeaders, body: params.toString() @@ -964,7 +596,7 @@ async function handleTokenRefresh( ): Promise { if (!auth.refreshToken || !auth.tokenUrl || !auth.clientId) return null; - const refreshResponse = await NodeFetch.default(auth.tokenUrl, { + const refreshResponse = await fetch(auth.tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' @@ -1003,9 +635,7 @@ async function handleTokenRefresh( // Generated HTTP Client Functions // ============================================================================ -export interface GetEchoContext extends HttpClientContext { - requestHeaders?: { marshal: () => string }; -} +export interface GetEchoContext extends HttpClientContext {} /** * Return a plain string body @@ -1013,8 +643,7 @@ export interface GetEchoContext extends HttpClientContext { async function getEcho(context: GetEchoContext = {}): Promise> { // Apply defaults const config = { - path: '/echo', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1024,18 +653,11 @@ async function getEcho(context: GetEchoContext = {}): Promise; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = `${config.server}${config.path}`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = `${config.baseUrl}/echo`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1101,17 +723,13 @@ async function getEcho(context: GetEchoContext = {}): Promise = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, getEcho), }; return result; @@ -1125,9 +743,7 @@ async function getEcho(context: GetEchoContext = {}): Promise string }; -} +export interface GetCountContext extends HttpClientContext {} /** * Return a plain number body @@ -1135,8 +751,7 @@ export interface GetCountContext extends HttpClientContext { async function getCount(context: GetCountContext = {}): Promise> { // Apply defaults const config = { - path: '/count', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1146,18 +761,11 @@ async function getCount(context: GetCountContext = {}): Promise; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = `${config.server}${config.path}`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = `${config.baseUrl}/count`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1223,17 +831,13 @@ async function getCount(context: GetCountContext = {}): Promise = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, getCount), }; return result; diff --git a/test/runtime/typescript/src/openapi-tag-organization/channels/headers/FindPetsByStatusAndCategoryHeaders.ts b/test/runtime/typescript/src/openapi-tag-organization/channels/headers/FindPetsByStatusAndCategoryHeaders.ts index 9ab9e878..1c3d9915 100644 --- a/test/runtime/typescript/src/openapi-tag-organization/channels/headers/FindPetsByStatusAndCategoryHeaders.ts +++ b/test/runtime/typescript/src/openapi-tag-organization/channels/headers/FindPetsByStatusAndCategoryHeaders.ts @@ -1,76 +1,19 @@ -import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import {default as addFormats} from 'ajv-formats'; -class FindPetsByStatusAndCategoryHeaders { - private _xRequestId?: string; - private _acceptLanguage?: string; - - constructor(input: { - xRequestId?: string, - acceptLanguage?: string, - }) { - this._xRequestId = input.xRequestId; - this._acceptLanguage = input.acceptLanguage; - } +interface FindPetsByStatusAndCategoryHeaders { /** * Unique request identifier for tracing */ - get xRequestId(): string | undefined { return this._xRequestId; } - set xRequestId(xRequestId: string | undefined) { this._xRequestId = xRequestId; } - + xMinusRequestMinusId?: string; /** * Preferred language for response messages */ - get acceptLanguage(): string | undefined { return this._acceptLanguage; } - set acceptLanguage(acceptLanguage: string | undefined) { this._acceptLanguage = acceptLanguage; } - - public marshal() : string { - let json = '{' - if(this.xRequestId !== undefined) { - json += `"X-Request-ID": ${typeof this.xRequestId === 'number' || typeof this.xRequestId === 'boolean' ? this.xRequestId : JSON.stringify(this.xRequestId)},`; - } - if(this.acceptLanguage !== undefined) { - json += `"Accept-Language": ${typeof this.acceptLanguage === 'number' || typeof this.acceptLanguage === 'boolean' ? this.acceptLanguage : JSON.stringify(this.acceptLanguage)},`; - } - - //Remove potential last comma - return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; - } - - public static unmarshal(json: string | object): FindPetsByStatusAndCategoryHeaders { - const obj = typeof json === "object" ? json : JSON.parse(json); - const instance = new FindPetsByStatusAndCategoryHeaders({} as any); - - if (obj["X-Request-ID"] !== undefined) { - instance.xRequestId = obj["X-Request-ID"]; - } - if (obj["Accept-Language"] !== undefined) { - instance.acceptLanguage = obj["Accept-Language"]; - } - - - return instance; - } - public static theCodeGenSchema = {"type":"object","additionalProperties":false,"properties":{"X-Request-ID":{"type":"string","format":"uuid","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","description":"Unique request identifier for tracing"},"Accept-Language":{"type":"string","pattern":"^[a-z]{2}(-[A-Z]{2})?$","default":"en-US","description":"Preferred language for response messages"}},"$id":"FindPetsByStatusAndCategoryHeaders","$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; - } - + acceptMinusLanguage?: string; } -export { FindPetsByStatusAndCategoryHeaders }; \ No newline at end of file +export { FindPetsByStatusAndCategoryHeaders }; + +export function serializeFindPetsByStatusAndCategoryHeadersHeaders(headers: FindPetsByStatusAndCategoryHeaders): Record { + const result: Record = {}; + if (headers.xMinusRequestMinusId !== undefined) { result['X-Request-ID'] = String(headers.xMinusRequestMinusId); } + if (headers.acceptMinusLanguage !== undefined) { result['Accept-Language'] = String(headers.acceptMinusLanguage); } + return result; +} \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi-tag-organization/channels/http_client.ts b/test/runtime/typescript/src/openapi-tag-organization/channels/http_client.ts index 6dcb861d..d1a9164b 100644 --- a/test/runtime/typescript/src/openapi-tag-organization/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi-tag-organization/channels/http_client.ts @@ -7,8 +7,8 @@ import {ItemStatus} from './payload/ItemStatus'; import {PetOrder} from './payload/PetOrder'; import {AUser} from './payload/AUser'; import {AnUploadedResponse} from './payload/AnUploadedResponse'; -import {FindPetsByStatusAndCategoryParameters} from './parameter/FindPetsByStatusAndCategoryParameters'; -import {FindPetsByStatusAndCategoryHeaders} from './headers/FindPetsByStatusAndCategoryHeaders'; +import {FindPetsByStatusAndCategoryParameters, FindPetsByStatusAndCategoryParametersInterface} from './parameter/FindPetsByStatusAndCategoryParameters'; +import {FindPetsByStatusAndCategoryHeaders, serializeFindPetsByStatusAndCategoryHeadersHeaders} from './headers/FindPetsByStatusAndCategoryHeaders'; // ============================================================================ // Common Types - Shared across all HTTP client functions @@ -25,26 +25,6 @@ export interface HttpResponse { json: () => Record | Promise>; } -/** - * Pagination info extracted from response - */ -export interface PaginationInfo { - /** Total number of items (if available from headers like X-Total-Count) */ - totalCount?: number; - /** Total number of pages (if available) */ - totalPages?: number; - /** Current page/offset */ - currentOffset?: number; - /** Items per page */ - limit?: number; - /** Next cursor (for cursor-based pagination) */ - nextCursor?: string; - /** Previous cursor */ - prevCursor?: string; - /** Whether there are more items */ - hasMore?: boolean; -} - /** * Rich response wrapper returned by HTTP client functions */ @@ -59,16 +39,6 @@ export interface HttpClientResponse { headers: Record; /** Raw JSON response before deserialization */ rawData: Record; - /** Pagination info extracted from response (if applicable) */ - pagination?: PaginationInfo; - /** Fetch the next page (if pagination is configured and more data exists) */ - getNextPage?: () => Promise>; - /** Fetch the previous page (if pagination is configured) */ - getPrevPage?: () => Promise>; - /** Check if there's a next page */ - hasNextPage?: () => boolean; - /** Check if there's a previous page */ - hasPrevPage?: () => boolean; } /** @@ -163,69 +133,6 @@ const API_KEY_DEFAULTS = { in: 'header' as 'header' | 'query' | 'cookie' } as const; -// ============================================================================ -// Pagination Types -// ============================================================================ - -/** - * Where to place pagination parameters - */ -export type PaginationLocation = 'query' | 'header'; - -/** - * Offset-based pagination configuration - */ -export interface OffsetPagination { - type: 'offset'; - in?: PaginationLocation; // Where to place params (default: 'query') - offset: number; - limit: number; - offsetParam?: string; // Param name for offset (default: 'offset' for query, 'X-Offset' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Cursor-based pagination configuration - */ -export interface CursorPagination { - type: 'cursor'; - in?: PaginationLocation; // Where to place params (default: 'query') - cursor?: string; - limit?: number; - cursorParam?: string; // Param name for cursor (default: 'cursor' for query, 'X-Cursor' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Page-based pagination configuration - */ -export interface PagePagination { - type: 'page'; - in?: PaginationLocation; // Where to place params (default: 'query') - page: number; - pageSize: number; - pageParam?: string; // Param name for page (default: 'page' for query, 'X-Page' for header) - pageSizeParam?: string; // Param name for page size (default: 'pageSize' for query, 'X-Page-Size' for header) -} - -/** - * Range-based pagination (typically used with headers) - * Follows RFC 7233 style: Range: items=0-24 - */ -export interface RangePagination { - type: 'range'; - in?: 'header'; // Range pagination is typically header-only - start: number; - end: number; - unit?: string; // Range unit (default: 'items') - rangeHeader?: string; // Header name (default: 'Range') -} - -/** - * Union type for all pagination methods - */ -export type PaginationConfig = OffsetPagination | CursorPagination | PagePagination | RangePagination; - // ============================================================================ // Retry Configuration // ============================================================================ @@ -283,15 +190,11 @@ export interface HttpHooks { * Base context shared by all HTTP client functions */ export interface HttpClientContext { - server?: string; - path?: string; + baseUrl?: string; // Authentication - grouped for better autocomplete auth?: AuthConfig; - // Pagination configuration - pagination?: PaginationConfig; - // Retry configuration retry?: RetryConfig; @@ -301,8 +204,8 @@ export interface HttpClientContext { // Additional options additionalHeaders?: Record; - // Query parameters - queryParams?: Record; + // Extra query parameters not covered by the typed parameters interface + additionalQueryParams?: Record; } // ============================================================================ @@ -384,94 +287,6 @@ function applyAuth( return { headers, url }; } -/** - * Apply pagination parameters to URL and/or headers based on configuration - */ -function applyPagination( - pagination: PaginationConfig | undefined, - url: string, - headers: Record -): { url: string; headers: Record } { - if (!pagination) return { url, headers }; - - const location = pagination.in ?? 'query'; - const isHeader = location === 'header'; - - // Helper to get default param names based on location - const getDefaultName = (queryName: string, headerName: string) => - isHeader ? headerName : queryName; - - const queryParams = new URLSearchParams(); - const headerParams: Record = {}; - - const addParam = (name: string, value: string) => { - if (isHeader) { - headerParams[name] = value; - } else { - queryParams.append(name, value); - } - }; - - switch (pagination.type) { - case 'offset': - addParam( - pagination.offsetParam ?? getDefaultName('offset', 'X-Offset'), - String(pagination.offset) - ); - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - break; - - case 'cursor': - if (pagination.cursor) { - addParam( - pagination.cursorParam ?? getDefaultName('cursor', 'X-Cursor'), - pagination.cursor - ); - } - if (pagination.limit !== undefined) { - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - } - break; - - case 'page': - addParam( - pagination.pageParam ?? getDefaultName('page', 'X-Page'), - String(pagination.page) - ); - addParam( - pagination.pageSizeParam ?? getDefaultName('pageSize', 'X-Page-Size'), - String(pagination.pageSize) - ); - break; - - case 'range': { - // Range pagination is always header-based (RFC 7233 style) - const unit = pagination.unit ?? 'items'; - const headerName = pagination.rangeHeader ?? 'Range'; - headerParams[headerName] = `${unit}=${pagination.start}-${pagination.end}`; - break; - } - } - - // Apply query params to URL - const queryString = queryParams.toString(); - if (queryString) { - const separator = url.includes('?') ? '&' : '?'; - url = `${url}${separator}${queryString}`; - } - - // Merge header params - const updatedHeaders = { ...headers, ...headerParams }; - - return { url, headers: updatedHeaders }; -} - /** * Apply query parameters to URL */ @@ -614,199 +429,6 @@ function extractHeaders(response: HttpResponse): Record { return headers; } -/** - * Extract pagination info from response headers - */ -function extractPaginationInfo( - headers: Record, - currentPagination?: PaginationConfig -): PaginationInfo | undefined { - const info: PaginationInfo = {}; - let hasPaginationInfo = false; - - // Common total count headers - const totalCount = headers['x-total-count'] || headers['x-total'] || headers['total-count']; - if (totalCount) { - info.totalCount = parseInt(totalCount, 10); - hasPaginationInfo = true; - } - - // Total pages - const totalPages = headers['x-total-pages'] || headers['x-page-count']; - if (totalPages) { - info.totalPages = parseInt(totalPages, 10); - hasPaginationInfo = true; - } - - // Next cursor - const nextCursor = headers['x-next-cursor'] || headers['x-cursor-next']; - if (nextCursor) { - info.nextCursor = nextCursor; - info.hasMore = true; - hasPaginationInfo = true; - } - - // Previous cursor - const prevCursor = headers['x-prev-cursor'] || headers['x-cursor-prev']; - if (prevCursor) { - info.prevCursor = prevCursor; - hasPaginationInfo = true; - } - - // Has more indicator - const hasMore = headers['x-has-more'] || headers['x-has-next']; - if (hasMore) { - info.hasMore = hasMore.toLowerCase() === 'true' || hasMore === '1'; - hasPaginationInfo = true; - } - - // Parse Link header (RFC 5988) - const linkHeader = headers['link']; - if (linkHeader) { - const links = parseLinkHeader(linkHeader); - if (links.next) { - info.hasMore = true; - hasPaginationInfo = true; - } - } - - // Include current pagination state - if (currentPagination) { - switch (currentPagination.type) { - case 'offset': - info.currentOffset = currentPagination.offset; - info.limit = currentPagination.limit; - break; - case 'cursor': - info.limit = currentPagination.limit; - break; - case 'page': - info.currentOffset = (currentPagination.page - 1) * currentPagination.pageSize; - info.limit = currentPagination.pageSize; - break; - case 'range': - info.currentOffset = currentPagination.start; - info.limit = currentPagination.end - currentPagination.start + 1; - break; - } - hasPaginationInfo = true; - } - - // Calculate hasMore based on total count - if (info.hasMore === undefined && info.totalCount !== undefined && - info.currentOffset !== undefined && info.limit !== undefined) { - info.hasMore = info.currentOffset + info.limit < info.totalCount; - } - - return hasPaginationInfo ? info : undefined; -} - -/** - * Parse RFC 5988 Link header - */ -function parseLinkHeader(header: string): Record { - const links: Record = {}; - const parts = header.split(','); - - for (const part of parts) { - const match = part.match(/<([^>]+)>;\s*rel="?([^";\s]+)"?/); - if (match) { - links[match[2]] = match[1]; - } - } - - return links; -} - -/** - * Create pagination helper functions for the response - */ -function createPaginationHelpers( - currentConfig: TContext, - paginationInfo: PaginationInfo | undefined, - requestFn: (config: TContext) => Promise> -): Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> { - const helpers: Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> = {}; - - if (!currentConfig.pagination) { - return helpers; - } - - const pagination = currentConfig.pagination; - - helpers.hasNextPage = () => { - if (paginationInfo?.hasMore !== undefined) return paginationInfo.hasMore; - if (paginationInfo?.nextCursor) return true; - if (paginationInfo?.totalCount !== undefined && - paginationInfo.currentOffset !== undefined && - paginationInfo.limit !== undefined) { - return paginationInfo.currentOffset + paginationInfo.limit < paginationInfo.totalCount; - } - return false; - }; - - helpers.hasPrevPage = () => { - if (paginationInfo?.prevCursor) return true; - if (paginationInfo?.currentOffset !== undefined) { - return paginationInfo.currentOffset > 0; - } - return false; - }; - - helpers.getNextPage = async () => { - let nextPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - nextPagination = { ...pagination, offset: pagination.offset + pagination.limit }; - break; - case 'cursor': - if (!paginationInfo?.nextCursor) throw new Error('No next cursor available'); - nextPagination = { ...pagination, cursor: paginationInfo.nextCursor }; - break; - case 'page': - nextPagination = { ...pagination, page: pagination.page + 1 }; - break; - case 'range': - const rangeSize = pagination.end - pagination.start + 1; - nextPagination = { ...pagination, start: pagination.end + 1, end: pagination.end + rangeSize }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: nextPagination }); - }; - - helpers.getPrevPage = async () => { - let prevPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - prevPagination = { ...pagination, offset: Math.max(0, pagination.offset - pagination.limit) }; - break; - case 'cursor': - if (!paginationInfo?.prevCursor) throw new Error('No previous cursor available'); - prevPagination = { ...pagination, cursor: paginationInfo.prevCursor }; - break; - case 'page': - prevPagination = { ...pagination, page: Math.max(1, pagination.page - 1) }; - break; - case 'range': - const size = pagination.end - pagination.start + 1; - const newStart = Math.max(0, pagination.start - size); - prevPagination = { ...pagination, start: newStart, end: newStart + size - 1 }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: prevPagination }); - }; - - return helpers; -} - /** * Builds a URL with path parameters replaced * @param server - Base server URL @@ -998,7 +620,6 @@ async function handleTokenRefresh( export interface AddPetContext extends HttpClientContext { payload: APet; - requestHeaders?: { marshal: () => string }; } /** @@ -1007,8 +628,7 @@ export interface AddPetContext extends HttpClientContext { async function addPet(context: AddPetContext): Promise> { // Apply defaults const config = { - path: '/pet', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1018,18 +638,11 @@ async function addPet(context: AddPetContext): Promise> } // Build headers - let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) - : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = `${config.server}${config.path}`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = `${config.baseUrl}/pet`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1095,17 +708,13 @@ async function addPet(context: AddPetContext): Promise> // Extract response metadata const responseHeaders = extractHeaders(response); - const paginationInfo = extractPaginationInfo(responseHeaders, config.pagination); - // Build response wrapper with pagination helpers const result: HttpClientResponse = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, addPet), }; return result; @@ -1121,7 +730,6 @@ async function addPet(context: AddPetContext): Promise> export interface UpdatePetContext extends HttpClientContext { payload: APet; - requestHeaders?: { marshal: () => string }; } /** @@ -1130,8 +738,7 @@ export interface UpdatePetContext extends HttpClientContext { async function updatePet(context: UpdatePetContext): Promise> { // Apply defaults const config = { - path: '/pet', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1141,18 +748,11 @@ async function updatePet(context: UpdatePetContext): Promise; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = `${config.server}${config.path}`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = `${config.baseUrl}/pet`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1218,17 +818,13 @@ async function updatePet(context: UpdatePetContext): Promise = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, updatePet), }; return result; @@ -1243,8 +839,8 @@ async function updatePet(context: UpdatePetContext): Promise string }; - requestHeaders?: { marshal: () => string }; + parameters: FindPetsByStatusAndCategoryParametersInterface | FindPetsByStatusAndCategoryParameters; + requestHeaders?: FindPetsByStatusAndCategoryHeaders; } /** @@ -1253,29 +849,23 @@ export interface FindPetsByStatusAndCategoryContext extends HttpClientContext { async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryContext): Promise> { // Apply defaults const config = { - path: '/pet/findByStatus/{status}/{categoryId}', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; + const parameters = context.parameters instanceof FindPetsByStatusAndCategoryParameters ? context.parameters : new FindPetsByStatusAndCategoryParameters(context.parameters); + // 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; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders, ...(context.requestHeaders ? serializeFindPetsByStatusAndCategoryHeadersHeaders(context.requestHeaders) : {}) } as Record; // Build URL - let url = buildUrlWithParameters(config.server, '/pet/findByStatus/{status}/{categoryId}', context.parameters); - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = buildUrlWithParameters(config.baseUrl, '/pet/findByStatus/{status}/{categoryId}', parameters); + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1341,17 +931,13 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Extract response metadata const responseHeaders = extractHeaders(response); - const paginationInfo = extractPaginationInfo(responseHeaders, config.pagination); - // Build response wrapper with pagination helpers const result: HttpClientResponse = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, findPetsByStatusAndCategory), }; return result; diff --git a/test/runtime/typescript/src/openapi-tag-organization/channels/parameter/FindPetsByStatusAndCategoryParameters.ts b/test/runtime/typescript/src/openapi-tag-organization/channels/parameter/FindPetsByStatusAndCategoryParameters.ts index f90e6da9..d978dc08 100644 --- a/test/runtime/typescript/src/openapi-tag-organization/channels/parameter/FindPetsByStatusAndCategoryParameters.ts +++ b/test/runtime/typescript/src/openapi-tag-organization/channels/parameter/FindPetsByStatusAndCategoryParameters.ts @@ -2,6 +2,17 @@ import {Status} from './Status'; import {SortBy} from './SortBy'; import {SortOrder} from './SortOrder'; import {Format} from './Format'; +interface FindPetsByStatusAndCategoryParametersInterface { + status: Status + categoryId: number + limit?: number + offset?: number + sortBy?: SortBy + sortOrder?: SortOrder + tags?: string[] + includePetDetails?: boolean + format?: Format +} class FindPetsByStatusAndCategoryParameters { private _status: Status; private _categoryId: number; @@ -13,17 +24,7 @@ class FindPetsByStatusAndCategoryParameters { private _includePetDetails?: boolean; private _format?: Format; - constructor(input: { - status: Status, - categoryId: number, - limit?: number, - offset?: number, - sortBy?: SortBy, - sortOrder?: SortOrder, - tags?: string[], - includePetDetails?: boolean, - format?: Format, - }) { + constructor(input: FindPetsByStatusAndCategoryParametersInterface) { this._status = input.status; this._categoryId = input.categoryId; this._limit = input.limit; @@ -388,4 +389,4 @@ class FindPetsByStatusAndCategoryParameters { return result; } } -export { FindPetsByStatusAndCategoryParameters }; \ No newline at end of file +export { FindPetsByStatusAndCategoryParameters, FindPetsByStatusAndCategoryParametersInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/channels/http_client.ts b/test/runtime/typescript/src/openapi/channels/http_client.ts index c2b07ea4..dad13807 100644 --- a/test/runtime/typescript/src/openapi/channels/http_client.ts +++ b/test/runtime/typescript/src/openapi/channels/http_client.ts @@ -7,8 +7,8 @@ import {ItemStatus} from './../payloads/ItemStatus'; import {PetOrder} from './../payloads/PetOrder'; import {AUser} from './../payloads/AUser'; import {AnUploadedResponse} from './../payloads/AnUploadedResponse'; -import {FindPetsByStatusAndCategoryParameters} from './../parameters/FindPetsByStatusAndCategoryParameters'; -import {FindPetsByStatusAndCategoryHeaders} from './../headers/FindPetsByStatusAndCategoryHeaders'; +import {FindPetsByStatusAndCategoryParameters, FindPetsByStatusAndCategoryParametersInterface} from './../parameters/FindPetsByStatusAndCategoryParameters'; +import {FindPetsByStatusAndCategoryHeaders, serializeFindPetsByStatusAndCategoryHeadersHeaders} from './../headers/FindPetsByStatusAndCategoryHeaders'; // ============================================================================ // Common Types - Shared across all HTTP client functions @@ -25,26 +25,6 @@ export interface HttpResponse { json: () => Record | Promise>; } -/** - * Pagination info extracted from response - */ -export interface PaginationInfo { - /** Total number of items (if available from headers like X-Total-Count) */ - totalCount?: number; - /** Total number of pages (if available) */ - totalPages?: number; - /** Current page/offset */ - currentOffset?: number; - /** Items per page */ - limit?: number; - /** Next cursor (for cursor-based pagination) */ - nextCursor?: string; - /** Previous cursor */ - prevCursor?: string; - /** Whether there are more items */ - hasMore?: boolean; -} - /** * Rich response wrapper returned by HTTP client functions */ @@ -59,16 +39,6 @@ export interface HttpClientResponse { headers: Record; /** Raw JSON response before deserialization */ rawData: Record; - /** Pagination info extracted from response (if applicable) */ - pagination?: PaginationInfo; - /** Fetch the next page (if pagination is configured and more data exists) */ - getNextPage?: () => Promise>; - /** Fetch the previous page (if pagination is configured) */ - getPrevPage?: () => Promise>; - /** Check if there's a next page */ - hasNextPage?: () => boolean; - /** Check if there's a previous page */ - hasPrevPage?: () => boolean; } /** @@ -163,69 +133,6 @@ const API_KEY_DEFAULTS = { in: 'header' as 'header' | 'query' | 'cookie' } as const; -// ============================================================================ -// Pagination Types -// ============================================================================ - -/** - * Where to place pagination parameters - */ -export type PaginationLocation = 'query' | 'header'; - -/** - * Offset-based pagination configuration - */ -export interface OffsetPagination { - type: 'offset'; - in?: PaginationLocation; // Where to place params (default: 'query') - offset: number; - limit: number; - offsetParam?: string; // Param name for offset (default: 'offset' for query, 'X-Offset' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Cursor-based pagination configuration - */ -export interface CursorPagination { - type: 'cursor'; - in?: PaginationLocation; // Where to place params (default: 'query') - cursor?: string; - limit?: number; - cursorParam?: string; // Param name for cursor (default: 'cursor' for query, 'X-Cursor' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Page-based pagination configuration - */ -export interface PagePagination { - type: 'page'; - in?: PaginationLocation; // Where to place params (default: 'query') - page: number; - pageSize: number; - pageParam?: string; // Param name for page (default: 'page' for query, 'X-Page' for header) - pageSizeParam?: string; // Param name for page size (default: 'pageSize' for query, 'X-Page-Size' for header) -} - -/** - * Range-based pagination (typically used with headers) - * Follows RFC 7233 style: Range: items=0-24 - */ -export interface RangePagination { - type: 'range'; - in?: 'header'; // Range pagination is typically header-only - start: number; - end: number; - unit?: string; // Range unit (default: 'items') - rangeHeader?: string; // Header name (default: 'Range') -} - -/** - * Union type for all pagination methods - */ -export type PaginationConfig = OffsetPagination | CursorPagination | PagePagination | RangePagination; - // ============================================================================ // Retry Configuration // ============================================================================ @@ -283,15 +190,11 @@ export interface HttpHooks { * Base context shared by all HTTP client functions */ export interface HttpClientContext { - server?: string; - path?: string; + baseUrl?: string; // Authentication - grouped for better autocomplete auth?: AuthConfig; - // Pagination configuration - pagination?: PaginationConfig; - // Retry configuration retry?: RetryConfig; @@ -301,8 +204,8 @@ export interface HttpClientContext { // Additional options additionalHeaders?: Record; - // Query parameters - queryParams?: Record; + // Extra query parameters not covered by the typed parameters interface + additionalQueryParams?: Record; } // ============================================================================ @@ -384,94 +287,6 @@ function applyAuth( return { headers, url }; } -/** - * Apply pagination parameters to URL and/or headers based on configuration - */ -function applyPagination( - pagination: PaginationConfig | undefined, - url: string, - headers: Record -): { url: string; headers: Record } { - if (!pagination) return { url, headers }; - - const location = pagination.in ?? 'query'; - const isHeader = location === 'header'; - - // Helper to get default param names based on location - const getDefaultName = (queryName: string, headerName: string) => - isHeader ? headerName : queryName; - - const queryParams = new URLSearchParams(); - const headerParams: Record = {}; - - const addParam = (name: string, value: string) => { - if (isHeader) { - headerParams[name] = value; - } else { - queryParams.append(name, value); - } - }; - - switch (pagination.type) { - case 'offset': - addParam( - pagination.offsetParam ?? getDefaultName('offset', 'X-Offset'), - String(pagination.offset) - ); - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - break; - - case 'cursor': - if (pagination.cursor) { - addParam( - pagination.cursorParam ?? getDefaultName('cursor', 'X-Cursor'), - pagination.cursor - ); - } - if (pagination.limit !== undefined) { - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - } - break; - - case 'page': - addParam( - pagination.pageParam ?? getDefaultName('page', 'X-Page'), - String(pagination.page) - ); - addParam( - pagination.pageSizeParam ?? getDefaultName('pageSize', 'X-Page-Size'), - String(pagination.pageSize) - ); - break; - - case 'range': { - // Range pagination is always header-based (RFC 7233 style) - const unit = pagination.unit ?? 'items'; - const headerName = pagination.rangeHeader ?? 'Range'; - headerParams[headerName] = `${unit}=${pagination.start}-${pagination.end}`; - break; - } - } - - // Apply query params to URL - const queryString = queryParams.toString(); - if (queryString) { - const separator = url.includes('?') ? '&' : '?'; - url = `${url}${separator}${queryString}`; - } - - // Merge header params - const updatedHeaders = { ...headers, ...headerParams }; - - return { url, headers: updatedHeaders }; -} - /** * Apply query parameters to URL */ @@ -614,199 +429,6 @@ function extractHeaders(response: HttpResponse): Record { return headers; } -/** - * Extract pagination info from response headers - */ -function extractPaginationInfo( - headers: Record, - currentPagination?: PaginationConfig -): PaginationInfo | undefined { - const info: PaginationInfo = {}; - let hasPaginationInfo = false; - - // Common total count headers - const totalCount = headers['x-total-count'] || headers['x-total'] || headers['total-count']; - if (totalCount) { - info.totalCount = parseInt(totalCount, 10); - hasPaginationInfo = true; - } - - // Total pages - const totalPages = headers['x-total-pages'] || headers['x-page-count']; - if (totalPages) { - info.totalPages = parseInt(totalPages, 10); - hasPaginationInfo = true; - } - - // Next cursor - const nextCursor = headers['x-next-cursor'] || headers['x-cursor-next']; - if (nextCursor) { - info.nextCursor = nextCursor; - info.hasMore = true; - hasPaginationInfo = true; - } - - // Previous cursor - const prevCursor = headers['x-prev-cursor'] || headers['x-cursor-prev']; - if (prevCursor) { - info.prevCursor = prevCursor; - hasPaginationInfo = true; - } - - // Has more indicator - const hasMore = headers['x-has-more'] || headers['x-has-next']; - if (hasMore) { - info.hasMore = hasMore.toLowerCase() === 'true' || hasMore === '1'; - hasPaginationInfo = true; - } - - // Parse Link header (RFC 5988) - const linkHeader = headers['link']; - if (linkHeader) { - const links = parseLinkHeader(linkHeader); - if (links.next) { - info.hasMore = true; - hasPaginationInfo = true; - } - } - - // Include current pagination state - if (currentPagination) { - switch (currentPagination.type) { - case 'offset': - info.currentOffset = currentPagination.offset; - info.limit = currentPagination.limit; - break; - case 'cursor': - info.limit = currentPagination.limit; - break; - case 'page': - info.currentOffset = (currentPagination.page - 1) * currentPagination.pageSize; - info.limit = currentPagination.pageSize; - break; - case 'range': - info.currentOffset = currentPagination.start; - info.limit = currentPagination.end - currentPagination.start + 1; - break; - } - hasPaginationInfo = true; - } - - // Calculate hasMore based on total count - if (info.hasMore === undefined && info.totalCount !== undefined && - info.currentOffset !== undefined && info.limit !== undefined) { - info.hasMore = info.currentOffset + info.limit < info.totalCount; - } - - return hasPaginationInfo ? info : undefined; -} - -/** - * Parse RFC 5988 Link header - */ -function parseLinkHeader(header: string): Record { - const links: Record = {}; - const parts = header.split(','); - - for (const part of parts) { - const match = part.match(/<([^>]+)>;\s*rel="?([^";\s]+)"?/); - if (match) { - links[match[2]] = match[1]; - } - } - - return links; -} - -/** - * Create pagination helper functions for the response - */ -function createPaginationHelpers( - currentConfig: TContext, - paginationInfo: PaginationInfo | undefined, - requestFn: (config: TContext) => Promise> -): Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> { - const helpers: Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> = {}; - - if (!currentConfig.pagination) { - return helpers; - } - - const pagination = currentConfig.pagination; - - helpers.hasNextPage = () => { - if (paginationInfo?.hasMore !== undefined) return paginationInfo.hasMore; - if (paginationInfo?.nextCursor) return true; - if (paginationInfo?.totalCount !== undefined && - paginationInfo.currentOffset !== undefined && - paginationInfo.limit !== undefined) { - return paginationInfo.currentOffset + paginationInfo.limit < paginationInfo.totalCount; - } - return false; - }; - - helpers.hasPrevPage = () => { - if (paginationInfo?.prevCursor) return true; - if (paginationInfo?.currentOffset !== undefined) { - return paginationInfo.currentOffset > 0; - } - return false; - }; - - helpers.getNextPage = async () => { - let nextPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - nextPagination = { ...pagination, offset: pagination.offset + pagination.limit }; - break; - case 'cursor': - if (!paginationInfo?.nextCursor) throw new Error('No next cursor available'); - nextPagination = { ...pagination, cursor: paginationInfo.nextCursor }; - break; - case 'page': - nextPagination = { ...pagination, page: pagination.page + 1 }; - break; - case 'range': - const rangeSize = pagination.end - pagination.start + 1; - nextPagination = { ...pagination, start: pagination.end + 1, end: pagination.end + rangeSize }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: nextPagination }); - }; - - helpers.getPrevPage = async () => { - let prevPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - prevPagination = { ...pagination, offset: Math.max(0, pagination.offset - pagination.limit) }; - break; - case 'cursor': - if (!paginationInfo?.prevCursor) throw new Error('No previous cursor available'); - prevPagination = { ...pagination, cursor: paginationInfo.prevCursor }; - break; - case 'page': - prevPagination = { ...pagination, page: Math.max(1, pagination.page - 1) }; - break; - case 'range': - const size = pagination.end - pagination.start + 1; - const newStart = Math.max(0, pagination.start - size); - prevPagination = { ...pagination, start: newStart, end: newStart + size - 1 }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: prevPagination }); - }; - - return helpers; -} - /** * Builds a URL with path parameters replaced * @param server - Base server URL @@ -998,7 +620,6 @@ async function handleTokenRefresh( export interface AddPetContext extends HttpClientContext { payload: APet; - requestHeaders?: { marshal: () => string }; } /** @@ -1007,8 +628,7 @@ export interface AddPetContext extends HttpClientContext { async function addPet(context: AddPetContext): Promise> { // Apply defaults const config = { - path: '/pet', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1018,18 +638,11 @@ async function addPet(context: AddPetContext): Promise> } // Build headers - let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) - : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = `${config.server}${config.path}`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = `${config.baseUrl}/pet`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1095,17 +708,13 @@ async function addPet(context: AddPetContext): Promise> // Extract response metadata const responseHeaders = extractHeaders(response); - const paginationInfo = extractPaginationInfo(responseHeaders, config.pagination); - // Build response wrapper with pagination helpers const result: HttpClientResponse = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, addPet), }; return result; @@ -1121,7 +730,6 @@ async function addPet(context: AddPetContext): Promise> export interface UpdatePetContext extends HttpClientContext { payload: APet; - requestHeaders?: { marshal: () => string }; } /** @@ -1130,8 +738,7 @@ export interface UpdatePetContext extends HttpClientContext { async function updatePet(context: UpdatePetContext): Promise> { // Apply defaults const config = { - path: '/pet', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1141,18 +748,11 @@ async function updatePet(context: UpdatePetContext): Promise; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = `${config.server}${config.path}`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = `${config.baseUrl}/pet`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1218,17 +818,13 @@ async function updatePet(context: UpdatePetContext): Promise = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, updatePet), }; return result; @@ -1243,8 +839,8 @@ async function updatePet(context: UpdatePetContext): Promise string }; - requestHeaders?: { marshal: () => string }; + parameters: FindPetsByStatusAndCategoryParametersInterface | FindPetsByStatusAndCategoryParameters; + requestHeaders?: FindPetsByStatusAndCategoryHeaders; } /** @@ -1253,29 +849,23 @@ export interface FindPetsByStatusAndCategoryContext extends HttpClientContext { async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryContext): Promise> { // Apply defaults const config = { - path: '/pet/findByStatus/{status}/{categoryId}', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; + const parameters = context.parameters instanceof FindPetsByStatusAndCategoryParameters ? context.parameters : new FindPetsByStatusAndCategoryParameters(context.parameters); + // 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; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders, ...(context.requestHeaders ? serializeFindPetsByStatusAndCategoryHeadersHeaders(context.requestHeaders) : {}) } as Record; // Build URL - let url = buildUrlWithParameters(config.server, '/pet/findByStatus/{status}/{categoryId}', context.parameters); - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = buildUrlWithParameters(config.baseUrl, '/pet/findByStatus/{status}/{categoryId}', parameters); + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1341,17 +931,13 @@ async function findPetsByStatusAndCategory(context: FindPetsByStatusAndCategoryC // Extract response metadata const responseHeaders = extractHeaders(response); - const paginationInfo = extractPaginationInfo(responseHeaders, config.pagination); - // Build response wrapper with pagination helpers const result: HttpClientResponse = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, findPetsByStatusAndCategory), }; return result; diff --git a/test/runtime/typescript/src/openapi/headers/FindPetsByStatusAndCategoryHeaders.ts b/test/runtime/typescript/src/openapi/headers/FindPetsByStatusAndCategoryHeaders.ts index 9ab9e878..1c3d9915 100644 --- a/test/runtime/typescript/src/openapi/headers/FindPetsByStatusAndCategoryHeaders.ts +++ b/test/runtime/typescript/src/openapi/headers/FindPetsByStatusAndCategoryHeaders.ts @@ -1,76 +1,19 @@ -import {Ajv, Options as AjvOptions, ErrorObject, ValidateFunction} from 'ajv'; -import {default as addFormats} from 'ajv-formats'; -class FindPetsByStatusAndCategoryHeaders { - private _xRequestId?: string; - private _acceptLanguage?: string; - - constructor(input: { - xRequestId?: string, - acceptLanguage?: string, - }) { - this._xRequestId = input.xRequestId; - this._acceptLanguage = input.acceptLanguage; - } +interface FindPetsByStatusAndCategoryHeaders { /** * Unique request identifier for tracing */ - get xRequestId(): string | undefined { return this._xRequestId; } - set xRequestId(xRequestId: string | undefined) { this._xRequestId = xRequestId; } - + xMinusRequestMinusId?: string; /** * Preferred language for response messages */ - get acceptLanguage(): string | undefined { return this._acceptLanguage; } - set acceptLanguage(acceptLanguage: string | undefined) { this._acceptLanguage = acceptLanguage; } - - public marshal() : string { - let json = '{' - if(this.xRequestId !== undefined) { - json += `"X-Request-ID": ${typeof this.xRequestId === 'number' || typeof this.xRequestId === 'boolean' ? this.xRequestId : JSON.stringify(this.xRequestId)},`; - } - if(this.acceptLanguage !== undefined) { - json += `"Accept-Language": ${typeof this.acceptLanguage === 'number' || typeof this.acceptLanguage === 'boolean' ? this.acceptLanguage : JSON.stringify(this.acceptLanguage)},`; - } - - //Remove potential last comma - return `${json.charAt(json.length-1) === ',' ? json.slice(0, json.length-1) : json}}`; - } - - public static unmarshal(json: string | object): FindPetsByStatusAndCategoryHeaders { - const obj = typeof json === "object" ? json : JSON.parse(json); - const instance = new FindPetsByStatusAndCategoryHeaders({} as any); - - if (obj["X-Request-ID"] !== undefined) { - instance.xRequestId = obj["X-Request-ID"]; - } - if (obj["Accept-Language"] !== undefined) { - instance.acceptLanguage = obj["Accept-Language"]; - } - - - return instance; - } - public static theCodeGenSchema = {"type":"object","additionalProperties":false,"properties":{"X-Request-ID":{"type":"string","format":"uuid","pattern":"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$","description":"Unique request identifier for tracing"},"Accept-Language":{"type":"string","pattern":"^[a-z]{2}(-[A-Z]{2})?$","default":"en-US","description":"Preferred language for response messages"}},"$id":"FindPetsByStatusAndCategoryHeaders","$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; - } - + acceptMinusLanguage?: string; } -export { FindPetsByStatusAndCategoryHeaders }; \ No newline at end of file +export { FindPetsByStatusAndCategoryHeaders }; + +export function serializeFindPetsByStatusAndCategoryHeadersHeaders(headers: FindPetsByStatusAndCategoryHeaders): Record { + const result: Record = {}; + if (headers.xMinusRequestMinusId !== undefined) { result['X-Request-ID'] = String(headers.xMinusRequestMinusId); } + if (headers.acceptMinusLanguage !== undefined) { result['Accept-Language'] = String(headers.acceptMinusLanguage); } + return result; +} \ No newline at end of file diff --git a/test/runtime/typescript/src/openapi/parameters/FindPetsByStatusAndCategoryParameters.ts b/test/runtime/typescript/src/openapi/parameters/FindPetsByStatusAndCategoryParameters.ts index f90e6da9..d978dc08 100644 --- a/test/runtime/typescript/src/openapi/parameters/FindPetsByStatusAndCategoryParameters.ts +++ b/test/runtime/typescript/src/openapi/parameters/FindPetsByStatusAndCategoryParameters.ts @@ -2,6 +2,17 @@ import {Status} from './Status'; import {SortBy} from './SortBy'; import {SortOrder} from './SortOrder'; import {Format} from './Format'; +interface FindPetsByStatusAndCategoryParametersInterface { + status: Status + categoryId: number + limit?: number + offset?: number + sortBy?: SortBy + sortOrder?: SortOrder + tags?: string[] + includePetDetails?: boolean + format?: Format +} class FindPetsByStatusAndCategoryParameters { private _status: Status; private _categoryId: number; @@ -13,17 +24,7 @@ class FindPetsByStatusAndCategoryParameters { private _includePetDetails?: boolean; private _format?: Format; - constructor(input: { - status: Status, - categoryId: number, - limit?: number, - offset?: number, - sortBy?: SortBy, - sortOrder?: SortOrder, - tags?: string[], - includePetDetails?: boolean, - format?: Format, - }) { + constructor(input: FindPetsByStatusAndCategoryParametersInterface) { this._status = input.status; this._categoryId = input.categoryId; this._limit = input.limit; @@ -388,4 +389,4 @@ class FindPetsByStatusAndCategoryParameters { return result; } } -export { FindPetsByStatusAndCategoryParameters }; \ No newline at end of file +export { FindPetsByStatusAndCategoryParameters, FindPetsByStatusAndCategoryParametersInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/parameters/UserSignedupParameters.ts b/test/runtime/typescript/src/parameters/UserSignedupParameters.ts index cf4660d5..4195c425 100644 --- a/test/runtime/typescript/src/parameters/UserSignedupParameters.ts +++ b/test/runtime/typescript/src/parameters/UserSignedupParameters.ts @@ -1,12 +1,13 @@ import {EnumParameter} from './EnumParameter'; +interface UserSignedupParametersInterface { + myParameter: string + enumParameter: EnumParameter +} class UserSignedupParameters { private _myParameter: string; private _enumParameter: EnumParameter; - constructor(input: { - myParameter: string, - enumParameter: EnumParameter, - }) { + constructor(input: UserSignedupParametersInterface) { this._myParameter = input.myParameter; this._enumParameter = input.enumParameter; } @@ -57,4 +58,4 @@ class UserSignedupParameters { return parameters; } } -export { UserSignedupParameters }; \ No newline at end of file +export { UserSignedupParameters, UserSignedupParametersInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/src/request-reply/channels/http_client.ts b/test/runtime/typescript/src/request-reply/channels/http_client.ts index 0eaee9d3..3380d116 100644 --- a/test/runtime/typescript/src/request-reply/channels/http_client.ts +++ b/test/runtime/typescript/src/request-reply/channels/http_client.ts @@ -8,7 +8,7 @@ import * as PingPayloadModule from './../payloads/PingPayload'; import * as UserItemsPayloadModule from './../payloads/UserItemsPayload'; import {NotFound} from './../payloads/NotFound'; import {ItemResponse} from './../payloads/ItemResponse'; -import {UserItemsParameters} from './../parameters/UserItemsParameters'; +import {UserItemsParameters, UserItemsParametersInterface} from './../parameters/UserItemsParameters'; import {ItemRequestHeaders} from './../headers/ItemRequestHeaders'; // ============================================================================ @@ -26,26 +26,6 @@ export interface HttpResponse { json: () => Record | Promise>; } -/** - * Pagination info extracted from response - */ -export interface PaginationInfo { - /** Total number of items (if available from headers like X-Total-Count) */ - totalCount?: number; - /** Total number of pages (if available) */ - totalPages?: number; - /** Current page/offset */ - currentOffset?: number; - /** Items per page */ - limit?: number; - /** Next cursor (for cursor-based pagination) */ - nextCursor?: string; - /** Previous cursor */ - prevCursor?: string; - /** Whether there are more items */ - hasMore?: boolean; -} - /** * Rich response wrapper returned by HTTP client functions */ @@ -60,16 +40,6 @@ export interface HttpClientResponse { headers: Record; /** Raw JSON response before deserialization */ rawData: Record; - /** Pagination info extracted from response (if applicable) */ - pagination?: PaginationInfo; - /** Fetch the next page (if pagination is configured and more data exists) */ - getNextPage?: () => Promise>; - /** Fetch the previous page (if pagination is configured) */ - getPrevPage?: () => Promise>; - /** Check if there's a next page */ - hasNextPage?: () => boolean; - /** Check if there's a previous page */ - hasPrevPage?: () => boolean; } /** @@ -180,69 +150,6 @@ const API_KEY_DEFAULTS = { in: 'header' as 'header' | 'query' | 'cookie' } as const; -// ============================================================================ -// Pagination Types -// ============================================================================ - -/** - * Where to place pagination parameters - */ -export type PaginationLocation = 'query' | 'header'; - -/** - * Offset-based pagination configuration - */ -export interface OffsetPagination { - type: 'offset'; - in?: PaginationLocation; // Where to place params (default: 'query') - offset: number; - limit: number; - offsetParam?: string; // Param name for offset (default: 'offset' for query, 'X-Offset' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Cursor-based pagination configuration - */ -export interface CursorPagination { - type: 'cursor'; - in?: PaginationLocation; // Where to place params (default: 'query') - cursor?: string; - limit?: number; - cursorParam?: string; // Param name for cursor (default: 'cursor' for query, 'X-Cursor' for header) - limitParam?: string; // Param name for limit (default: 'limit' for query, 'X-Limit' for header) -} - -/** - * Page-based pagination configuration - */ -export interface PagePagination { - type: 'page'; - in?: PaginationLocation; // Where to place params (default: 'query') - page: number; - pageSize: number; - pageParam?: string; // Param name for page (default: 'page' for query, 'X-Page' for header) - pageSizeParam?: string; // Param name for page size (default: 'pageSize' for query, 'X-Page-Size' for header) -} - -/** - * Range-based pagination (typically used with headers) - * Follows RFC 7233 style: Range: items=0-24 - */ -export interface RangePagination { - type: 'range'; - in?: 'header'; // Range pagination is typically header-only - start: number; - end: number; - unit?: string; // Range unit (default: 'items') - rangeHeader?: string; // Header name (default: 'Range') -} - -/** - * Union type for all pagination methods - */ -export type PaginationConfig = OffsetPagination | CursorPagination | PagePagination | RangePagination; - // ============================================================================ // Retry Configuration // ============================================================================ @@ -300,15 +207,11 @@ export interface HttpHooks { * Base context shared by all HTTP client functions */ export interface HttpClientContext { - server?: string; - path?: string; + baseUrl?: string; // Authentication - grouped for better autocomplete auth?: AuthConfig; - // Pagination configuration - pagination?: PaginationConfig; - // Retry configuration retry?: RetryConfig; @@ -318,8 +221,8 @@ export interface HttpClientContext { // Additional options additionalHeaders?: Record; - // Query parameters - queryParams?: Record; + // Extra query parameters not covered by the typed parameters interface + additionalQueryParams?: Record; } // ============================================================================ @@ -411,94 +314,6 @@ function applyAuth( return { headers, url }; } -/** - * Apply pagination parameters to URL and/or headers based on configuration - */ -function applyPagination( - pagination: PaginationConfig | undefined, - url: string, - headers: Record -): { url: string; headers: Record } { - if (!pagination) return { url, headers }; - - const location = pagination.in ?? 'query'; - const isHeader = location === 'header'; - - // Helper to get default param names based on location - const getDefaultName = (queryName: string, headerName: string) => - isHeader ? headerName : queryName; - - const queryParams = new URLSearchParams(); - const headerParams: Record = {}; - - const addParam = (name: string, value: string) => { - if (isHeader) { - headerParams[name] = value; - } else { - queryParams.append(name, value); - } - }; - - switch (pagination.type) { - case 'offset': - addParam( - pagination.offsetParam ?? getDefaultName('offset', 'X-Offset'), - String(pagination.offset) - ); - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - break; - - case 'cursor': - if (pagination.cursor) { - addParam( - pagination.cursorParam ?? getDefaultName('cursor', 'X-Cursor'), - pagination.cursor - ); - } - if (pagination.limit !== undefined) { - addParam( - pagination.limitParam ?? getDefaultName('limit', 'X-Limit'), - String(pagination.limit) - ); - } - break; - - case 'page': - addParam( - pagination.pageParam ?? getDefaultName('page', 'X-Page'), - String(pagination.page) - ); - addParam( - pagination.pageSizeParam ?? getDefaultName('pageSize', 'X-Page-Size'), - String(pagination.pageSize) - ); - break; - - case 'range': { - // Range pagination is always header-based (RFC 7233 style) - const unit = pagination.unit ?? 'items'; - const headerName = pagination.rangeHeader ?? 'Range'; - headerParams[headerName] = `${unit}=${pagination.start}-${pagination.end}`; - break; - } - } - - // Apply query params to URL - const queryString = queryParams.toString(); - if (queryString) { - const separator = url.includes('?') ? '&' : '?'; - url = `${url}${separator}${queryString}`; - } - - // Merge header params - const updatedHeaders = { ...headers, ...headerParams }; - - return { url, headers: updatedHeaders }; -} - /** * Apply query parameters to URL */ @@ -641,199 +456,6 @@ function extractHeaders(response: HttpResponse): Record { return headers; } -/** - * Extract pagination info from response headers - */ -function extractPaginationInfo( - headers: Record, - currentPagination?: PaginationConfig -): PaginationInfo | undefined { - const info: PaginationInfo = {}; - let hasPaginationInfo = false; - - // Common total count headers - const totalCount = headers['x-total-count'] || headers['x-total'] || headers['total-count']; - if (totalCount) { - info.totalCount = parseInt(totalCount, 10); - hasPaginationInfo = true; - } - - // Total pages - const totalPages = headers['x-total-pages'] || headers['x-page-count']; - if (totalPages) { - info.totalPages = parseInt(totalPages, 10); - hasPaginationInfo = true; - } - - // Next cursor - const nextCursor = headers['x-next-cursor'] || headers['x-cursor-next']; - if (nextCursor) { - info.nextCursor = nextCursor; - info.hasMore = true; - hasPaginationInfo = true; - } - - // Previous cursor - const prevCursor = headers['x-prev-cursor'] || headers['x-cursor-prev']; - if (prevCursor) { - info.prevCursor = prevCursor; - hasPaginationInfo = true; - } - - // Has more indicator - const hasMore = headers['x-has-more'] || headers['x-has-next']; - if (hasMore) { - info.hasMore = hasMore.toLowerCase() === 'true' || hasMore === '1'; - hasPaginationInfo = true; - } - - // Parse Link header (RFC 5988) - const linkHeader = headers['link']; - if (linkHeader) { - const links = parseLinkHeader(linkHeader); - if (links.next) { - info.hasMore = true; - hasPaginationInfo = true; - } - } - - // Include current pagination state - if (currentPagination) { - switch (currentPagination.type) { - case 'offset': - info.currentOffset = currentPagination.offset; - info.limit = currentPagination.limit; - break; - case 'cursor': - info.limit = currentPagination.limit; - break; - case 'page': - info.currentOffset = (currentPagination.page - 1) * currentPagination.pageSize; - info.limit = currentPagination.pageSize; - break; - case 'range': - info.currentOffset = currentPagination.start; - info.limit = currentPagination.end - currentPagination.start + 1; - break; - } - hasPaginationInfo = true; - } - - // Calculate hasMore based on total count - if (info.hasMore === undefined && info.totalCount !== undefined && - info.currentOffset !== undefined && info.limit !== undefined) { - info.hasMore = info.currentOffset + info.limit < info.totalCount; - } - - return hasPaginationInfo ? info : undefined; -} - -/** - * Parse RFC 5988 Link header - */ -function parseLinkHeader(header: string): Record { - const links: Record = {}; - const parts = header.split(','); - - for (const part of parts) { - const match = part.match(/<([^>]+)>;\s*rel="?([^";\s]+)"?/); - if (match) { - links[match[2]] = match[1]; - } - } - - return links; -} - -/** - * Create pagination helper functions for the response - */ -function createPaginationHelpers( - currentConfig: TContext, - paginationInfo: PaginationInfo | undefined, - requestFn: (config: TContext) => Promise> -): Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> { - const helpers: Pick, 'getNextPage' | 'getPrevPage' | 'hasNextPage' | 'hasPrevPage'> = {}; - - if (!currentConfig.pagination) { - return helpers; - } - - const pagination = currentConfig.pagination; - - helpers.hasNextPage = () => { - if (paginationInfo?.hasMore !== undefined) return paginationInfo.hasMore; - if (paginationInfo?.nextCursor) return true; - if (paginationInfo?.totalCount !== undefined && - paginationInfo.currentOffset !== undefined && - paginationInfo.limit !== undefined) { - return paginationInfo.currentOffset + paginationInfo.limit < paginationInfo.totalCount; - } - return false; - }; - - helpers.hasPrevPage = () => { - if (paginationInfo?.prevCursor) return true; - if (paginationInfo?.currentOffset !== undefined) { - return paginationInfo.currentOffset > 0; - } - return false; - }; - - helpers.getNextPage = async () => { - let nextPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - nextPagination = { ...pagination, offset: pagination.offset + pagination.limit }; - break; - case 'cursor': - if (!paginationInfo?.nextCursor) throw new Error('No next cursor available'); - nextPagination = { ...pagination, cursor: paginationInfo.nextCursor }; - break; - case 'page': - nextPagination = { ...pagination, page: pagination.page + 1 }; - break; - case 'range': - const rangeSize = pagination.end - pagination.start + 1; - nextPagination = { ...pagination, start: pagination.end + 1, end: pagination.end + rangeSize }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: nextPagination }); - }; - - helpers.getPrevPage = async () => { - let prevPagination: PaginationConfig; - - switch (pagination.type) { - case 'offset': - prevPagination = { ...pagination, offset: Math.max(0, pagination.offset - pagination.limit) }; - break; - case 'cursor': - if (!paginationInfo?.prevCursor) throw new Error('No previous cursor available'); - prevPagination = { ...pagination, cursor: paginationInfo.prevCursor }; - break; - case 'page': - prevPagination = { ...pagination, page: Math.max(1, pagination.page - 1) }; - break; - case 'range': - const size = pagination.end - pagination.start + 1; - const newStart = Math.max(0, pagination.start - size); - prevPagination = { ...pagination, start: newStart, end: newStart + size - 1 }; - break; - default: - throw new Error('Unsupported pagination type'); - } - - return requestFn({ ...currentConfig, pagination: prevPagination }); - }; - - return helpers; -} - /** * Builds a URL with path parameters replaced * @param server - Base server URL @@ -1025,7 +647,6 @@ async function handleTokenRefresh( export interface PostPingPostRequestContext extends HttpClientContext { payload: Ping; - requestHeaders?: { marshal: () => string }; } /** @@ -1034,8 +655,7 @@ export interface PostPingPostRequestContext extends HttpClientContext { async function postPingPostRequest(context: PostPingPostRequestContext): Promise> { // Apply defaults const config = { - path: '/ping', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1045,18 +665,11 @@ async function postPingPostRequest(context: PostPingPostRequestContext): Promise } // Build headers - let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) - : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = `${config.server}${config.path}`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = `${config.baseUrl}/ping`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1122,17 +735,13 @@ async function postPingPostRequest(context: PostPingPostRequestContext): Promise // Extract response metadata const responseHeaders = extractHeaders(response); - const paginationInfo = extractPaginationInfo(responseHeaders, config.pagination); - // Build response wrapper with pagination helpers const result: HttpClientResponse = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, postPingPostRequest), }; return result; @@ -1146,9 +755,7 @@ async function postPingPostRequest(context: PostPingPostRequestContext): Promise } } -export interface GetPingGetRequestContext extends HttpClientContext { - requestHeaders?: { marshal: () => string }; -} +export interface GetPingGetRequestContext extends HttpClientContext {} /** * HTTP GET request to /ping @@ -1156,8 +763,7 @@ export interface GetPingGetRequestContext extends HttpClientContext { async function getPingGetRequest(context: GetPingGetRequestContext = {}): Promise> { // Apply defaults const config = { - path: '/ping', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1167,18 +773,11 @@ async function getPingGetRequest(context: GetPingGetRequestContext = {}): Promis } // Build headers - let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) - : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = `${config.server}${config.path}`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = `${config.baseUrl}/ping`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1244,17 +843,13 @@ async function getPingGetRequest(context: GetPingGetRequestContext = {}): Promis // 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, getPingGetRequest), }; return result; @@ -1270,7 +865,6 @@ async function getPingGetRequest(context: GetPingGetRequestContext = {}): Promis export interface PutPingPutRequestContext extends HttpClientContext { payload: Ping; - requestHeaders?: { marshal: () => string }; } /** @@ -1279,8 +873,7 @@ export interface PutPingPutRequestContext extends HttpClientContext { async function putPingPutRequest(context: PutPingPutRequestContext): Promise> { // Apply defaults const config = { - path: '/ping', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1290,18 +883,11 @@ async function putPingPutRequest(context: PutPingPutRequestContext): Promise; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = `${config.server}${config.path}`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = `${config.baseUrl}/ping`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1367,17 +953,13 @@ async function putPingPutRequest(context: PutPingPutRequestContext): Promise = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, putPingPutRequest), }; return result; @@ -1391,9 +973,7 @@ async function putPingPutRequest(context: PutPingPutRequestContext): Promise string }; -} +export interface DeletePingDeleteRequestContext extends HttpClientContext {} /** * HTTP DELETE request to /ping @@ -1401,8 +981,7 @@ export interface DeletePingDeleteRequestContext extends HttpClientContext { async function deletePingDeleteRequest(context: DeletePingDeleteRequestContext = {}): Promise> { // Apply defaults const config = { - path: '/ping', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1412,18 +991,11 @@ async function deletePingDeleteRequest(context: DeletePingDeleteRequestContext = } // Build headers - let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) - : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = `${config.server}${config.path}`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = `${config.baseUrl}/ping`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1489,17 +1061,13 @@ async function deletePingDeleteRequest(context: DeletePingDeleteRequestContext = // 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, deletePingDeleteRequest), }; return result; @@ -1515,7 +1083,6 @@ async function deletePingDeleteRequest(context: DeletePingDeleteRequestContext = export interface PatchPingPatchRequestContext extends HttpClientContext { payload: Ping; - requestHeaders?: { marshal: () => string }; } /** @@ -1524,8 +1091,7 @@ export interface PatchPingPatchRequestContext extends HttpClientContext { async function patchPingPatchRequest(context: PatchPingPatchRequestContext): Promise> { // Apply defaults const config = { - path: '/ping', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1535,18 +1101,11 @@ async function patchPingPatchRequest(context: PatchPingPatchRequestContext): Pro } // Build headers - let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) - : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = `${config.server}${config.path}`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = `${config.baseUrl}/ping`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1612,17 +1171,13 @@ async function patchPingPatchRequest(context: PatchPingPatchRequestContext): Pro // 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, patchPingPatchRequest), }; return result; @@ -1636,9 +1191,7 @@ async function patchPingPatchRequest(context: PatchPingPatchRequestContext): Pro } } -export interface HeadPingHeadRequestContext extends HttpClientContext { - requestHeaders?: { marshal: () => string }; -} +export interface HeadPingHeadRequestContext extends HttpClientContext {} /** * HTTP HEAD request to /ping @@ -1646,8 +1199,7 @@ export interface HeadPingHeadRequestContext extends HttpClientContext { async function headPingHeadRequest(context: HeadPingHeadRequestContext = {}): Promise> { // Apply defaults const config = { - path: '/ping', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1657,18 +1209,11 @@ async function headPingHeadRequest(context: HeadPingHeadRequestContext = {}): Pr } // Build headers - let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) - : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = `${config.server}${config.path}`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = `${config.baseUrl}/ping`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1734,17 +1279,13 @@ async function headPingHeadRequest(context: HeadPingHeadRequestContext = {}): Pr // 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, headPingHeadRequest), }; return result; @@ -1758,9 +1299,7 @@ async function headPingHeadRequest(context: HeadPingHeadRequestContext = {}): Pr } } -export interface OptionsPingOptionsRequestContext extends HttpClientContext { - requestHeaders?: { marshal: () => string }; -} +export interface OptionsPingOptionsRequestContext extends HttpClientContext {} /** * HTTP OPTIONS request to /ping @@ -1768,8 +1307,7 @@ export interface OptionsPingOptionsRequestContext extends HttpClientContext { async function optionsPingOptionsRequest(context: OptionsPingOptionsRequestContext = {}): Promise> { // Apply defaults const config = { - path: '/ping', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1779,18 +1317,11 @@ async function optionsPingOptionsRequest(context: OptionsPingOptionsRequestConte } // Build headers - let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) - : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = `${config.server}${config.path}`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = `${config.baseUrl}/ping`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1856,17 +1387,13 @@ async function optionsPingOptionsRequest(context: OptionsPingOptionsRequestConte // 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, optionsPingOptionsRequest), }; return result; @@ -1880,9 +1407,7 @@ async function optionsPingOptionsRequest(context: OptionsPingOptionsRequestConte } } -export interface GetMultiStatusResponseContext extends HttpClientContext { - requestHeaders?: { marshal: () => string }; -} +export interface GetMultiStatusResponseContext extends HttpClientContext {} /** * HTTP GET request to /ping @@ -1890,8 +1415,7 @@ export interface GetMultiStatusResponseContext extends HttpClientContext { async function getMultiStatusResponse(context: GetMultiStatusResponseContext = {}): Promise> { // Apply defaults const config = { - path: '/ping', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; @@ -1901,18 +1425,11 @@ async function getMultiStatusResponse(context: GetMultiStatusResponseContext = { } // Build headers - let headers = context.requestHeaders - ? applyTypedHeaders(context.requestHeaders, config.additionalHeaders) - : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; + let headers = { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record; // Build URL - let url = `${config.server}${config.path}`; - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = `${config.baseUrl}/ping`; + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -1978,17 +1495,13 @@ async function getMultiStatusResponse(context: GetMultiStatusResponseContext = { // 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, getMultiStatusResponse), }; return result; @@ -2003,7 +1516,7 @@ async function getMultiStatusResponse(context: GetMultiStatusResponseContext = { } export interface GetGetUserItemContext extends HttpClientContext { - parameters: UserItemsParameters; + parameters: UserItemsParametersInterface | UserItemsParameters; requestHeaders?: ItemRequestHeaders; } @@ -2013,11 +1526,12 @@ export interface GetGetUserItemContext extends HttpClientContext { async function getGetUserItem(context: GetGetUserItemContext): Promise> { // Apply defaults const config = { - path: '/users/{userId}/items/{itemId}', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; + const parameters = context.parameters instanceof UserItemsParameters ? context.parameters : new UserItemsParameters(context.parameters); + // Validate OAuth2 config if present if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { validateOAuth2Config(config.auth); @@ -2029,13 +1543,8 @@ async function getGetUserItem(context: GetGetUserItemContext): Promise; // Build URL - let url = buildUrlWithParameters(config.server, '/users/{userId}/items/{itemId}', context.parameters); - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = buildUrlWithParameters(config.baseUrl, '/users/{userId}/items/{itemId}', parameters); + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -2101,17 +1610,13 @@ async function getGetUserItem(context: GetGetUserItemContext): Promise = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, getGetUserItem), }; return result; @@ -2127,7 +1632,7 @@ async function getGetUserItem(context: GetGetUserItemContext): Promise> { // Apply defaults const config = { - path: '/users/{userId}/items/{itemId}', - server: 'localhost:3000', + baseUrl: 'http://localhost:3000', ...context, }; + const parameters = context.parameters instanceof UserItemsParameters ? context.parameters : new UserItemsParameters(context.parameters); + // Validate OAuth2 config if present if (config.auth?.type === 'oauth2' && AUTH_FEATURES.oauth2) { validateOAuth2Config(config.auth); @@ -2153,13 +1659,8 @@ async function putUpdateUserItem(context: PutUpdateUserItemContext): Promise; // Build URL - let url = buildUrlWithParameters(config.server, '/users/{userId}/items/{itemId}', context.parameters); - url = applyQueryParams(config.queryParams, url); - - // Apply pagination (can affect URL and/or headers) - const paginationResult = applyPagination(config.pagination, url, headers); - url = paginationResult.url; - headers = paginationResult.headers; + let url = buildUrlWithParameters(config.baseUrl, '/users/{userId}/items/{itemId}', parameters); + url = applyQueryParams(config.additionalQueryParams, url); // Apply authentication const authResult = applyAuth(config.auth, headers, url); @@ -2225,17 +1726,13 @@ async function putUpdateUserItem(context: PutUpdateUserItemContext): Promise = { data: responseData, status: response.status, statusText: response.statusText, headers: responseHeaders, rawData, - pagination: paginationInfo, - ...createPaginationHelpers(config, paginationInfo, putUpdateUserItem), }; return result; diff --git a/test/runtime/typescript/src/request-reply/channels/nats.ts b/test/runtime/typescript/src/request-reply/channels/nats.ts index f3b0d3bb..7e73acfa 100644 --- a/test/runtime/typescript/src/request-reply/channels/nats.ts +++ b/test/runtime/typescript/src/request-reply/channels/nats.ts @@ -8,7 +8,7 @@ import * as PingPayloadModule from './../payloads/PingPayload'; import * as UserItemsPayloadModule from './../payloads/UserItemsPayload'; import {NotFound} from './../payloads/NotFound'; import {ItemResponse} from './../payloads/ItemResponse'; -import {UserItemsParameters} from './../parameters/UserItemsParameters'; +import {UserItemsParameters, UserItemsParametersInterface} from './../parameters/UserItemsParameters'; import {ItemRequestHeaders} from './../headers/ItemRequestHeaders'; import * as Nats from 'nats'; diff --git a/test/runtime/typescript/src/request-reply/client/NatsClient.ts b/test/runtime/typescript/src/request-reply/client/NatsClient.ts index 403051bd..97f9ea3c 100644 --- a/test/runtime/typescript/src/request-reply/client/NatsClient.ts +++ b/test/runtime/typescript/src/request-reply/client/NatsClient.ts @@ -18,7 +18,7 @@ export {PingPayloadModule}; export {UserItemsPayloadModule}; export {NotFound}; export {ItemResponse}; -import {UserItemsParameters} from './../parameters/UserItemsParameters'; +import {UserItemsParameters, UserItemsParametersInterface} from './../parameters/UserItemsParameters'; export {UserItemsParameters}; //Import channel functions diff --git a/test/runtime/typescript/src/request-reply/parameters/UserItemsParameters.ts b/test/runtime/typescript/src/request-reply/parameters/UserItemsParameters.ts index 3c8656f8..bfd07942 100644 --- a/test/runtime/typescript/src/request-reply/parameters/UserItemsParameters.ts +++ b/test/runtime/typescript/src/request-reply/parameters/UserItemsParameters.ts @@ -1,12 +1,13 @@ +interface UserItemsParametersInterface { + userId: string + itemId: string +} class UserItemsParameters { private _userId: string; private _itemId: string; - constructor(input: { - userId: string, - itemId: string, - }) { + constructor(input: UserItemsParametersInterface) { this._userId = input.userId; this._itemId = input.itemId; } @@ -57,4 +58,4 @@ class UserItemsParameters { return parameters; } } -export { UserItemsParameters }; \ No newline at end of file +export { UserItemsParameters, UserItemsParametersInterface }; \ No newline at end of file diff --git a/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts b/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts index 2f0091b3..6f875992 100644 --- a/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts +++ b/test/runtime/typescript/test/channels/request_reply/http_client/parameters-headers.spec.ts @@ -79,6 +79,42 @@ describe('HTTP Client - Parameters and Headers', () => { }); }); + it('should accept a plain object satisfying the parameter interface, with parity to a class instance', async () => { + const { app, router, port } = createTestServer(); + + const receivedPaths: string[] = []; + + router.get('/users/:userId/items/:itemId', (req, res) => { + receivedPaths.push(req.path); + res.json({ + id: req.params.itemId, + userId: req.params.userId, + name: 'Item', + quantity: 1 + }); + }); + + return runWithServer(app, port, async (_server, actualPort) => { + // Class instance + await getGetUserItem({ + baseUrl: `http://localhost:${actualPort}`, + parameters: new UserItemsParameters({ userId: 'parity-user', itemId: '77' }) + }); + + // Plain object satisfying the companion interface — no class construction + await getGetUserItem({ + baseUrl: `http://localhost:${actualPort}`, + parameters: { userId: 'parity-user', itemId: '77' } + }); + + // Both produce the identical resolved path + expect(receivedPaths).toEqual([ + '/users/parity-user/items/77', + '/users/parity-user/items/77' + ]); + }); + }); + it('should combine parameters with authentication', async () => { const { app, router, port } = createTestServer();