From 6fc85ca449ff7b7253408251dd319287aed7eaa8 Mon Sep 17 00:00:00 2001 From: Cedric Champeau Date: Thu, 23 Nov 2023 16:33:58 +0100 Subject: [PATCH] Fix OpenAPI Kotlin integration - moves tests to functional tests, since we want to make sure that the plugins can work _without_ the Kotlin plugin on classpath. The tests which involve external plugins must live in `functionalTests` - fixes missing integration with Kotlin plugin --- functional-tests/build.gradle | 1 + .../fixtures/AbstractFunctionalTest.groovy | 1 + .../AbstractOpenApiWithKotlinSpec.groovy | 16 + .../OpenApiClientWithKotlinSpec.groovy | 109 ++ .../OpenApiServerWithKotlinSpec.groovy | 119 +++ .../src/test/resources/petstore.json | 973 ++++++++++++++++++ .../openapi/DefaultOpenApiExtension.java | 37 +- .../gradle/OpenApiClientGeneratorSpec.groovy | 102 -- .../gradle/OpenApiServerGeneratorSpec.groovy | 112 -- 9 files changed, 1246 insertions(+), 224 deletions(-) create mode 100644 functional-tests/src/test/groovy/io/micronaut/gradle/openapi/AbstractOpenApiWithKotlinSpec.groovy create mode 100644 functional-tests/src/test/groovy/io/micronaut/gradle/openapi/OpenApiClientWithKotlinSpec.groovy create mode 100644 functional-tests/src/test/groovy/io/micronaut/gradle/openapi/OpenApiServerWithKotlinSpec.groovy create mode 100644 functional-tests/src/test/resources/petstore.json 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/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..92e8818a --- /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/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() + } + + 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/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() + } +} 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/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/DefaultOpenApiExtension.java b/openapi-plugin/src/main/java/io/micronaut/gradle/openapi/DefaultOpenApiExtension.java index e1a68657..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 @@ -15,29 +15,28 @@ */ package io.micronaut.gradle.openapi; -import java.io.File; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.function.Consumer; - -import javax.inject.Inject; - import io.micronaut.gradle.PluginsHelper; import io.micronaut.gradle.openapi.tasks.AbstractOpenApiGenerator; import io.micronaut.gradle.openapi.tasks.OpenApiClientGenerator; import io.micronaut.gradle.openapi.tasks.OpenApiServerGenerator; - import org.gradle.api.Action; import org.gradle.api.GradleException; import org.gradle.api.Project; 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; +import javax.inject.Inject; +import java.io.File; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + import static org.codehaus.groovy.runtime.StringGroovyMethods.capitalize; public abstract class DefaultOpenApiExtension implements OpenApiExtension { @@ -95,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); @@ -196,6 +202,13 @@ public void client(String name, Provider 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(); @@ -210,8 +223,12 @@ public void client(String name, Provider definition, Action mainSrcDir(AbstractOpenApiGenerator t, String language) { + return t.getOutputDirectory().dir("src/main/" + language); + } + private static Provider mainSrcDir(AbstractOpenApiGenerator t) { - return t.getOutputDirectory().dir("src/main/java"); + return mainSrcDir(t, "java"); } private static void configureClientTask(OpenApiClientSpec clientSpec, OpenApiClientGenerator task) { 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 fa5d29b7..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 @@ -98,106 +98,4 @@ class OpenApiClientGeneratorSpec extends AbstractOpenApiGeneratorSpec { and: file("build/generated/openapi/generateClientOpenApiModels/src/main/java/io/micronaut/openapi/model/Pet.java").exists() } - - 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 "1.9.20" - id "org.jetbrains.kotlin.plugin.allopen" version "1.9.20" - id "org.jetbrains.kotlin.kapt" version "1.9.20" - } - - 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(":compileJava").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 "1.9.20" - id "org.jetbrains.kotlin.plugin.allopen" version "1.9.20" - id "com.google.devtools.ksp" version "1.9.20-1.0.14" - } - - 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(":compileJava").outcome == TaskOutcome.SUCCESS - - and: - file("build/generated/openapi/generateClientOpenApiModels/src/main/kotlin/io/micronaut/openapi/model/Pet.kt").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 35d0be11..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 @@ -106,116 +106,4 @@ class OpenApiServerGeneratorSpec extends AbstractOpenApiGeneratorSpec { 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 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 "1.9.20" - id "org.jetbrains.kotlin.plugin.allopen" version "1.9.20" - id "org.jetbrains.kotlin.kapt" version "1.9.20" - } - - 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(":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() - } - - 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 "1.9.20" - id "org.jetbrains.kotlin.plugin.allopen" version "1.9.20" - id "com.google.devtools.ksp" version "1.9.20-1.0.14" - } - - 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(":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() - } }