uidsonic.baku.0.9.22.source-code.Baku.kt Maven / Gradle / Ivy
package com.github.fluidsonic.baku
import com.github.fluidsonic.fluid.json.JSONCodecProvider
import com.github.fluidsonic.fluid.json.extended
import com.github.fluidsonic.fluid.mongo.MongoClients
import io.ktor.application.Application
import io.ktor.application.ApplicationStarting
import io.ktor.application.call
import io.ktor.application.install
import io.ktor.features.CORS
import io.ktor.features.CallLogging
import io.ktor.features.DefaultHeaders
import io.ktor.features.XForwardedHeaderSupport
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.request.ApplicationReceiveRequest
import io.ktor.response.respond
import io.ktor.routing.Route
import io.ktor.routing.routing
import io.ktor.server.engine.commandLineEnvironment
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.runBlocking
import org.bson.codecs.configuration.CodecConfigurationException
import org.bson.codecs.configuration.CodecRegistries
import org.bson.codecs.configuration.CodecRegistry
import org.slf4j.event.Level
import kotlin.reflect.KClass
class Baku internal constructor() {
class Builder internal constructor() {
private val environment = commandLineEnvironment(arrayOf())
private var modules: List>? = null
private val providerBasedBSONCodecRegistry = ProviderBasedBSONCodecRegistry()
private var service: BakuService? = null
val bsonCodecRegistry = CodecRegistries.fromRegistries(
MongoClients.defaultCodecRegistry,
providerBasedBSONCodecRegistry
)!!
val config = environment.config
internal fun build(): Baku {
// create engine before monitoring the start event because Netty's subscriptions must be processed first
val engine = embeddedServer(Netty, environment)
val subscription = environment.monitor.subscribe(ApplicationStarting) { application ->
application.apply {
configureBasics()
configureModules()
}
}
engine.start()
subscription.dispose()
return Baku()
}
internal fun configure(assemble: suspend Builder.() -> BakuService) {
runBlocking {
service = assemble()
}
}
fun modules(vararg modules: BakuModule) {
check(this.modules == null) { "modules() can only be specified once" }
this.modules = modules.toList()
}
private fun Application.configureBasics() {
install(CallLogging) {
level = Level.INFO
}
install(DefaultHeaders) {
header(HttpHeaders.Server, "Baku")
}
install(CORS) {
anyHost()
exposeHeader(HttpHeaders.WWWAuthenticate)
header(HttpHeaders.Accept) // https://github.com/ktorio/ktor/issues/939
header(HttpHeaders.AcceptLanguage) // https://github.com/ktorio/ktor/issues/939
header(HttpHeaders.Authorization)
method(HttpMethod.Delete)
method(HttpMethod.Patch)
}
install(XForwardedHeaderSupport)
install(EncryptionEnforcementFeature)
}
private fun Application.configureModules() {
val modules = (modules ?: error("modules() must be specified")) + StandardModule
val service = service!!
val configurations = modules.map { it.configure() }
val context = runBlocking { service.createContext() }
val idFactoriesByType = configurations.flatMap { it.idFactories }.associateBy { it.type }
val bsonCodecProviders: MutableList> = mutableListOf()
bsonCodecProviders += configurations.flatMap { it.bsonCodecProviders }
bsonCodecProviders += configurations.flatMap { it.idFactories }.map { EntityIdBSONCodec(factory = it) }
bsonCodecProviders += TypedIdBSONCodec(idFactoryProvider = object : EntityIdFactoryProvider {
override fun idFactoryForType(type: String) = idFactoriesByType[type]
})
val jsonCodecProviders: MutableList> = mutableListOf()
jsonCodecProviders += configurations.flatMap { it.jsonCodecProviders }
jsonCodecProviders += configurations.flatMap { it.idFactories }.map { EntityIdJSONCodec(factory = it) }
jsonCodecProviders += JSONCodecProvider.extended
val jsonCodecProvider = JSONCodecProvider(jsonCodecProviders)
providerBasedBSONCodecRegistry.context = context
providerBasedBSONCodecRegistry.provider = BSONCodecProvider.of(bsonCodecProviders)
providerBasedBSONCodecRegistry.rootRegistry = bsonCodecRegistry
val entityResolverSources: MutableMap, BakuModule<*, *>> = mutableMapOf()
val entityResolvers: MutableMap, suspend Transaction.(ids: Set) -> ReceiveChannel> =
mutableMapOf()
for (configuration in configurations) {
for ((idClass, entityResolver) in configuration.entities.resolvers) {
val previousModule = entityResolverSources.putIfAbsent(idClass, configuration.module)
if (previousModule != null) {
error("Cannot add entity resolver for $idClass of ${configuration.module} because $previousModule already provides one")
}
entityResolvers[idClass] = entityResolver
}
}
configurations.forEach { it.customConfigurations.forEach { it() } }
install(BakuTransactionFeature(
service = service,
context = context
))
install(BakuCommandFailureFeature)
install(BakuCommandRequestFeature(
jsonCodecProvider = jsonCodecProvider
))
install(BakuCommandResponseFeature(
additionalEncodings = configurations.flatMap { it.additionalResponseEncodings },
codecProvider = jsonCodecProvider,
entityResolver = EntityResolver(resolvers = entityResolvers)
))
var topRouteBuilder: Route.() -> Unit = {
for (configuration in configurations) {
for (routingConfiguration in configuration.routeConfigurations) {
routingConfiguration()
}
}
}
configurations.forEach {
it.routeWrappers.forEach {
val next = topRouteBuilder
topRouteBuilder = { it(next) }
}
}
routing {
topRouteBuilder()
}
val commandNames = mutableSetOf()
val commandHandlers: MutableMap, BakuCommandHandler<*, *, *>> = hashMapOf()
for (configuration in configurations) {
for (handler in configuration.commands.handlers) {
val name = handler.factory.name
if (commandNames.contains(name)) {
error("Multiple commands have the same name: $name")
}
else {
commandNames.add(name)
}
if (commandHandlers.putIfAbsent(handler.factory, handler) != null) {
error("Cannot register multiple handlers for command factory ${handler.factory}")
}
}
}
for (configuration in configurations) {
for (route in configuration.commandRoutes) {
val factory = route.factory
@Suppress("UNCHECKED_CAST")
val handler = commandHandlers[factory]
as BakuCommandHandler?
?: error("No handler registered for command factory $factory")
route.route.handle {
val request = call.request.pipeline.execute(call, ApplicationReceiveRequest(
type = BakuCommand::class,
value = factory
)).value as BakuCommandRequest
call.respond(BakuCommandResponse(
factory = factory,
result = handler.run { transaction.handler() }(request.command)
))
}
}
}
runBlocking {
service.onStart(context = context) // TODO we could make BakuContext and BakuService one thing
}
}
}
private class ProviderBasedBSONCodecRegistry : CodecRegistry {
lateinit var context: Context
lateinit var provider: BSONCodecProvider
lateinit var rootRegistry: CodecRegistry
override fun get(clazz: Class): BSONCodec {
val codec = provider.codecForClass(clazz.kotlin) ?: throw CodecConfigurationException("No BSON codec provided for $clazz")
if (codec is AbstractBSONCodec) {
codec.configure(context = context, rootRegistry = rootRegistry)
}
return codec
}
}
}
fun baku(
assemble: suspend Baku.Builder.() -> BakuService
) {
Baku.Builder().apply { configure(assemble) }.build()
}