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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/generators/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ All available generators, across languages and inputs:
| **Inputs** | [`payloads`](./payloads.md) | [`parameters`](./parameters.md) | [`headers`](./headers.md) | [`types`](./types.md) | [`channels`](./channels.md) | [`client`](./client.md) | [`custom`](./custom.md) |
|---|---|---|---|---|---|---|---|
| AsyncAPI | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
| OpenAPI | ✔️ | | ✔️ | ➗ | ➗ | ➗ | ✔️ |
| OpenAPI | ✔️ | ✔️ | ✔️ | ➗ | ➗ | ➗ | ✔️ |

| **Languages** | [`payloads`](./payloads.md) | [`parameters`](./parameters.md) | [`headers`](./headers.md) | [`types`](./types.md) | [`channels`](./channels.md) | [`client`](./client.md) | [`custom`](./custom.md) |
|---|---|---|---|---|---|---|---|
Expand Down
3 changes: 2 additions & 1 deletion docs/generators/headers.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export default {
{
preset: 'headers',
outputPath: './src/headers',
serializationType: 'json',
serializationType: 'json',
includeValidation: true,
language: 'typescript',
}
]
Expand Down
153 changes: 149 additions & 4 deletions docs/generators/parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,157 @@ sidebar_position: 99

# Parameters

Input support; `asyncapi`
```js
export default {
...,
generators: [
{
preset: 'parameters',
outputPath: './src/parameters',
serializationType: 'json',
language: 'typescript',
}
]
};
```

Language support; `typescript`
`parameters` preset is for generating models that represent typed models for parameters used in API operations.

