From 0d81c03ba1bc745140317cac01e2a6063b44d7fd Mon Sep 17 00:00:00 2001 From: dzikoysk Date: Mon, 29 Jan 2024 20:47:54 +0100 Subject: [PATCH] GH-211 Support response headers (Resolve #211) --- .../openapi/plugin/test/JavalinTest.java | 9 ++- .../processor/generators/OpenApiGenerator.kt | 58 ++++++++++++------- .../io/javalin/openapi/OpenApiAnnotations.kt | 4 +- .../processor/shared/JsonExtensions.kt | 6 ++ 4 files changed, 49 insertions(+), 28 deletions(-) diff --git a/examples/javalin-gradle-kotlin/src/main/java/io/javalin/openapi/plugin/test/JavalinTest.java b/examples/javalin-gradle-kotlin/src/main/java/io/javalin/openapi/plugin/test/JavalinTest.java index 76842f1..96ff4f6 100644 --- a/examples/javalin-gradle-kotlin/src/main/java/io/javalin/openapi/plugin/test/JavalinTest.java +++ b/examples/javalin-gradle-kotlin/src/main/java/io/javalin/openapi/plugin/test/JavalinTest.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.databind.node.TextNode; import io.javalin.Javalin; import io.javalin.http.Context; -import io.javalin.http.CreatedResponse; import io.javalin.http.Handler; import io.javalin.openapi.ApiKeyAuth; import io.javalin.openapi.BasicAuth; @@ -39,12 +38,9 @@ import io.javalin.openapi.OpenID; import io.javalin.openapi.Security; import io.javalin.openapi.Visibility; -import io.javalin.openapi.plugin.OpenApiPluginConfiguration; import io.javalin.openapi.plugin.OpenApiPlugin; import io.javalin.openapi.plugin.SecurityComponentConfiguration; -import io.javalin.openapi.plugin.redoc.ReDocConfiguration; import io.javalin.openapi.plugin.redoc.ReDocPlugin; -import io.javalin.openapi.plugin.swagger.SwaggerConfiguration; import io.javalin.openapi.plugin.swagger.SwaggerPlugin; import lombok.Data; import org.bson.types.ObjectId; @@ -187,7 +183,10 @@ public static void main(String[] args) { @OpenApiResponse( status = "400", description = "Error message related to the invalid command format (0 < command length < " + 10 + ")", - content = @OpenApiContent(from = EntityDto[].class) + content = @OpenApiContent(from = EntityDto[].class), + headers = { + @OpenApiParam(name = "X-Error-Message", description = "Error message", type = String.class) + } ), @OpenApiResponse(status = "401", description = "Error message related to the unauthorized access", content = { @OpenApiContent(from = EntityDto[].class) diff --git a/openapi-annotation-processor/src/main/kotlin/io/javalin/openapi/processor/generators/OpenApiGenerator.kt b/openapi-annotation-processor/src/main/kotlin/io/javalin/openapi/processor/generators/OpenApiGenerator.kt index 2d2cdb2..0ac6341 100644 --- a/openapi-annotation-processor/src/main/kotlin/io/javalin/openapi/processor/generators/OpenApiGenerator.kt +++ b/openapi-annotation-processor/src/main/kotlin/io/javalin/openapi/processor/generators/OpenApiGenerator.kt @@ -16,6 +16,7 @@ import io.javalin.openapi.OpenApiResponse import io.javalin.openapi.OpenApis import io.javalin.openapi.experimental.ClassDefinition import io.javalin.openapi.experimental.StructureType.ARRAY +import io.javalin.openapi.experimental.processor.shared.addIfNotEmpty import io.javalin.openapi.experimental.processor.shared.addString import io.javalin.openapi.experimental.processor.shared.computeIfAbsent import io.javalin.openapi.experimental.processor.shared.getTypeMirror @@ -146,10 +147,9 @@ internal class OpenApiGenerator { ) parameterAnnotations.forEach { (parameterType, annotations) -> - annotations - .forEach { parameterAnnotation -> - parameters.add(fromParameter(parameterType, parameterAnnotation)) - } + annotations.forEach { parameterAnnotation -> + parameters.add(fromParameter(parameterType, parameterAnnotation, explicit = true)) + } } operation.add("parameters", parameters) @@ -262,11 +262,7 @@ internal class OpenApiGenerator { val requestBody = JsonObject() requestBody.addString("description", requestBodyAnnotation.description) requestBody.addContent(openApiElement, requestBodyAnnotation.content) - - if (requestBody.size() > 0) { - add("requestBody", requestBody) - } - + addIfNotEmpty("requestBody", requestBody) requestBody.addProperty("required", requestBodyAnnotation.required) } @@ -278,13 +274,19 @@ internal class OpenApiGenerator { val description = responseAnnotation.description .takeIf { it != NULL_STRING } - ?: responseAnnotation.status + ?: responseAnnotation + .status .toIntOrNull() - ?.let { HttpStatus.forStatus(it) }?.message + ?.let { HttpStatus.forStatus(it) } + ?.message response.addString("description", description) response.addContent(openApiElement, responseAnnotation.content) responses.add(responseAnnotation.status, response) + + val headers = JsonObject() + responseAnnotation.headers.forEach { headers.add(it.name, fromParameter(HEADER, it, explicit = false)) } + response.addIfNotEmpty("headers", headers) } add("responses", responses) @@ -300,21 +302,35 @@ internal class OpenApiGenerator { // Parameter // https://swagger.io/specification/#parameter-object - private fun fromParameter(`in`: In, parameterInstance: OpenApiParam?): JsonObject { + private fun fromParameter(`in`: In, parameterInstance: OpenApiParam, explicit: Boolean): JsonObject { val parameter = JsonObject() - parameter.addString("name", parameterInstance!!.name) - parameter.addString("in", `in`.identifier) + + if (explicit) { + parameter.addString("name", parameterInstance.name) + parameter.addString("in", `in`.identifier) + } + parameter.addString("description", parameterInstance.description) - parameter.addProperty("required", parameterInstance.required) - parameter.addProperty("deprecated", parameterInstance.deprecated) - parameter.addProperty("allowEmptyValue", parameterInstance.allowEmptyValue) + + if (explicit || parameterInstance.required) { + parameter.addProperty("required", parameterInstance.required) + } + + if (explicit || parameterInstance.deprecated) { + parameter.addProperty("deprecated", parameterInstance.deprecated) + } + + if (explicit || parameterInstance.allowEmptyValue) { + parameter.addProperty("allowEmptyValue", parameterInstance.allowEmptyValue) + } val schema = createTypeDescriptionWithReferences(parameterInstance.getTypeMirror { type }) - parameterInstance.example - .takeIf { it.isNotEmpty() } - .let { schema.addProperty("example", it) } - parameter.add("schema", schema) + if (parameterInstance.example.isNotEmpty()) { + schema.addProperty("example", parameterInstance.example) + } + + parameter.add("schema", schema) return parameter } diff --git a/openapi-specification/src/main/kotlin/io/javalin/openapi/OpenApiAnnotations.kt b/openapi-specification/src/main/kotlin/io/javalin/openapi/OpenApiAnnotations.kt index ee823e5..766a83c 100644 --- a/openapi-specification/src/main/kotlin/io/javalin/openapi/OpenApiAnnotations.kt +++ b/openapi-specification/src/main/kotlin/io/javalin/openapi/OpenApiAnnotations.kt @@ -96,7 +96,8 @@ annotation class OpenApis( annotation class OpenApiResponse( val status: String, val content: Array = [], - val description: String = NULL_STRING + val description: String = NULL_STRING, + val headers: Array = [], ) @Target() @@ -108,7 +109,6 @@ annotation class OpenApiParam( val deprecated: Boolean = false, val required: Boolean = false, val allowEmptyValue: Boolean = false, - val isRepeatable: Boolean = false, val example: String = "" ) diff --git a/openapi-specification/src/main/kotlin/io/javalin/openapi/experimental/processor/shared/JsonExtensions.kt b/openapi-specification/src/main/kotlin/io/javalin/openapi/experimental/processor/shared/JsonExtensions.kt index 8b86581..ebfc6d1 100644 --- a/openapi-specification/src/main/kotlin/io/javalin/openapi/experimental/processor/shared/JsonExtensions.kt +++ b/openapi-specification/src/main/kotlin/io/javalin/openapi/experimental/processor/shared/JsonExtensions.kt @@ -45,6 +45,12 @@ fun JsonObject.addString(key: String, value: String?): JsonObject = also { } } +fun JsonObject.addIfNotEmpty(key: String, value: JsonObject): JsonObject = also { + if (value.size() > 0) { + add(key, value) + } +} + fun createJsonObjectOf(key: String, value: String): JsonObject { val jsonObject = JsonObject() jsonObject.addProperty(key, value)