Skip to content

Commit

Permalink
introduce support for SecuritySchemes
Browse files Browse the repository at this point in the history
  • Loading branch information
angryziber committed Aug 22, 2023
1 parent 96ddcfd commit bba7aba
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 12 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ subprojects {
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-opt-in=kotlin.ExperimentalStdlibApi"
freeCompilerArgs += "-Xcontext-receivers"
}
}

Expand Down
35 changes: 23 additions & 12 deletions openapi/src/OpenAPI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ 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.security.SecurityScheme
import io.swagger.v3.oas.annotations.security.SecuritySchemes
import io.swagger.v3.oas.annotations.tags.Tag
import klite.*
import klite.RequestMethod.GET
Expand All @@ -35,26 +37,35 @@ import kotlin.reflect.full.primaryConstructor

/**
* Adds an /openapi endpoint to the context, listing all the routes.
* - Pass [OpenAPIDefinition][io.swagger.v3.oas.annotations.OpenAPIDefinition] or only [Info][io.swagger.v3.oas.annotations.info.Info] annotation to this function to specify general info
* - Pass [@OpenAPIDefinition][io.swagger.v3.oas.annotations.OpenAPIDefinition] or only [@Info][io.swagger.v3.oas.annotations.info.Info] annotation to this function to specify general info
* - Pass [@SecurityScheme][io.swagger.v3.oas.annotations.security.SecurityScheme] to define authorization
* - Use [@Operation][io.swagger.v3.oas.annotations.Operation] annotation to describe each route
* - Use [@Hidden][io.swagger.v3.oas.annotations.Hidden] to hide a route from the spec
* - [@Parameter][io.swagger.v3.oas.annotations.Parameter] annotation can be used on method parameters directly
* - [@Tag][io.swagger.v3.oas.annotations.tags.Tag] annotation is supported on route classes for grouping of routes
*/
fun Router.openApi(path: String = "/openapi", annotations: List<Annotation> = emptyList()) {
add(Route(GET, path.toRegex(), annotations.toList()) {
mapOf(
"openapi" to "3.1.0",
"info" to route.annotation<Info>()?.toNonEmptyValues(),
"servers" to listOf(mapOf("url" to fullUrl(prefix))),
"tags" to toTags(routes),
"paths" to routes.filter { !it.hasAnnotation<Hidden>() }.groupBy { pathParamRegexer.toOpenApi(it.path) }.mapValues { (_, routes) ->
routes.associate(::toOperation)
}
) + (route.annotation<OpenAPIDefinition>()?.toNonEmptyValues() ?: emptyMap())
})
add(Route(GET, path.toRegex(), annotations.toList()) { generateOpenAPI() })
}

context(HttpExchange)
internal fun Router.generateOpenAPI() = mapOf(
"openapi" to "3.1.0",
"info" to route.annotation<Info>()?.toNonEmptyValues(),
"servers" to listOf(mapOf("url" to fullUrl(prefix))),
"tags" to toTags(routes),
"components" to mapOfNotNull(
"securitySchemes" to (route.annotations.filterIsInstance<SecurityScheme>() + (route.annotation<SecuritySchemes>()?.value ?: emptyArray())).associate { s ->
s.name to s.toNonEmptyValues { it.name != "paramName" }.let { it + ("name" to s.paramName) }
}.takeIf { it.isNotEmpty() }
).takeIf { it.isNotEmpty() },
"paths" to routes.filter { !it.hasAnnotation<Hidden>() }.groupBy { pathParamRegexer.toOpenApi(it.path) }.mapValues { (_, routes) ->
routes.associate(::toOperation)
},
) + (route.annotation<OpenAPIDefinition>()?.let {
it.toNonEmptyValues() + ("security" to it.security.associate { it.name to it.scopes.toList() })
} ?: emptyMap())

internal fun toTags(routes: List<Route>) = routes.asSequence()
.map { it.handler }
.filterIsInstance<FunHandler>()
Expand Down
32 changes: 32 additions & 0 deletions openapi/test/OpenAPITest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,35 @@ package klite.openapi
import ch.tutteli.atrium.api.fluent.en_GB.toContainExactly
import ch.tutteli.atrium.api.fluent.en_GB.toEqual
import ch.tutteli.atrium.api.verbs.expect
import io.mockk.every
import io.mockk.mockk
import io.swagger.v3.oas.annotations.OpenAPIDefinition
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.enums.ParameterIn.*
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
import io.swagger.v3.oas.annotations.info.Info
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.parameters.RequestBody
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.security.SecurityScheme
import io.swagger.v3.oas.annotations.tags.Tag
import klite.HttpExchange
import klite.MimeTypes
import klite.RequestMethod.GET
import klite.RequestMethod.POST
import klite.Route
import klite.Router
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.net.URI
import java.time.DayOfWeek
import java.time.LocalDate
import java.util.*
Expand Down Expand Up @@ -145,4 +154,27 @@ class OpenAPITest {
"summary" to "summary"
))
}

@Test fun `definition and security`() {
@SecurityScheme(name = "bearerApiKey", type = SecuritySchemeType.HTTP, scheme = "bearer", bearerFormat = "API-Key", paramName = "Authorization")
@OpenAPIDefinition(info = Info(title = "Mega API", version = "1.x"), security = [SecurityRequirement(name = "bearerApiKey")])
class MyRoutes {}

mockk<HttpExchange>(relaxed = true).apply {
every { fullUrl(any()) } returns URI("https://base")
every { route } returns Route(GET, "/".toRegex(), MyRoutes::class.annotations) {}
val openApi = mockk<Router>(relaxed = true).generateOpenAPI()
expect(openApi).toEqual(mapOf(
"openapi" to "3.1.0",
"info" to mapOf("title" to "Mega API", "version" to "1.x"),
"servers" to listOf(mapOf("url" to URI("https://base"))),
"tags" to emptySet<String>(),
"components" to mapOf(
"securitySchemes" to mapOf("bearerApiKey" to mapOf("type" to SecuritySchemeType.HTTP, "scheme" to "bearer", "bearerFormat" to "API-Key", "name" to "Authorization"))
),
"paths" to emptyMap<String, Any?>(),
"security" to mapOf("bearerApiKey" to emptyList<Any>())
))
}
}
}

0 comments on commit bba7aba

Please sign in to comment.