Repository: OpenAPITools/openapi-generator (language: python)

Issue:
[BUG] Single quotes in the url path are generated as &#39;
#### Bug Report Checklist

- [x] Have you provided a full/minimal spec to reproduce the issue?
- [ ] Have you validated the input using an OpenAPI validator ([example](https://apidevtools.org/swagger-parser/online/))?
- [ ] Have you [tested with the latest master](https://github.com/OpenAPITools/openapi-generator/wiki/FAQ#how-to-test-with-the-latest-master-of-openapi-generator) to confirm the issue still exists?
- [x] Have you searched for related issues/PRs?
- [ ] What's the actual output vs expected output?

##### Description

Single quotes in the url path are generated as `&#39;` 

Based on the given specs, the plugin generates 

```kotlin
fun articleIdGetRequestConfig(id: kotlin.String) : RequestConfig<Unit> {
        val localVariableBody = null
        val localVariableQuery = mutableMapOf<kotlin.String, kotlin.collections.List<kotlin.String>>()
        val localVariableHeaders: MutableMap<String, String> = mutableMapOf()
        
        val params = mutableMapOf<String, Any>(
            "Id" to id,
        )

        return RequestConfig(
            method = RequestMethod.GET,
            path = "/article(&#39;{Id}&#39;)",
            params = params,
            query = localVariableQuery,
            headers = localVariableHeaders,
            requiresAuthentication = false,
            body = localVariableBody
        )
    }
```

while it should be
```kotlin
fun articleIdGetRequestConfig(id: kotlin.String) : RequestConfig<Unit> {
        val localVariableBody = null
        val localVariableQuery = mutableMapOf<kotlin.String, kotlin.collections.List<kotlin.String>>()
        val localVariableHeaders: MutableMap<String, String> = mutableMapOf()
        
        val params = mutableMapOf<String, Any>(
            "Id" to id,
        )

        return RequestConfig(
            method = RequestMethod.GET,
            path = "/article('{Id}')",
            params = params,
            query = localVariableQuery,
            headers = localVariableHeaders,
            requiresAuthentication = false,
            body = localVariableBody
        )
    }

```


##### openapi-generator version

7.9.0

##### OpenAPI declaration file content or url
https://gist.github.com/detomarco/3bfe686176d9978ee31809eaf70654b7
  -->

##### Generation Details

Gradle plugin with this configs
```kotlin
openApiGenerate {
    val groupId = "******"
    generatorName.set("kotlin")
    inputSpec.set("****")
    outputDir.set("****")
    apiPackage.set("$groupId.api")
    invokerPackage.set("$groupId.invoker")
    modelPackage.set("$groupId.model")
    additionalProperties.set(
        mapOf(
            "library" to "jvm-spring-webclient",
            "omitGradleWrapper" to "true",
            "serializationLibrary" to "jackson",
            "useSpringBoot3" to "true",
            "useSettingsGradle" to "true",
            "omitGradleWrapper" to "true",
        )
    )
}
```

##### Steps to reproduce

<!-- unambiguous set of steps to reproduce the bug.-->

##### Related issues/PRs


##### Suggest a fix

<!-- if you can't fix the bug yourself, perhaps you can point to what might be
  causing the problem (line of code or commit), or simply make a suggestion -->



Discussion / hints:

Fix patch:
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/api.mustache
index c6ca0ce0c06d..c246003421c6 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/api.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-ktor/api.mustache
@@ -134,7 +134,7 @@ import com.fasterxml.jackson.databind.ObjectMapper
 
             val localVariableConfig = RequestConfig<kotlin.Any?>(
             RequestMethod.{{httpMethod}},
-            "{{path}}"{{#pathParams}}.replace("{" + "{{baseName}}" + "}", "${{{paramName}}}"){{/pathParams}},
+            "{{{path}}}"{{#pathParams}}.replace("{" + "{{baseName}}" + "}", "${{{paramName}}}"){{/pathParams}},
             query = localVariableQuery,
             headers = localVariableHeaders,
             requiresAuthentication = {{#hasAuthMethods}}true{{/hasAuthMethods}}{{^hasAuthMethods}}false{{/hasAuthMethods}},
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache
index 642794399125..bd262322887c 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-okhttp/api.mustache
@@ -232,7 +232,7 @@ import {{packageName}}.infrastructure.toMultiValue
 
         return RequestConfig(
             method = RequestMethod.{{httpMethod}},
-            path = "{{path}}"{{#pathParams}}.replace("{"+"{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"+"}", encodeURIComponent({{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}.toString(){{/isContainer}})){{/pathParams}},
+            path = "{{{path}}}"{{#pathParams}}.replace("{"+"{{#lambda.escapeDollar}}{{baseName}}{{/lambda.escapeDollar}}"+"}", encodeURIComponent({{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}.toString(){{/isContainer}})){{/pathParams}},
             query = localVariableQuery,
             headers = localVariableHeaders,
             requiresAuthentication = {{#hasAuthMethods}}true{{/hasAuthMethods}}{{^hasAuthMethods}}false{{/hasAuthMethods}},
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-restclient/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-restclient/api.mustache
index 1c533aae70a5..e1ad1abe32fb 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-restclient/api.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-restclient/api.mustache
@@ -133,7 +133,7 @@ import {{packageName}}.infrastructure.*
 
         return RequestConfig(
             method = RequestMethod.{{httpMethod}},
-            path = "{{path}}",
+            path = "{{{path}}}",
             params = params,
             query = localVariableQuery,
             headers = localVariableHeaders,
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-webclient/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-webclient/api.mustache
index 556397463513..177a918b06ef 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-webclient/api.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-spring-webclient/api.mustache
@@ -135,7 +135,7 @@ import {{packageName}}.infrastructure.*
 
         return RequestConfig(
             method = RequestMethod.{{httpMethod}},
-            path = "{{path}}",
+            path = "{{{path}}}",
             params = params,
             query = localVariableQuery,
             headers = localVariableHeaders,
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-vertx/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-vertx/api.mustache
index c84f15f0b3fb..1ccca7f298f1 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-vertx/api.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/jvm-vertx/api.mustache
@@ -136,7 +136,7 @@ import {{packageName}}.infrastructure.*
     {{/isDeprecated}}
     fun {{operationId}}WithHttpInfo({{#allParams}}{{{paramName}}}: {{#isEnum}}{{#isContainer}}kotlin.collections.List<{{enumName}}{{operationIdCamelCase}}>{{/isContainer}}{{^isContainer}}{{enumName}}{{operationIdCamelCase}}{{/isContainer}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{^required}}?{{/required}}{{^-last}}, {{/-last}}{{/allParams}}) : Future<ApiResponse<{{#returnType}}{{{returnType}}}?{{/returnType}}{{^returnType}}Unit?{{/returnType}}>> {
         val vertxClient = WebClient.create(vertx)
-        val request = vertxClient.requestAbs(HttpMethod.{{httpMethod}}, UriTemplate.of("$basePath{{path}}"{{#pathParams}}.replace("{"+"{{baseName}}"+"}", encodeURIComponent({{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}.toString(){{/isContainer}})){{/pathParams}}))
+        val request = vertxClient.requestAbs(HttpMethod.{{httpMethod}}, UriTemplate.of("$basePath{{{path}}}"{{#pathParams}}.replace("{"+"{{baseName}}"+"}", encodeURIComponent({{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{{paramName}}}{{#isEnum}}{{^required}}?{{/required}}.value{{/isEnum}}.toString(){{/isContainer}})){{/pathParams}}))
 
         {{#hasFormParams}}request.putHeader("Content-Type", {{^consumes}}"multipart/form-data"{{/consumes}}{{#consumes.0}}"{{{mediaType}}}"{{/consumes.0}}){{/hasFormParams}}
         {{#headerParams}}{{{paramName}}}{{^required}}?{{/required}}.apply { request.putHeader("{{baseName}}", {{#isContainer}}this.joinToString(separator = collectionDelimiter("{{collectionFormat}}")){{/isContainer}}{{^isContainer}}this.toString(){{/isContainer}})}{{/headerParams}}
diff --git a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/api.mustache
index 249f8edbb632..6566f40d9651 100644
--- a/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/api.mustache
+++ b/modules/openapi-generator/src/main/resources/kotlin-client/libraries/multiplatform/api.mustache
@@ -105,7 +105,7 @@ import kotlinx.serialization.encoding.*
 
         val localVariableConfig = RequestConfig<kotlin.Any?>(
             RequestMethod.{{httpMethod}},
-            "{{path}}"{{#pathParams}}.replace("{" + "{{baseName}}" + "}", {{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{^isEnum}}"${{{paramName}}}"{{/isEnum}}{{#isEnum}}"${ {{paramName}}.value }"{{/isEnum}}{{/isContainer}}){{/pathParams}},
+            "{{{path}}}"{{#pathParams}}.replace("{" + "{{baseName}}" + "}", {{#isContainer}}{{paramName}}.joinToString(","){{/isContainer}}{{^isContainer}}{{^isEnum}}"${{{paramName}}}"{{/isEnum}}{{#isEnum}}"${ {{paramName}}.value }"{{/isEnum}}{{/isContainer}}){{/pathParams}},
             query = localVariableQuery,
             headers = localVariableHeaders,
             requiresAuthentication = {{#hasAuthMethods}}true{{/hasAuthMethods}}{{^hasAuthMethods}}false{{/hasAuthMethods}},


Test patch:
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java
new file mode 100644
index 000000000000..4349e88d4438
--- /dev/null
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinClientCodegenApiTest.java
@@ -0,0 +1,104 @@
+package org.openapitools.codegen.kotlin;
+
+import io.swagger.parser.OpenAPIParser;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.parser.core.models.ParseOptions;
+import lombok.Getter;
+import org.jetbrains.kotlin.com.intellij.openapi.util.text.Strings;
+import org.openapitools.codegen.ClientOptInput;
+import org.openapitools.codegen.CodegenConstants;
+import org.openapitools.codegen.DefaultGenerator;
+import org.openapitools.codegen.languages.KotlinClientCodegen;
+import org.openapitools.codegen.languages.features.CXFServerFeatures;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+import static org.openapitools.codegen.TestUtils.assertFileContains;
+
+public class KotlinClientCodegenApiTest {
+
+    @DataProvider(name = "pathResponses")
+    public Object[][] pathResponses() {
+        return new Object[][]{
+                {ClientLibrary.JVM_KTOR},
+                {ClientLibrary.JVM_OKHTTP4},
+                {ClientLibrary.JVM_SPRING_WEBCLIENT},
+                {ClientLibrary.JVM_SPRING_RESTCLIENT},
+                {ClientLibrary.JVM_RETROFIT2},
+                {ClientLibrary.MULTIPLATFORM},
+                {ClientLibrary.JVM_VOLLEY},
+                {ClientLibrary.JVM_VERTX}
+        };
+    }
+
+    @Test(dataProvider = "pathResponses")
+    void testPathVariableIsNotEscaped_19930(ClientLibrary library) throws IOException {
+
+        OpenAPI openAPI = new OpenAPIParser()
+                .readLocation("src/test/resources/3_0/kotlin/issue19930-path-escaping.json", null, new ParseOptions()).getOpenAPI();
+
+        KotlinClientCodegen codegen = createCodegen(library);
+
+        String outputPath = codegen.getOutputDir().replace('\\', '/');
+        ClientOptInput input = new ClientOptInput();
+        input.openAPI(openAPI);
+        input.config(codegen);
+
+        DefaultGenerator generator = new DefaultGenerator();
+
+        generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "false");
+        generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "false");
+        generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false");
+        generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true");
+        generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false");
+
+        generator.opts(input).generate();
+
+        System.out.println(outputPath);
+
+        assertFileContains(Paths.get(outputPath + "/src/" + library.getSourceRoot() + "/org/openapitools/client/apis/ArticleApi.kt"), "article('{Id}')");
+    }
+
+    private KotlinClientCodegen createCodegen(ClientLibrary library) throws IOException {
+        File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
+        output.deleteOnExit();
+        KotlinClientCodegen codegen = new KotlinClientCodegen();
+        codegen.setLibrary(library.getLibraryName());
+        codegen.setOutputDir(output.getAbsolutePath());
+        codegen.setSerializationLibrary(library.getSerializationLibrary());
+        codegen.additionalProperties().put(CXFServerFeatures.LOAD_TEST_DATA_FROM_FILE, "true");
+        codegen.additionalProperties().put(KotlinClientCodegen.USE_SPRING_BOOT3, "true");
+        codegen.additionalProperties().put(KotlinClientCodegen.DATE_LIBRARY, "kotlinx-datetime");
+        return codegen;
+    }
+
+    @Getter
+    private enum ClientLibrary {
+        JVM_KTOR("main/kotlin"),
+        JVM_OKHTTP4("main/kotlin"),
+        JVM_SPRING_WEBCLIENT("main/kotlin"),
+        JVM_SPRING_RESTCLIENT("main/kotlin"),
+        JVM_RETROFIT2("main/kotlin"),
+        MULTIPLATFORM("commonMain/kotlin"),
+        JVM_VOLLEY("gson", "main/java"),
+        JVM_VERTX("main/kotlin");
+        private final String serializationLibrary;
+        private final String libraryName;
+        private final String sourceRoot;
+
+        ClientLibrary(String serializationLibrary, String sourceRoot) {
+            this.serializationLibrary = serializationLibrary;
+            this.sourceRoot = sourceRoot;
+            this.libraryName = Strings.toLowerCase(this.name()).replace("_", "-");
+        }
+
+        ClientLibrary(String sourceRoot) {
+            this("jackson", sourceRoot);
+        }
+    }
+}
diff --git a/modules/openapi-generator/src/test/resources/3_0/kotlin/issue19930-path-escaping.json b/modules/openapi-generator/src/test/resources/3_0/kotlin/issue19930-path-escaping.json
new file mode 100644
index 000000000000..3f6ac196203b
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/kotlin/issue19930-path-escaping.json
@@ -0,0 +1,40 @@
+{
+  "openapi": "3.0.1",
+  "info": {
+    "title": "",
+    "description": "",
+    "version": "1.0.0"
+  },
+  "servers": [
+    {
+      "url": "https://localhost:8080"
+    }
+  ],
+  "paths": {
+    "/article('{Id}')": {
+      "get": {
+        "tags": [
+          "Article"
+        ],
+        "parameters": [
+          {
+            "name": "Id",
+            "in": "path",
+            "description": "key: Id of Article",
+            "required": true,
+            "schema": {
+              "type": "string"
+            }
+          }
+        ],
+        "responses": {
+          "200": {
+            "description": "Retrieved entity",
+            "content": {
+            }
+          }
+        }
+      }
+    }
+  }
+}
\ No newline at end of file