diff --git a/docs/README.md b/docs/README.md index 94a1b2e6..6e053b5e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -94,6 +94,8 @@ Connect AI assistants like Claude Code, Cursor, and Windsurf to The Codegen Proj + + diff --git a/docs/contributing.md b/docs/contributing.md index 1e2c7aea..4a1d9f09 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -212,6 +212,8 @@ Prefix that follows specification is not enough though. Remember that the title + + diff --git a/docs/generators/profiles.md b/docs/generators/profiles.md new file mode 100644 index 00000000..115d67e9 --- /dev/null +++ b/docs/generators/profiles.md @@ -0,0 +1,66 @@ +--- +sidebar_position: 1 +--- + +# 🎯 Profiles (OpenAPI) + +Profiles are high-level sugar for OpenAPI inputs that expand into the granular +generators tuned for **REST consumers**: plain TypeScript `interface` models with +**no** `marshal`/`unmarshal`/`validate` ceremony, plus standalone serializer +functions and an optional fetch client. They are the idiomatic shape expected by +tools like `openapi-typescript`, `orval`, `kubb` and `heyapi`. + +```js +export default { + inputType: 'openapi', + inputPath: './openapi.json', + language: 'typescript', + profile: 'client', // or 'types' + generators: [] // optional explicit generators are appended as overrides +}; +``` + +## Available profiles + +| Profile | Expands into | Emits a client? | +|----------|--------------|-----------------| +| `types` | interface `payloads` + interface `parameters` (with standalone serializers) + `headers` | No | +| `client` | everything in `types` **plus** `channels` (`http_client`) + `client` (`http`) | Yes | + +### `types` + +Generates the data model surface only — one plain interface per payload, +parameter set and header group. Parameter models come with free functions +(`serializeQueryParameters`, `serializeUrl`) that carry the OpenAPI +style/explode logic, so you can build URLs without instantiating a class. + +### `client` + +Everything in `types`, plus a fetch-based HTTP client. Request bodies are +serialized with `JSON.stringify(payload)` and responses are returned as the +plain interface (`await res.json()` cast to the type) — no runtime marshalling. + +## Relationship to the `types` **preset** + +The `types` *profile* is distinct from the [`types` preset](./types.md): the +preset emits simple type aliases and enums derived from the whole document, +while the profile emits idiomatic **interface models** (payloads, parameters, +headers) for REST consumers. Use the profile when you want ready-to-consume +request/response types; use the preset when you just want shared type aliases. + +## `modelType` (opt in without a profile) + +Both the [`payloads`](./payloads.md) and [`parameters`](./parameters.md) +generators accept a `modelType: 'class' | 'interface'` option (default +`'class'`, which preserves the AsyncAPI/broker output). Setting +`modelType: 'interface'` is exactly what the profiles do under the hood, so you +can opt individual generators into interface output without adopting a whole +profile: + +```js +{ + preset: 'payloads', + outputPath: './src/payloads', + modelType: 'interface' +} +``` diff --git a/docs/migrations/v0.md b/docs/migrations/v0.md index e6fcc654..2bbdfcea 100644 --- a/docs/migrations/v0.md +++ b/docs/migrations/v0.md @@ -241,5 +241,7 @@ import * as NodeFetch from 'node-fetch'; + + diff --git a/docs/usage.md b/docs/usage.md index 699ce202..a5a8a778 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -9,7 +9,7 @@ $ npm install -g @the-codegen-project/cli $ codegen COMMAND running command... $ codegen (--version) -@the-codegen-project/cli/0.76.0 linux-x64 node-v22.23.1 +@the-codegen-project/cli/0.76.0 darwin-arm64 node-v25.9.0 $ codegen --help [COMMAND] USAGE $ codegen COMMAND @@ -20,7 +20,16 @@ USAGE ## Table of contents -* [CLI Usage](#cli-usage) + +- [Commands](#commands) +- [`codegen autocomplete [SHELL]`](#codegen-autocomplete-shell) +- [`codegen base`](#codegen-base) +- [`codegen generate [FILE]`](#codegen-generate-file) +- [`codegen help [COMMAND]`](#codegen-help-command) +- [`codegen init`](#codegen-init) +- [`codegen telemetry ACTION`](#codegen-telemetry-action) +- [`codegen version`](#codegen-version) + ## Commands @@ -235,3 +244,4 @@ FLAG DESCRIPTIONS _See code: [@oclif/plugin-version](https://github.com/oclif/plugin-version/blob/v2.1.2/src/commands/version.ts)_ + diff --git a/schemas/configuration-schema-0-with-docs.json b/schemas/configuration-schema-0-with-docs.json index b9057439..2fb857d1 100644 --- a/schemas/configuration-schema-0-with-docs.json +++ b/schemas/configuration-schema-0-with-docs.json @@ -28,6 +28,14 @@ "importExtension": { "$ref": "#/definitions/AsyncAPICodegenConfiguration/properties/importExtension" }, + "profile": { + "type": "string", + "enum": [ + "types", + "client" + ], + "markdownDescription": "High-level profile that expands into the granular generators tuned for OpenAPI REST consumers (plain interfaces, no marshal/unmarshal). \"types\" generates interface payloads + parameters + headers (with standalone serializer functions); \"client\" adds the HTTP fetch client on top. Any explicit generators listed alongside a profile are appended, letting you override individual outputs. [Read more about profiles here](https://the-codegen-project.org/docs/generators)" + }, "generators": { "type": "array", "items": { @@ -255,6 +263,15 @@ "const": "typescript", "default": "typescript" }, + "modelType": { + "type": "string", + "enum": [ + "class", + "interface" + ], + "default": "class", + "markdownDescription": "How payload models are rendered. \"class\" (default) emits class-based models with marshal/unmarshal/validate methods (the AsyncAPI/broker shape). \"interface\" emits plain TypeScript interfaces with no methods — the idiomatic shape for OpenAPI REST consumers, used by the OpenAPI interface/client profiles. [Read more about the payloads generator here](https://the-codegen-project.org/docs/generators/payloads)" + }, "enum": { "type": "string", "enum": [ @@ -329,6 +346,15 @@ "type": "string", "const": "typescript", "default": "typescript" + }, + "modelType": { + "type": "string", + "enum": [ + "class", + "interface" + ], + "default": "class", + "markdownDescription": "How parameter models are rendered. \"class\" (default) emits a class with serialization methods (the AsyncAPI/broker shape). \"interface\" emits a plain interface plus standalone serializer functions (serializeQueryParameters / serializeUrl) — the idiomatic shape for OpenAPI REST consumers, used by the OpenAPI interface/client profiles. [Read more about the parameters generator here](https://the-codegen-project.org/docs/generators/parameters)" } }, "additionalProperties": false diff --git a/schemas/configuration-schema-0.json b/schemas/configuration-schema-0.json index 1b3309f6..3aec15fa 100644 --- a/schemas/configuration-schema-0.json +++ b/schemas/configuration-schema-0.json @@ -28,6 +28,14 @@ "importExtension": { "$ref": "#/definitions/AsyncAPICodegenConfiguration/properties/importExtension" }, + "profile": { + "type": "string", + "enum": [ + "types", + "client" + ], + "description": "High-level profile that expands into the granular generators tuned for OpenAPI REST consumers (plain interfaces, no marshal/unmarshal). \"types\" generates interface payloads + parameters + headers (with standalone serializer functions); \"client\" adds the HTTP fetch client on top. Any explicit generators listed alongside a profile are appended, letting you override individual outputs. Read more about profiles here" + }, "generators": { "type": "array", "items": { @@ -255,6 +263,15 @@ "const": "typescript", "default": "typescript" }, + "modelType": { + "type": "string", + "enum": [ + "class", + "interface" + ], + "default": "class", + "description": "How payload models are rendered. \"class\" (default) emits class-based models with marshal/unmarshal/validate methods (the AsyncAPI/broker shape). \"interface\" emits plain TypeScript interfaces with no methods — the idiomatic shape for OpenAPI REST consumers, used by the OpenAPI interface/client profiles. Read more about the payloads generator here" + }, "enum": { "type": "string", "enum": [ @@ -329,6 +346,15 @@ "type": "string", "const": "typescript", "default": "typescript" + }, + "modelType": { + "type": "string", + "enum": [ + "class", + "interface" + ], + "default": "class", + "description": "How parameter models are rendered. \"class\" (default) emits a class with serialization methods (the AsyncAPI/broker shape). \"interface\" emits a plain interface plus standalone serializer functions (serializeQueryParameters / serializeUrl) — the idiomatic shape for OpenAPI REST consumers, used by the OpenAPI interface/client profiles. Read more about the parameters generator here" } }, "additionalProperties": false diff --git a/src/codegen/configurations.ts b/src/codegen/configurations.ts index e9894093..93b88639 100644 --- a/src/codegen/configurations.ts +++ b/src/codegen/configurations.ts @@ -124,10 +124,60 @@ export async function loadAndRealizeConfigFile(filePath?: string): Promise<{ /** * Ensure that each generator has the default options along side custom properties */ +/** + * Expand a high-level OpenAPI `profile` into the granular generator list, in + * front of any explicitly-listed generators (which are treated as overrides). + * The profile generators use the default generator ids so the channels/client + * dependency injection in {@link ensureProperGenerators} reuses them instead of + * adding class-mode defaults. + * + * - `types` → interface payloads + parameters (with standalone serializers) + + * headers. No channels, no client. + * - `client` → everything in `types` plus the HTTP fetch client. + * + * This is distinct from the `types` *preset* (simple type aliases/enums): the + * profile emits idiomatic interface models for REST consumers. + */ +function expandOpenAPIProfile(config: TheCodegenConfiguration): void { + const profile = (config as {profile?: 'types' | 'client'}).profile; + if (!profile) { + return; + } + const profileGenerators: Generators[] = [ + { + preset: 'payloads', + modelType: 'interface', + outputPath: 'src/payloads' + } as unknown as Generators, + { + preset: 'parameters', + modelType: 'interface', + outputPath: 'src/parameters' + } as unknown as Generators, + {preset: 'headers', outputPath: 'src/headers'} as unknown as Generators + ]; + if (profile === 'client') { + profileGenerators.push( + { + preset: 'channels', + protocols: ['http_client'], + outputPath: 'src/channels' + } as unknown as Generators, + { + preset: 'client', + protocols: ['http'], + outputPath: 'src/client' + } as unknown as Generators + ); + } + config.generators = [...profileGenerators, ...(config.generators ?? [])]; +} + export function realizeConfiguration( config: TheCodegenConfiguration ): TheCodegenConfigurationInternal { config.generators = config.generators ?? []; + expandOpenAPIProfile(config); const generatorIds: string[] = []; for (const [index, generator] of config.generators.entries()) { @@ -341,14 +391,17 @@ export async function realizeInMemoryGeneratorContext({ configFilePath: '/virtual-config.mjs' }; if (config.inputType === 'asyncapi') { - context.asyncapiDocument = - await loadAsyncapiFromMemory(specificationDocument); + context.asyncapiDocument = await loadAsyncapiFromMemory( + specificationDocument + ); } else if (config.inputType === 'openapi') { - context.openapiDocument = - await loadOpenapiFromMemory(specificationDocument); + context.openapiDocument = await loadOpenapiFromMemory( + specificationDocument + ); } else if (config.inputType === 'jsonschema') { - context.jsonSchemaDocument = - loadJsonSchemaFromMemory(specificationDocument); + context.jsonSchemaDocument = loadJsonSchemaFromMemory( + specificationDocument + ); } return context; } diff --git a/src/codegen/generators/typescript/channels/openapi.ts b/src/codegen/generators/typescript/channels/openapi.ts index cf8b6caa..873e7323 100644 --- a/src/codegen/generators/typescript/channels/openapi.ts +++ b/src/codegen/generators/typescript/channels/openapi.ts @@ -101,13 +101,20 @@ export async function generateTypeScriptChannelsForOpenAPI( // type-check. const oauth2Enabled = analyzeSecuritySchemes(securitySchemes).oauth2; + // Interface mode (OpenAPI interface/client profile) changes how bodies, + // responses, and parameters are serialized in the generated client. + const payloadInterfaceMode = payloads.generator.modelType === 'interface'; + const parameterInterfaceMode = parameters.generator.modelType === 'interface'; + // Process all operations and collect renders const renders = processOpenAPIOperations( openapiDocument, payloads, parameters, headers, - oauth2Enabled + oauth2Enabled, + payloadInterfaceMode, + parameterInterfaceMode ); // Generate common types once (stateless check) @@ -138,7 +145,9 @@ function processOpenAPIOperations( payloads: TypeScriptPayloadRenderType, parameters: TypeScriptParameterRenderType, headers: TypeScriptHeadersRenderType, - oauth2Enabled: boolean + oauth2Enabled: boolean, + payloadInterfaceMode: boolean, + parameterInterfaceMode: boolean ): ReturnType[] { const renders: ReturnType[] = []; @@ -148,15 +157,17 @@ function processOpenAPIOperations( } for (const method of HTTP_METHODS) { - const render = processOperation( + const render = processOperation({ pathItem, method, path, payloads, parameters, headers, - oauth2Enabled - ); + oauth2Enabled, + payloadInterfaceMode, + parameterInterfaceMode + }); if (render) { renders.push(render); } @@ -169,15 +180,27 @@ function processOpenAPIOperations( /** * Process a single OpenAPI operation and generate an HTTP client function. */ -function processOperation( - pathItem: OpenAPIV3.PathItemObject | OpenAPIV2.PathsObject, - method: HttpMethod, - path: string, - payloads: TypeScriptPayloadRenderType, - parameters: TypeScriptParameterRenderType, - headers: TypeScriptHeadersRenderType, - oauth2Enabled: boolean -): ReturnType | undefined { +function processOperation({ + pathItem, + method, + path, + payloads, + parameters, + headers, + oauth2Enabled, + payloadInterfaceMode, + parameterInterfaceMode +}: { + pathItem: OpenAPIV3.PathItemObject | OpenAPIV2.PathsObject; + method: HttpMethod; + path: string; + payloads: TypeScriptPayloadRenderType; + parameters: TypeScriptParameterRenderType; + headers: TypeScriptHeadersRenderType; + oauth2Enabled: boolean; + payloadInterfaceMode: boolean; + parameterInterfaceMode: boolean; +}): ReturnType | undefined { // eslint-disable-next-line security/detect-object-injection const operation = (pathItem as Record)[method] as | OpenAPIOperation @@ -270,7 +293,9 @@ function processOperation( description, deprecated, oauth2Enabled, - hasSerializeHeaders: headersModel !== undefined + hasSerializeHeaders: headersModel !== undefined, + payloadInterfaceMode, + parameterInterfaceMode }); // Grouping metadata for the `organization` option (consumed in diff --git a/src/codegen/generators/typescript/channels/protocols/http/client.ts b/src/codegen/generators/typescript/channels/protocols/http/client.ts index b0e8ba49..444db365 100644 --- a/src/codegen/generators/typescript/channels/protocols/http/client.ts +++ b/src/codegen/generators/typescript/channels/protocols/http/client.ts @@ -30,7 +30,9 @@ export function renderHttpFetchClient({ description, deprecated, oauth2Enabled = true, - hasSerializeHeaders = false + hasSerializeHeaders = false, + payloadInterfaceMode = false, + parameterInterfaceMode = false }: RenderHttpParameters): HttpRenderType { const messageType = requestMessageModule ? `${requestMessageModule}.${requestMessageType}` @@ -52,7 +54,8 @@ export function renderHttpFetchClient({ messageType, channelParameters?.type, channelHeaders?.type, - method + method, + parameterInterfaceMode ); // Generate JSDoc for the function @@ -80,7 +83,9 @@ export function renderHttpFetchClient({ servers, includesStatusCodes, jsDoc, - oauth2Enabled + oauth2Enabled, + payloadInterfaceMode, + parameterInterfaceMode }); const code = `${contextInterface} @@ -106,7 +111,8 @@ function generateContextInterface( messageType: string | undefined, parametersType: string | undefined, headersType: string | undefined, - method: string + method: string, + parameterInterfaceMode: boolean ): string { const fields: string[] = []; @@ -115,13 +121,19 @@ function generateContextInterface( fields.push(` payload: ${messageType};`); } - // 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. + // Add parameters field if the operation has path/query parameters. In + // interface mode the field is the plain parameter interface — serialized by + // the standalone `serializeUrl` function. In class mode it accepts + // either a plain object satisfying the interface (ergonomic) or a concrete + // class instance (rich behavior), normalized to an instance before use. if (parametersType) { - fields.push(` parameters: ${parameterUnionType(parametersType)};`); + fields.push( + ` parameters: ${ + parameterInterfaceMode + ? parametersType + : parameterUnionType(parametersType) + };` + ); } // Emit requestHeaders only when the spec defines operation headers so the @@ -158,6 +170,87 @@ function generateHeadersInit(params: { : { 'Content-Type': 'application/json', ...config.additionalHeaders } as Record;`; } +/** + * Build the `let url = ...` initializer. Interface mode calls the standalone + * `serializeUrl` free function; class mode uses the normalized instance + * via `buildUrlWithParameters`; parameterless operations interpolate directly. + */ +function generateUrlBuildCode(params: { + hasParameters: boolean; + parameterInterfaceMode: boolean; + parameterModelName: string | undefined; + requestTopic: string; +}): string { + const { + hasParameters, + parameterInterfaceMode, + parameterModelName, + requestTopic + } = params; + if (!hasParameters) { + return `let url = \`\${config.baseUrl}${requestTopic}\`;`; + } + if (parameterInterfaceMode && parameterModelName) { + return `let url = \`\${config.baseUrl}\${serialize${parameterModelName}Url('${requestTopic}', context.parameters)}\`;`; + } + return `let url = buildUrlWithParameters(config.baseUrl, '${requestTopic}', parameters);`; +} + +/** + * Build the `const body = ...` initializer. Interface-mode payloads have no + * `marshal()`, so the plain object is serialized with `JSON.stringify`. + */ +function generateBodyPrep(params: { + hasBody: boolean | '' | undefined; + payloadInterfaceMode: boolean; +}): string { + const {hasBody, payloadInterfaceMode} = params; + if (!hasBody) { + return `const body = undefined;`; + } + if (payloadInterfaceMode) { + return `const body = context.payload !== undefined ? JSON.stringify(context.payload) : undefined;`; + } + return `const body = context.payload?.marshal();`; +} + +/** + * Build the `const responseData = ...` initializer. Interface-mode payloads have + * no `unmarshal()` — the raw JSON already matches the interface shape, so it is + * cast directly. Class mode unmarshals the raw JSON text (see the note below on + * why the text, not the parsed object, is passed). + * + * unmarshal receives the raw JSON text (JSON.stringify(rawData)) rather than the + * parsed object: object/array models accept either, but primitive-typed payloads + * (e.g. `type X = string`) generate `unmarshal(json: string)` which JSON.parses + * its argument, so passing the already-parsed value would both fail to + * type-check and throw at runtime. + */ +function generateResponseParseCode(params: { + payloadInterfaceMode: boolean; + replyType: string; + replyMessageModule: string | undefined; + replyMessageType: string; + includesStatusCodes: boolean; +}): string { + const { + payloadInterfaceMode, + replyType, + replyMessageModule, + replyMessageType, + includesStatusCodes + } = params; + if (payloadInterfaceMode) { + return `const responseData = rawData as unknown as ${replyType};`; + } + if (replyMessageModule) { + return includesStatusCodes + ? `const responseData = ${replyMessageModule}.unmarshalByStatusCode(JSON.stringify(rawData), response.status);` + : `const responseData = ${replyMessageModule}.unmarshal(JSON.stringify(rawData));`; + } + return `const responseData = ${replyMessageType}.unmarshal(JSON.stringify(rawData));`; +} + /** * Generate the function implementation */ @@ -179,6 +272,8 @@ function generateFunctionImplementation(params: { includesStatusCodes: boolean; jsDoc: string; oauth2Enabled: boolean; + payloadInterfaceMode: boolean; + parameterInterfaceMode: boolean; }): string { const { functionName, @@ -197,7 +292,9 @@ function generateFunctionImplementation(params: { servers, includesStatusCodes, jsDoc, - oauth2Enabled + oauth2Enabled, + payloadInterfaceMode, + parameterInterfaceMode } = params; const defaultServer = servers[0] ?? "'http://localhost:3000'"; @@ -206,8 +303,10 @@ function generateFunctionImplementation(params: { // Normalize the user-provided parameters (interface object or class instance) // to a concrete class instance so the URL builder gets the rich behavior. + // Interface mode needs no normalization — the standalone serializer reads the + // plain interface directly. const parameterNormalization = - hasParameters && parameterModelName + hasParameters && parameterModelName && !parameterInterfaceMode ? ` ${renderParameterNormalization({ modelName: parameterModelName, source: 'context.parameters', @@ -215,10 +314,15 @@ function generateFunctionImplementation(params: { })}\n\n` : ''; - // Generate URL building code - const urlBuildCode = hasParameters - ? `let url = buildUrlWithParameters(config.baseUrl, '${requestTopic}', parameters);` - : `let url = \`\${config.baseUrl}${requestTopic}\`;`; + // Generate URL building code. Interface mode calls the standalone + // `serializeUrl` free function; class mode uses the normalized instance + // via buildUrlWithParameters. + const urlBuildCode = generateUrlBuildCode({ + hasParameters, + parameterInterfaceMode, + parameterModelName, + requestTopic + }); // Generate headers initialization const headersInit = generateHeadersInit({ @@ -228,25 +332,16 @@ function generateFunctionImplementation(params: { }); // Generate body preparation - const bodyPrep = hasBody - ? `const body = context.payload?.marshal();` - : `const body = undefined;`; - - // Generate response parsing. - // Use unmarshalByStatusCode if the payload is a union type with status code support. - // unmarshal receives the raw JSON text (JSON.stringify(rawData)) rather than the - // parsed object: object/array models accept either, but primitive-typed payloads - // (e.g. `type X = string`) generate `unmarshal(json: string)` which JSON.parses its - // argument, so passing the already-parsed value would both fail to type-check and - // throw at runtime. - let responseParseCode: string; - if (replyMessageModule) { - responseParseCode = includesStatusCodes - ? `const responseData = ${replyMessageModule}.unmarshalByStatusCode(JSON.stringify(rawData), response.status);` - : `const responseData = ${replyMessageModule}.unmarshal(JSON.stringify(rawData));`; - } else { - responseParseCode = `const responseData = ${replyMessageType}.unmarshal(JSON.stringify(rawData));`; - } + const bodyPrep = generateBodyPrep({hasBody, payloadInterfaceMode}); + + // Generate response parsing + const responseParseCode = generateResponseParseCode({ + payloadInterfaceMode, + replyType, + replyMessageModule, + replyMessageType, + includesStatusCodes + }); // Generate default context for optional context parameter const contextDefault = !hasBody && !hasParameters ? ' = {}' : ''; diff --git a/src/codegen/generators/typescript/channels/types.ts b/src/codegen/generators/typescript/channels/types.ts index 3ecee502..394c0f7a 100644 --- a/src/codegen/generators/typescript/channels/types.ts +++ b/src/codegen/generators/typescript/channels/types.ts @@ -310,6 +310,20 @@ export interface RenderHttpParameters { * `.marshal()` method (AsyncAPI style). Defaults to false. */ hasSerializeHeaders?: boolean; + /** + * Whether the request/response payload models are plain interfaces (OpenAPI + * interface/client profile) rather than classes. When true the body is built + * with `JSON.stringify(context.payload)` and the response is cast to the + * interface instead of `marshal()`/`unmarshal()`. Defaults to false. + */ + payloadInterfaceMode?: boolean; + /** + * Whether the parameter model is a plain interface with standalone serializer + * functions rather than a class with methods. When true the URL is built via + * `serializeUrl(path, context.parameters)` and no class normalization is + * emitted. Defaults to false. + */ + parameterInterfaceMode?: boolean; } export type SupportedProtocols = diff --git a/src/codegen/generators/typescript/channels/utils.ts b/src/codegen/generators/typescript/channels/utils.ts index 2f30af16..1bd57b6d 100644 --- a/src/codegen/generators/typescript/channels/utils.ts +++ b/src/codegen/generators/typescript/channels/utils.ts @@ -76,7 +76,8 @@ export function addParametersToDependencies( parameterGenerator: {outputPath: string}, currentGenerator: {outputPath: string}, dependencies: string[], - importExtension: ImportExtension = 'none' + importExtension: ImportExtension = 'none', + parameterFunctions?: Record ) { Object.values(parameters) .filter((model) => model !== undefined) @@ -93,9 +94,15 @@ export function addParametersToDependencies( importExtension ); - dependencies.push( - `import {${parameter.modelName}, ${parameter.modelName}Interface} from '${importPath}';` - ); + // Interface mode: import the plain interface plus its standalone + // serializer functions (serializeUrl, …). Class mode: import the + // class and its companion `Interface`. + const fns = parameterFunctions?.[parameter.modelName]; + const importNames = + fns && fns.length > 0 + ? `${parameter.modelName}, ${fns.join(', ')}` + : `${parameter.modelName}, ${parameter.modelName}Interface`; + dependencies.push(`import {${importNames}} from '${importPath}';`); }); } /** @@ -280,7 +287,8 @@ export function collectProtocolDependencies( parameters.generator, context.generator, protocolDeps, - importExtension + importExtension, + parameters.parameterFunctions ); // Add header imports diff --git a/src/codegen/generators/typescript/client/protocols/http.ts b/src/codegen/generators/typescript/client/protocols/http.ts index 02ec46e3..ed82e174 100644 --- a/src/codegen/generators/typescript/client/protocols/http.ts +++ b/src/codegen/generators/typescript/client/protocols/http.ts @@ -148,7 +148,8 @@ export async function generateHttpClient( parameters.generator, context.generator, dependencies, - importExtension + importExtension, + parameters.parameterFunctions ); addParametersToExports(parameters.channelModels, dependencies); diff --git a/src/codegen/generators/typescript/parameters.ts b/src/codegen/generators/typescript/parameters.ts index 6cb9dd89..e7d5e4ec 100644 --- a/src/codegen/generators/typescript/parameters.ts +++ b/src/codegen/generators/typescript/parameters.ts @@ -15,8 +15,11 @@ import { } from '../../inputs/asyncapi/generators/parameters'; import { createOpenAPIGenerator, + createOpenAPIInterfaceParameterGenerator, + generateOpenAPIParameterFunctions, processOpenAPIParameters } from '../../inputs/openapi/generators/parameters'; +import {ConstrainedObjectModel} from '@asyncapi/modelina'; import {createMissingInputDocumentError} from '../../errors'; import {generateModels} from '../../output'; @@ -54,7 +57,14 @@ export const zodTypescriptParametersGenerator = z.object({ .describe( 'The serialization format used by the generated parameter models. Currently only "json" is supported. [Read more about the parameters generator here](https://the-codegen-project.org/docs/generators/parameters)' ), - language: z.literal('typescript').optional().default('typescript') + language: z.literal('typescript').optional().default('typescript'), + modelType: z + .enum(['class', 'interface']) + .optional() + .default('class') + .describe( + 'How parameter models are rendered. "class" (default) emits a class with serialization methods (the AsyncAPI/broker shape). "interface" emits a plain interface plus standalone serializer functions (serializeQueryParameters / serializeUrl) — the idiomatic shape for OpenAPI REST consumers, used by the OpenAPI interface/client profiles. [Read more about the parameters generator here](https://the-codegen-project.org/docs/generators/parameters)' + ) }); export type TypescriptParametersGenerator = z.input< @@ -118,9 +128,16 @@ export async function generateTypescriptParameters( const channelModels: Record = {}; const files: GeneratedFile[] = []; + const parameterFunctions: Record = {}; let processedSchemaData: ProcessedParameterSchemaData; let parameterGenerator: TypeScriptFileGenerator; + // Interface mode (OpenAPI REST consumers) emits a plain interface plus + // standalone serializer functions; class mode keeps the AsyncAPI/broker + // shape. Interface mode is only meaningful for OpenAPI input. + const isInterface = + generator.modelType === 'interface' && inputType === 'openapi'; + // Process input based on type switch (inputType) { case 'asyncapi': { @@ -144,7 +161,9 @@ export async function generateTypescriptParameters( } processedSchemaData = processOpenAPIParameters(openapiDocument); - parameterGenerator = createOpenAPIGenerator(); + parameterGenerator = isInterface + ? createOpenAPIInterfaceParameterGenerator() + : createOpenAPIGenerator(); break; } default: @@ -155,28 +174,34 @@ export async function generateTypescriptParameters( for (const [channelId, schemaData] of Object.entries( processedSchemaData.channelParameters )) { - if (schemaData) { - const result = await generateModels({ - generator: parameterGenerator, - input: schemaData.schema, - outputPath: generator.outputPath - }); - 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 { + if (!schemaData) { channelModels[channelId] = undefined; + continue; + } + const result = await generateModels({ + generator: parameterGenerator, + input: schemaData.schema, + outputPath: generator.outputPath + }); + if (isInterface) { + appendInterfaceParameterFunctions(result, files, parameterFunctions); + channelModels[channelId] = + result.models.length > 0 ? result.models[0] : undefined; + continue; + } + 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); + } } } @@ -193,6 +218,34 @@ export async function generateTypescriptParameters( return { channelModels, generator, - files: uniqueFiles + files: uniqueFiles, + parameterFunctions }; } + +/** + * Append the standalone serializer functions to each interface-mode parameter + * model's own file and record the exported function names keyed by model name + * (consumed by the channels/HTTP-client generators, mirroring header + * functions). Only object models carry parameters worth serializing. + */ +function appendInterfaceParameterFunctions( + result: {models: OutputModel[]; files: GeneratedFile[]}, + files: GeneratedFile[], + parameterFunctions: Record +): void { + for (const file of result.files) { + const model = result.models.find((candidate) => + file.path.endsWith(`/${candidate.modelName}.ts`) + ); + if (!model || !(model.model instanceof ConstrainedObjectModel)) { + files.push(file); + continue; + } + const {functions, functionNames} = generateOpenAPIParameterFunctions( + model.model + ); + files.push({path: file.path, content: `${file.content}\n\n${functions}`}); + parameterFunctions[model.modelName] = functionNames; + } +} diff --git a/src/codegen/generators/typescript/payloads.ts b/src/codegen/generators/typescript/payloads.ts index 8e362419..7e6060fb 100644 --- a/src/codegen/generators/typescript/payloads.ts +++ b/src/codegen/generators/typescript/payloads.ts @@ -63,6 +63,13 @@ export const zodTypeScriptPayloadGenerator = z.object({ 'The serialization format used by the generated payload models. Currently only "json" is supported. [Read more about the payloads generator here](https://the-codegen-project.org/docs/generators/payloads)' ), language: z.literal('typescript').optional().default('typescript'), + modelType: z + .enum(['class', 'interface']) + .optional() + .default('class') + .describe( + 'How payload models are rendered. "class" (default) emits class-based models with marshal/unmarshal/validate methods (the AsyncAPI/broker shape). "interface" emits plain TypeScript interfaces with no methods — the idiomatic shape for OpenAPI REST consumers, used by the OpenAPI interface/client profiles. [Read more about the payloads generator here](https://the-codegen-project.org/docs/generators/payloads)' + ), enum: z .enum(['enum', 'union']) .optional() @@ -164,35 +171,45 @@ export async function generateTypescriptPayloadsCoreFromSchemas({ }): Promise { const generator = context.generator; - const modelinaGenerator = new TypeScriptFileGenerator({ - ...defaultCodegenTypescriptModelinaOptions, - presets: [ - TS_DESCRIPTION_PRESET, - { - preset: TS_COMMON_PRESET, - options: { - marshalling: true - } - }, - createValidationPreset( + // Interface mode (OpenAPI REST consumers) emits plain interfaces with no + // marshal/unmarshal/validate methods, so it drops the marshalling and + // validation presets that only make sense for class-based models. The + // class path is untouched to preserve the AsyncAPI/broker output. + const isInterface = generator.modelType === 'interface'; + const presets = isInterface + ? [TS_DESCRIPTION_PRESET] + : [ + TS_DESCRIPTION_PRESET, { - includeValidation: generator.includeValidation + preset: TS_COMMON_PRESET, + options: { + marshalling: true + } }, - context - ), - createUnionPreset( - { - includeValidation: generator.includeValidation - }, - context - ), - createPrimitivesPreset( - { - includeValidation: generator.includeValidation - }, - context - ) - ], + createValidationPreset( + { + includeValidation: generator.includeValidation + }, + context + ), + createUnionPreset( + { + includeValidation: generator.includeValidation + }, + context + ), + createPrimitivesPreset( + { + includeValidation: generator.includeValidation + }, + context + ) + ]; + + const modelinaGenerator = new TypeScriptFileGenerator({ + ...defaultCodegenTypescriptModelinaOptions, + ...(isInterface ? {modelType: 'interface' as const} : {}), + presets, enumType: generator.enum, mapType: generator.map, rawPropertyNames: generator.rawPropertyNames, diff --git a/src/codegen/inputs/openapi/generators/parameters.ts b/src/codegen/inputs/openapi/generators/parameters.ts index 94242cb3..d1d86b04 100644 --- a/src/codegen/inputs/openapi/generators/parameters.ts +++ b/src/codegen/inputs/openapi/generators/parameters.ts @@ -1266,3 +1266,162 @@ export function createOpenAPIGenerator() { ] }); } + +/** + * Interface-mode parameter generator for OpenAPI REST consumers. Emits the + * parameter shape as a plain `interface ` (no class, no methods) — the + * companion serializer logic is emitted separately as free functions by + * {@link generateOpenAPIParameterFunctions}, mirroring how header serializers + * are emitted as `serializeHeaders`. + */ +export function createOpenAPIInterfaceParameterGenerator() { + return new TypeScriptFileGenerator({ + ...defaultCodegenTypescriptModelinaOptions, + enumType: 'union', + useJavascriptReservedKeywords: false, + modelType: 'interface', + presets: [TS_DESCRIPTION_PRESET] + }); +} + +/** + * Collect the path/query parameter configs for a model, resolving each raw + * OpenAPI parameter to its constrained TypeScript property name. Shared by the + * class-method emitter and the free-function emitter so both see the same set. + */ +function collectParameterConfigs(model: ConstrainedObjectModel): { + pathParams: ParameterConfig[]; + queryParams: ParameterConfig[]; +} { + const properties = model.originalInput?.properties ?? {}; + const pathParams: ParameterConfig[] = []; + const queryParams: ParameterConfig[] = []; + + for (const [propName, propSchema] of Object.entries(properties)) { + const paramConfig = processParameterSchema(propName, propSchema); + if (!paramConfig) { + continue; + } + const parameterConfig: ParameterConfig = { + name: paramConfig.name, + propertyName: getConstrainedPropertyName({ + model, + parameterName: propName + }), + style: paramConfig.style, + explode: paramConfig.explode, + allowReserved: paramConfig.allowReserved + }; + if (paramConfig.location === 'path') { + pathParams.push(parameterConfig); + } else if (paramConfig.location === 'query') { + queryParams.push(parameterConfig); + } + } + + return {pathParams, queryParams}; +} + +/** + * Emit the per-parameter serialization block for interface mode, reading from a + * `parameters` argument instead of `this`. Reuses the exact style/explode logic + * generators shared with the class-based path so the two can never diverge. + */ +function generateFreeParameterSerialization( + param: ParameterConfig, + kind: 'path' | 'query' +): string { + const {name, propertyName, style, explode, allowReserved} = param; + const encoding = allowReserved ? '' : 'encodeURIComponent'; + const logic = + kind === 'path' + ? generatePathSerializationLogic(name, style, explode, encoding) + : generateQuerySerializationLogic(name, style, explode, encoding); + return ` // Serialize ${kind} parameter: ${name} (style: ${style}, explode: ${explode}) + if (parameters.${propertyName} !== undefined && parameters.${propertyName} !== null) { + const value = parameters.${propertyName}; + ${logic} + }`; +} + +/** + * Generate standalone serializer functions for an interface-mode parameter + * model: an optional path serializer, an optional query serializer, and a + * combined `serializeUrl(basePath, parameters)` that substitutes path + * placeholders and appends the query string. The URL helper replaces the + * class-based `getChannelWithParameters` used by the HTTP client. + */ +export function generateOpenAPIParameterFunctions( + model: ConstrainedObjectModel +): { + functions: string; + functionNames: string[]; +} { + const modelName = model.name; + const {pathParams, queryParams} = collectParameterConfigs(model); + const functions: string[] = []; + const functionNames: string[] = []; + + if (pathParams.length > 0) { + const serializations = pathParams + .map((param) => generateFreeParameterSerialization(param, 'path')) + .join('\n'); + functions.push(`/** + * Serialize path parameters for ${modelName} according to the OpenAPI 2.0/3.x specification. + * @returns Record of parameter names to their serialized values for path substitution + */ +export function serialize${modelName}PathParameters(parameters: ${modelName}): Record { + const result: Record = {}; +${serializations} + return result; +}`); + functionNames.push(`serialize${modelName}PathParameters`); + } + + if (queryParams.length > 0) { + const serializations = queryParams + .map((param) => generateFreeParameterSerialization(param, 'query')) + .join('\n'); + functions.push(`/** + * Serialize query parameters for ${modelName} according to the OpenAPI 2.0/3.x specification. + * @returns URLSearchParams with the serialized query parameters + */ +export function serialize${modelName}QueryParameters(parameters: ${modelName}): URLSearchParams { + const params = new URLSearchParams(); +${serializations} + return params; +}`); + functionNames.push(`serialize${modelName}QueryParameters`); + } + + const pathLogic = + pathParams.length > 0 + ? ` + const pathParams = serialize${modelName}PathParameters(parameters); + for (const [name, value] of Object.entries(pathParams)) { + url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); + }` + : ''; + const queryLogic = + queryParams.length > 0 + ? ` + const queryString = serialize${modelName}QueryParameters(parameters).toString(); + if (queryString) { + url += (url.includes('?') ? '&' : '?') + queryString; + }` + : ''; + + functions.push(`/** + * Build the URL for ${modelName} by substituting path parameters into the base + * path template and appending serialized query parameters. + * @param basePath The path template (e.g. '/pet/findByStatus/{status}') + * @returns The path with parameters applied + */ +export function serialize${modelName}Url(basePath: string, parameters: ${modelName}): string { + let url = basePath;${pathLogic}${queryLogic} + return url; +}`); + functionNames.push(`serialize${modelName}Url`); + + return {functions: functions.join('\n\n'), functionNames}; +} diff --git a/src/codegen/inputs/openapi/index.ts b/src/codegen/inputs/openapi/index.ts index 5fb8453e..4c035206 100644 --- a/src/codegen/inputs/openapi/index.ts +++ b/src/codegen/inputs/openapi/index.ts @@ -1,2 +1,6 @@ -export {loadOpenapi, loadOpenapiDocument, loadOpenapiFromMemory} from './parser'; +export { + loadOpenapi, + loadOpenapiDocument, + loadOpenapiFromMemory +} from './parser'; export {processOpenAPIPayloads} from './generators/payloads'; diff --git a/src/codegen/types.ts b/src/codegen/types.ts index 78cf38ac..de660775 100644 --- a/src/codegen/types.ts +++ b/src/codegen/types.ts @@ -156,6 +156,12 @@ export interface ParameterRenderType { generator: GeneratorType; /** Generated files with path and content */ files: GeneratedFile[]; + /** + * Map from model name to the list of exported standalone parameter helper + * function names (interface mode / OpenAPI only), e.g. + * `serializeUrl`. Empty/undefined in class mode. + */ + parameterFunctions?: Record; } export interface HeadersRenderType { channelModels: Record; @@ -353,12 +359,21 @@ export const zodAsyncAPICodegenConfiguration = zodAsyncAPITypescriptConfig; /** * TypeScript configuration for OpenAPI input. */ +export const zodOpenAPIProfile = z + .enum(['types', 'client']) + .describe( + 'High-level profile that expands into the granular generators tuned for OpenAPI REST consumers (plain interfaces, no marshal/unmarshal). "types" generates interface payloads + parameters + headers (with standalone serializer functions); "client" adds the HTTP fetch client on top. Any explicit generators listed alongside a profile are appended, letting you override individual outputs. [Read more about profiles here](https://the-codegen-project.org/docs/generators)' + ); + +export type OpenAPIProfile = z.infer; + export const zodOpenAPITypescriptConfig = z.object({ $schema: z.string().optional().describe(SCHEMA_DESCRIPTION), inputType: z.literal('openapi').describe(DOCUMENT_TYPE_DESCRIPTION), inputPath: z.string().describe(INPUT_PATH_DESCRIPTION), auth: zodInputAuth, ...zodTypeScriptConfigOptions, + profile: zodOpenAPIProfile.optional(), generators: z .array(zodOpenAPITypeScriptGenerators) .describe(GENERATORS_DESCRIPTION), diff --git a/test/codegen/configurations.spec.ts b/test/codegen/configurations.spec.ts index 5a1a6d6e..f5cc73e8 100644 --- a/test/codegen/configurations.spec.ts +++ b/test/codegen/configurations.spec.ts @@ -199,6 +199,44 @@ describe('configuration manager', () => { expect(realizedConfiguration.generators.length).toEqual(4); }); }); + + describe('OpenAPI profiles', () => { + it('expands the "types" profile into interface payloads, parameters and headers', () => { + const configuration: any = { + inputType: 'openapi', + inputPath: 'openapi.json', + language: 'typescript', + profile: 'types', + generators: [] + }; + const realized = realizeConfiguration(configuration); + const byPreset = (preset: string) => + realized.generators.find((generator: any) => generator.preset === preset) as any; + expect(byPreset('payloads')?.modelType).toEqual('interface'); + expect(byPreset('parameters')?.modelType).toEqual('interface'); + expect(byPreset('headers')).toBeDefined(); + // The types profile does not emit channels or a client. + expect(byPreset('channels')).toBeUndefined(); + expect(byPreset('client')).toBeUndefined(); + }); + + it('expands the "client" profile to also include the HTTP fetch client', () => { + const configuration: any = { + inputType: 'openapi', + inputPath: 'openapi.json', + language: 'typescript', + profile: 'client', + generators: [] + }; + const realized = realizeConfiguration(configuration); + const presets = realized.generators.map((generator: any) => generator.preset); + expect(presets).toEqual(expect.arrayContaining(['payloads', 'parameters', 'headers', 'channels', 'client'])); + // Channels reuse the interface-mode payload/parameter generators rather than + // injecting class-mode defaults, so there is exactly one of each. + expect(presets.filter((preset: string) => preset === 'payloads').length).toEqual(1); + expect(presets.filter((preset: string) => preset === 'parameters').length).toEqual(1); + }); + }); }); describe('realizeGeneratorContext with detection', () => { diff --git a/test/codegen/generators/typescript/__snapshots__/parameters.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/parameters.spec.ts.snap index d638d4c2..ffb7eab1 100644 --- a/test/codegen/generators/typescript/__snapshots__/parameters.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/parameters.spec.ts.snap @@ -203,6 +203,52 @@ class MultipleParameterParameters { export { MultipleParameterParameters, MultipleParameterParametersInterface };" `; +exports[`parameters typescript openapi should render plain interfaces with standalone serializer functions in interface mode 1`] = ` +"import {StatusItem} from './StatusItem'; +interface FindPetsByStatusParameters { + /** + * Status values that need to be considered for filter + */ + status: StatusItem[]; +} +export { FindPetsByStatusParameters }; + +/** + * Serialize query parameters for FindPetsByStatusParameters according to the OpenAPI 2.0/3.x specification. + * @returns URLSearchParams with the serialized query parameters + */ +export function serializeFindPetsByStatusParametersQueryParameters(parameters: FindPetsByStatusParameters): URLSearchParams { + const params = new URLSearchParams(); + // Serialize query parameter: status (style: form, explode: false) + if (parameters.status !== undefined && parameters.status !== null) { + const value = parameters.status; + if (Array.isArray(value)) { + params.append('status', value.map(val => encodeURIComponent(String(val))).join(',')); + } else if (typeof value === 'object' && value !== null) { + params.append('status', Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(',')); + } else { + params.append('status', encodeURIComponent(String(value))); + } + } + return params; +} + +/** + * Build the URL for FindPetsByStatusParameters by substituting path parameters into the base + * path template and appending serialized query parameters. + * @param basePath The path template (e.g. '/pet/findByStatus/{status}') + * @returns The path with parameters applied + */ +export function serializeFindPetsByStatusParametersUrl(basePath: string, parameters: FindPetsByStatusParameters): string { + let url = basePath; + const queryString = serializeFindPetsByStatusParametersQueryParameters(parameters).toString(); + if (queryString) { + url += (url.includes('?') ? '&' : '?') + queryString; + } + return url; +}" +`; + exports[`parameters typescript openapi should use constrained property names and skip deserializeUrl without query parameters 1`] = ` " interface GetDocumentParametersInterface { diff --git a/test/codegen/generators/typescript/parameters.spec.ts b/test/codegen/generators/typescript/parameters.spec.ts index 7f15e871..a52a1382 100644 --- a/test/codegen/generators/typescript/parameters.spec.ts +++ b/test/codegen/generators/typescript/parameters.spec.ts @@ -15,6 +15,7 @@ describe('parameters', () => { serializationType: 'json', outputPath: path.resolve(__dirname, './output'), preset: 'parameters', + modelType: 'class', language: 'typescript', dependencies: [], id: 'test' @@ -34,6 +35,7 @@ describe('parameters', () => { serializationType: 'json', outputPath: path.resolve(__dirname, './output'), preset: 'parameters', + modelType: 'class', language: 'typescript', dependencies: [], id: 'test' @@ -53,6 +55,7 @@ describe('parameters', () => { serializationType: 'json', outputPath: path.resolve(__dirname, './output'), preset: 'parameters', + modelType: 'class', language: 'typescript', dependencies: [], id: 'test' @@ -74,6 +77,7 @@ describe('parameters', () => { serializationType: 'json', outputPath: path.resolve(__dirname, './output'), preset: 'parameters', + modelType: 'class', language: 'typescript', dependencies: [], id: 'test' @@ -98,7 +102,43 @@ describe('parameters', () => { expect(renderedContent.channelModels['updateUser']?.result).toMatchSnapshot(); expect(renderedContent.channelModels['uploadFile']?.result).toMatchSnapshot(); }); - + + it('should render plain interfaces with standalone serializer functions in interface mode', async () => { + const parsedOpenAPIDocument = await loadOpenapiDocument(path.resolve(__dirname, '../../../configs/openapi-3.json')); + + const renderedContent = await generateTypescriptParameters({ + generator: { + serializationType: 'json', + outputPath: path.resolve(__dirname, './output'), + preset: 'parameters', + modelType: 'interface', + language: 'typescript', + dependencies: [], + id: 'test' + }, + inputType: 'openapi', + openapiDocument: parsedOpenAPIDocument, + dependencyOutputs: { } + }); + + const findByStatus = renderedContent.channelModels['findPetsByStatus']; + const findByStatusFile = renderedContent.files.find((file) => + file.path.endsWith(`/${findByStatus?.modelName}.ts`) + ); + // Plain interface, no class / marshalling methods. + expect(findByStatusFile?.content).toContain(`interface ${findByStatus?.modelName} {`); + expect(findByStatusFile?.content).not.toContain('class '); + expect(findByStatusFile?.content).not.toContain('marshal'); + // Standalone serializer functions are appended and exported. + expect(findByStatusFile?.content).toContain(`export function serialize${findByStatus?.modelName}QueryParameters`); + expect(findByStatusFile?.content).toContain(`export function serialize${findByStatus?.modelName}Url`); + // The exported function names are recorded for the channels/client generators. + expect(renderedContent.parameterFunctions?.[findByStatus?.modelName ?? '']).toContain( + `serialize${findByStatus?.modelName}Url` + ); + expect(findByStatusFile?.content).toMatchSnapshot(); + }); + it('should work with OpenAPI 3.1 that contains parameters', async () => { const parsedOpenAPIDocument = await loadOpenapiDocument(path.resolve(__dirname, '../../../configs/openapi-3_1.json')); @@ -107,6 +147,7 @@ describe('parameters', () => { serializationType: 'json', outputPath: path.resolve(__dirname, './output'), preset: 'parameters', + modelType: 'class', language: 'typescript', dependencies: [], id: 'test' @@ -140,6 +181,7 @@ describe('parameters', () => { serializationType: 'json', outputPath: path.resolve(__dirname, './output'), preset: 'parameters', + modelType: 'class', language: 'typescript', dependencies: [], id: 'test' @@ -215,6 +257,7 @@ describe('parameters', () => { serializationType: 'json', outputPath: path.resolve(__dirname, './output'), preset: 'parameters', + modelType: 'class', language: 'typescript', dependencies: [], id: 'test' @@ -259,6 +302,7 @@ describe('parameters', () => { serializationType: 'json', outputPath: path.resolve(__dirname, './output'), preset: 'parameters', + modelType: 'class', language: 'typescript', dependencies: [], id: 'test'