diff --git a/functional-tests/build.gradle b/functional-tests/build.gradle index fa0352b0..3979f02f 100644 --- a/functional-tests/build.gradle +++ b/functional-tests/build.gradle @@ -18,6 +18,7 @@ functionalTesting { pluginUnderTest('docker') pluginUnderTest('aot') pluginUnderTest('gradle') + pluginUnderTest('openapi') pluginUnderTest('test-resources') } diff --git a/functional-tests/src/test/groovy/io/micronaut/gradle/aot/MicronautAOTDockerSpec.groovy b/functional-tests/src/test/groovy/io/micronaut/gradle/aot/MicronautAOTDockerSpec.groovy index 06270ccd..5b725566 100644 --- a/functional-tests/src/test/groovy/io/micronaut/gradle/aot/MicronautAOTDockerSpec.groovy +++ b/functional-tests/src/test/groovy/io/micronaut/gradle/aot/MicronautAOTDockerSpec.groovy @@ -82,7 +82,7 @@ RUN mkdir -p /home/app/config-dirs/ch.qos.logback/logback-classic/4.0.0 COPY config-dirs/generateResourcesConfigFile /home/app/config-dirs/generateResourcesConfigFile COPY config-dirs/io.netty/netty-common/4.0.0.Final /home/app/config-dirs/io.netty/netty-common/4.0.0.Final COPY config-dirs/ch.qos.logback/logback-classic/4.0.0 /home/app/config-dirs/ch.qos.logback/logback-classic/4.0.0 -RUN native-image --exclude-config .*/libs/netty-transport-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-buffer-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-codec-http2-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-codec-http-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-common-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-handler-4.0.0.Final.jar ^/META-INF/native-image/.* -cp /home/app/libs/*.jar:/home/app/resources:/home/app/application.jar --no-fallback -o application -H:ConfigurationFileDirectories=/home/app/config-dirs/generateResourcesConfigFile,/home/app/config-dirs/io.netty/netty-buffer/4.0.0.Final,/home/app/config-dirs/io.netty/netty-common/4.0.0.Final,/home/app/config-dirs/io.netty/netty-codec-http/4.0.0.Final,/home/app/config-dirs/io.netty/netty-transport/4.0.0.Final,/home/app/config-dirs/io.netty/netty-handler/4.0.0.Final,/home/app/config-dirs/io.netty/netty-codec-http2/4.0.0.Final,/home/app/config-dirs/ch.qos.logback/logback-classic/4.0.0 demo.app.Application +RUN native-image --exclude-config .*/libs/netty-transport-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-buffer-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-codec-http-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-handler-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-common-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-codec-http2-4.0.0.Final.jar ^/META-INF/native-image/.* -cp /home/app/libs/*.jar:/home/app/resources:/home/app/application.jar --no-fallback -o application -H:ConfigurationFileDirectories=/home/app/config-dirs/generateResourcesConfigFile,/home/app/config-dirs/io.netty/netty-buffer/4.0.0.Final,/home/app/config-dirs/io.netty/netty-common/4.0.0.Final,/home/app/config-dirs/io.netty/netty-codec-http/4.0.0.Final,/home/app/config-dirs/io.netty/netty-transport/4.0.0.Final,/home/app/config-dirs/io.netty/netty-handler/4.0.0.Final,/home/app/config-dirs/io.netty/netty-codec-http2/4.0.0.Final,/home/app/config-dirs/ch.qos.logback/logback-classic/4.0.0 demo.app.Application FROM frolvlad/alpine-glibc:alpine-${DefaultVersions.ALPINE} RUN apk --no-cache update && apk add libstdc++ EXPOSE 8080 diff --git a/functional-tests/src/test/groovy/io/micronaut/gradle/docker/DockerNativeFunctionalTest.groovy b/functional-tests/src/test/groovy/io/micronaut/gradle/docker/DockerNativeFunctionalTest.groovy index 2cd1cf5a..00fd1776 100644 --- a/functional-tests/src/test/groovy/io/micronaut/gradle/docker/DockerNativeFunctionalTest.groovy +++ b/functional-tests/src/test/groovy/io/micronaut/gradle/docker/DockerNativeFunctionalTest.groovy @@ -592,7 +592,7 @@ RUN mkdir -p /home/alternate/config-dirs/generateResourcesConfigFile RUN mkdir -p /home/alternate/config-dirs/io.netty/netty-common/4.0.0.Final COPY config-dirs/generateResourcesConfigFile /home/alternate/config-dirs/generateResourcesConfigFile COPY config-dirs/io.netty/netty-common/4.0.0.Final /home/alternate/config-dirs/io.netty/netty-common/4.0.0.Final -RUN native-image --exclude-config .*/libs/netty-transport-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-buffer-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-codec-http2-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-codec-http-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-common-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-handler-4.0.0.Final.jar ^/META-INF/native-image/.* -cp /home/alternate/libs/*.jar:/home/alternate/resources:/home/alternate/application.jar --no-fallback -o application -H:ConfigurationFileDirectories=/home/alternate/config-dirs/generateResourcesConfigFile,/home/alternate/config-dirs/io.netty/netty-buffer/4.0.0.Final,/home/alternate/config-dirs/io.netty/netty-common/4.0.0.Final,/home/alternate/config-dirs/io.netty/netty-codec-http/4.0.0.Final,/home/alternate/config-dirs/io.netty/netty-transport/4.0.0.Final,/home/alternate/config-dirs/io.netty/netty-handler/4.0.0.Final,/home/alternate/config-dirs/io.netty/netty-codec-http2/4.0.0.Final example.Application +RUN native-image --exclude-config .*/libs/netty-transport-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-buffer-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-codec-http-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-handler-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-common-4.0.0.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-codec-http2-4.0.0.Final.jar ^/META-INF/native-image/.* -cp /home/alternate/libs/*.jar:/home/alternate/resources:/home/alternate/application.jar --no-fallback -o application -H:ConfigurationFileDirectories=/home/alternate/config-dirs/generateResourcesConfigFile,/home/alternate/config-dirs/io.netty/netty-buffer/4.0.0.Final,/home/alternate/config-dirs/io.netty/netty-common/4.0.0.Final,/home/alternate/config-dirs/io.netty/netty-codec-http/4.0.0.Final,/home/alternate/config-dirs/io.netty/netty-transport/4.0.0.Final,/home/alternate/config-dirs/io.netty/netty-handler/4.0.0.Final,/home/alternate/config-dirs/io.netty/netty-codec-http2/4.0.0.Final example.Application ${defaultDockerFrom} EXPOSE 8080 HEALTHCHECK CMD curl -s localhost:8090/health | grep '"status":"UP"' diff --git a/functional-tests/src/test/groovy/io/micronaut/gradle/fixtures/AbstractFunctionalTest.groovy b/functional-tests/src/test/groovy/io/micronaut/gradle/fixtures/AbstractFunctionalTest.groovy index 25e4fe4c..afbd654b 100644 --- a/functional-tests/src/test/groovy/io/micronaut/gradle/fixtures/AbstractFunctionalTest.groovy +++ b/functional-tests/src/test/groovy/io/micronaut/gradle/fixtures/AbstractFunctionalTest.groovy @@ -35,6 +35,7 @@ abstract class AbstractFunctionalTest extends AbstractGradleBuildSpec { id 'io.micronaut.graalvm' version '${version}' id 'io.micronaut.docker' version '${version}' id 'io.micronaut.aot' version '${version}' + id 'io.micronaut.openapi' version '${version}' id 'io.micronaut.test-resources' version '${version}' } } diff --git a/functional-tests/src/test/groovy/io/micronaut/gradle/openapi/AbstractOpenApiWithKotlinSpec.groovy b/functional-tests/src/test/groovy/io/micronaut/gradle/openapi/AbstractOpenApiWithKotlinSpec.groovy new file mode 100644 index 00000000..76a6e42a --- /dev/null +++ b/functional-tests/src/test/groovy/io/micronaut/gradle/openapi/AbstractOpenApiWithKotlinSpec.groovy @@ -0,0 +1,16 @@ +package io.micronaut.gradle.openapi + +import io.micronaut.gradle.fixtures.AbstractEagerConfiguringFunctionalTest +import spock.lang.Shared + +class AbstractOpenApiWithKotlinSpec extends AbstractEagerConfiguringFunctionalTest { + @Shared + protected final String kotlinVersion = System.getProperty("kotlinVersion") + + @Shared + protected final String kspVersion = System.getProperty("kspVersion") + + protected void withPetstore() { + file("petstore.json").text = this.class.getResourceAsStream("/petstore.json").getText("UTF-8") + } +} diff --git a/functional-tests/src/test/groovy/io/micronaut/gradle/openapi/OpenApiClientWithKotlinSpec.groovy b/functional-tests/src/test/groovy/io/micronaut/gradle/openapi/OpenApiClientWithKotlinSpec.groovy new file mode 100644 index 00000000..8ffb4208 --- /dev/null +++ b/functional-tests/src/test/groovy/io/micronaut/gradle/openapi/OpenApiClientWithKotlinSpec.groovy @@ -0,0 +1,109 @@ +package io.micronaut.gradle.openapi + + +import org.gradle.testkit.runner.TaskOutcome + +class OpenApiClientWithKotlinSpec extends AbstractOpenApiWithKotlinSpec { + + def "can generate an kotlin OpenAPI client implementation with some properties (KAPT)"() { + given: + settingsFile << "rootProject.name = 'openapi-client'" + buildFile << """ + plugins { + id "io.micronaut.minimal.application" + id "io.micronaut.openapi" + id "org.jetbrains.kotlin.jvm" version "$kotlinVersion" + id "org.jetbrains.kotlin.plugin.allopen" version "$kotlinVersion" + id "org.jetbrains.kotlin.kapt" version "$kotlinVersion" + } + + micronaut { + version "$micronautVersion" + openapi { + client(file("petstore.json")) { + lang = "kotlin" + useReactive = true + generatedAnnotation = false + fluxForArrays = true + } + } + } + + $repositoriesBlock + + dependencies { + + kapt "io.micronaut.serde:micronaut-serde-processor" + + implementation "io.micronaut.serde:micronaut-serde-jackson" + implementation "io.micronaut.reactor:micronaut-reactor" + implementation "io.micronaut:micronaut-inject-kotlin" + } + + """ + + withPetstore() + + when: + def result = build('test') + + then: + result.task(":generateClientOpenApiApis").outcome == TaskOutcome.SUCCESS + result.task(":generateClientOpenApiModels").outcome == TaskOutcome.SUCCESS + result.task(":compileKotlin").outcome == TaskOutcome.SUCCESS + + and: + file("build/generated/openapi/generateClientOpenApiModels/src/main/kotlin/io/micronaut/openapi/model/Pet.kt").exists() + } + + def "can generate an kotlin OpenAPI client implementation with some properties (KSP)"() { + given: + settingsFile << "rootProject.name = 'openapi-client'" + buildFile << """ + plugins { + id "io.micronaut.minimal.application" + id "io.micronaut.openapi" + id "org.jetbrains.kotlin.jvm" version "$kotlinVersion" + id "org.jetbrains.kotlin.plugin.allopen" version "$kotlinVersion" + id "com.google.devtools.ksp" version "$kspVersion" + } + + micronaut { + version "$micronautVersion" + openapi { + client(file("petstore.json")) { + lang = "kotlin" + useReactive = true + generatedAnnotation = false + fluxForArrays = true + } + } + } + + $repositoriesBlock + + dependencies { + + ksp "io.micronaut.serde:micronaut-serde-processor" + + implementation "io.micronaut.serde:micronaut-serde-jackson" + implementation "io.micronaut.reactor:micronaut-reactor" + implementation "io.micronaut:micronaut-inject-kotlin" + } + + """ + + withPetstore() + + when: + def result = build('test') + + then: + result.task(":generateClientOpenApiApis").outcome == TaskOutcome.SUCCESS + result.task(":generateClientOpenApiModels").outcome == TaskOutcome.SUCCESS + result.task(":compileKotlin").outcome == TaskOutcome.SUCCESS + + and: + file("build/generated/openapi/generateClientOpenApiModels/src/main/kotlin/io/micronaut/openapi/model/Pet.kt").exists() + } +} diff --git a/functional-tests/src/test/groovy/io/micronaut/gradle/openapi/OpenApiServerWithKotlinSpec.groovy b/functional-tests/src/test/groovy/io/micronaut/gradle/openapi/OpenApiServerWithKotlinSpec.groovy new file mode 100644 index 00000000..c9d4ae92 --- /dev/null +++ b/functional-tests/src/test/groovy/io/micronaut/gradle/openapi/OpenApiServerWithKotlinSpec.groovy @@ -0,0 +1,119 @@ +package io.micronaut.gradle.openapi + + +import org.gradle.testkit.runner.TaskOutcome + +class OpenApiServerWithKotlinSpec extends AbstractOpenApiWithKotlinSpec { + + def "can generate an kotlin OpenAPI server implementation with properties (KAPT)"() { + given: + settingsFile << "rootProject.name = 'openapi-server'" + buildFile << """ + plugins { + id "io.micronaut.minimal.application" + id "io.micronaut.openapi" + id "org.jetbrains.kotlin.jvm" version "$kotlinVersion" + id "org.jetbrains.kotlin.plugin.allopen" version "$kotlinVersion" + id "org.jetbrains.kotlin.kapt" version "$kotlinVersion" + } + + micronaut { + version "$micronautVersion" + runtime "netty" + testRuntime "junit5" + openapi { + server(file("petstore.json")) { + lang = "kotlin" + useReactive = true + generatedAnnotation = false + fluxForArrays = true + aot = true + } + } + } + + $repositoriesBlock + mainClassName="example.Application" + + dependencies { + + kapt "io.micronaut.serde:micronaut-serde-processor" + + implementation "io.micronaut.security:micronaut-security" + implementation "io.micronaut.serde:micronaut-serde-jackson" + } + """ + + withPetstore() + + when: + def result = build('test') + + then: + result.task(":generateServerOpenApiApis").outcome == TaskOutcome.SUCCESS + result.task(":generateServerOpenApiModels").outcome == TaskOutcome.SUCCESS + result.task(":compileKotlin").outcome == TaskOutcome.SUCCESS + + and: + file("build/generated/openapi/generateServerOpenApiApis/src/main/kotlin/io/micronaut/openapi/api/PetApi.kt").exists() + file("build/generated/openapi/generateServerOpenApiModels/src/main/kotlin/io/micronaut/openapi/model/Pet.kt").exists() + file("build/classes/kotlin/main/io/micronaut/openapi/api/PetApi.class").exists() + file("build/classes/kotlin/main/io/micronaut/openapi/model/Pet.class").exists() + } + + def "can generate an kotlin OpenAPI server implementation with properties (KSP)"() { + given: + settingsFile << "rootProject.name = 'openapi-server'" + buildFile << """ + plugins { + id "io.micronaut.minimal.application" + id "io.micronaut.openapi" + id "org.jetbrains.kotlin.jvm" version "$kotlinVersion" + id "org.jetbrains.kotlin.plugin.allopen" version "$kotlinVersion" + id "com.google.devtools.ksp" version "$kspVersion" + } + + micronaut { + version "$micronautVersion" + runtime "netty" + testRuntime "junit5" + openapi { + server(file("petstore.json")) { + lang = "kotlin" + useReactive = true + generatedAnnotation = false + fluxForArrays = true + aot = true + } + } + } + + $repositoriesBlock + mainClassName="example.Application" + + dependencies { + + ksp "io.micronaut.serde:micronaut-serde-processor" + + implementation "io.micronaut.security:micronaut-security" + implementation "io.micronaut.serde:micronaut-serde-jackson" + } + """ + + withPetstore() + + when: + def result = build('test') + + then: + result.task(":generateServerOpenApiApis").outcome == TaskOutcome.SUCCESS + result.task(":generateServerOpenApiModels").outcome == TaskOutcome.SUCCESS + result.task(":compileKotlin").outcome == TaskOutcome.SUCCESS + + and: + file("build/generated/openapi/generateServerOpenApiApis/src/main/kotlin/io/micronaut/openapi/api/PetApi.kt").exists() + file("build/generated/openapi/generateServerOpenApiModels/src/main/kotlin/io/micronaut/openapi/model/Pet.kt").exists() + file("build/classes/kotlin/main/io/micronaut/openapi/api/PetApi.class").exists() + file("build/classes/kotlin/main/io/micronaut/openapi/model/Pet.class").exists() + } +} diff --git a/functional-tests/src/test/resources/petstore.json b/functional-tests/src/test/resources/petstore.json new file mode 100644 index 00000000..ad552318 --- /dev/null +++ b/functional-tests/src/test/resources/petstore.json @@ -0,0 +1,973 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample server Petstore server. For this sample, you can use the api key \"special-key\" to test the authorization filters", + "version": "1.0.0", + "title": "OpenAPI Petstore", + "license": { + "name": "Apache 2.0", + "url": "https://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "host": "petstore.swagger.io", + "basePath": "/v2", + "schemes": [ + "http" + ], + "paths": { + "/pet": { + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "", + "operationId": "addPet", + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": false, + "schema": { + "$ref": "#/definitions/Pet" + } + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "put": { + "tags": [ + "pet" + ], + "summary": "Update an existing pet", + "description": "", + "operationId": "updatePet", + "consumes": [ + "application/json", + "application/xml" + ], + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Pet object that needs to be added to the store", + "required": false, + "schema": { + "$ref": "#/definitions/Pet" + } + } + ], + "responses": { + "405": { + "description": "Validation exception" + }, + "404": { + "description": "Pet not found" + }, + "400": { + "description": "Invalid ID supplied" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByStatus": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Status values that need to be considered for filter", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi", + "default": ["available"] + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "description": "Invalid status value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByTags": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "Tags to filter by", + "required": false, + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "multi" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Pet" + } + } + }, + "400": { + "description": "Invalid tag value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/{petId}": { + "get": { + "tags": [ + "pet" + ], + "summary": "Find pet by ID", + "description": "Returns a pet when ID < 10. ID > 10 or nonintegers will simulate API error conditions", + "operationId": "getPetById", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be fetched", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "404": { + "description": "Pet not found" + }, + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Pet" + } + }, + "400": { + "description": "Invalid ID supplied" + } + }, + "security": [ + { + "api_key": [] + }, + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "description": "", + "operationId": "updatePetWithForm", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": true, + "type": "string" + }, + { + "name": "name", + "in": "formData", + "description": "Updated name of the pet", + "required": false, + "type": "string" + }, + { + "name": "status", + "in": "formData", + "description": "Updated status of the pet", + "required": false, + "type": "string" + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "delete": { + "tags": [ + "pet" + ], + "summary": "Deletes a pet", + "description": "", + "operationId": "deletePet", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "api_key", + "in": "header", + "description": "", + "required": false, + "type": "string" + }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "type": "integer", + "format": "int64" + } + ], + "responses": { + "400": { + "description": "Invalid pet value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/{petId}/uploadImage": { + "post": { + "tags": [ + "pet" + ], + "summary": "uploads an image", + "description": "", + "operationId": "uploadFile", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to update", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "additionalMetadata", + "in": "formData", + "description": "Additional data to pass to server", + "required": false, + "type": "string" + }, + { + "name": "file", + "in": "formData", + "description": "file to upload", + "required": false, + "type": "file" + } + ], + "responses": { + "default": { + "description": "successful operation" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/store/inventory": { + "get": { + "tags": [ + "store" + ], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "produces": [ + "application/json", + "application/xml" + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/store/order": { + "post": { + "tags": [ + "store" + ], + "summary": "Place an order for a pet", + "description": "", + "operationId": "placeOrder", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "order placed for purchasing the pet", + "required": false, + "schema": { + "$ref": "#/definitions/Order" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Order" + } + }, + "400": { + "description": "Invalid Order" + } + } + } + }, + "/store/order/{orderId}": { + "get": { + "tags": [ + "store" + ], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions", + "operationId": "getOrderById", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of pet that needs to be fetched", + "required": true, + "type": "string" + } + ], + "responses": { + "404": { + "description": "Order not found" + }, + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Order" + } + }, + "400": { + "description": "Invalid ID supplied" + } + } + }, + "delete": { + "tags": [ + "store" + ], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", + "operationId": "deleteOrder", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of the order that needs to be deleted", + "required": true, + "type": "string" + } + ], + "responses": { + "404": { + "description": "Order not found" + }, + "400": { + "description": "Invalid ID supplied" + } + } + } + }, + "/user": { + "post": { + "tags": [ + "user" + ], + "summary": "Create user", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "Created user object", + "required": false, + "schema": { + "$ref": "#/definitions/User" + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/createWithArray": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "", + "operationId": "createUsersWithArrayInput", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "List of user object", + "required": false, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/createWithList": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "", + "operationId": "createUsersWithListInput", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "in": "body", + "name": "body", + "description": "List of user object", + "required": false, + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + } + } + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/login": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs user into the system", + "description": "", + "operationId": "loginUser", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "username", + "in": "query", + "description": "The user name for login", + "required": false, + "type": "string" + }, + { + "name": "password", + "in": "query", + "description": "The password for login in clear text", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Invalid username/password supplied" + } + } + } + }, + "/user/logout": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs out current logged in user session", + "description": "", + "operationId": "logoutUser", + "produces": [ + "application/json", + "application/xml" + ], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/{username}": { + "get": { + "tags": [ + "user" + ], + "summary": "Get user by user name", + "description": "", + "operationId": "getUserByName", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be fetched. Use user1 for testing. ", + "required": true, + "type": "string" + } + ], + "responses": { + "404": { + "description": "User not found" + }, + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/User" + }, + "examples": { + "application/json": { + "id": 1, + "username": "johnp", + "firstName": "John", + "lastName": "Public", + "email": "johnp@swagger.io", + "password": "-secret-", + "phone": "0123456789", + "userStatus": 0 + } + } + }, + "400": { + "description": "Invalid username supplied" + } + } + }, + "put": { + "tags": [ + "user" + ], + "summary": "Updated user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "name that need to be deleted", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "body", + "description": "Updated user object", + "required": false, + "schema": { + "$ref": "#/definitions/User" + } + } + ], + "responses": { + "404": { + "description": "User not found" + }, + "400": { + "description": "Invalid user supplied" + } + } + }, + "delete": { + "tags": [ + "user" + ], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be deleted", + "required": true, + "type": "string" + } + ], + "responses": { + "404": { + "description": "User not found" + }, + "400": { + "description": "Invalid username supplied" + } + } + } + } + }, + "securityDefinitions": { + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + }, + "petstore_auth": { + "type": "oauth2", + "authorizationUrl": "http://petstore.swagger.io/api/oauth/dialog", + "flow": "implicit", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + } + }, + "definitions": { + "User": { + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "username": { + "type": "string" + }, + "firstName": { + "type": "string" + }, + "lastName": { + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "userStatus": { + "type": "integer", + "format": "int32", + "description": "User Status" + } + }, + "xml": { + "name": "User" + } + }, + "Category": { + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Category" + } + }, + "Pet": { + "required": [ + "name", + "photoUrls" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "category": { + "$ref": "#/definitions/Category" + }, + "name": { + "type": "string", + "example": "doggie" + }, + "photoUrls": { + "type": "array", + "xml": { + "name": "photoUrl", + "wrapped": true + }, + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "xml": { + "name": "tag", + "wrapped": true + }, + "items": { + "$ref": "#/definitions/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "xml": { + "name": "Pet" + } + }, + "Tag": { + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "Tag" + } + }, + "Order": { + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "petId": { + "type": "integer", + "format": "int64" + }, + "quantity": { + "type": "integer", + "format": "int32" + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": "boolean" + } + }, + "xml": { + "name": "Order" + } + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f6b77251..4e4c201e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,10 +9,10 @@ spock = "2.1-groovy-3.0" oraclelinux = "9" alpine="3.16" graalvmPlugin = "0.9.28" -micronaut-platform = "4.1.2" # This is the platform version, used in our tests +micronaut-platform = "4.2.0" # This is the platform version, used in our tests micronaut-aot = "2.1.1" micronaut-testresources = "2.2.0" -micronaut-openapi = "5.1.1" +micronaut-openapi = "6.2.1" log4j2 = { require = "2.17.1", reject = ["]0, 2.17["] } jetbrains-annotations = "24.1.0" tomlj = "1.1.0" diff --git a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/DefaultOpenApiExtension.java b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/DefaultOpenApiExtension.java index 3e61b46a..03afbe04 100644 --- a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/DefaultOpenApiExtension.java +++ b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/DefaultOpenApiExtension.java @@ -25,6 +25,7 @@ import org.gradle.api.artifacts.Configuration; import org.gradle.api.file.Directory; import org.gradle.api.file.RegularFile; +import org.gradle.api.file.SourceDirectorySet; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; @@ -39,6 +40,7 @@ import static org.codehaus.groovy.runtime.StringGroovyMethods.capitalize; public abstract class DefaultOpenApiExtension implements OpenApiExtension { + public static final String OPENAPI_GROUP = "Micronaut OpenAPI"; // We use a String here because the type is not available at runtime because of classpath isolation private static final String DEFAULT_SERIALIZATION_FRAMEWORK = "MICRONAUT_SERDE_JACKSON"; @@ -72,15 +74,18 @@ public void server(String name, Provider definition, Action { configureCommonProperties(name, task, serverSpec, definition); + task.getAot().set(serverSpec.getAot()); task.setDescription("Generates OpenAPI controllers from an OpenAPI definition"); configureServerTask(serverSpec, task); task.getOutputKinds().addAll("APIS", "SUPPORTING_FILES"); }); var models = project.getTasks().register(generateModelsTaskName(name), OpenApiServerGenerator.class, task -> { configureCommonProperties(name, task, serverSpec, definition); + task.getAot().set(serverSpec.getAot()); task.setDescription("Generates OpenAPI models from an OpenAPI definition"); configureServerTask(serverSpec, task); task.getOutputKinds().add("MODELS"); @@ -89,6 +94,13 @@ public void server(String name, Provider definition, Action { + var ext = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getExtensions().getByName("kotlin"); + if (ext instanceof SourceDirectorySet kotlinMain) { + kotlinMain.srcDir(controllers.map(d -> DefaultOpenApiExtension.mainSrcDir(d, "kotlin"))); + kotlinMain.srcDir(models.map(d -> DefaultOpenApiExtension.mainSrcDir(d, "kotlin"))); + } + }); }); } else { throwDuplicateEntryFor(name); @@ -103,13 +115,27 @@ private void configureCommonExtensionDefaults(OpenApiSpec spec) { spec.getUseBeanValidation().convention(true); spec.getUseOptional().convention(false); spec.getUseReactive().convention(true); + spec.getLombok().convention(false); + spec.getGeneratedAnnotation().convention(true); + spec.getFluxForArrays().convention(false); spec.getSerializationFramework().convention(DEFAULT_SERIALIZATION_FRAMEWORK); spec.getAlwaysUseGenerateHttpResponse().convention(false); spec.getGenerateHttpResponseWhereRequired().convention(false); spec.getDateTimeFormat().convention("ZONED_DATETIME"); + spec.getLang().convention("java"); withJava(() -> { + var compileOnlyDeps = project.getConfigurations().getByName("compileOnly").getDependencies(); + if ("java".equalsIgnoreCase(spec.getLang().get())) { + compileOnlyDeps.addAllLater(spec.getLombok().map(lombok -> { + if (Boolean.TRUE.equals(lombok)) { + return List.of(project.getDependencies().create("org.projectlombok:lombok")); + } + return List.of(); + })); + } + compileOnlyDeps.add(project.getDependencies().create("io.micronaut.openapi:micronaut-openapi")); + var implDeps = project.getConfigurations().getByName("implementation").getDependencies(); - implDeps.add(project.getDependencies().create("io.micronaut.openapi:micronaut-openapi")); implDeps.addAllLater(spec.getUseReactive().map(reactive -> { if (Boolean.TRUE.equals(reactive)) { return List.of(project.getDependencies().create("io.projectreactor:reactor-core")); @@ -138,6 +164,10 @@ private void configureCommonProperties(String name, AbstractOpenApiGenerator definition, Action { + var ext = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getExtensions().getByName("kotlin"); + if (ext instanceof SourceDirectorySet kotlinMain) { + kotlinMain.srcDir(client.map(d -> DefaultOpenApiExtension.mainSrcDir(d, "kotlin"))); + kotlinMain.srcDir(models.map(d -> DefaultOpenApiExtension.mainSrcDir(d, "kotlin"))); + } + }); }); withJava(() -> { var implDeps = project.getConfigurations().getByName("implementation").getDependencies(); @@ -186,8 +223,12 @@ public void client(String name, Provider definition, Action mainSrcDir(AbstractOpenApiGenerator t) { - return t.getOutputDirectory().dir("src/main/java"); + private static Provider mainSrcDir(AbstractOpenApiGenerator t, String language) { + return t.getOutputDirectory().dir("src/main/" + language); + } + + private static Provider mainSrcDir(AbstractOpenApiGenerator t) { + return mainSrcDir(t, "java"); } private static void configureClientTask(OpenApiClientSpec clientSpec, OpenApiClientGenerator task) { @@ -213,6 +254,7 @@ private static String generateApisTaskName(String name) { private static void configureServerTask(OpenApiServerSpec serverSpec, OpenApiServerGenerator task) { task.getControllerPackage().convention(serverSpec.getControllerPackage()); task.getUseAuth().convention(serverSpec.getUseAuth()); + task.getAot().convention(serverSpec.getAot()); } } diff --git a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/OpenApiClientSpec.java b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/OpenApiClientSpec.java index e3619de5..ce495f2b 100644 --- a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/OpenApiClientSpec.java +++ b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/OpenApiClientSpec.java @@ -19,6 +19,7 @@ import org.gradle.api.provider.Property; public interface OpenApiClientSpec extends OpenApiSpec { + Property getClientId(); Property getUseAuth(); diff --git a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/OpenApiExtension.java b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/OpenApiExtension.java index 1731e0a4..88081d39 100644 --- a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/OpenApiExtension.java +++ b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/OpenApiExtension.java @@ -26,6 +26,7 @@ * Configures the OpenAPI code generator. */ public interface OpenApiExtension { + /** * The version of the Micronaut OpenAPI generator. * @return the version diff --git a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/OpenApiServerSpec.java b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/OpenApiServerSpec.java index a94d9822..92d61111 100644 --- a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/OpenApiServerSpec.java +++ b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/OpenApiServerSpec.java @@ -18,7 +18,10 @@ import org.gradle.api.provider.Property; public interface OpenApiServerSpec extends OpenApiSpec { + Property getControllerPackage(); Property getUseAuth(); + + Property getAot(); } diff --git a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/OpenApiSpec.java b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/OpenApiSpec.java index a454cb7c..b62e5d28 100644 --- a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/OpenApiSpec.java +++ b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/OpenApiSpec.java @@ -19,6 +19,9 @@ import org.gradle.api.provider.Property; public interface OpenApiSpec { + + Property getLang(); + Property getInvokerPackageName(); Property getApiPackageName(); @@ -37,6 +40,12 @@ public interface OpenApiSpec { Property getGenerateHttpResponseWhereRequired(); + Property getLombok(); + + Property getGeneratedAnnotation(); + + Property getFluxForArrays(); + Property getDateTimeFormat(); ListProperty getParameterMappings(); diff --git a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/AbstractOpenApiGenerator.java b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/AbstractOpenApiGenerator.java index c9b960b8..fcd206e2 100644 --- a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/AbstractOpenApiGenerator.java +++ b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/AbstractOpenApiGenerator.java @@ -44,6 +44,9 @@ public abstract class AbstractOpenApiGenerator getLang(); + @Input public abstract Property getInvokerPackageName(); @@ -80,6 +83,15 @@ public abstract class AbstractOpenApiGenerator getParameterMappings(); + @Input + public abstract Property getLombok(); + + @Input + public abstract Property getGeneratedAnnotation(); + + @Input + public abstract Property getFluxForArrays(); + @Input public abstract ListProperty getResponseBodyMappings(); @@ -98,6 +110,7 @@ public abstract class AbstractOpenApiGenerator spec.getClasspath().from(getClasspath())) .submit(getWorkerAction(), params -> { + params.getLang().set(getLang()); params.getApiPackageName().set(getApiPackageName()); params.getInvokerPackageName().set(getInvokerPackageName()); params.getSerializationFramework().set(getSerializationFramework()); @@ -113,6 +126,9 @@ public final void execute() { params.getDateTimeFormat().set(getDateTimeFormat()); params.getParameterMappings().set(getParameterMappings()); params.getResponseBodyMappings().set(getResponseBodyMappings()); + params.getFluxForArrays().set(getFluxForArrays()); + params.getGeneratedAnnotation().set(getGeneratedAnnotation()); + params.getLombok().set(getLombok()); configureWorkerParameters(params); }); } diff --git a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/AbstractOpenApiWorkAction.java b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/AbstractOpenApiWorkAction.java index e75a789b..b6543c28 100644 --- a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/AbstractOpenApiWorkAction.java +++ b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/AbstractOpenApiWorkAction.java @@ -15,13 +15,18 @@ */ package io.micronaut.gradle.openapi.tasks; +import java.util.Locale; + import io.micronaut.gradle.openapi.ParameterMappingModel; import io.micronaut.gradle.openapi.ResponseBodyMappingModel; -import io.micronaut.openapi.generator.AbstractMicronautJavaCodegen; import io.micronaut.openapi.generator.MicronautCodeGeneratorBuilder; import io.micronaut.openapi.generator.MicronautCodeGeneratorEntryPoint; import io.micronaut.openapi.generator.MicronautCodeGeneratorOptionsBuilder; +import io.micronaut.openapi.generator.MicronautCodeGeneratorOptionsBuilder.GeneratorLanguage; +import io.micronaut.openapi.generator.ParameterMapping; +import io.micronaut.openapi.generator.ResponseBodyMapping; import io.micronaut.openapi.generator.SerializationLibraryKind; + import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.ListProperty; @@ -29,10 +34,12 @@ import org.gradle.workers.WorkAction; import org.gradle.workers.WorkParameters; -import java.util.Locale; - public abstract class AbstractOpenApiWorkAction implements WorkAction { + interface OpenApiParameters extends WorkParameters { + + Property getLang(); + RegularFileProperty getDefinitionFile(); Property getInvokerPackageName(); @@ -59,6 +66,12 @@ interface OpenApiParameters extends WorkParameters { Property getDateTimeFormat(); + Property getLombok(); + + Property getGeneratedAnnotation(); + + Property getFluxForArrays(); + ListProperty getParameterMappings(); ListProperty getResponseBodyMappings(); @@ -69,6 +82,7 @@ interface OpenApiParameters extends WorkParameters { @Override public void execute() { var parameters = getParameters(); + var lang = parameters.getLang().get(); var builder = MicronautCodeGeneratorEntryPoint.builder() .withDefinitionFile(parameters.getDefinitionFile().get().getAsFile().toURI()) .withOutputDirectory(parameters.getOutputDirectory().getAsFile().get()) @@ -78,36 +92,36 @@ public void execute() { .map(s -> MicronautCodeGeneratorEntryPoint.OutputKind.valueOf(s.toUpperCase(Locale.US))) .toArray(MicronautCodeGeneratorEntryPoint.OutputKind[]::new) ) - .withOptions(options -> { - options.withInvokerPackage(parameters.getInvokerPackageName().get()); - options.withApiPackage(parameters.getApiPackageName().get()); - options.withModelPackage(parameters.getModelPackageName().get()); - options.withBeanValidation(parameters.getUseBeanValidation().get()); - options.withOptional(parameters.getUseOptional().get()); - options.withReactive(parameters.getUseReactive().get()); - options.withSerializationLibrary(SerializationLibraryKind.valueOf(parameters.getSerializationFramework().get().toUpperCase(Locale.US))); - options.withGenerateHttpResponseAlways(parameters.getAlwaysUseGenerateHttpResponse().get()); - options.withGenerateHttpResponseWhereRequired(parameters.getGenerateHttpResponseWhereRequired().get()); - options.withDateTimeFormat(MicronautCodeGeneratorOptionsBuilder.DateTimeFormat.valueOf(parameters.getDateTimeFormat().get().toUpperCase(Locale.US))); - options.withParameterMappings(parameters.getParameterMappings() - .get() - .stream() - .map(mapping -> new AbstractMicronautJavaCodegen.ParameterMapping( - mapping.getName(), - AbstractMicronautJavaCodegen.ParameterMapping.ParameterLocation.valueOf(mapping.getLocation().name()), - mapping.getMappedType(), - mapping.getMappedName(), - mapping.isValidated()) - ) - .toList() - ); - options.withResponseBodyMappings(parameters.getResponseBodyMappings() - .get() - .stream() - .map(mapping -> new AbstractMicronautJavaCodegen.ResponseBodyMapping(mapping.getHeaderName(), mapping.getMappedBodyType(), mapping.isListWrapper(), mapping.isValidated())) - .toList() - ); - }); + .withOptions(options -> options.withInvokerPackage(parameters.getInvokerPackageName().get()) + .withLang("kotlin".equalsIgnoreCase(lang) ? GeneratorLanguage.KOTLIN : GeneratorLanguage.JAVA) + .withApiPackage(parameters.getApiPackageName().get()) + .withModelPackage(parameters.getModelPackageName().get()) + .withBeanValidation(parameters.getUseBeanValidation().get()) + .withOptional(parameters.getUseOptional().get()) + .withReactive(parameters.getUseReactive().get()) + .withSerializationLibrary(SerializationLibraryKind.valueOf(parameters.getSerializationFramework().get().toUpperCase(Locale.US))) + .withGenerateHttpResponseAlways(parameters.getAlwaysUseGenerateHttpResponse().get()) + .withGenerateHttpResponseWhereRequired(parameters.getGenerateHttpResponseWhereRequired().get()) + .withDateTimeFormat(MicronautCodeGeneratorOptionsBuilder.DateTimeFormat.valueOf(parameters.getDateTimeFormat().get().toUpperCase(Locale.US))) + .withParameterMappings(parameters.getParameterMappings() + .get() + .stream() + .map(mapping -> new ParameterMapping( + mapping.getName(), + ParameterMapping.ParameterLocation.valueOf(mapping.getLocation().name()), + mapping.getMappedType(), + mapping.getMappedName(), + mapping.isValidated()) + ) + .toList() + ) + .withResponseBodyMappings(parameters.getResponseBodyMappings() + .get() + .stream() + .map(mapping -> new ResponseBodyMapping(mapping.getHeaderName(), mapping.getMappedBodyType(), mapping.isListWrapper(), mapping.isValidated())) + .toList() + )); + configureBuilder(builder); builder.build().generate(); } diff --git a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/OpenApiClientWorkAction.java b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/OpenApiClientWorkAction.java index 1c8d853c..932f898c 100644 --- a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/OpenApiClientWorkAction.java +++ b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/OpenApiClientWorkAction.java @@ -15,14 +15,18 @@ */ package io.micronaut.gradle.openapi.tasks; +import java.util.List; + import io.micronaut.openapi.generator.MicronautCodeGeneratorBuilder; +import io.micronaut.openapi.generator.MicronautCodeGeneratorOptionsBuilder.GeneratorLanguage; + import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; -import java.util.List; - public abstract class OpenApiClientWorkAction extends AbstractOpenApiWorkAction { + interface ClientParameters extends AbstractOpenApiWorkAction.OpenApiParameters { + Property getClientId(); Property getUseAuth(); @@ -37,19 +41,41 @@ interface ClientParameters extends AbstractOpenApiWorkAction.OpenApiParameters { @Override protected void configureBuilder(MicronautCodeGeneratorBuilder builder) { var parameters = getParameters(); - builder.forClient(spec -> { - spec.withAuthorization(parameters.getUseAuth().get()); - if (parameters.getClientId().isPresent()) { - spec.withClientId(parameters.getClientId().get()); - } - spec.withAdditionalClientTypeAnnotations(parameters.getAdditionalClientTypeAnnotations().getOrElse(List.of())); - if (parameters.getBasePathSeparator().isPresent()) { - spec.withBasePathSeparator(parameters.getBasePathSeparator().get()); - } - if (parameters.getAuthorizationFilterPattern().isPresent()) { - spec.withAuthorizationFilterPattern(parameters.getAuthorizationFilterPattern().get()); - } - }); + if ("kotlin".equalsIgnoreCase(parameters.getLang().get())) { + builder.forKotlinClient(spec -> { + spec.withAuthorization(parameters.getUseAuth().get()) + .withAdditionalClientTypeAnnotations(parameters.getAdditionalClientTypeAnnotations().getOrElse(List.of())) + .withGeneratedAnnotation(parameters.getGeneratedAnnotation().get()); + + if (parameters.getClientId().isPresent()) { + spec.withClientId(parameters.getClientId().get()); + } + if (parameters.getBasePathSeparator().isPresent()) { + spec.withBasePathSeparator(parameters.getBasePathSeparator().get()); + } + if (parameters.getAuthorizationFilterPattern().isPresent()) { + spec.withAuthorizationFilterPattern(parameters.getAuthorizationFilterPattern().get()); + } + }); + } else { + builder.forJavaClient(spec -> { + spec.withAuthorization(parameters.getUseAuth().get()) + .withAdditionalClientTypeAnnotations(parameters.getAdditionalClientTypeAnnotations().getOrElse(List.of())) + .withLombok(parameters.getLombok().get()) + .withGeneratedAnnotation(parameters.getGeneratedAnnotation().get()) + .withFluxForArrays(parameters.getFluxForArrays().get()); + + if (parameters.getClientId().isPresent()) { + spec.withClientId(parameters.getClientId().get()); + } + if (parameters.getBasePathSeparator().isPresent()) { + spec.withBasePathSeparator(parameters.getBasePathSeparator().get()); + } + if (parameters.getAuthorizationFilterPattern().isPresent()) { + spec.withAuthorizationFilterPattern(parameters.getAuthorizationFilterPattern().get()); + } + }); + } } } diff --git a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/OpenApiServerGenerator.java b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/OpenApiServerGenerator.java index 43d81486..b7e75abe 100644 --- a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/OpenApiServerGenerator.java +++ b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/OpenApiServerGenerator.java @@ -18,15 +18,21 @@ import org.gradle.api.provider.Property; import org.gradle.api.tasks.CacheableTask; import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; @CacheableTask public abstract class OpenApiServerGenerator extends AbstractOpenApiGenerator { + @Input public abstract Property getControllerPackage(); @Input public abstract Property getUseAuth(); + @Optional + @Input + public abstract Property getAot(); + @Override protected Class getWorkerAction() { return OpenApiServerWorkAction.class; @@ -36,5 +42,6 @@ protected Class getWorkerAction() { protected void configureWorkerParameters(OpenApiServerWorkAction.ServerParameters params) { params.getControllerPackage().set(getControllerPackage()); params.getUseAuth().set(getUseAuth()); + params.getAot().set(getAot()); } } diff --git a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/OpenApiServerWorkAction.java b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/OpenApiServerWorkAction.java index 5d831b30..87055bfb 100644 --- a/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/OpenApiServerWorkAction.java +++ b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/tasks/OpenApiServerWorkAction.java @@ -16,25 +16,45 @@ package io.micronaut.gradle.openapi.tasks; import io.micronaut.openapi.generator.MicronautCodeGeneratorBuilder; +import io.micronaut.openapi.generator.MicronautCodeGeneratorOptionsBuilder.GeneratorLanguage; + import org.gradle.api.provider.Property; public abstract class OpenApiServerWorkAction extends AbstractOpenApiWorkAction { + interface ServerParameters extends OpenApiParameters { + Property getControllerPackage(); Property getUseAuth(); + + Property getAot(); } @Override protected void configureBuilder(MicronautCodeGeneratorBuilder builder) { var parameters = getParameters(); - builder.forServer(spec -> { - spec.withControllerPackage(parameters.getControllerPackage().get()); - spec.withAuthentication(parameters.getUseAuth().get()); - spec.withGenerateImplementationFiles(false); - spec.withGenerateControllerFromExamples(false); - spec.withGenerateOperationsToReturnNotImplemented(false); - }); - } + if ("kotlin".equalsIgnoreCase(parameters.getLang().get())) { + builder.forKotlinServer(spec -> spec.withControllerPackage(parameters.getControllerPackage().get()) + .withAuthentication(parameters.getUseAuth().get()) + .withAot(parameters.getAot().get()) + .withGenerateImplementationFiles(false) + .withGenerateControllerFromExamples(false) + .withGenerateOperationsToReturnNotImplemented(false) + .withGeneratedAnnotation(parameters.getGeneratedAnnotation().get()) + ); + } else { + builder.forJavaServer(spec -> spec.withControllerPackage(parameters.getControllerPackage().get()) + .withAuthentication(parameters.getUseAuth().get()) + .withAot(parameters.getAot().get()) + .withGenerateImplementationFiles(false) + .withGenerateControllerFromExamples(false) + .withGenerateOperationsToReturnNotImplemented(false) + .withGeneratedAnnotation(parameters.getGeneratedAnnotation().get()) + .withLombok(parameters.getLombok().get()) + .withFluxForArrays(parameters.getFluxForArrays().get()) + ); + } + } } diff --git a/openapi-plugin/src/test/groovy/io/micronaut/openapi/gradle/AbstractOpenApiGeneratorSpec.groovy b/openapi-plugin/src/test/groovy/io/micronaut/openapi/gradle/AbstractOpenApiGeneratorSpec.groovy index 1d8dc466..679701ad 100644 --- a/openapi-plugin/src/test/groovy/io/micronaut/openapi/gradle/AbstractOpenApiGeneratorSpec.groovy +++ b/openapi-plugin/src/test/groovy/io/micronaut/openapi/gradle/AbstractOpenApiGeneratorSpec.groovy @@ -3,6 +3,7 @@ package io.micronaut.openapi.gradle import io.micronaut.gradle.AbstractGradleBuildSpec class AbstractOpenApiGeneratorSpec extends AbstractGradleBuildSpec { + protected void withPetstore() { file("petstore.json").text = this.class.getResourceAsStream("/petstore.json").getText("UTF-8") } diff --git a/openapi-plugin/src/test/groovy/io/micronaut/openapi/gradle/OpenApiClientGeneratorSpec.groovy b/openapi-plugin/src/test/groovy/io/micronaut/openapi/gradle/OpenApiClientGeneratorSpec.groovy index 86bf5ef9..23cef57b 100644 --- a/openapi-plugin/src/test/groovy/io/micronaut/openapi/gradle/OpenApiClientGeneratorSpec.groovy +++ b/openapi-plugin/src/test/groovy/io/micronaut/openapi/gradle/OpenApiClientGeneratorSpec.groovy @@ -1,13 +1,12 @@ package io.micronaut.openapi.gradle - import org.gradle.testkit.runner.TaskOutcome class OpenApiClientGeneratorSpec extends AbstractOpenApiGeneratorSpec { - def "can generate an OpenAPI client implementation"() { + def "can generate an java OpenAPI client implementation"() { given: - settingsFile << "rootProject.name = 'openapi-server'" + settingsFile << "rootProject.name = 'openapi-client'" buildFile << """ plugins { id "io.micronaut.minimal.application" @@ -26,6 +25,9 @@ class OpenApiClientGeneratorSpec extends AbstractOpenApiGeneratorSpec { $repositoriesBlock dependencies { + + annotationProcessor "io.micronaut.serde:micronaut-serde-processor" + implementation "io.micronaut.serde:micronaut-serde-jackson" } @@ -46,4 +48,54 @@ class OpenApiClientGeneratorSpec extends AbstractOpenApiGeneratorSpec { } + def "can generate an java OpenAPI client implementation with some properties"() { + given: + settingsFile << "rootProject.name = 'openapi-client'" + buildFile << """ + plugins { + id "io.micronaut.minimal.application" + id "io.micronaut.openapi" + } + + micronaut { + version "$micronautVersion" + openapi { + client(file("petstore.json")) { + lang = "java" + lombok = true + useReactive = true + generatedAnnotation = false + fluxForArrays = true + } + } + } + + $repositoriesBlock + + dependencies { + + annotationProcessor "org.projectlombok:lombok" + annotationProcessor "io.micronaut.serde:micronaut-serde-processor" + + compileOnly "org.projectlombok:lombok" + + implementation "io.micronaut.serde:micronaut-serde-jackson" + implementation "io.micronaut.reactor:micronaut-reactor" + } + + """ + + withPetstore() + + when: + def result = build('test') + + then: + result.task(":generateClientOpenApiApis").outcome == TaskOutcome.SUCCESS + result.task(":generateClientOpenApiModels").outcome == TaskOutcome.SUCCESS + result.task(":compileJava").outcome == TaskOutcome.SUCCESS + + and: + file("build/generated/openapi/generateClientOpenApiModels/src/main/java/io/micronaut/openapi/model/Pet.java").exists() + } } diff --git a/openapi-plugin/src/test/groovy/io/micronaut/openapi/gradle/OpenApiServerGeneratorSpec.groovy b/openapi-plugin/src/test/groovy/io/micronaut/openapi/gradle/OpenApiServerGeneratorSpec.groovy index a991e908..ca53b472 100644 --- a/openapi-plugin/src/test/groovy/io/micronaut/openapi/gradle/OpenApiServerGeneratorSpec.groovy +++ b/openapi-plugin/src/test/groovy/io/micronaut/openapi/gradle/OpenApiServerGeneratorSpec.groovy @@ -1,11 +1,10 @@ package io.micronaut.openapi.gradle - import org.gradle.testkit.runner.TaskOutcome class OpenApiServerGeneratorSpec extends AbstractOpenApiGeneratorSpec { - def "can generate an OpenAPI server implementation"() { + def "can generate an java OpenAPI server implementation"() { given: settingsFile << "rootProject.name = 'openapi-server'" buildFile << """ @@ -49,7 +48,62 @@ class OpenApiServerGeneratorSpec extends AbstractOpenApiGeneratorSpec { file("build/generated/openapi/generateServerOpenApiModels/src/main/java/io/micronaut/openapi/model/Pet.java").exists() file("build/classes/java/main/io/micronaut/openapi/api/PetApi.class").exists() file("build/classes/java/main/io/micronaut/openapi/model/Pet.class").exists() - } + def "can generate an java OpenAPI server implementation with properties"() { + given: + settingsFile << "rootProject.name = 'openapi-server'" + buildFile << """ + plugins { + id "io.micronaut.minimal.application" + id "io.micronaut.openapi" + } + + micronaut { + version "$micronautVersion" + runtime "netty" + testRuntime "junit5" + openapi { + server(file("petstore.json")) { + lang = "java" + lombok = true + useReactive = true + generatedAnnotation = false + fluxForArrays = true + aot = true + } + } + } + + $repositoriesBlock + mainClassName="example.Application" + + dependencies { + + annotationProcessor "org.projectlombok:lombok" + annotationProcessor "io.micronaut.serde:micronaut-serde-processor" + + compileOnly "org.projectlombok:lombok" + + implementation "io.micronaut.security:micronaut-security" + implementation "io.micronaut.serde:micronaut-serde-jackson" + } + """ + + withPetstore() + + when: + def result = build('test') + + then: + result.task(":generateServerOpenApiApis").outcome == TaskOutcome.SUCCESS + result.task(":generateServerOpenApiModels").outcome == TaskOutcome.SUCCESS + result.task(":compileJava").outcome == TaskOutcome.SUCCESS + + and: + file("build/generated/openapi/generateServerOpenApiApis/src/main/java/io/micronaut/openapi/api/PetApi.java").exists() + file("build/generated/openapi/generateServerOpenApiModels/src/main/java/io/micronaut/openapi/model/Pet.java").exists() + file("build/classes/java/main/io/micronaut/openapi/api/PetApi.class").exists() + file("build/classes/java/main/io/micronaut/openapi/model/Pet.class").exists() + } }