Skip to content

Commit

Permalink
support for @ApiResponse annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
angryziber committed Aug 21, 2023
1 parent d4a9353 commit 8a02636
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 16 deletions.
25 changes: 17 additions & 8 deletions openapi/src/OpenAPI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.enums.ParameterIn
import io.swagger.v3.oas.annotations.media.Schema.AccessMode
import io.swagger.v3.oas.annotations.parameters.RequestBody
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import io.swagger.v3.oas.annotations.tags.Tag
import klite.*
import klite.RequestMethod.GET
Expand Down Expand Up @@ -58,19 +60,15 @@ internal fun toTags(routes: List<Route>) = routes.asSequence()
internal fun toOperation(route: Route): Pair<String, Any> {
val op = route.annotation<Operation>()
val funHandler = route.handler as? FunHandler
val returnType = funHandler?.f?.returnType
return (op?.method?.trimToNull() ?: route.method.name).lowercase() to mapOf(
"operationId" to route.handler.let { (if (it is FunHandler) it.instance::class.simpleName + "." + it.f.name else it::class.simpleName) },
"tags" to listOfNotNull(funHandler?.let { it.instance::class.annotation<Tag>()?.name ?: it.instance::class.simpleName }),
"parameters" to funHandler?.let {
it.params.filter { it.source != null }.map { p -> toParameter(p, op) }
},
"requestBody" to toRequestBody(route, route.annotation<RequestBody>() ?: op?.requestBody),
"responses" to if (returnType?.classifier == Unit::class) mapOf(NoContent.value to mapOf("description" to "No content"))
else mapOf(OK.value to mapOfNotNull("description" to "OK", "content" to returnType?.toJsonContent()))
) + (op?.let { it.toNonEmptyValues { it.name != "method" } + mapOf(
"responses" to op.responses.associate { it.responseCode to it.toNonEmptyValues { it.name != "responseCode" } }.takeIf { it.isNotEmpty() }
) } ?: emptyMap())
"responses" to toResponsesByCode(route, op, funHandler?.f?.returnType)
) + (op?.let { it.toNonEmptyValues { it.name !in setOf("method", "requestBody", "responses") } } ?: emptyMap())
}

fun toParameter(p: Param, op: Operation? = null) = mapOf(
Expand Down Expand Up @@ -124,9 +122,20 @@ private fun toRequestBody(route: Route, annotation: RequestBody?): Map<String, A
if (it.schema.implementation != Void::class.java) content["schema"] = it.schema.implementation.createType().toJsonSchema()
it.mediaType to content
}
requestBody.putIfAbsent("required", true)
if (bodyParam != null) requestBody.putIfAbsent("content", bodyParam.type.toJsonContent())
return requestBody.takeIf { it.isNotEmpty() }
if (requestBody.isEmpty()) return null
requestBody.putIfAbsent("required", bodyParam == null || !bodyParam.isOptional)
return requestBody
}

private fun toResponsesByCode(route: Route, op: Operation?, returnType: KType?): Map<StatusCode, Any?> {
val responses = LinkedHashMap<StatusCode, Any?>()
if (returnType?.classifier == Unit::class) responses[NoContent] = mapOf("description" to "No content")
else if (op?.responses?.isEmpty() != false) responses[OK] = mapOfNotNull("description" to "OK", "content" to returnType?.toJsonContent())
(route.annotations.filterIsInstance<ApiResponse>() + (route.annotation<ApiResponses>()?.value ?: emptyArray()) + (op?.responses ?: emptyArray())).forEach {
responses[StatusCode(it.responseCode.toInt())] = it.toNonEmptyValues { it.name != "responseCode" }
}
return responses
}

