tagTwoOps = getOperationsList(tagTwoData);
+ String tagTwoCommonPath = (String) tagTwoData.get("commonPath");
+ Assert.assertEquals(tagTwoOps.size(), 2);
+ for (CodegenOperation op : tagTwoOps) {
+ String fullPath = tagTwoCommonPath + op.path;
+ Assert.assertTrue(
+ fullPath.equals("/foo/bar/three") || fullPath.equals("/baz/bar/four"),
+ "Unexpected full path for tag-two operation: " + fullPath);
+ }
+ }
+
private void assertOperation(CodegenOperation op, String expectedBasename, String expectedPath, boolean expectedSubResourceOp) {
Assert.assertEquals(op.path, expectedPath);
Assert.assertEquals(op.baseName, expectedBasename);
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java
index abe3fcc3da50..87176dc4eaa3 100644
--- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/kotlin/KotlinServerCodegenTest.java
@@ -9,13 +9,13 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
-import org.openapitools.codegen.CodegenOperation;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.openapitools.codegen.ClientOptInput;
import org.openapitools.codegen.CodegenConstants;
+import org.openapitools.codegen.CodegenOperation;
import org.openapitools.codegen.DefaultGenerator;
import org.openapitools.codegen.TestUtils;
import org.openapitools.codegen.antlr4.KotlinLexer;
@@ -751,4 +751,38 @@ public void testFloatingPointMultipleOfValidationUsesTolerance() throws IOExcept
"if (intVal % 2 != 0) {"
);
}
+
+ // ==================== Cross-tag path shadowing (issue #23414) ====================
+
+ @Test
+ public void testCommonPathDoesNotShadowOtherTags_jaxrsSpec() throws IOException {
+ // Regression test for https://github.com/OpenAPITools/openapi-generator/issues/23414
+ // tag-one owns /foo/bar/one and /foo/bar/two
+ // tag-two owns /foo/bar/three and /baz/bar/four
+ // TagOneApi must NOT have @Path("/foo/bar") at class level because that would shadow
+ // TagTwoApi's /foo/bar/three route in the JAX-RS runtime.
+ File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
+ output.deleteOnExit();
+
+ KotlinServerCodegen codegen = new KotlinServerCodegen();
+ codegen.setOutputDir(output.getAbsolutePath());
+ codegen.additionalProperties().put(LIBRARY, JAXRS_SPEC);
+ codegen.additionalProperties().put(USE_TAGS, true);
+
+ new DefaultGenerator().opts(new ClientOptInput()
+ .openAPI(TestUtils.parseSpec("src/test/resources/3_0/issue_23414.yaml"))
+ .config(codegen))
+ .generate();
+
+ String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server";
+ Path tagOneApi = Paths.get(outputPath + "/apis/TagOneApi.kt");
+ Path tagTwoApi = Paths.get(outputPath + "/apis/TagTwoApi.kt");
+
+ // TagOneApi must NOT have @Path("/foo/bar") — this shadows TagTwoApi's /foo/bar/three
+ assertFileNotContains(tagOneApi, "@Path(\"/foo/bar\")");
+
+ // All operations must still be reachable with their full paths
+ assertFileContains(tagOneApi, "@Path(\"/foo/bar/one\")", "@Path(\"/foo/bar/two\")");
+ assertFileContains(tagTwoApi, "@Path(\"/foo/bar/three\")", "@Path(\"/baz/bar/four\")");
+ }
}
diff --git a/modules/openapi-generator/src/test/resources/3_0/issue_23414.yaml b/modules/openapi-generator/src/test/resources/3_0/issue_23414.yaml
new file mode 100644
index 000000000000..014b81e96d53
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/issue_23414.yaml
@@ -0,0 +1,49 @@
+openapi: 3.0.3
+info:
+ title: Cross-tag path shadowing test
+ version: 1.0.0
+paths:
+ /foo/bar/one:
+ get:
+ tags: [tag-one]
+ operationId: getOne
+ responses:
+ '200':
+ description: OK
+ content:
+ text/plain:
+ schema:
+ type: string
+ /foo/bar/two:
+ get:
+ tags: [tag-one]
+ operationId: getTwo
+ responses:
+ '200':
+ description: OK
+ content:
+ text/plain:
+ schema:
+ type: string
+ /foo/bar/three:
+ get:
+ tags: [tag-two]
+ operationId: getThree
+ responses:
+ '200':
+ description: OK
+ content:
+ text/plain:
+ schema:
+ type: string
+ /baz/bar/four:
+ get:
+ tags: [tag-two]
+ operationId: getFour
+ responses:
+ '200':
+ description: OK
+ content:
+ text/plain:
+ schema:
+ type: string
diff --git a/samples/server/petstore/jaxrs-cxf/src/gen/java/org/openapitools/api/FakeApi.java b/samples/server/petstore/jaxrs-cxf/src/gen/java/org/openapitools/api/FakeApi.java
index 287f3b59203f..c54871e99dc3 100644
--- a/samples/server/petstore/jaxrs-cxf/src/gen/java/org/openapitools/api/FakeApi.java
+++ b/samples/server/petstore/jaxrs-cxf/src/gen/java/org/openapitools/api/FakeApi.java
@@ -30,7 +30,7 @@
* This spec is mainly for testing Petstore server and contains fake endpoints, models. Please do not use this for any other purpose. Special characters: \" \\
*
*/
-@Path("/fake")
+@Path("")
@Api(value = "/", description = "")
public interface FakeApi {
@@ -41,7 +41,7 @@ public interface FakeApi {
*
*/
@POST
- @Path("/create_xml_item")
+ @Path("/fake/create_xml_item")
@Consumes({ "application/xml", "application/xml; charset=utf-8", "application/xml; charset=utf-16", "text/xml", "text/xml; charset=utf-8", "text/xml; charset=utf-16" })
@ApiOperation(value = "creates an XmlItem", tags={ "fake" })
@ApiResponses(value = {
@@ -49,7 +49,7 @@ public interface FakeApi {
public void createXmlItem(@Valid @NotNull XmlItem xmlItem);
@POST
- @Path("/outer/boolean")
+ @Path("/fake/outer/boolean")
@Produces({ "*/*" })
@ApiOperation(value = "", tags={ "fake" })
@ApiResponses(value = {
@@ -57,7 +57,7 @@ public interface FakeApi {
public Boolean fakeOuterBooleanSerialize(@Valid Boolean body);
@POST
- @Path("/outer/composite")
+ @Path("/fake/outer/composite")
@Produces({ "*/*" })
@ApiOperation(value = "", tags={ "fake" })
@ApiResponses(value = {
@@ -65,7 +65,7 @@ public interface FakeApi {
public OuterComposite fakeOuterCompositeSerialize(@Valid OuterComposite body);
@POST
- @Path("/outer/number")
+ @Path("/fake/outer/number")
@Produces({ "*/*" })
@ApiOperation(value = "", tags={ "fake" })
@ApiResponses(value = {
@@ -73,7 +73,7 @@ public interface FakeApi {
public BigDecimal fakeOuterNumberSerialize(@Valid BigDecimal body);
@POST
- @Path("/outer/string")
+ @Path("/fake/outer/string")
@Produces({ "*/*" })
@ApiOperation(value = "", tags={ "fake" })
@ApiResponses(value = {
@@ -81,7 +81,7 @@ public interface FakeApi {
public String fakeOuterStringSerialize(@Valid String body);
@PUT
- @Path("/body-with-file-schema")
+ @Path("/fake/body-with-file-schema")
@Consumes({ "application/json" })
@ApiOperation(value = "", tags={ "fake" })
@ApiResponses(value = {
@@ -89,7 +89,7 @@ public interface FakeApi {
public void testBodyWithFileSchema(@Valid @NotNull FileSchemaTestClass body);
@PUT
- @Path("/body-with-query-params")
+ @Path("/fake/body-with-query-params")
@Consumes({ "application/json" })
@ApiOperation(value = "", tags={ "fake" })
@ApiResponses(value = {
@@ -103,7 +103,7 @@ public interface FakeApi {
*
*/
@PATCH
-
+ @Path("/fake")
@Consumes({ "application/json" })
@Produces({ "application/json" })
@ApiOperation(value = "To test \"client\" model", tags={ "fake" })
@@ -118,7 +118,7 @@ public interface FakeApi {
*
*/
@POST
-
+ @Path("/fake")
@Consumes({ "application/x-www-form-urlencoded" })
@ApiOperation(value = "Fake endpoint for testing various parameters 假端點 偽のエンドポイント 가짜 엔드 포인트", tags={ "fake" })
@ApiResponses(value = {
@@ -133,7 +133,7 @@ public interface FakeApi {
*
*/
@GET
-
+ @Path("/fake")
@Consumes({ "application/x-www-form-urlencoded" })
@ApiOperation(value = "To test enum parameters", tags={ "fake" })
@io.swagger.annotations.ApiImplicitParams({
@@ -151,7 +151,7 @@ public interface FakeApi {
*
*/
@DELETE
-
+ @Path("/fake")
@ApiOperation(value = "Fake endpoint to test group parameters (optional)", tags={ "fake" })
@ApiResponses(value = {
@ApiResponse(code = 400, message = "Something wrong") })
@@ -162,7 +162,7 @@ public interface FakeApi {
*
*/
@POST
- @Path("/inline-additionalProperties")
+ @Path("/fake/inline-additionalProperties")
@Consumes({ "application/json" })
@ApiOperation(value = "test inline additionalProperties", tags={ "fake" })
@ApiResponses(value = {
@@ -174,7 +174,7 @@ public interface FakeApi {
*
*/
@GET
- @Path("/jsonFormData")
+ @Path("/fake/jsonFormData")
@Consumes({ "application/x-www-form-urlencoded" })
@ApiOperation(value = "test json serialization of form data", tags={ "fake" })
@ApiResponses(value = {
@@ -182,7 +182,7 @@ public interface FakeApi {
public void testJsonFormData(@Multipart(value = "param") String param, @Multipart(value = "param2") String param2);
@PUT
- @Path("/test-query-parameters")
+ @Path("/fake/test-query-parameters")
@ApiOperation(value = "", tags={ "fake" })
@ApiResponses(value = {
@ApiResponse(code = 200, message = "Success") })
diff --git a/samples/server/petstore/jaxrs/jersey2-useTags/src/gen/java/org/openapitools/api/FakeApi.java b/samples/server/petstore/jaxrs/jersey2-useTags/src/gen/java/org/openapitools/api/FakeApi.java
index 673aee5cf776..a3d636a77f16 100644
--- a/samples/server/petstore/jaxrs/jersey2-useTags/src/gen/java/org/openapitools/api/FakeApi.java
+++ b/samples/server/petstore/jaxrs/jersey2-useTags/src/gen/java/org/openapitools/api/FakeApi.java
@@ -33,7 +33,7 @@
import javax.validation.constraints.*;
import javax.validation.Valid;
-@Path("/fake")
+@Path("")
@io.swagger.annotations.Api(description = "the Fake API")
@@ -63,7 +63,7 @@ public FakeApi(@Context ServletConfig servletContext) {
}
@javax.ws.rs.POST
- @Path("/create_xml_item")
+ @Path("/fake/create_xml_item")
@Consumes({ "application/xml", "application/xml; charset=utf-8", "application/xml; charset=utf-16", "text/xml", "text/xml; charset=utf-8", "text/xml; charset=utf-16" })
@io.swagger.annotations.ApiOperation(value = "creates an XmlItem", notes = "this route creates an XmlItem", response = Void.class, tags={ "fake", })
@@ -75,7 +75,7 @@ public Response createXmlItem(@ApiParam(value = "XmlItem Body", required = true)
return delegate.createXmlItem(xmlItem, securityContext);
}
@javax.ws.rs.POST
- @Path("/outer/boolean")
+ @Path("/fake/outer/boolean")
@Produces({ "*/*" })
@io.swagger.annotations.ApiOperation(value = "", notes = "Test serialization of outer boolean types", response = Boolean.class, tags={ "fake", })
@@ -87,7 +87,7 @@ public Response fakeOuterBooleanSerialize(@ApiParam(value = "Input boolean as po
return delegate.fakeOuterBooleanSerialize(body, securityContext);
}
@javax.ws.rs.POST
- @Path("/outer/composite")
+ @Path("/fake/outer/composite")
@Produces({ "*/*" })
@io.swagger.annotations.ApiOperation(value = "", notes = "Test serialization of object with outer number type", response = OuterComposite.class, tags={ "fake", })
@@ -99,7 +99,7 @@ public Response fakeOuterCompositeSerialize(@ApiParam(value = "Input composite a
return delegate.fakeOuterCompositeSerialize(body, securityContext);
}
@javax.ws.rs.POST
- @Path("/outer/number")
+ @Path("/fake/outer/number")
@Produces({ "*/*" })
@io.swagger.annotations.ApiOperation(value = "", notes = "Test serialization of outer number types", response = BigDecimal.class, tags={ "fake", })
@@ -111,7 +111,7 @@ public Response fakeOuterNumberSerialize(@ApiParam(value = "Input number as post
return delegate.fakeOuterNumberSerialize(body, securityContext);
}
@javax.ws.rs.POST
- @Path("/outer/string")
+ @Path("/fake/outer/string")
@Produces({ "*/*" })
@io.swagger.annotations.ApiOperation(value = "", notes = "Test serialization of outer string types", response = String.class, tags={ "fake", })
@@ -123,7 +123,7 @@ public Response fakeOuterStringSerialize(@ApiParam(value = "Input string as post
return delegate.fakeOuterStringSerialize(body, securityContext);
}
@javax.ws.rs.PUT
- @Path("/body-with-file-schema")
+ @Path("/fake/body-with-file-schema")
@Consumes({ "application/json" })
@io.swagger.annotations.ApiOperation(value = "", notes = "For this test, the body for this request much reference a schema named `File`.", response = Void.class, tags={ "fake", })
@@ -135,7 +135,7 @@ public Response testBodyWithFileSchema(@ApiParam(value = "", required = true) @N
return delegate.testBodyWithFileSchema(body, securityContext);
}
@javax.ws.rs.PUT
- @Path("/body-with-query-params")
+ @Path("/fake/body-with-query-params")
@Consumes({ "application/json" })
@io.swagger.annotations.ApiOperation(value = "", notes = "", response = Void.class, tags={ "fake", })
@@ -147,7 +147,7 @@ public Response testBodyWithQueryParams(@ApiParam(value = "", required = true) @
return delegate.testBodyWithQueryParams(query, body, securityContext);
}
@javax.ws.rs.PATCH
-
+ @Path("/fake")
@Consumes({ "application/json" })
@Produces({ "application/json" })
@io.swagger.annotations.ApiOperation(value = "To test \"client\" model", notes = "To test \"client\" model", response = Client.class, tags={ "fake", })
@@ -159,7 +159,7 @@ public Response testClientModel(@ApiParam(value = "client model", required = tru
return delegate.testClientModel(body, securityContext);
}
@javax.ws.rs.POST
-
+ @Path("/fake")
@Consumes({ "application/x-www-form-urlencoded" })
@io.swagger.annotations.ApiOperation(value = "Fake endpoint for testing various parameters 假端點 偽のエンドポイント 가짜 엔드 포인트", notes = "Fake endpoint for testing various parameters 假端點 偽のエンドポイント 가짜 엔드 포인트", response = Void.class, authorizations = {
@@ -175,7 +175,7 @@ public Response testEndpointParameters(@ApiParam(value = "None", required=true)
return delegate.testEndpointParameters(number, _double, patternWithoutDelimiter, _byte, integer, int32, int64, _float, string, binaryBodypart, date, dateTime, password, paramCallback, securityContext);
}
@javax.ws.rs.GET
-
+ @Path("/fake")
@Consumes({ "application/x-www-form-urlencoded" })
@io.swagger.annotations.ApiOperation(value = "To test enum parameters", notes = "To test enum parameters", response = Void.class, tags={ "fake", })
@@ -188,7 +188,7 @@ public Response testEnumParameters(@ApiParam(value = "Header parameter enum test
return delegate.testEnumParameters(enumHeaderStringArray, enumHeaderString, enumQueryStringArray, enumQueryString, enumQueryInteger, enumQueryDouble, enumFormStringArray, enumFormString, securityContext);
}
@javax.ws.rs.DELETE
-
+ @Path("/fake")
@io.swagger.annotations.ApiOperation(value = "Fake endpoint to test group parameters (optional)", notes = "Fake endpoint to test group parameters (optional)", response = Void.class, tags={ "fake", })
@@ -200,7 +200,7 @@ public Response testGroupParameters(@ApiParam(value = "Required String in group
return delegate.testGroupParameters(requiredStringGroup, requiredBooleanGroup, requiredInt64Group, stringGroup, booleanGroup, int64Group, securityContext);
}
@javax.ws.rs.POST
- @Path("/inline-additionalProperties")
+ @Path("/fake/inline-additionalProperties")
@Consumes({ "application/json" })
@io.swagger.annotations.ApiOperation(value = "test inline additionalProperties", notes = "", response = Void.class, tags={ "fake", })
@@ -212,7 +212,7 @@ public Response testInlineAdditionalProperties(@ApiParam(value = "request body",
return delegate.testInlineAdditionalProperties(param, securityContext);
}
@javax.ws.rs.GET
- @Path("/jsonFormData")
+ @Path("/fake/jsonFormData")
@Consumes({ "application/x-www-form-urlencoded" })
@io.swagger.annotations.ApiOperation(value = "test json serialization of form data", notes = "", response = Void.class, tags={ "fake", })
@@ -224,7 +224,7 @@ public Response testJsonFormData(@ApiParam(value = "field1", required=true) @Fo
return delegate.testJsonFormData(param, param2, securityContext);
}
@javax.ws.rs.PUT
- @Path("/test-query-parameters")
+ @Path("/fake/test-query-parameters")
@io.swagger.annotations.ApiOperation(value = "", notes = "To test the collection format in query parameters", response = Void.class, tags={ "fake", })
diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/AppMain.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/AppMain.kt
new file mode 100644
index 000000000000..4053d223ac9c
--- /dev/null
+++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/AppMain.kt
@@ -0,0 +1,68 @@
+package org.openapitools.server
+
+import io.ktor.server.application.*
+import io.ktor.http.*
+import io.ktor.server.resources.*
+import io.ktor.server.plugins.autohead.*
+import io.ktor.server.plugins.compression.*
+import io.ktor.server.plugins.contentnegotiation.*
+import io.ktor.server.plugins.defaultheaders.*
+import io.ktor.server.plugins.hsts.*
+import com.codahale.metrics.Slf4jReporter
+import io.ktor.server.metrics.dropwizard.*
+import java.util.concurrent.TimeUnit
+import io.ktor.server.routing.*
+import io.ktor.serialization.kotlinx.json.json
+import com.typesafe.config.ConfigFactory
+import io.ktor.client.HttpClient
+import io.ktor.client.engine.apache.Apache
+import io.ktor.server.config.HoconApplicationConfig
+import io.ktor.server.auth.*
+import org.openapitools.server.infrastructure.*
+import org.openapitools.server.apis.PetApi
+import org.openapitools.server.apis.StoreApi
+import org.openapitools.server.apis.UserApi
+
+
+fun Application.main() {
+ install(DefaultHeaders)
+ install(DropwizardMetrics) {
+ val reporter = Slf4jReporter.forRegistry(registry)
+ .outputTo(this@main.log)
+ .convertRatesTo(TimeUnit.SECONDS)
+ .convertDurationsTo(TimeUnit.MILLISECONDS)
+ .build()
+ reporter.start(10, TimeUnit.SECONDS)
+ }
+ install(ContentNegotiation) {
+ json()
+ }
+ install(AutoHeadResponse) // see https://ktor.io/docs/autoheadresponse.html
+ install(Compression, ApplicationCompressionConfiguration()) // see https://ktor.io/docs/compression.html
+ install(HSTS, ApplicationHstsConfiguration()) // see https://ktor.io/docs/hsts.html
+ install(Resources)
+ install(Authentication) {
+ oauth("petstore_auth") {
+ client = HttpClient(Apache)
+ providerLookup = { applicationAuthProvider(this@main.environment.config) }
+ urlProvider = { _ ->
+ // TODO: define a callback url here.
+ "/"
+ }
+ }
+ // "Implement API key auth (api_key) for parameter name 'api_key'."
+ apiKeyAuth("api_key") {
+ validate { apikeyCredential: ApiKeyCredential ->
+ when {
+ apikeyCredential.value == "keyboardcat" -> ApiPrincipal(apikeyCredential)
+ else -> null
+ }
+ }
+ }
+ }
+ routing {
+ PetApi()
+ StoreApi()
+ UserApi()
+ }
+}
diff --git a/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/Configuration.kt b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/Configuration.kt
new file mode 100644
index 000000000000..f78654c60f0f
--- /dev/null
+++ b/samples/server/petstore/kotlin-server/ktor-delegate-pattern/generated/src/main/kotlin/org/openapitools/server/Configuration.kt
@@ -0,0 +1,63 @@
+package org.openapitools.server
+
+// Use this file to hold package-level internal functions that return receiver object passed to the `install` method.
+import io.ktor.http.*
+import io.ktor.server.auth.*
+import io.ktor.server.config.*
+import io.ktor.util.*
+import java.time.Duration
+import java.util.concurrent.TimeUnit
+import io.ktor.server.plugins.compression.*
+import io.ktor.server.plugins.hsts.*
+
+
+/**
+ * Application block for [HSTS] configuration.
+ *
+ * This file may be excluded in .openapi-generator-ignore,
+ * and application-specific configuration can be applied in this function.
+ *
+ * See http://ktor.io/features/hsts.html
+ */
+internal fun ApplicationHstsConfiguration(): HSTSConfig.() -> Unit {
+ return {
+ maxAgeInSeconds = TimeUnit.DAYS.toSeconds(365)
+ includeSubDomains = true
+ preload = false
+
+ // You may also apply any custom directives supported by specific user-agent. For example:
+ // customDirectives.put("redirectHttpToHttps", "false")
+ }
+}
+
+/**
+ * Application block for [Compression] configuration.
+ *
+ * This file may be excluded in .openapi-generator-ignore,
+ * and application-specific configuration can be applied in this function.
+ *
+ * See http://ktor.io/features/compression.html
+ */
+internal fun ApplicationCompressionConfiguration(): CompressionConfig.() -> Unit {
+ return {
+ gzip {
+ priority = 1.0
+ }
+ deflate {
+ priority = 10.0
+ minimumSize(1024) // condition
+ }
+ }
+}
+
+// Defines authentication mechanisms used throughout the application.
+fun applicationAuthProvider(config: ApplicationConfig): OAuthServerSettings =
+ OAuthServerSettings.OAuth2ServerSettings(
+ name = "petstore_auth",
+ authorizeUrl = "http://petstore.swagger.io/api/oauth/dialog",
+ accessTokenUrl = "",
+ requestMethod = HttpMethod.Get,
+ clientId = config.property("auth.oauth.petstore_auth.clientId").getString(),
+ clientSecret = config.property("auth.oauth.petstore_auth.clientSecret").getString(),
+ defaultScopes = listOf("write:pets", "read:pets")
+ )