From c8b439a518247dc3ae5d541714ddff078d5a40ec Mon Sep 17 00:00:00 2001 From: Lagoni Date: Mon, 6 Jul 2026 20:11:05 +0200 Subject: [PATCH 1/2] fix: use constrained property names in generated OpenAPI parameter models The OpenAPI parameters preset interpolated the raw parameter name into this. accesses of generated serialization/deserialization methods, while Modelina constrains the actual class property to camelCase. Any non-camelCase parameter (e.g. 'Skip', 'Cvr') produced code that fails to compile with TS2551. The generated static fromUrl method also unconditionally called instance.deserializeUrl(url), but deserializeUrl is only generated when the operation has query parameters, so path-parameter-only operations produced code that fails to compile. Generated methods now use the Modelina-constrained property name for property accesses while keeping the raw name for the wire format (URL templates and query string keys), and fromUrl only calls deserializeUrl when it exists. Co-Authored-By: Claude Fable 5 --- .../inputs/openapi/generators/parameters.ts | 373 +++++++++--------- .../__snapshots__/parameters.spec.ts.snap | 292 ++++++++++++-- .../generators/typescript/parameters.spec.ts | 78 ++++ 3 files changed, 528 insertions(+), 215 deletions(-) diff --git a/src/codegen/inputs/openapi/generators/parameters.ts b/src/codegen/inputs/openapi/generators/parameters.ts index 8e75944c..7a9cb3bc 100644 --- a/src/codegen/inputs/openapi/generators/parameters.ts +++ b/src/codegen/inputs/openapi/generators/parameters.ts @@ -18,6 +18,40 @@ const X_PARAMETER_EXPLODE = 'x-parameter-explode'; const X_PARAMETER_ALLOW_RESERVED = 'x-parameter-allowReserved'; const X_PARAMETER_COLLECTION_FORMAT = 'x-parameter-collectionFormat'; +/** + * Serialization configuration for a single path or query parameter. + * + * `name` is the raw parameter name from the OpenAPI document and is what goes + * on the wire (URL templates, query string keys). `propertyName` is the + * constrained TypeScript property name rendered by Modelina (e.g. `Skip` -> + * `skip`) and is what `this.` accesses in generated methods must use. + */ +interface ParameterConfig { + name: string; + propertyName: string; + style: string; + explode: boolean; + allowReserved: boolean; +} + +/** + * Map the raw OpenAPI parameter name to the constrained TypeScript property + * name rendered by Modelina, falling back to the raw name when the model does + * not expose the property. + */ +function getConstrainedPropertyName({ + model, + parameterName +}: { + model: ConstrainedObjectModel; + parameterName: string; +}): string { + const constrainedProperty = Object.values(model.properties).find( + (property) => property.unconstrainedPropertyName === parameterName + ); + return constrainedProperty?.propertyName ?? parameterName; +} + // OpenAPI parameter processor export function processOpenAPIParameters( openapiDocument: @@ -175,26 +209,26 @@ function generateOpenAPIParameterMethods(model: ConstrainedObjectModel) { const properties = model.originalInput?.properties ?? {}; // Collect path and query parameters - const pathParams: Array<{ - name: string; - style: string; - explode: boolean; - allowReserved: boolean; - }> = []; - const queryParams: Array<{ - name: string; - style: string; - explode: boolean; - allowReserved: boolean; - }> = []; + const pathParams: ParameterConfig[] = []; + const queryParams: ParameterConfig[] = []; for (const [propName, propSchema] of Object.entries(properties)) { const paramConfig = processParameterSchema(propName, propSchema); if (paramConfig) { + 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(paramConfig); + pathParams.push(parameterConfig); } else if (paramConfig.location === 'query') { - queryParams.push(paramConfig); + queryParams.push(parameterConfig); } } } @@ -274,18 +308,8 @@ function processParameterSchema( * Generate all serialization methods */ function generateSerializationMethods( - pathParams: Array<{ - name: string; - style: string; - explode: boolean; - allowReserved: boolean; - }>, - queryParams: Array<{ - name: string; - style: string; - explode: boolean; - allowReserved: boolean; - }> + pathParams: ParameterConfig[], + queryParams: ParameterConfig[] ): string { let methods = ''; @@ -312,12 +336,7 @@ function generateSerializationMethods( * Generate path parameter serialization method */ function generatePathSerializationMethod( - pathParams: Array<{ - name: string; - style: string; - explode: boolean; - allowReserved: boolean; - }> + pathParams: ParameterConfig[] ): string { const paramSerializations = pathParams .map((param) => generatePathParameterSerialization(param)) @@ -341,12 +360,7 @@ ${paramSerializations} * Generate query parameter serialization method */ function generateQuerySerializationMethod( - queryParams: Array<{ - name: string; - style: string; - explode: boolean; - allowReserved: boolean; - }> + queryParams: ParameterConfig[] ): string { const paramSerializations = queryParams .map((param) => generateQueryParameterSerialization(param)) @@ -421,18 +435,13 @@ getChannelWithParameters(basePath: string): string { /** * Generate serialization code for a single path parameter */ -function generatePathParameterSerialization(param: { - name: string; - style: string; - explode: boolean; - allowReserved: boolean; -}): string { - const {name, style, explode, allowReserved} = param; +function generatePathParameterSerialization(param: ParameterConfig): string { + const {name, propertyName, style, explode, allowReserved} = param; const encoding = allowReserved ? '' : 'encodeURIComponent'; return ` // Serialize path parameter: ${name} (style: ${style}, explode: ${explode}) - if (this.${name} !== undefined && this.${name} !== null) { - const value = this.${name}; + if (this.${propertyName} !== undefined && this.${propertyName} !== null) { + const value = this.${propertyName}; ${generatePathSerializationLogic(name, style, explode, encoding)} }`; } @@ -440,18 +449,13 @@ function generatePathParameterSerialization(param: { /** * Generate serialization code for a single query parameter */ -function generateQueryParameterSerialization(param: { - name: string; - style: string; - explode: boolean; - allowReserved: boolean; -}): string { - const {name, style, explode, allowReserved} = param; +function generateQueryParameterSerialization(param: ParameterConfig): string { + const {name, propertyName, style, explode, allowReserved} = param; const encoding = allowReserved ? '' : 'encodeURIComponent'; return ` // Serialize query parameter: ${name} (style: ${style}, explode: ${explode}) - if (this.${name} !== undefined && this.${name} !== null) { - const value = this.${name}; + if (this.${propertyName} !== undefined && this.${propertyName} !== null) { + const value = this.${propertyName}; ${generateQuerySerializationLogic(name, style, explode, encoding)} }`; } @@ -740,18 +744,8 @@ function convertCollectionFormatToStyleAndExplode( * Generate all deserialization methods */ function generateDeserializationMethods( - pathParams: Array<{ - name: string; - style: string; - explode: boolean; - allowReserved: boolean; - }>, - queryParams: Array<{ - name: string; - style: string; - explode: boolean; - allowReserved: boolean; - }>, + pathParams: ParameterConfig[], + queryParams: ParameterConfig[], model: ConstrainedObjectModel ): string { let methods = ''; @@ -762,7 +756,11 @@ function generateDeserializationMethods( } // Generate static fromUrl method - methods += generateFromUrlStaticMethod(pathParams, model); + methods += generateFromUrlStaticMethod({ + pathParams, + hasQueryParams: queryParams.length > 0, + model + }); return methods; } @@ -771,12 +769,7 @@ function generateDeserializationMethods( * Generate URL deserialization method */ function generateUrlDeserializationMethod( - queryParams: Array<{ - name: string; - style: string; - explode: boolean; - allowReserved: boolean; - }>, + queryParams: ParameterConfig[], model: ConstrainedObjectModel ): string { const paramDeserializations = queryParams @@ -811,15 +804,15 @@ ${paramDeserializations} /** * Generate static fromUrl method */ -function generateFromUrlStaticMethod( - pathParams: Array<{ - name: string; - style: string; - explode: boolean; - allowReserved: boolean; - }>, - model: ConstrainedObjectModel -): string { +function generateFromUrlStaticMethod({ + pathParams, + hasQueryParams, + model +}: { + pathParams: ParameterConfig[]; + hasQueryParams: boolean; + model: ConstrainedObjectModel; +}): string { const properties = model.originalInput?.properties ?? {}; const requiredParams: string[] = []; @@ -837,13 +830,16 @@ function generateFromUrlStaticMethod( // Generate constructor arguments with path parameters first, then required non-path parameters const pathParamArgs = pathParams - .map((param) => `${param.name}: pathParams.${param.name}`) + .map((param) => `${param.propertyName}: pathParams.${param.propertyName}`) .join(', '); const requiredNonPathParams = requiredParams.filter( (param) => !pathParams.some((pathParam) => pathParam.name === param) ); const requiredParamArgs = requiredNonPathParams - .map((param) => `${param}: default${pascalCase(param)}`) + .map( + (param) => + `${getConstrainedPropertyName({model, parameterName: param})}: default${pascalCase(param)}` + ) .join(', '); let constructorArgs = ''; @@ -877,6 +873,12 @@ function generateFromUrlStaticMethod( .join('') : ''; + // deserializeUrl only exists on the class when there are query parameters + const deserializeUrlCall = hasQueryParams + ? ` + instance.deserializeUrl(url);` + : ''; + return ` /** @@ -888,8 +890,7 @@ ${paramDocs} */ static fromUrl(url: string, basePath: string${functionParams}): ${model.type} { ${pathParamExtraction} - const instance = new ${model.type}({ ${constructorArgs} }); - instance.deserializeUrl(url); + const instance = new ${model.type}({ ${constructorArgs} });${deserializeUrlCall} return instance; }`; } @@ -898,12 +899,7 @@ static fromUrl(url: string, basePath: string${functionParams}): ${model.type} { * Generate path parameter extraction logic for the fromUrl method */ function generatePathParameterExtraction( - pathParams: Array<{ - name: string; - style: string; - explode: boolean; - allowReserved: boolean; - }> + pathParams: ParameterConfig[] ): string { if (pathParams.length === 0) { return ''; @@ -917,24 +913,14 @@ function generatePathParameterExtraction( * Generate deserialization code for a single query parameter */ function generateQueryParameterDeserialization( - param: { - name: string; - style: string; - explode: boolean; - allowReserved: boolean; - }, + param: ParameterConfig, model: ConstrainedObjectModel ): string { const {name, style, explode} = param; const properties = model.originalInput?.properties ?? {}; const propSchema = properties[name]; - const logicCode = generateQueryDeserializationLogic( - name, - style, - explode, - propSchema - ); + const logicCode = generateQueryDeserializationLogic({param, propSchema}); return ` // Deserialize query parameter: ${name} (style: ${style}, explode: ${explode}) if (params.has('${name}')) { @@ -946,96 +932,100 @@ function generateQueryParameterDeserialization( /** * Generate query parameter deserialization logic */ -function generateQueryDeserializationLogic( - name: string, - style: string, - explode: boolean, - propSchema: any -): string { +function generateQueryDeserializationLogic({ + param, + propSchema +}: { + param: ParameterConfig; + propSchema: any; +}): string { const isArray = propSchema?.type === 'array' || propSchema?.items; const isBoolean = propSchema?.type === 'boolean'; const isNumber = propSchema?.type === 'integer' || propSchema?.type === 'number'; const paramType = getParameterType(propSchema); - switch (style) { + switch (param.style) { case 'form': - return generateFormStyleDeserializationLogic( - name, - explode, + return generateFormStyleDeserializationLogic({ + param, isArray, isBoolean, isNumber, paramType - ); + }); case 'spaceDelimited': - return generateSpaceDelimitedDeserializationLogic( - name, - explode, + return generateSpaceDelimitedDeserializationLogic({ + param, isArray, isBoolean, isNumber, paramType - ); + }); case 'pipeDelimited': - return generatePipeDelimitedDeserializationLogic( - name, - explode, + return generatePipeDelimitedDeserializationLogic({ + param, isArray, isBoolean, isNumber, paramType - ); + }); case 'deepObject': - return generateDeepObjectDeserializationLogic( - name, + return generateDeepObjectDeserializationLogic({ + param, isBoolean, isNumber, paramType - ); + }); default: - throw new Error(`Unsupported style: ${style}`); + throw new Error(`Unsupported style: ${param.style}`); } } /** * Generate deserialization logic for form style */ -function generateFormStyleDeserializationLogic( - name: string, - explode: boolean, - isArray: boolean, - isBoolean: boolean, - isNumber: boolean, - paramType: string -): string { +function generateFormStyleDeserializationLogic({ + param, + isArray, + isBoolean, + isNumber, + paramType +}: { + param: ParameterConfig; + isArray: boolean; + isBoolean: boolean; + isNumber: boolean; + paramType: string; +}): string { + const {name, propertyName, explode} = param; if (isArray && !explode) { const typecast = paramType.includes('[]') ? ` as ${paramType}` : ''; return `if (value === '') { - this.${name} = []; + this.${propertyName} = []; } else if (value) { // Split by comma and decode const decodedValues = value.split(',').map(val => decodeURIComponent(val.trim())); - this.${name} = decodedValues${typecast}; + this.${propertyName} = decodedValues${typecast}; }`; } else if (isArray && explode) { const typecast = paramType.includes('[]') ? ` as ${paramType}` : ''; return `const allValues = params.getAll('${name}'); if (allValues.length > 0) { const decodedValues = allValues.map(val => decodeURIComponent(val)); - this.${name} = decodedValues${typecast}; + this.${propertyName} = decodedValues${typecast}; }`; } else if (isBoolean) { return `if (value) { const decodedValue = decodeURIComponent(value); - this.${name} = decodedValue.toLowerCase() === 'true'; + this.${propertyName} = decodedValue.toLowerCase() === 'true'; }`; } else if (isNumber) { return `if (value) { const decodedValue = decodeURIComponent(value); const numValue = Number(decodedValue); if (!isNaN(numValue)) { - this.${name} = numValue; + this.${propertyName} = numValue; } }`; } @@ -1045,81 +1035,93 @@ function generateFormStyleDeserializationLogic( : ''; return `if (value) { const decodedValue = decodeURIComponent(value); - this.${name} = decodedValue${typecast}; + this.${propertyName} = decodedValue${typecast}; }`; } /** * Generate deserialization logic for space delimited style */ -function generateSpaceDelimitedDeserializationLogic( - name: string, - explode: boolean, - isArray: boolean, - isBoolean: boolean, - isNumber: boolean, - paramType: string -): string { - if (isArray && !explode) { +function generateSpaceDelimitedDeserializationLogic({ + param, + isArray, + isBoolean, + isNumber, + paramType +}: { + param: ParameterConfig; + isArray: boolean; + isBoolean: boolean; + isNumber: boolean; + paramType: string; +}): string { + if (isArray && !param.explode) { const typecast = paramType.includes('[]') ? ` as ${paramType}` : ''; return `if (value === '') { - this.${name} = []; + this.${param.propertyName} = []; } else if (value) { // Split by space and decode const decodedValues = value.split(' ').map(val => decodeURIComponent(val.trim())); - this.${name} = decodedValues${typecast}; + this.${param.propertyName} = decodedValues${typecast}; }`; } - return generateFormStyleDeserializationLogic( - name, - explode, + return generateFormStyleDeserializationLogic({ + param, isArray, isBoolean, isNumber, paramType - ); + }); } /** * Generate deserialization logic for pipe delimited style */ -function generatePipeDelimitedDeserializationLogic( - name: string, - explode: boolean, - isArray: boolean, - isBoolean: boolean, - isNumber: boolean, - paramType: string -): string { - if (isArray && !explode) { +function generatePipeDelimitedDeserializationLogic({ + param, + isArray, + isBoolean, + isNumber, + paramType +}: { + param: ParameterConfig; + isArray: boolean; + isBoolean: boolean; + isNumber: boolean; + paramType: string; +}): string { + if (isArray && !param.explode) { const typecast = paramType.includes('[]') ? ` as ${paramType}` : ''; return `if (value === '') { - this.${name} = []; + this.${param.propertyName} = []; } else if (value) { // Split by pipe and decode const decodedValues = value.split('|').map(val => decodeURIComponent(val.trim())); - this.${name} = decodedValues${typecast}; + this.${param.propertyName} = decodedValues${typecast}; }`; } - return generateFormStyleDeserializationLogic( - name, - explode, + return generateFormStyleDeserializationLogic({ + param, isArray, isBoolean, isNumber, paramType - ); + }); } /** * Generate deserialization logic for deep object style */ -function generateDeepObjectDeserializationLogic( - name: string, - isBoolean: boolean, - isNumber: boolean, - paramType: string -): string { +function generateDeepObjectDeserializationLogic({ + param, + paramType +}: { + param: ParameterConfig; + isBoolean: boolean; + isNumber: boolean; + paramType: string; +}): string { + const {name, propertyName} = param; const nameLength = name.length + 1; const typecast = paramType !== 'any' && paramType !== 'any | undefined' @@ -1134,7 +1136,7 @@ function generateDeepObjectDeserializationLogic( } } if (Object.keys(deepObjectParams).length > 0) { - this.${name} = deepObjectParams${typecast}; + this.${propertyName} = deepObjectParams${typecast}; }`; } @@ -1190,12 +1192,7 @@ function generateDefaultValue(propSchema: any, paramName: string): string { * Generate the extractPathParameters static method */ function generateExtractPathParametersMethod( - pathParams: Array<{ - name: string; - style: string; - explode: boolean; - allowReserved: boolean; - }>, + pathParams: ParameterConfig[], model: ConstrainedObjectModel ): string { const properties = model.originalInput?.properties ?? {}; @@ -1209,7 +1206,7 @@ function generateExtractPathParametersMethod( const typecast = paramType !== 'string' ? ` as ${paramType}` : ''; return ` case '${param.name}': - result.${param.name} = ${conversion}${typecast}; + result.${param.propertyName} = ${conversion}${typecast}; break;`; }) .join('\n'); @@ -1222,7 +1219,7 @@ function generateExtractPathParametersMethod( * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') * @returns Object containing extracted path parameter values */ -private static extractPathParameters(url: string, basePath: string): { ${pathParams.map((p) => `${p.name}: ${getParameterType(properties[p.name])}`).join(', ')} } { +private static extractPathParameters(url: string, basePath: string): { ${pathParams.map((p) => `${p.propertyName}: ${getParameterType(properties[p.name])}`).join(', ')} } { // Remove query string from URL for path matching const urlPath = url.split('?')[0]; diff --git a/test/codegen/generators/typescript/__snapshots__/parameters.spec.ts.snap b/test/codegen/generators/typescript/__snapshots__/parameters.spec.ts.snap index babddc6e..11d024f6 100644 --- a/test/codegen/generators/typescript/__snapshots__/parameters.spec.ts.snap +++ b/test/codegen/generators/typescript/__snapshots__/parameters.spec.ts.snap @@ -199,6 +199,271 @@ class MultipleParameterParameters { export { MultipleParameterParameters };" `; +exports[`parameters typescript openapi should use constrained property names and skip deserializeUrl without query parameters 1`] = ` +" +class GetDocumentParameters { + private _documentId: string; + + constructor(input: { + documentId: string, + }) { + this._documentId = input.documentId; + } + + get documentId(): string { return this._documentId; } + set documentId(documentId: string) { this._documentId = documentId; } + + + + /** + * Serialize path parameters according to OpenAPI 2.0/3.x specification + * @returns Record of parameter names to their serialized values for path substitution + */ + serializePathParameters(): Record { + const result: Record = {}; + + // Serialize path parameter: documentId (style: simple, explode: false) + if (this.documentId !== undefined && this.documentId !== null) { + const value = this.documentId; + if (Array.isArray(value)) { + result['documentId'] = value.map(val => encodeURIComponent(String(val))).join(','); + } else if (typeof value === 'object' && value !== null) { + result['documentId'] = Object.entries(value).map(([key, val]) => \`\${encodeURIComponent(key)},\${encodeURIComponent(String(val))}\`).join(','); + } else { + result['documentId'] = encodeURIComponent(String(value)); + } + } + + return result; + } + /** + * Get the complete serialized URL with path and query parameters + * @param basePath The base path template (e.g., '/users/{id}') + * @returns The complete URL with serialized parameters + */ + serializeUrl(basePath: string): string { + let url = basePath; + + // Replace path parameters + + const pathParams = this.serializePathParameters(); + for (const [name, value] of Object.entries(pathParams)) { + url = url.replace(new RegExp(\`{\${name}}\`, 'g'), value); + } + + // Add query parameters + + + return url; + } + + /** + * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) + * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') + * @returns The path with parameters replaced + */ + getChannelWithParameters(basePath: string): string { + return this.serializeUrl(basePath); + } + + /** + * Static method to create a new instance from a URL + * @param url The URL to parse + * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') + + * @returns A new GetDocumentParameters instance + */ + static fromUrl(url: string, basePath: string): GetDocumentParameters { + // Extract path parameters from URL + const pathParams = this.extractPathParameters(url, basePath); + const instance = new GetDocumentParameters({ documentId: pathParams.documentId }); + return instance; + } + + /** + * Extract path parameters from a URL using a base path template + * @param url The URL to extract parameters from + * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') + * @returns Object containing extracted path parameter values + */ + private static extractPathParameters(url: string, basePath: string): { documentId: string } { + // Remove query string from URL for path matching + const urlPath = url.split('?')[0]; + + // Create regex pattern from base path template + const regexPattern = basePath.replace(/\\{([^}]+)\\}/g, '([^/]+)'); + const regex = new RegExp('^' + regexPattern + '$'); + + const match = urlPath.match(regex); + if (!match) { + throw new Error(\`URL path '\${urlPath}' does not match base path template '\${basePath}'\`); + } + + // Extract parameter names from base path template + const paramNames = basePath.match(/\\{([^}]+)\\}/g)?.map(p => p.slice(1, -1)) || []; + + // Map matched values to parameter names + const result: any = {}; + paramNames.forEach((paramName, index) => { + const rawValue = match[index + 1]; + const decodeValue = decodeURIComponent(rawValue); + switch (paramName) { + case 'documentId': + result.documentId = decodeValue; + break; + default: + result[paramName] = decodeValue; + } + }); + + return result; + } +} +export { GetDocumentParameters };" +`; + +exports[`parameters typescript openapi should use constrained property names and skip deserializeUrl without query parameters 2`] = ` +" +class GetTransactionsParameters { + private _skip?: number; + private _cvr?: string; + + constructor(input: { + skip?: number, + cvr?: string, + }) { + this._skip = input.skip; + this._cvr = input.cvr; + } + + get skip(): number | undefined { return this._skip; } + set skip(skip: number | undefined) { this._skip = skip; } + + get cvr(): string | undefined { return this._cvr; } + set cvr(cvr: string | undefined) { this._cvr = cvr; } + + + + /** + * Serialize query parameters according to OpenAPI 2.0/3.x specification + * @returns URLSearchParams object with serialized query parameters + */ + serializeQueryParameters(): URLSearchParams { + const params = new URLSearchParams(); + + // Serialize query parameter: Skip (style: form, explode: true) + if (this.skip !== undefined && this.skip !== null) { + const value = this.skip; + if (Array.isArray(value)) { + value.forEach(val => params.append('Skip', encodeURIComponent(String(val)))); + } else if (typeof value === 'object' && value !== null) { + Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); + } else { + params.append('Skip', encodeURIComponent(String(value))); + } + } + // Serialize query parameter: Cvr (style: form, explode: true) + if (this.cvr !== undefined && this.cvr !== null) { + const value = this.cvr; + if (Array.isArray(value)) { + value.forEach(val => params.append('Cvr', encodeURIComponent(String(val)))); + } else if (typeof value === 'object' && value !== null) { + Object.entries(value).forEach(([key, val]) => params.append(encodeURIComponent(key), encodeURIComponent(String(val)))); + } else { + params.append('Cvr', encodeURIComponent(String(value))); + } + } + + return params; + } + /** + * Get the complete serialized URL with path and query parameters + * @param basePath The base path template (e.g., '/users/{id}') + * @returns The complete URL with serialized parameters + */ + serializeUrl(basePath: string): string { + let url = basePath; + + // Replace path parameters + + + // Add query parameters + + const queryParams = this.serializeQueryParameters(); + const queryString = queryParams.toString(); + if (queryString) { + url += (url.includes('?') ? '&' : '?') + queryString; + } + + return url; + } + + /** + * Get the channel path with parameters substituted (compatible with AsyncAPI channel interface) + * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') + * @returns The path with parameters replaced + */ + getChannelWithParameters(basePath: string): string { + return this.serializeUrl(basePath); + } + /** + * Deserialize URL and populate instance properties from query parameters + * @param url The URL to parse (can be full URL or just query string) + */ + deserializeUrl(url: string): void { + // Extract query string from URL + let queryString = ''; + if (url.includes('?')) { + queryString = url.split('?')[1]; + } else if (url.includes('=')) { + // Assume it's already a query string + queryString = url; + } + + if (!queryString) { + return; + } + + const params = new URLSearchParams(queryString); + + // Deserialize query parameter: Skip (style: form, explode: true) + if (params.has('Skip')) { + const value = params.get('Skip'); + if (value) { + const decodedValue = decodeURIComponent(value); + const numValue = Number(decodedValue); + if (!isNaN(numValue)) { + this.skip = numValue; + } + } + } + // Deserialize query parameter: Cvr (style: form, explode: true) + if (params.has('Cvr')) { + const value = params.get('Cvr'); + if (value) { + const decodedValue = decodeURIComponent(value); + this.cvr = decodedValue; + } + } + } + + /** + * Static method to create a new instance from a URL + * @param url The URL to parse + * @param basePath The base path template (e.g., '/pet/findByStatus/{status}/{categoryId}') + + * @returns A new GetTransactionsParameters instance + */ + static fromUrl(url: string, basePath: string): GetTransactionsParameters { + + const instance = new GetTransactionsParameters({ }); + instance.deserializeUrl(url); + return instance; + } +} +export { GetTransactionsParameters };" +`; + exports[`parameters typescript openapi should work with OpenAPI 2.0 that contains parameters 1`] = ` " class DeleteOrderParameters { @@ -280,7 +545,6 @@ class DeleteOrderParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new DeleteOrderParameters({ orderId: pathParams.orderId }); - instance.deserializeUrl(url); return instance; } @@ -407,7 +671,6 @@ class DeletePetParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new DeletePetParameters({ petId: pathParams.petId }); - instance.deserializeUrl(url); return instance; } @@ -534,7 +797,6 @@ class DeleteUserParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new DeleteUserParameters({ username: pathParams.username }); - instance.deserializeUrl(url); return instance; } @@ -897,7 +1159,6 @@ class GetOrderByIdParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new GetOrderByIdParameters({ orderId: pathParams.orderId }); - instance.deserializeUrl(url); return instance; } @@ -1024,7 +1285,6 @@ class GetPetByIdParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new GetPetByIdParameters({ petId: pathParams.petId }); - instance.deserializeUrl(url); return instance; } @@ -1151,7 +1411,6 @@ class GetUserByNameParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new GetUserByNameParameters({ username: pathParams.username }); - instance.deserializeUrl(url); return instance; } @@ -1424,7 +1683,6 @@ class UpdatePetWithFormParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new UpdatePetWithFormParameters({ petId: pathParams.petId }); - instance.deserializeUrl(url); return instance; } @@ -1551,7 +1809,6 @@ class UpdateUserParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new UpdateUserParameters({ username: pathParams.username }); - instance.deserializeUrl(url); return instance; } @@ -1678,7 +1935,6 @@ class UploadFileParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new UploadFileParameters({ petId: pathParams.petId }); - instance.deserializeUrl(url); return instance; } @@ -1805,7 +2061,6 @@ class DeleteOrderParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new DeleteOrderParameters({ orderId: pathParams.orderId }); - instance.deserializeUrl(url); return instance; } @@ -1932,7 +2187,6 @@ class DeletePetParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new DeletePetParameters({ petId: pathParams.petId }); - instance.deserializeUrl(url); return instance; } @@ -2059,7 +2313,6 @@ class DeleteUserParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new DeleteUserParameters({ username: pathParams.username }); - instance.deserializeUrl(url); return instance; } @@ -2426,7 +2679,6 @@ class GetOrderByIdParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new GetOrderByIdParameters({ orderId: pathParams.orderId }); - instance.deserializeUrl(url); return instance; } @@ -2553,7 +2805,6 @@ class GetPetByIdParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new GetPetByIdParameters({ petId: pathParams.petId }); - instance.deserializeUrl(url); return instance; } @@ -2680,7 +2931,6 @@ class GetUserByNameParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new GetUserByNameParameters({ username: pathParams.username }); - instance.deserializeUrl(url); return instance; } @@ -2953,7 +3203,6 @@ class UpdatePetWithFormParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new UpdatePetWithFormParameters({ petId: pathParams.petId }); - instance.deserializeUrl(url); return instance; } @@ -3080,7 +3329,6 @@ class UpdateUserParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new UpdateUserParameters({ username: pathParams.username }); - instance.deserializeUrl(url); return instance; } @@ -3207,7 +3455,6 @@ class UploadFileParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new UploadFileParameters({ petId: pathParams.petId }); - instance.deserializeUrl(url); return instance; } @@ -3334,7 +3581,6 @@ class DeleteOrderParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new DeleteOrderParameters({ orderId: pathParams.orderId }); - instance.deserializeUrl(url); return instance; } @@ -3461,7 +3707,6 @@ class DeletePetParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new DeletePetParameters({ petId: pathParams.petId }); - instance.deserializeUrl(url); return instance; } @@ -3588,7 +3833,6 @@ class DeleteUserParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new DeleteUserParameters({ username: pathParams.username }); - instance.deserializeUrl(url); return instance; } @@ -3955,7 +4199,6 @@ class GetOrderByIdParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new GetOrderByIdParameters({ orderId: pathParams.orderId }); - instance.deserializeUrl(url); return instance; } @@ -4082,7 +4325,6 @@ class GetPetByIdParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new GetPetByIdParameters({ petId: pathParams.petId }); - instance.deserializeUrl(url); return instance; } @@ -4209,7 +4451,6 @@ class GetUserByNameParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new GetUserByNameParameters({ username: pathParams.username }); - instance.deserializeUrl(url); return instance; } @@ -4482,7 +4723,6 @@ class UpdatePetWithFormParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new UpdatePetWithFormParameters({ petId: pathParams.petId }); - instance.deserializeUrl(url); return instance; } @@ -4609,7 +4849,6 @@ class UpdateUserParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new UpdateUserParameters({ username: pathParams.username }); - instance.deserializeUrl(url); return instance; } @@ -4736,7 +4975,6 @@ class UploadFileParameters { // Extract path parameters from URL const pathParams = this.extractPathParameters(url, basePath); const instance = new UploadFileParameters({ petId: pathParams.petId }); - instance.deserializeUrl(url); return instance; } diff --git a/test/codegen/generators/typescript/parameters.spec.ts b/test/codegen/generators/typescript/parameters.spec.ts index de161cb0..27a37f6e 100644 --- a/test/codegen/generators/typescript/parameters.spec.ts +++ b/test/codegen/generators/typescript/parameters.spec.ts @@ -164,6 +164,84 @@ describe('parameters', () => { expect(renderedContent.channelModels['uploadFile']?.result).toMatchSnapshot(); }); + it('should use constrained property names and skip deserializeUrl without query parameters', async () => { + const openAPIDoc = { + openapi: '3.0.0', + info: { + title: 'Test API', + version: '1.0.0' + }, + paths: { + '/v2/documents/{documentId}': { + get: { + operationId: 'getDocument', + parameters: [ + { + name: 'documentId', + in: 'path', + required: true, + schema: { type: 'string' } + } + ], + responses: { '200': { description: 'ok' } } + } + }, + '/v2/transactions': { + get: { + operationId: 'getTransactions', + parameters: [ + { + name: 'Skip', + in: 'query', + required: false, + schema: { type: 'integer' } + }, + { + name: 'Cvr', + in: 'query', + required: false, + schema: { type: 'string' } + } + ], + responses: { '200': { description: 'ok' } } + } + } + } + }; + + const renderedContent = await generateTypescriptParameters({ + generator: { + serializationType: 'json', + outputPath: path.resolve(__dirname, './output'), + preset: 'parameters', + language: 'typescript', + dependencies: [], + id: 'test' + }, + inputType: 'openapi', + openapiDocument: openAPIDoc, + dependencyOutputs: { } + }); + + // Path-only parameters must not reference the deserializeUrl method that only exists with query parameters + const pathOnlyResult = renderedContent.channelModels['getDocument']?.result; + expect(pathOnlyResult).toBeDefined(); + expect(pathOnlyResult).not.toContain('deserializeUrl'); + + // Non-camelCase parameter names must access the constrained property while keeping the raw wire name + const queryResult = renderedContent.channelModels['getTransactions']?.result; + expect(queryResult).toBeDefined(); + expect(queryResult).not.toContain('this.Skip'); + expect(queryResult).not.toContain('this.Cvr'); + expect(queryResult).toContain('this.skip'); + expect(queryResult).toContain('this.cvr'); + expect(queryResult).toContain(`params.append('Skip'`); + expect(queryResult).toContain(`params.has('Cvr')`); + + expect(pathOnlyResult).toMatchSnapshot(); + expect(queryResult).toMatchSnapshot(); + }); + it('should handle empty OpenAPI document with no operations', async () => { // Create a minimal OpenAPI document with no paths/operations const minimalOpenAPIDoc = { From 1ad59c587ff4340d5d38af3c3bd25b85d8342b46 Mon Sep 17 00:00:00 2001 From: Lagoni Date: Mon, 6 Jul 2026 20:17:34 +0200 Subject: [PATCH 2/2] fix: resolve lint and test typecheck errors in parameters spec Co-Authored-By: Claude Fable 5 --- test/codegen/generators/typescript/parameters.spec.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/codegen/generators/typescript/parameters.spec.ts b/test/codegen/generators/typescript/parameters.spec.ts index 27a37f6e..7f15e871 100644 --- a/test/codegen/generators/typescript/parameters.spec.ts +++ b/test/codegen/generators/typescript/parameters.spec.ts @@ -1,4 +1,5 @@ import path from "node:path"; +import type { OpenAPIV3 } from "openapi-types"; import { generateTypescriptParameters } from "../../../../src/codegen/generators"; import { loadAsyncapiDocument } from "../../../../src/codegen/inputs/asyncapi"; import { loadOpenapiDocument } from "../../../../src/codegen/inputs/openapi"; @@ -165,7 +166,7 @@ describe('parameters', () => { }); it('should use constrained property names and skip deserializeUrl without query parameters', async () => { - const openAPIDoc = { + const openAPIDoc: OpenAPIV3.Document = { openapi: '3.0.0', info: { title: 'Test API', @@ -183,7 +184,7 @@ describe('parameters', () => { schema: { type: 'string' } } ], - responses: { '200': { description: 'ok' } } + responses: { 200: { description: 'ok' } } } }, '/v2/transactions': { @@ -203,7 +204,7 @@ describe('parameters', () => { schema: { type: 'string' } } ], - responses: { '200': { description: 'ok' } } + responses: { 200: { description: 'ok' } } } } }