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..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"; @@ -164,6 +165,84 @@ describe('parameters', () => { expect(renderedContent.channelModels['uploadFile']?.result).toMatchSnapshot(); }); + it('should use constrained property names and skip deserializeUrl without query parameters', async () => { + const openAPIDoc: OpenAPIV3.Document = { + 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 = {