private fun KType.toJsonContent() = mapOf(MimeTypes.json to mapOf("schema" to toJsonSchema()))
Expand Down
24 changes: 16 additions & 8 deletions openapi/test/OpenAPITest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ import klite.MimeTypes
import klite.RequestMethod.GET
import klite.RequestMethod.POST
import klite.Route
import klite.StatusCode.Companion.BadRequest
import klite.StatusCode.Companion.Found
import klite.StatusCode.Companion.NoContent
import klite.StatusCode.Companion.OK
import klite.StatusCode.Companion.Unauthorized
import klite.annotations.*
import org.junit.jupiter.api.Test
import java.time.DayOfWeek
Expand Down Expand Up @@ -65,7 +68,7 @@ class OpenAPITest {
mapOf("name" to "force", "required" to false, "in" to QUERY, "schema" to mapOf("type" to "boolean"))
),
"requestBody" to null,
"responses" to mapOf(OK.value to mapOf("description" to "OK", "content" to mapOf(MimeTypes.json to mapOf("schema" to mapOf("type" to "null")))))
"responses" to mapOf(OK to mapOf("description" to "OK", "content" to mapOf(MimeTypes.json to mapOf("schema" to mapOf("type" to "null")))))
))
}

Expand All @@ -84,29 +87,34 @@ class OpenAPITest {
class MyRoutes {
fun saveUser(e: HttpExchange, @PathParam userId: UUID, body: User) {}
}

expect(toOperation(Route(POST, "/x".toRegex(), handler = FunHandler(MyRoutes(), MyRoutes::saveUser)))).toEqual("post" to mapOf(
"operationId" to "MyRoutes.saveUser",
"tags" to listOf("MyRoutes"),
"parameters" to listOf(
mapOf("name" to "userId", "required" to true, "in" to PATH, "schema" to mapOf("type" to "string", "format" to "uuid"))
),
"requestBody" to mapOf("content" to userSchema),
"responses" to mapOf(NoContent.value to mapOf("description" to "No content"))
"requestBody" to mapOf("content" to userSchema, "required" to true),
"responses" to mapOf(NoContent to mapOf("description" to "No content"))
))
}

@Test fun `request body from annotation's implementation field`() {
class MyRoutes {
@RequestBody(description = "Application and applicant", content = [Content(mediaType = MimeTypes.json, schema = Schema(implementation = User::class))])
@ApiResponse(responseCode = "400", description = "Very bad request")
@ApiResponse(responseCode = "401", description = "Unauthorized")
fun saveUser(e: HttpExchange): User = User("x", UUID.randomUUID())
}
expect(toOperation(Route(POST, "/x".toRegex(), handler = FunHandler(MyRoutes(), MyRoutes::saveUser), annotations = MyRoutes::saveUser.annotations))).toEqual("post" to mapOf(
"operationId" to "MyRoutes.saveUser",
"tags" to listOf("MyRoutes"),
"parameters" to emptyList<Any>(),
"requestBody" to mapOf("description" to "Application and applicant", "required" to true, "content" to userSchema),
"responses" to mapOf(OK.value to mapOf("description" to "OK", "content" to userSchema))
"requestBody" to mapOf("description" to "Application and applicant", "required" to true, "content" to userSchema, "required" to true),
"responses" to mapOf(
OK to mapOf("description" to "OK", "content" to userSchema),
BadRequest to mapOf("description" to "Very bad request"),
Unauthorized to mapOf("description" to "Unauthorized"),
)
))
}

Expand All @@ -116,7 +124,7 @@ class OpenAPITest {
"tags" to emptyList<Any>(),
"parameters" to null,
"requestBody" to null,
"responses" to mapOf(OK.value to mapOf("description" to "OK"))
"responses" to mapOf(OK to mapOf("description" to "OK"))
))
}

Expand All @@ -133,7 +141,7 @@ class OpenAPITest {
"tags" to listOf("my-tag"),
"parameters" to listOf(mapOf("name" to "param", "in" to QUERY, "description" to "description")),
"requestBody" to null,
"responses" to mapOf("302" to mapOf("description" to "desc")),
"responses" to mapOf(Found to mapOf("description" to "desc")),
"summary" to "summary"
))
}
Expand Down

0 comments on commit 8a02636

Please sign in to comment.