## AsyncAPI
This is supported through the following inputs: [`asyncapi`](#inputs), [`openapi`](#inputs)

It supports the following languages; `typescript`

## Inputs

### `asyncapi`
The `parameters` preset with `asyncapi` input generates all the parameters for each channel in the AsyncAPI document.

The return type is a map of channels and the model that represent the parameters
The return type is a map of channels and the model that represent the parameters.

### `openapi`
The `parameters` preset with `openapi` input generates all the parameters for each operation in the OpenAPI document, including both path and query parameters.

The return type is a map of operations and the model that represent the parameters.

## Typescript

### AsyncAPI Functions

Each generated AsyncAPI parameter class includes the following methods:

#### Channel Parameter Substitution
- `getChannelWithParameters(channel: string): string`: Replaces parameter placeholders in the channel/topic string with actual parameter values.

```typescript
// Example
const params = new UserSignedupParameters({
myParameter: 'test',
enumParameter: 'openapi'
});
const channel = params.getChannelWithParameters('user/{my_parameter}/signup/{enum_parameter}');
// Result: 'user/test/signup/openapi'
```

#### Static Factory Method
- `static createFromChannel(msgSubject: string, channel: string, regex: RegExp): ParameterClass`: Creates a parameter instance by extracting values from a message subject using the provided channel template and regex.

```typescript
// Example
const params = UserSignedupParameters.createFromChannel(
'user.test.signup.openapi',
'user/{my_parameter}/signup/{enum_parameter}',
/user\.(.+)\.signup\.(.+)/
);
```

### OpenAPI Functions

Each generated OpenAPI parameter class includes comprehensive serialization and deserialization capabilities:

#### Path Parameter Serialization
- `serializePathParameters(): Record<string, string>`: Serializes path parameters according to OpenAPI 2.0/3.x specification for URL path substitution.

```typescript
// Example
const params = new FindPetsByStatusParameters({
status: 'available',
categoryId: 123
});
const pathParams = params.serializePathParameters();
// Result: { status: 'available', categoryId: '123' }
```

#### Query Parameter Serialization
- `serializeQueryParameters(): URLSearchParams`: Serializes query parameters according to OpenAPI specification with proper encoding and style handling.

```typescript
// Example
const queryParams = params.serializeQueryParameters();
const queryString = queryParams.toString();
// Result: 'limit=10&offset=0&tags=dog,cat'
```

#### Complete URL Serialization
- `serializeUrl(basePath: string): string`: Generates the complete URL with both path and query parameters properly serialized.

```typescript
// Example
const url = params.serializeUrl('/pet/findByStatus/{status}/{categoryId}');
// Result: '/pet/findByStatus/available/123?limit=10&offset=0&tags=dog,cat'
```

#### URL Deserialization
- `deserializeUrl(url: string): void`: Parses a URL and populates the instance properties from query parameters.

```typescript
// Example
const params = new FindPetsByStatusParameters({ status: 'available', categoryId: 123 });
params.deserializeUrl('/pet/findByStatus/available/123?limit=5&tags=dog,cat');
// params.limit is now 5, params.tags is now ['dog', 'cat']
```

#### Static Factory Methods
- `static fromUrl(url: string, basePath: string, ...requiredDefaults): ParameterClass`: Creates a new parameter instance from a complete URL by extracting both path and query parameters.

```typescript
// Example
const params = FindPetsByStatusParameters.fromUrl(
'/pet/findByStatus/available/123?limit=5&tags=dog',
'/pet/findByStatus/{status}/{categoryId}'
);
// params.status is 'available', params.categoryId is 123, params.limit is 5
```

### Parameter Style Support

The OpenAPI generator supports all OpenAPI parameter styles and serialization formats:

#### Path Parameters
- **simple** (default): `value1,value2` or `key1,value1,key2,value2`
- **label**: `.value1.value2` or `.key1.value1.key2.value2`
- **matrix**: `;param=value1,value2` or `;key1=value1;key2=value2`

#### Query Parameters
- **form** (default): `param=value1&param=value2` (exploded) or `param=value1,value2`
- **spaceDelimited**: `param=value1 value2`
- **pipeDelimited**: `param=value1|value2`
- **deepObject**: `param[key1]=value1&param[key2]=value2`

### Type Safety

All parameter classes are fully typed with:
- Enum parameter types for restricted values
- Required vs optional parameter distinction
- Proper TypeScript casting for different parameter types (string, number, boolean, arrays)
- Support for complex parameter schemas including nested objects and arrays

### OpenAPI 2.0 Compatibility

The generator supports OpenAPI 2.0 `collectionFormat` parameter serialization:
- `csv`: Comma-separated values
- `ssv`: Space-separated values
- `tsv`: Tab-separated values (treated as CSV)
- `pipes`: Pipe-separated values
- `multi`: Multiple parameter instances

These are automatically converted to equivalent OpenAPI 3.0 style/explode combinations for consistent handling.
183 changes: 46 additions & 137 deletions src/codegen/generators/typescript/parameters.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
/* eslint-disable security/detect-object-injection */
import {
ConstrainedEnumModel,
ConstrainedObjectModel,
ConstrainedReferenceModel,
OutputModel,
TS_DESCRIPTION_PRESET,
TypeScriptFileGenerator
} from '@asyncapi/modelina';
import {AsyncAPIDocumentInterface} from '@asyncapi/parser';
import {GenericCodegenContext, ParameterRenderType} from '../../types';
import {z} from 'zod';
import {findNameFromChannel} from '../../utils';
import {defaultCodegenTypescriptModelinaOptions, pascalCase} from './utils';
import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types';
import {
createAsyncAPIGenerator,
processAsyncAPIParameters,
ProcessedParameterSchemaData
} from '../../inputs/asyncapi/generators/parameters';
import {createOpenAPIGenerator, processOpenAPIParameters} from '../../inputs/openapi/generators/parameters';

export const zodTypescriptParametersGenerator = z.object({
id: z.string().optional().default('parameters-typescript'),
Expand Down Expand Up @@ -42,154 +43,62 @@ export interface TypescriptParametersContext extends GenericCodegenContext {
generator: TypescriptParametersGeneratorInternal;
}

/**
* Component which contains the parameter unwrapping functionality.
*
*
* Example
const regex = /^adeo-([^.]*)-case-study-COSTING-REQUEST-([^.]*)$/;
const match = channel.match(regex);

const parameters = new CostingRequestChannelParameters({env: "dev", version: ''});
if (match) {
const envMatch = match.at(1)
if(envMatch && envMatch !== '') {
parameters.env = envMatch as any
} else {
throw new Error(`Parameter: 'env' is not valid. Abort! `)
}
const versionMatch = match.at(2)
if(versionMatch && versionMatch !== '') {
parameters.version = versionMatch as any
} else {
throw new Error(`Parameter: 'version' is not valid. Abort! `)
}
} else {
throw new Error(`Unable to find parameters in channe/topic, topic was ${channel}`)
}
return parameters;
*
*/
export function unwrap(channelParameters: ConstrainedObjectModel) {
// Nothing to unwrap if no parameters are used
if (Object.keys(channelParameters.properties).length === 0) {
return '';
}

// Use channel to iterate over matches as channelParameters.properties might be in incorrect order.

const parameterReplacement = Object.values(channelParameters.properties).map(
(parameter) => {
const variableName = `${parameter.propertyName}Match`;
return `const ${variableName} = match[sequentialParameters.indexOf('{${parameter.unconstrainedPropertyName}}')+1];
if(${variableName} && ${variableName} !== '') {
parameters.${parameter.propertyName} = ${variableName} as any
} else {
throw new Error(\`Parameter: '${parameter.propertyName}' is not valid. Abort! \`)
}`;
}
);

const parameterInitializer = Object.values(channelParameters.properties).map(
(parameter) => {
if (parameter.property.options.isNullable) {
return `${parameter.propertyName}: null`;
}
const property = parameter.property;
if (
property instanceof ConstrainedReferenceModel &&
property.ref instanceof ConstrainedEnumModel
) {
return `${parameter.propertyName}: ${property.ref.values[0].value}`;
}
return `${parameter.propertyName}: ''`;
}
);

return `const parameters = new ${channelParameters.name}({${parameterInitializer.join(', ')}});
const match = msgSubject.match(regex);
const sequentialParameters: string[] = channel.match(/\\{(\\w+)\\}/g) || [];

if (match) {
${parameterReplacement.join('\n')}
} else {
throw new Error(\`Unable to find parameters in channel/topic, topic was \${channel}\`)
}
return parameters;`;
}

export type TypeScriptParameterRenderType =
ParameterRenderType<TypescriptParametersGeneratorInternal>;

// Main generator function that orchestrates input processing and generation
export async function generateTypescriptParameters(
context: TypescriptParametersContext
): Promise<TypeScriptParameterRenderType> {
const {asyncapiDocument, inputType, generator} = context;
if (inputType === 'asyncapi' && asyncapiDocument === undefined) {
throw new Error('Expected AsyncAPI input, was not given');
}
const modelinaGenerator = new TypeScriptFileGenerator({
...defaultCodegenTypescriptModelinaOptions,
enumType: 'union',
useJavascriptReservedKeywords: false,
presets: [
TS_DESCRIPTION_PRESET,
{
class: {
additionalContent: ({content, model, renderer}) => {
const parameters = Object.entries(model.properties).map(
([, parameter]) => {
return `channel = channel.replace(/\\{${parameter.unconstrainedPropertyName}\\}/g, this.${parameter.propertyName})`;
}
);
return `${content}
/**
* Realize the channel/topic with the parameters added to this class.
*/
public getChannelWithParameters(channel: string) {
${renderer.renderBlock(parameters)};
return channel;
}

public static createFromChannel(msgSubject: string, channel: string, regex: RegExp): ${model.type} {
${unwrap(model)}
}`;
}
}
const {asyncapiDocument, openapiDocument, inputType, generator} = context;

const channelModels: Record<string, OutputModel | undefined> = {};
let processedSchemaData: ProcessedParameterSchemaData;
let parameterGenerator: TypeScriptFileGenerator;

// Process input based on type
switch (inputType) {
case 'asyncapi': {
if (!asyncapiDocument) {
throw new Error('Expected AsyncAPI input, was not given');
}
]
});
const returnType: Record<string, OutputModel | undefined> = {};
for (const channel of asyncapiDocument!.allChannels().all()) {
const parameters = channel.parameters().all();
if (parameters.length > 0) {
const schemaObj: any = {
type: 'object',
$id: pascalCase(`${findNameFromChannel(channel)}_parameters`),
$schema: 'http://json-schema.org/draft-07/schema',
required: [],
properties: {},
additionalProperties: false,
'x-channel-address': channel.address()
};
for (const parameter of channel.parameters().all()) {
schemaObj.properties[parameter.id()] = parameter.schema()?.json();
schemaObj.required.push(parameter.id());

processedSchemaData = await processAsyncAPIParameters(asyncapiDocument);
parameterGenerator = createAsyncAPIGenerator();
break;
}
case 'openapi': {
if (!openapiDocument) {
throw new Error('Expected OpenAPI input, was not given');
}
const models = await modelinaGenerator.generateToFiles(
schemaObj,

processedSchemaData = processOpenAPIParameters(openapiDocument);
parameterGenerator = createOpenAPIGenerator();
break;
}
default:
throw new Error(`Unsupported input type: ${inputType}`);
}

// Generate models for channel parameters
for (const [channelId, schemaData] of Object.entries(
processedSchemaData.channelParameters
)) {
if (schemaData) {
const models = await parameterGenerator.generateToFiles(
schemaData.schema,
generator.outputPath,
{exportType: 'named'},
true
);
returnType[channel.id()] = models[0];
channelModels[channelId] = models.length > 0 ? models[0] : undefined;
} else {
returnType[channel.id()] = undefined;
channelModels[channelId] = undefined;
}
}

return {
channelModels: returnType,
channelModels,
generator
};
}
Loading