Skip to content

Commit 176be78

Browse files
authored
feat: enable openapi parameters (#228)
1 parent 4bd94dc commit 176be78

File tree

16 files changed

+6590
-202
lines changed

16 files changed

+6590
-202
lines changed

docs/generators/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ All available generators, across languages and inputs:
2020
| **Inputs** | [`payloads`](./payloads.md) | [`parameters`](./parameters.md) | [`headers`](./headers.md) | [`types`](./types.md) | [`channels`](./channels.md) | [`client`](./client.md) | [`custom`](./custom.md) |
2121
|---|---|---|---|---|---|---|---|
2222
| AsyncAPI | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |
23-
| OpenAPI | ✔️ | | ✔️ |||| ✔️ |
23+
| OpenAPI | ✔️ | ✔️ | ✔️ |||| ✔️ |
2424

2525
| **Languages** | [`payloads`](./payloads.md) | [`parameters`](./parameters.md) | [`headers`](./headers.md) | [`types`](./types.md) | [`channels`](./channels.md) | [`client`](./client.md) | [`custom`](./custom.md) |
2626
|---|---|---|---|---|---|---|---|

docs/generators/headers.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export default {
1111
{
1212
preset: 'headers',
1313
outputPath: './src/headers',
14-
serializationType: 'json',
14+
serializationType: 'json',
15+
includeValidation: true,
1516
language: 'typescript',
1617
}
1718
]

docs/generators/parameters.md

Lines changed: 149 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,157 @@ sidebar_position: 99
44

55
# Parameters
66

7-
Input support; `asyncapi`
7+
```js
8+
export default {
9+
...,
10+
generators: [
11+
{
12+
preset: 'parameters',
13+
outputPath: './src/parameters',
14+
serializationType: 'json',
15+
language: 'typescript',
16+
}
17+
]
18+
};
19+
```
820

9-
Language support; `typescript`
21+
`parameters` preset is for generating models that represent typed models for parameters used in API operations.
1022

11-
## AsyncAPI
23+
This is supported through the following inputs: [`asyncapi`](#inputs), [`openapi`](#inputs)
1224

25+
It supports the following languages; `typescript`
26+
27+
## Inputs
28+
29+
### `asyncapi`
1330
The `parameters` preset with `asyncapi` input generates all the parameters for each channel in the AsyncAPI document.
1431

15-
The return type is a map of channels and the model that represent the parameters
32+
The return type is a map of channels and the model that represent the parameters.
33+
34+
### `openapi`
35+
The `parameters` preset with `openapi` input generates all the parameters for each operation in the OpenAPI document, including both path and query parameters.
36+
37+
The return type is a map of operations and the model that represent the parameters.
38+
39+
## Typescript
40+
41+
### AsyncAPI Functions
42+
43+
Each generated AsyncAPI parameter class includes the following methods:
44+
45+
#### Channel Parameter Substitution
46+
- `getChannelWithParameters(channel: string): string`: Replaces parameter placeholders in the channel/topic string with actual parameter values.
47+
48+
```typescript
49+
// Example
50+
const params = new UserSignedupParameters({
51+
myParameter: 'test',
52+
enumParameter: 'openapi'
53+
});
54+
const channel = params.getChannelWithParameters('user/{my_parameter}/signup/{enum_parameter}');
55+
// Result: 'user/test/signup/openapi'
56+
```
57+
58+
#### Static Factory Method
59+
- `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.
60+
61+
```typescript
62+
// Example
63+
const params = UserSignedupParameters.createFromChannel(
64+
'user.test.signup.openapi',
65+
'user/{my_parameter}/signup/{enum_parameter}',
66+
/user\.(.+)\.signup\.(.+)/
67+
);
68+
```
69+
70+
### OpenAPI Functions
71+
72+
Each generated OpenAPI parameter class includes comprehensive serialization and deserialization capabilities:
73+
74+
#### Path Parameter Serialization
75+
- `serializePathParameters(): Record<string, string>`: Serializes path parameters according to OpenAPI 2.0/3.x specification for URL path substitution.
76+
77+
```typescript
78+
// Example
79+
const params = new FindPetsByStatusParameters({
80+
status: 'available',
81+
categoryId: 123
82+
});
83+
const pathParams = params.serializePathParameters();
84+
// Result: { status: 'available', categoryId: '123' }
85+
```
86+
87+
#### Query Parameter Serialization
88+
- `serializeQueryParameters(): URLSearchParams`: Serializes query parameters according to OpenAPI specification with proper encoding and style handling.
89+
90+
```typescript
91+
// Example
92+
const queryParams = params.serializeQueryParameters();
93+
const queryString = queryParams.toString();
94+
// Result: 'limit=10&offset=0&tags=dog,cat'
95+
```
96+
97+
#### Complete URL Serialization
98+
- `serializeUrl(basePath: string): string`: Generates the complete URL with both path and query parameters properly serialized.
99+
100+
```typescript
101+
// Example
102+
const url = params.serializeUrl('/pet/findByStatus/{status}/{categoryId}');
103+
// Result: '/pet/findByStatus/available/123?limit=10&offset=0&tags=dog,cat'
104+
```
105+
106+
#### URL Deserialization
107+
- `deserializeUrl(url: string): void`: Parses a URL and populates the instance properties from query parameters.
108+
109+
```typescript
110+
// Example
111+
const params = new FindPetsByStatusParameters({ status: 'available', categoryId: 123 });
112+
params.deserializeUrl('/pet/findByStatus/available/123?limit=5&tags=dog,cat');
113+
// params.limit is now 5, params.tags is now ['dog', 'cat']
114+
```
115+
116+
#### Static Factory Methods
117+
- `static fromUrl(url: string, basePath: string, ...requiredDefaults): ParameterClass`: Creates a new parameter instance from a complete URL by extracting both path and query parameters.
118+
119+
```typescript
120+
// Example
121+
const params = FindPetsByStatusParameters.fromUrl(
122+
'/pet/findByStatus/available/123?limit=5&tags=dog',
123+
'/pet/findByStatus/{status}/{categoryId}'
124+
);
125+
// params.status is 'available', params.categoryId is 123, params.limit is 5
126+
```
127+
128+
### Parameter Style Support
129+
130+
The OpenAPI generator supports all OpenAPI parameter styles and serialization formats:
131+
132+
#### Path Parameters
133+
- **simple** (default): `value1,value2` or `key1,value1,key2,value2`
134+
- **label**: `.value1.value2` or `.key1.value1.key2.value2`
135+
- **matrix**: `;param=value1,value2` or `;key1=value1;key2=value2`
136+
137+
#### Query Parameters
138+
- **form** (default): `param=value1&param=value2` (exploded) or `param=value1,value2`
139+
- **spaceDelimited**: `param=value1 value2`
140+
- **pipeDelimited**: `param=value1|value2`
141+
- **deepObject**: `param[key1]=value1&param[key2]=value2`
142+
143+
### Type Safety
144+
145+
All parameter classes are fully typed with:
146+
- Enum parameter types for restricted values
147+
- Required vs optional parameter distinction
148+
- Proper TypeScript casting for different parameter types (string, number, boolean, arrays)
149+
- Support for complex parameter schemas including nested objects and arrays
150+
151+
### OpenAPI 2.0 Compatibility
152+
153+
The generator supports OpenAPI 2.0 `collectionFormat` parameter serialization:
154+
- `csv`: Comma-separated values
155+
- `ssv`: Space-separated values
156+
- `tsv`: Tab-separated values (treated as CSV)
157+
- `pipes`: Pipe-separated values
158+
- `multi`: Multiple parameter instances
159+
160+
These are automatically converted to equivalent OpenAPI 3.0 style/explode combinations for consistent handling.
Lines changed: 46 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1+
/* eslint-disable security/detect-object-injection */
12
import {
2-
ConstrainedEnumModel,
3-
ConstrainedObjectModel,
4-
ConstrainedReferenceModel,
53
OutputModel,
6-
TS_DESCRIPTION_PRESET,
74
TypeScriptFileGenerator
85
} from '@asyncapi/modelina';
96
import {AsyncAPIDocumentInterface} from '@asyncapi/parser';
107
import {GenericCodegenContext, ParameterRenderType} from '../../types';
118
import {z} from 'zod';
12-
import {findNameFromChannel} from '../../utils';
13-
import {defaultCodegenTypescriptModelinaOptions, pascalCase} from './utils';
149
import {OpenAPIV2, OpenAPIV3, OpenAPIV3_1} from 'openapi-types';
10+
import {
11+
createAsyncAPIGenerator,
12+
processAsyncAPIParameters,
13+
ProcessedParameterSchemaData
14+
} from '../../inputs/asyncapi/generators/parameters';
15+
import {createOpenAPIGenerator, processOpenAPIParameters} from '../../inputs/openapi/generators/parameters';
1516

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

45-
/**
46-
* Component which contains the parameter unwrapping functionality.
47-
*
48-
*
49-
* Example
50-
const regex = /^adeo-([^.]*)-case-study-COSTING-REQUEST-([^.]*)$/;
51-
const match = channel.match(regex);
52-
53-
const parameters = new CostingRequestChannelParameters({env: "dev", version: ''});
54-
if (match) {
55-
const envMatch = match.at(1)
56-
if(envMatch && envMatch !== '') {
57-
parameters.env = envMatch as any
58-
} else {
59-
throw new Error(`Parameter: 'env' is not valid. Abort! `)
60-
}
61-
const versionMatch = match.at(2)
62-
if(versionMatch && versionMatch !== '') {
63-
parameters.version = versionMatch as any
64-
} else {
65-
throw new Error(`Parameter: 'version' is not valid. Abort! `)
66-
}
67-
} else {
68-
throw new Error(`Unable to find parameters in channe/topic, topic was ${channel}`)
69-
}
70-
return parameters;
71-
*
72-
*/
73-
export function unwrap(channelParameters: ConstrainedObjectModel) {
74-
// Nothing to unwrap if no parameters are used
75-
if (Object.keys(channelParameters.properties).length === 0) {
76-
return '';
77-
}
78-
79-
// Use channel to iterate over matches as channelParameters.properties might be in incorrect order.
80-
81-
const parameterReplacement = Object.values(channelParameters.properties).map(
82-
(parameter) => {
83-
const variableName = `${parameter.propertyName}Match`;
84-
return `const ${variableName} = match[sequentialParameters.indexOf('{${parameter.unconstrainedPropertyName}}')+1];
85-
if(${variableName} && ${variableName} !== '') {
86-
parameters.${parameter.propertyName} = ${variableName} as any
87-
} else {
88-
throw new Error(\`Parameter: '${parameter.propertyName}' is not valid. Abort! \`)
89-
}`;
90-
}
91-
);
92-
93-
const parameterInitializer = Object.values(channelParameters.properties).map(
94-
(parameter) => {
95-
if (parameter.property.options.isNullable) {
96-
return `${parameter.propertyName}: null`;
97-
}
98-
const property = parameter.property;
99-
if (
100-
property instanceof ConstrainedReferenceModel &&
101-
property.ref instanceof ConstrainedEnumModel
102-
) {
103-
return `${parameter.propertyName}: ${property.ref.values[0].value}`;
104-
}
105-
return `${parameter.propertyName}: ''`;
106-
}
107-
);
108-
109-
return `const parameters = new ${channelParameters.name}({${parameterInitializer.join(', ')}});
110-
const match = msgSubject.match(regex);
111-
const sequentialParameters: string[] = channel.match(/\\{(\\w+)\\}/g) || [];
112-
113-
if (match) {
114-
${parameterReplacement.join('\n')}
115-
} else {
116-
throw new Error(\`Unable to find parameters in channel/topic, topic was \${channel}\`)
117-
}
118-
return parameters;`;
119-
}
120-
12146
export type TypeScriptParameterRenderType =
12247
ParameterRenderType<TypescriptParametersGeneratorInternal>;
12348

49+
// Main generator function that orchestrates input processing and generation
12450
export async function generateTypescriptParameters(
12551
context: TypescriptParametersContext
12652
): Promise<TypeScriptParameterRenderType> {
127-
const {asyncapiDocument, inputType, generator} = context;
128-
if (inputType === 'asyncapi' && asyncapiDocument === undefined) {
129-
throw new Error('Expected AsyncAPI input, was not given');
130-
}
131-
const modelinaGenerator = new TypeScriptFileGenerator({
132-
...defaultCodegenTypescriptModelinaOptions,
133-
enumType: 'union',
134-
useJavascriptReservedKeywords: false,
135-
presets: [
136-
TS_DESCRIPTION_PRESET,
137-
{
138-
class: {
139-
additionalContent: ({content, model, renderer}) => {
140-
const parameters = Object.entries(model.properties).map(
141-
([, parameter]) => {
142-
return `channel = channel.replace(/\\{${parameter.unconstrainedPropertyName}\\}/g, this.${parameter.propertyName})`;
143-
}
144-
);
145-
return `${content}
146-
/**
147-
* Realize the channel/topic with the parameters added to this class.
148-
*/
149-
public getChannelWithParameters(channel: string) {
150-
${renderer.renderBlock(parameters)};
151-
return channel;
152-
}
153-
154-
public static createFromChannel(msgSubject: string, channel: string, regex: RegExp): ${model.type} {
155-
${unwrap(model)}
156-
}`;
157-
}
158-
}
53+
const {asyncapiDocument, openapiDocument, inputType, generator} = context;
54+
55+
const channelModels: Record<string, OutputModel | undefined> = {};
56+
let processedSchemaData: ProcessedParameterSchemaData;
57+
let parameterGenerator: TypeScriptFileGenerator;
58+
59+
// Process input based on type
60+
switch (inputType) {
61+
case 'asyncapi': {
62+
if (!asyncapiDocument) {
63+
throw new Error('Expected AsyncAPI input, was not given');
15964
}
160-
]
161-
});
162-
const returnType: Record<string, OutputModel | undefined> = {};
163-
for (const channel of asyncapiDocument!.allChannels().all()) {
164-
const parameters = channel.parameters().all();
165-
if (parameters.length > 0) {
166-
const schemaObj: any = {
167-
type: 'object',
168-
$id: pascalCase(`${findNameFromChannel(channel)}_parameters`),
169-
$schema: 'http://json-schema.org/draft-07/schema',
170-
required: [],
171-
properties: {},
172-
additionalProperties: false,
173-
'x-channel-address': channel.address()
174-
};
175-
for (const parameter of channel.parameters().all()) {
176-
schemaObj.properties[parameter.id()] = parameter.schema()?.json();
177-
schemaObj.required.push(parameter.id());
65+
66+
processedSchemaData = await processAsyncAPIParameters(asyncapiDocument);
67+
parameterGenerator = createAsyncAPIGenerator();
68+
break;
69+
}
70+
case 'openapi': {
71+
if (!openapiDocument) {
72+
throw new Error('Expected OpenAPI input, was not given');
17873
}
179-
const models = await modelinaGenerator.generateToFiles(
180-
schemaObj,
74+
75+
processedSchemaData = processOpenAPIParameters(openapiDocument);
76+
parameterGenerator = createOpenAPIGenerator();
77+
break;
78+
}
79+
default:
80+
throw new Error(`Unsupported input type: ${inputType}`);
81+
}
82+
83+
// Generate models for channel parameters
84+
for (const [channelId, schemaData] of Object.entries(
85+
processedSchemaData.channelParameters
86+
)) {
87+
if (schemaData) {
88+
const models = await parameterGenerator.generateToFiles(
89+
schemaData.schema,
18190
generator.outputPath,
18291
{exportType: 'named'},
18392
true
18493
);
185-
returnType[channel.id()] = models[0];
94+
channelModels[channelId] = models.length > 0 ? models[0] : undefined;
18695
} else {
187-
returnType[channel.id()] = undefined;
96+
channelModels[channelId] = undefined;
18897
}
18998
}
19099

191100
return {
192-
channelModels: returnType,
101+
channelModels,
193102
generator
194103
};
195104
}

0 commit comments

Comments
 (0)