diff --git a/bin/configs/kotlin-server-jaxrs-spec.yaml b/bin/configs/kotlin-server-jaxrs-spec.yaml index 11d4fcec7a8d..14afd5eb5fb9 100644 --- a/bin/configs/kotlin-server-jaxrs-spec.yaml +++ b/bin/configs/kotlin-server-jaxrs-spec.yaml @@ -5,3 +5,4 @@ inputSpec: modules/openapi-generator/src/test/resources/2_0/petstore.yaml templateDir: modules/openapi-generator/src/main/resources/kotlin-server additionalProperties: useCoroutines: "true" + useTags: "true" diff --git a/docs/generators/kotlin-server.md b/docs/generators/kotlin-server.md index 86859ea27401..9e3df3f97fbf 100644 --- a/docs/generators/kotlin-server.md +++ b/docs/generators/kotlin-server.md @@ -45,6 +45,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl |sourceFolder|source folder for generated code| |src/main/kotlin| |useBeanValidation|Use BeanValidation API annotations. This option is currently supported only when using jaxrs-spec library.| |false| |useCoroutines|Whether to use the Coroutines. This option is currently supported only when using jaxrs-spec library.| |false| +|useTags|use tags for creating interface and controller classnames. This option is currently supported only when using jaxrs-spec library.| |false| |useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false| |useMutiny|Whether to use Mutiny (should not be used with useCoroutines). This option is currently supported only when using jaxrs-spec library.| |false| diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java index b46c23aa7b54..71374aee0ebd 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractKotlinCodegen.java @@ -56,6 +56,8 @@ public abstract class AbstractKotlinCodegen extends DefaultCodegen implements Co public static final String JAVAX_PACKAGE = "javaxPackage"; public static final String USE_JAKARTA_EE = "useJakartaEe"; + public static final String USE_TAGS = "useTags"; + public static final String USE_TAGS_DESC = "use tags for creating interface and controller classnames"; public static final String SCHEMA_IMPLEMENTS = "schemaImplements"; public static final String SCHEMA_IMPLEMENTS_FIELDS = "schemaImplementsFields"; public static final String X_KOTLIN_IMPLEMENTS_SKIP = "xKotlinImplementsSkip"; diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java index 06755794d42e..b6e5edd84fd6 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/KotlinServerCodegen.java @@ -18,6 +18,7 @@ package org.openapitools.codegen.languages; import com.google.common.collect.ImmutableMap; +import io.swagger.v3.oas.models.Operation; import lombok.Getter; import lombok.Setter; import org.apache.commons.lang3.StringUtils; @@ -64,6 +65,7 @@ public class KotlinServerCodegen extends AbstractKotlinCodegen implements BeanVa private Boolean metricsFeatureEnabled = true; private boolean interfaceOnly = false; private boolean useBeanValidation = false; + private boolean useTags = false; private boolean useCoroutines = false; private boolean useMutiny = false; private boolean returnResponse = false; @@ -97,6 +99,7 @@ public class KotlinServerCodegen extends AbstractKotlinCodegen implements BeanVa )) .put(Constants.JAXRS_SPEC, Arrays.asList( USE_BEANVALIDATION, + USE_TAGS, Constants.USE_COROUTINES, Constants.USE_MUTINY, Constants.RETURN_RESPONSE, @@ -170,6 +173,7 @@ public KotlinServerCodegen() { addSwitch(Constants.METRICS, Constants.METRICS_DESC, getMetricsFeatureEnabled()); addSwitch(Constants.INTERFACE_ONLY, Constants.INTERFACE_ONLY_DESC, interfaceOnly); addSwitch(USE_BEANVALIDATION, Constants.USE_BEANVALIDATION_DESC, useBeanValidation); + addSwitch(USE_TAGS, USE_TAGS_DESC, useTags); addSwitch(Constants.USE_COROUTINES, Constants.USE_COROUTINES_DESC, useCoroutines); addSwitch(Constants.USE_MUTINY, Constants.USE_MUTINY_DESC, useMutiny); addSwitch(Constants.RETURN_RESPONSE, Constants.RETURN_RESPONSE_DESC, returnResponse); @@ -241,6 +245,10 @@ public void processOpts() { setUseBeanValidation(convertPropertyToBoolean(USE_BEANVALIDATION)); } + if (additionalProperties.containsKey(USE_TAGS)) { + useTags = Boolean.parseBoolean(additionalProperties.get(USE_TAGS).toString()); + } + if (additionalProperties.containsKey(Constants.OMIT_GRADLE_WRAPPER)) { setOmitGradleWrapper(Boolean.parseBoolean(additionalProperties.get(Constants.OMIT_GRADLE_WRAPPER).toString())); } @@ -698,6 +706,23 @@ public void postProcess() { @Override public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List allModels) { OperationMap operations = objs.getOperations(); + // For JAXRS_SPEC library, compute commonPath similar to JavaJaxRS generators + if (operations != null && Objects.equals(library, Constants.JAXRS_SPEC)) { + String commonPath = null; + List ops = operations.getOperation(); + for (CodegenOperation operation : ops) { + if (commonPath == null) { + commonPath = operation.path; + } else { + commonPath = getCommonPath(commonPath, operation.path); + } + } + for (CodegenOperation co : ops) { + co.path = StringUtils.removeStart(co.path, commonPath); + co.subresourceOperation = co.path.length() > 1; + } + objs.put("commonPath", "/".equals(commonPath) ? StringUtils.EMPTY : commonPath); + } // The following processing breaks the JAX-RS spec, so we only do this for the other libs. if (operations != null && !Objects.equals(library, Constants.JAXRS_SPEC)) { List ops = operations.getOperation(); @@ -758,6 +783,24 @@ public void setReturnContainer(final String returnContainer) { return objs; } + @Override + public void addOperationToGroup(String tag, String resourcePath, Operation operation, CodegenOperation co, Map> operations) { + if (Objects.equals(library, Constants.JAXRS_SPEC) && additionalProperties.containsKey(USE_TAGS) && !useTags) { + String basePath = StringUtils.substringBefore(StringUtils.removeStart(resourcePath, "/"), "/"); + if (!StringUtils.isEmpty(basePath)) { + co.subresourceOperation = !co.path.isEmpty(); + } + co.baseName = basePath; + if (StringUtils.isEmpty(co.baseName) || StringUtils.containsAny(co.baseName, "{", "}")) { + co.baseName = "default"; + } + final List opList = operations.computeIfAbsent(co.baseName, k -> new ArrayList<>()); + opList.add(co); + } else { + super.addOperationToGroup(tag, resourcePath, operation, co, operations); + } + } + private boolean isJavalin() { return Constants.JAVALIN5.equals(library) || Constants.JAVALIN6.equals(library); } @@ -788,4 +831,17 @@ private boolean isKtor() { private boolean isKtor2() { return Constants.KTOR2.equals(library); } + + private static String getCommonPath(String path1, String path2) { + final String[] parts1 = StringUtils.split(path1, "/"); + final String[] parts2 = StringUtils.split(path2, "/"); + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < Math.min(parts1.length, parts2.length); i++) { + if (!parts1[i].equals(parts2[i])) { + break; + } + builder.append("/").append(parts1[i]); + } + return builder.toString(); + } } diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/README.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/README.mustache index 01001ae1d834..7e8ec09875ab 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/README.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/README.mustache @@ -35,7 +35,7 @@ All URIs are relative to *{{{basePath}}}* Class | Method | HTTP request | Description ------------ | ------------- | ------------- | ------------- -{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{path}} | {{{summary}}} +{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}*{{classname}}* | [**{{operationId}}**]({{apiDocPath}}{{classname}}.md#{{operationIdLowerCase}}) | **{{httpMethod}}** {{#commonPath}}{{commonPath}}{{/commonPath}}{{path}} | {{{summary}}} {{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} {{/generateApiDocs}} diff --git a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/api.mustache b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/api.mustache index 6794c91ecd4f..f2ccb1a41dc9 100644 --- a/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/api.mustache +++ b/modules/openapi-generator/src/main/resources/kotlin-server/libraries/jaxrs-spec/api.mustache @@ -18,7 +18,7 @@ import {{javaxPackage}}.validation.Valid{{/useBeanValidation}} @Api(description = "the {{{baseName}}} API"){{/useSwaggerAnnotations}}{{#hasConsumes}} @Consumes({ {{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}} }){{/hasConsumes}}{{#hasProduces}} @Produces({ {{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}} }){{/hasProduces}} -@Path("/") +@Path("{{commonPath}}") {{>generatedAnnotation}} {{#interfaceOnly}}interface{{/interfaceOnly}}{{^interfaceOnly}}class{{/interfaceOnly}} {{classname}} { 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 0850466701e1..67f6c7a88dd7 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 @@ -30,6 +30,7 @@ import static org.openapitools.codegen.TestUtils.assertFileContains; import static org.openapitools.codegen.TestUtils.assertFileNotContains; import static org.openapitools.codegen.languages.AbstractKotlinCodegen.USE_JAKARTA_EE; +import static org.openapitools.codegen.languages.AbstractKotlinCodegen.USE_TAGS; import static org.openapitools.codegen.languages.KotlinServerCodegen.Constants.*; import static org.openapitools.codegen.languages.features.BeanValidationFeatures.USE_BEANVALIDATION; @@ -509,4 +510,60 @@ public void fixJacksonJsonTypeInfoInheritance_canBeDisabled() throws IOException "visible = false" ); } + + @Test + public void useTags_commonPathIsComputedForJaxrsSpecLibrary() throws IOException { + 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/2_0/petstore.yaml")) + .config(codegen)) + .generate(); + + String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server"; + Path petApi = Paths.get(outputPath + "/apis/PetApi.kt"); + + assertFileContains( + petApi, + "@Path(\"/pet\")" + ); + assertFileNotContains( + petApi, + "@Path(\"/\")" + ); + } + + @Test + public void useTags_false_operationsGroupedByPathBaseForJaxrsSpecLibrary() throws IOException { + 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, false); + + new DefaultGenerator().opts(new ClientOptInput() + .openAPI(TestUtils.parseSpec("src/test/resources/2_0/petstore.yaml")) + .config(codegen)) + .generate(); + + String outputPath = output.getAbsolutePath() + "/src/main/kotlin/org/openapitools/server"; + Path petApi = Paths.get(outputPath + "/apis/PetApi.kt"); + + assertFileContains( + petApi, + "class PetApi" + ); + assertFileNotContains( + petApi, + "class DefaultApi" + ); + } } diff --git a/samples/server/petstore/kotlin-server/jaxrs-spec/src/main/kotlin/org/openapitools/server/apis/PetApi.kt b/samples/server/petstore/kotlin-server/jaxrs-spec/src/main/kotlin/org/openapitools/server/apis/PetApi.kt index 74ef411027a4..ba16a19e2d61 100644 --- a/samples/server/petstore/kotlin-server/jaxrs-spec/src/main/kotlin/org/openapitools/server/apis/PetApi.kt +++ b/samples/server/petstore/kotlin-server/jaxrs-spec/src/main/kotlin/org/openapitools/server/apis/PetApi.kt @@ -11,7 +11,7 @@ import java.io.InputStream -@Path("/") +@Path("/pet") @javax.annotation.Generated(value = arrayOf("org.openapitools.codegen.languages.KotlinServerCodegen"), comments = "Generator version: 7.20.0-SNAPSHOT") class PetApi { @@ -22,23 +22,27 @@ class PetApi { } @DELETE + @Path("/{petId}") suspend fun deletePet(@PathParam("petId") petId: kotlin.Long,@HeaderParam("api_key") apiKey: kotlin.String?): Response { return Response.ok().entity("magic!").build(); } @GET + @Path("/findByStatus") @Produces("application/xml", "application/json") suspend fun findPetsByStatus(@QueryParam("status") status: kotlin.collections.List): Response { return Response.ok().entity("magic!").build(); } @GET + @Path("/findByTags") @Produces("application/xml", "application/json") suspend fun findPetsByTags(@QueryParam("tags") tags: kotlin.collections.List): Response { return Response.ok().entity("magic!").build(); } @GET + @Path("/{petId}") @Produces("application/xml", "application/json") suspend fun getPetById(@PathParam("petId") petId: kotlin.Long): Response { return Response.ok().entity("magic!").build(); @@ -51,12 +55,14 @@ class PetApi { } @POST + @Path("/{petId}") @Consumes("application/x-www-form-urlencoded") suspend fun updatePetWithForm(@PathParam("petId") petId: kotlin.Long,@FormParam(value = "name") name: kotlin.String?,@FormParam(value = "status") status: kotlin.String?): Response { return Response.ok().entity("magic!").build(); } @POST + @Path("/{petId}/uploadImage") @Consumes("multipart/form-data") @Produces("application/json") suspend fun uploadFile(@PathParam("petId") petId: kotlin.Long,@FormParam(value = "additionalMetadata") additionalMetadata: kotlin.String?, @FormParam(value = "file") fileInputStream: InputStream?): Response { diff --git a/samples/server/petstore/kotlin-server/jaxrs-spec/src/main/kotlin/org/openapitools/server/apis/StoreApi.kt b/samples/server/petstore/kotlin-server/jaxrs-spec/src/main/kotlin/org/openapitools/server/apis/StoreApi.kt index 2bee190b87ab..412acc0c2276 100644 --- a/samples/server/petstore/kotlin-server/jaxrs-spec/src/main/kotlin/org/openapitools/server/apis/StoreApi.kt +++ b/samples/server/petstore/kotlin-server/jaxrs-spec/src/main/kotlin/org/openapitools/server/apis/StoreApi.kt @@ -10,28 +10,32 @@ import java.io.InputStream -@Path("/") +@Path("/store") @javax.annotation.Generated(value = arrayOf("org.openapitools.codegen.languages.KotlinServerCodegen"), comments = "Generator version: 7.20.0-SNAPSHOT") class StoreApi { @DELETE + @Path("/order/{orderId}") suspend fun deleteOrder(@PathParam("orderId") orderId: kotlin.String): Response { return Response.ok().entity("magic!").build(); } @GET + @Path("/inventory") @Produces("application/json") suspend fun getInventory(): Response { return Response.ok().entity("magic!").build(); } @GET + @Path("/order/{orderId}") @Produces("application/xml", "application/json") suspend fun getOrderById(@PathParam("orderId") orderId: kotlin.Long): Response { return Response.ok().entity("magic!").build(); } @POST + @Path("/order") @Produces("application/xml", "application/json") suspend fun placeOrder( body: Order): Response { return Response.ok().entity("magic!").build(); diff --git a/samples/server/petstore/kotlin-server/jaxrs-spec/src/main/kotlin/org/openapitools/server/apis/UserApi.kt b/samples/server/petstore/kotlin-server/jaxrs-spec/src/main/kotlin/org/openapitools/server/apis/UserApi.kt index d58197c8acc4..c6727e5d24dd 100644 --- a/samples/server/petstore/kotlin-server/jaxrs-spec/src/main/kotlin/org/openapitools/server/apis/UserApi.kt +++ b/samples/server/petstore/kotlin-server/jaxrs-spec/src/main/kotlin/org/openapitools/server/apis/UserApi.kt @@ -10,7 +10,7 @@ import java.io.InputStream -@Path("/") +@Path("/user") @javax.annotation.Generated(value = arrayOf("org.openapitools.codegen.languages.KotlinServerCodegen"), comments = "Generator version: 7.20.0-SNAPSHOT") class UserApi { @@ -20,38 +20,45 @@ class UserApi { } @POST + @Path("/createWithArray") suspend fun createUsersWithArrayInput( body: kotlin.collections.List): Response { return Response.ok().entity("magic!").build(); } @POST + @Path("/createWithList") suspend fun createUsersWithListInput( body: kotlin.collections.List): Response { return Response.ok().entity("magic!").build(); } @DELETE + @Path("/{username}") suspend fun deleteUser(@PathParam("username") username: kotlin.String): Response { return Response.ok().entity("magic!").build(); } @GET + @Path("/{username}") @Produces("application/xml", "application/json") suspend fun getUserByName(@PathParam("username") username: kotlin.String): Response { return Response.ok().entity("magic!").build(); } @GET + @Path("/login") @Produces("application/xml", "application/json") suspend fun loginUser(@QueryParam("username") username: kotlin.String,@QueryParam("password") password: kotlin.String): Response { return Response.ok().entity("magic!").build(); } @GET + @Path("/logout") suspend fun logoutUser(): Response { return Response.ok().entity("magic!").build(); } @PUT + @Path("/{username}") suspend fun updateUser(@PathParam("username") username: kotlin.String, body: User): Response { return Response.ok().entity("magic!").build(); }