Skip to content

Commit

Permalink
♻️ Refactor swagger/openapi v3 support (#497)
Browse files Browse the repository at this point in the history
  • Loading branch information
devkanro authored Mar 22, 2023
1 parent 049eb23 commit 710a098
Show file tree
Hide file tree
Showing 25 changed files with 765 additions and 1,008 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ class ConfigArtifactProvider : EnvironmentPostProcessor, ApplicationListener<App
val properties = binder.bind("sisyphus", SisyphusProperty::class.java)
.orElse(null) ?: return

if (properties.config.artifacts.isEmpty()) {
logger.warn("Skip load config artifacts due to empty artifacts list, artifacts list can be set by 'sisyphus.config.artifacts' property.")
return
}

for (repositoryKey in properties.dependency.repositories) {
val repository = when (repositoryKey) {
"local" -> properties.repositories[repositoryKey] ?: run {
Expand Down
3 changes: 1 addition & 2 deletions middleware/sisyphus-jdbc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ dependencies {
api(libs.spring.boot)
api(libs.jooq)
api(libs.kotlin.coroutines)
api(projects.lib.sisyphusDsl)
implementation(libs.hikari)

runtimeOnly(libs.mysql.connector)
runtimeOnly(libs.postgresql.connector)

compileOnly(projects.lib.sisyphusDsl)

testImplementation(projects.lib.sisyphusDsl)
testImplementation(libs.h2)
testImplementation(libs.spring.boot.test)
Expand Down
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ include("middleware:sisyphus-grpc-client-kubernetes")
include("starter:sisyphus-jackson-starter")
include("starter:sisyphus-webflux-starter")
include("starter:sisyphus-grpc-server-starter")
include("starter:sisyphus-grpc-openapi-starter")
include("starter:sisyphus-grpc-transcoding-starter")
include("starter:sisyphus-protobuf-type-server-starter")
include("starter:sisyphus-spring-boot-test-starter")
Expand Down
13 changes: 13 additions & 0 deletions starter/sisyphus-grpc-openapi-starter/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
starter

plugins {
`java-library`
}

description = "Starter for building gRPC server which with HTTP and gRPC Transcoding in Sisyphus Framework"

dependencies {
implementation(projects.starter.sisyphusGrpcTranscodingStarter)
api(libs.swagger)
api(projects.starter.sisyphusWebfluxStarter)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package com.bybutter.sisyphus.starter.grpc.openapi

import com.bybutter.sisyphus.starter.grpc.ServiceRegistrar
import com.bybutter.sisyphus.starter.grpc.transcoding.EnableHttpToGrpcTranscoding
import io.grpc.Server
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory
import org.springframework.beans.factory.support.BeanDefinitionBuilder
import org.springframework.beans.factory.support.BeanDefinitionRegistry
import org.springframework.boot.context.properties.bind.Binder
import org.springframework.context.EnvironmentAware
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar
import org.springframework.core.env.Environment
import org.springframework.core.type.AnnotationMetadata
import org.springframework.http.HttpMethod
import org.springframework.web.cors.CorsConfiguration
import org.springframework.web.cors.reactive.CorsConfigurationSource
import org.springframework.web.cors.reactive.CorsWebFilter
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource
import org.springframework.web.reactive.function.server.RouterFunction

/**
* Config of gRPC transcoding, it will register webflux router and CORS filter based on the gRPC server.
*
* It imported by [EnableHttpToGrpcTranscoding] annotation, and it will found all services registered by
* [ServiceRegistrar], create [TranscodingRouterFunction] and register into spring context for handling
* HTTP requests.
*
* Also, CORS requests have been supported too, it will analyze service and register [CorsWebFilter] based
* on [TranscodingCorsConfigurationSource] into spring context.
*/
@Configuration
class ApiDocConfig : ImportBeanDefinitionRegistrar, EnvironmentAware {
private lateinit var environment: Environment

override fun setEnvironment(environment: Environment) {
this.environment = environment
}

private val swaggerProperty by lazy {
Binder.get(environment).bind("openapi", ApiDocProperty::class.java).orElse(null) ?: ApiDocProperty()
}

override fun registerBeanDefinitions(importingClassMetadata: AnnotationMetadata, registry: BeanDefinitionRegistry) {
// Find the [EnableHttpToGrpcTranscoding] annotation.
val enableAnnotation =
importingClassMetadata.getAnnotationAttributes(EnableHttpToGrpcTranscoding::class.java.name) ?: return
// Get the enabled transcoding service in [EnableHttpToGrpcTranscoding] annotation.
val enableServices =
(enableAnnotation[EnableHttpToGrpcTranscoding::services.name] as? Array<String>)?.asList() ?: listOf()
registerSwaggerRouterFunction(registry, enableServices)
registerSwaggerCorsConfigSource(registry)
}

/**
* Register swagger router function bean definition to spring context.
*/
private fun registerSwaggerRouterFunction(registry: BeanDefinitionRegistry, enableServices: Collection<String>) {
val definitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(RouterFunction::class.java) {
val server =
(registry as ConfigurableListableBeanFactory).getBean(ServiceRegistrar.QUALIFIER_AUTO_CONFIGURED_GRPC_SERVER) as Server
ApiDocRouterFunction(
server,
enableServices,
swaggerProperty,
(registry as ConfigurableListableBeanFactory).getBeansOfType(ApiDocRequestInterceptor::class.java).values.toList(),
(registry as ConfigurableListableBeanFactory).getBeansOfType(ApiDocInterceptor::class.java).values.toList()
)
}
registry.registerBeanDefinition(
QUALIFIER_AUTO_CONFIGURED_GRPC_OPENAPI_ROUTER_FUNCTION,
definitionBuilder.beanDefinition
)
}

/**
* Register gRPC swagger CORS config source bean definition to spring context.
*/
private fun registerSwaggerCorsConfigSource(registry: BeanDefinitionRegistry) {
val definitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(CorsConfigurationSource::class.java) {
UrlBasedCorsConfigurationSource().apply {
registerCorsConfiguration(
swaggerProperty.path,
CorsConfiguration().apply {
addAllowedHeader(CorsConfiguration.ALL)
addAllowedOrigin(CorsConfiguration.ALL)
addAllowedMethod(HttpMethod.OPTIONS)
addAllowedMethod(HttpMethod.HEAD)
addAllowedMethod(HttpMethod.GET)
}
)
}
}
registry.registerBeanDefinition(
QUALIFIER_AUTO_CONFIGURED_GRPC_OPENAPI_CORS_CONFIG,
definitionBuilder.beanDefinition
)
}

companion object {
/**
* Bean name for registered swagger router function, you can use it to refer it.
*/
const val QUALIFIER_AUTO_CONFIGURED_GRPC_OPENAPI_ROUTER_FUNCTION = "sisyphus:grpc:openapi-router"

/**
* Bean name for registered transcoding CORS filter, you can use it to refer it.
*/
const val QUALIFIER_AUTO_CONFIGURED_GRPC_OPENAPI_CORS_CONFIG = "sisyphus:grpc:openapi-cors"
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.bybutter.sisyphus.starter.grpc.transcoding.support.swagger
package com.bybutter.sisyphus.starter.grpc.openapi

import com.bybutter.sisyphus.middleware.configuration.ConfigFormatFilePropertyExporter

/**
* The configuration of swagger uses 'swagger/config' by default.
* This configuration can be overridden in the application.
* */
object SwaggerConfigArtifactPropertyExporter : ConfigFormatFilePropertyExporter() {
override val names: Collection<String> = listOf("swagger/config")
object ApiDocConfigArtifactPropertyExporter : ConfigFormatFilePropertyExporter() {
override val names: Collection<String> = listOf("openapi/config")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.bybutter.sisyphus.starter.grpc.openapi

import io.swagger.v3.oas.models.OpenAPI
import org.springframework.web.reactive.function.server.ServerRequest

interface ApiDocInterceptor {
fun intercept(request: ServerRequest, openAPI: OpenAPI)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.bybutter.sisyphus.starter.grpc.openapi

data class ApiDocProperty(
var path: String = "/api-docs"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.bybutter.sisyphus.starter.grpc.openapi

import org.springframework.web.reactive.function.server.ServerRequest

interface ApiDocRequestInterceptor {
fun intercept(request: ServerRequest)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.bybutter.sisyphus.starter.grpc.openapi

import com.bybutter.sisyphus.starter.webflux.EmptyRouterFunction
import io.grpc.Server
import io.grpc.ServerServiceDefinition
import io.swagger.v3.core.util.Json
import org.springframework.web.reactive.function.server.HandlerFunction
import org.springframework.web.reactive.function.server.RouterFunction
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import reactor.core.publisher.Mono

class ApiDocRouterFunction private constructor(
private val services: List<ServerServiceDefinition>,
private val apiDocProperty: ApiDocProperty,
private val requestInterceptors: List<ApiDocRequestInterceptor>,
private val interceptors: List<ApiDocInterceptor>
) : RouterFunction<ServerResponse>, HandlerFunction<ServerResponse> {

override fun route(request: ServerRequest): Mono<HandlerFunction<ServerResponse>> {
return if (request.path() != apiDocProperty.path) {
Mono.empty()
} else {
Mono.just(this)
}
}

override fun handle(request: ServerRequest): Mono<ServerResponse> {
requestInterceptors.forEach {
it.intercept(request)
}
val openApi = openApi {
for (service in services) {
addService(service)
}
}.apply {
interceptors.forEach {
it.intercept(request, this)
}
}
return ServerResponse.ok().bodyValue(Json.mapper().writeValueAsString(openApi))
}

companion object {
const val COMPONENTS_SCHEMAS_PREFIX = "#/components/schemas/"
operator fun invoke(
server: Server,
enableServices: Collection<String> = listOf(),
apiDocProperty: ApiDocProperty,
requestInterceptors: List<ApiDocRequestInterceptor>,
interceptors: List<ApiDocInterceptor>
): RouterFunction<ServerResponse> {
val enableServicesSet = enableServices.toSet()
val enableServicesDefinition = mutableListOf<ServerServiceDefinition>()
server.services.forEach {
if (enableServicesSet.isEmpty() || enableServicesSet.contains(it.serviceDescriptor.name)) {
enableServicesDefinition.add(it)
}
}
if (enableServicesDefinition.isEmpty()) return EmptyRouterFunction
return ApiDocRouterFunction(enableServicesDefinition, apiDocProperty, requestInterceptors, interceptors)
}
}
}
Loading

0 comments on commit 710a098

Please sign in to comment.