Implement first routes & tests

install Koin
install kotest
declare first events
create EventStream
This commit is contained in:
2024-02-28 23:31:55 +01:00
parent 5434c59129
commit 43b5f27e50
21 changed files with 422 additions and 72 deletions

View File

@@ -1,16 +1,17 @@
package io.github.flecomte
package eventDemo
import io.github.flecomte.plugins.configureHTTP
import io.github.flecomte.plugins.configureRouting
import io.github.flecomte.plugins.configureSecurity
import io.github.flecomte.plugins.configureSerialization
import io.github.flecomte.plugins.configureSockets
import eventDemo.plugins.configureHTTP
import eventDemo.plugins.configureKoin
import eventDemo.plugins.configureRouting
import eventDemo.plugins.configureSecurity
import eventDemo.plugins.configureSerialization
import eventDemo.plugins.configureSockets
import io.ktor.server.application.Application
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
fun main() {
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module)
embeddedServer(Netty, port = 8080, host = "0.0.0.0", module = Application::module, watchPaths = listOf("classes"))
.start(wait = true)
}
@@ -20,4 +21,5 @@ fun Application.module() {
configureSockets()
configureHTTP()
configureRouting()
configureKoin()
}

View File

@@ -0,0 +1,14 @@
package eventDemo.app
import java.util.UUID
sealed interface AggregateId {
val id: UUID
}
@JvmInline
value class GameId(override val id: UUID = UUID.randomUUID()) : AggregateId {
constructor(id: String) : this(UUID.fromString(id))
override fun toString(): String = id.toString()
}

View File

@@ -0,0 +1,30 @@
package eventDemo.app
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
sealed interface Card {
@Serializable
enum class Color {
Blue,
Red,
Yellow,
Green,
}
@Serializable
@SerialName("Simple")
data class Simple(
val number: Int,
val color: Color,
) : Card
sealed interface Special : Card
@Serializable
@SerialName("Reverse")
data class ReverseCard(
val color: Color,
) : Special
}

View File

@@ -0,0 +1,27 @@
package eventDemo.app
import io.github.oshai.kotlinlogging.KotlinLogging
class EventStream<ID : AggregateId> {
private val logger = KotlinLogging.logger {}
private val eventBus: MutableMap<ID, MutableList<Event<ID>>> = mutableMapOf()
fun publish(event: Event<ID>) {
eventBus.getOrPut(event.aggregateId) { mutableListOf() }.add(event)
logger.atInfo {
message = "Event published"
payload = mapOf("event" to event)
}
}
fun <U : Event<ID>> read(
aggregateId: ID,
eventClass: Class<U>,
): U? {
return eventBus.get(aggregateId)?.filterIsInstance(eventClass)?.firstOrNull()
}
}
inline fun <reified U : Event<ID>, ID : AggregateId> EventStream<ID>.read(aggregateId: ID): U? {
return this.read(aggregateId, U::class.java)
}

View File

@@ -0,0 +1,10 @@
package eventDemo.app
sealed interface Event<ID : AggregateId> {
val aggregateId: ID
}
data class PlayCardEvent(
override val aggregateId: GameId,
val card: Card,
) : Event<GameId>

View File

@@ -0,0 +1,54 @@
package eventDemo.app.actions
import eventDemo.app.Card
import eventDemo.app.EventStream
import eventDemo.app.GameId
import eventDemo.app.PlayCardEvent
import eventDemo.app.read
import eventDemo.plugins.GameIdSerializer
import io.ktor.http.HttpStatusCode
import io.ktor.resources.Resource
import io.ktor.server.application.call
import io.ktor.server.request.receive
import io.ktor.server.resources.get
import io.ktor.server.resources.post
import io.ktor.server.response.respond
import io.ktor.server.response.respondNullable
import io.ktor.server.routing.Routing
import kotlinx.serialization.Serializable
import org.koin.ktor.ext.inject
@Serializable
@Resource("/game/{id}")
class Game(
@Serializable(with = GameIdSerializer::class)
val id: GameId,
) {
@Serializable
@Resource("card")
class Card(val game: Game) {
@Serializable
@Resource("")
class PutCard(val card: Card)
@Serializable
@Resource("last")
class LastCard(val card: Card)
}
}
fun Routing.putCard() {
val eventStream by inject<EventStream<GameId>>()
post<Game.Card.PutCard> {
val card = call.receive<Card.Simple>()
eventStream.publish(PlayCardEvent(it.card.game.id, card))
call.respondNullable<Any?>(HttpStatusCode.OK, null)
}
get<Game.Card.LastCard> {
eventStream.read<PlayCardEvent, GameId>(it.card.game.id)
?.let { it1 -> call.respond<Card>(it1.card) }
?: call.response.status(HttpStatusCode.BadRequest)
}
}

View File

@@ -1,7 +1,8 @@
package io.github.flecomte.plugins
package eventDemo.plugins
import io.ktor.http.HttpHeaders
import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.plugins.cors.routing.CORS
@@ -10,6 +11,7 @@ fun Application.configureHTTP() {
install(CORS) {
allowMethod(HttpMethod.Options)
allowMethod(HttpMethod.Put)
allowMethod(HttpMethod.Post)
allowMethod(HttpMethod.Delete)
allowMethod(HttpMethod.Patch)
allowHeader(HttpHeaders.Authorization)
@@ -17,3 +19,18 @@ fun Application.configureHTTP() {
anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
}
}
class BadRequestException(val httpError: HttpErrorBadRequest) : Exception()
class HttpErrorBadRequest(
statusCode: HttpStatusCode,
val title: String = statusCode.description,
val invalidParams: List<InvalidParam>,
) {
val statusCode: Int = statusCode.value
data class InvalidParam(
val name: String,
val reason: String,
)
}

View File

@@ -0,0 +1,22 @@
package eventDemo.plugins
import eventDemo.app.EventStream
import eventDemo.app.GameId
import io.ktor.server.application.Application
import io.ktor.server.application.install
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
import org.koin.ktor.plugin.Koin
import org.koin.logger.slf4jLogger
fun Application.configureKoin() {
install(Koin) {
slf4jLogger()
modules(appModule)
}
}
val appModule =
module {
singleOf<EventStream<GameId>>(::EventStream)
}

View File

@@ -1,39 +1,31 @@
package io.github.flecomte.plugins
package eventDemo.plugins
import eventDemo.app.actions.putCard
import io.ktor.http.HttpStatusCode
import io.ktor.resources.Resource
import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.plugins.autohead.AutoHeadResponse
import io.ktor.server.plugins.statuspages.StatusPages
import io.ktor.server.resources.Resources
import io.ktor.server.resources.get
import io.ktor.server.response.respond
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.routing
import kotlinx.serialization.Serializable
import io.ktor.util.converters.DataConversion.Configuration
fun Application.configureRouting() {
install(AutoHeadResponse)
install(Resources)
install(StatusPages) {
exception<BadRequestException> { call, cause ->
call.respondText(text = "400: $cause", status = HttpStatusCode.BadRequest)
}
exception<Throwable> { call, cause ->
call.respondText(text = "500: $cause", status = HttpStatusCode.InternalServerError)
}
}
routing {
get("/") {
call.respondText("Hello World!")
}
get<Articles> { article ->
// Get all articles ...
call.respond("List of articles sorted starting from ${article.sort}")
}
putCard()
}
}
@Serializable
@Resource("/articles")
class Articles(val sort: String? = "new")
private typealias ConverterDeclaration = Configuration.() -> Unit

View File

@@ -1,4 +1,4 @@
package io.github.flecomte.plugins
package eventDemo.plugins
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm

View File

@@ -0,0 +1,55 @@
package eventDemo.plugins
import eventDemo.app.GameId
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.application.install
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import java.util.UUID
fun Application.configureSerialization() {
install(ContentNegotiation) {
json(
Json {
serializersModule =
SerializersModule {
contextual(UUID::class) { UUIDSerializer }
}
},
)
}
}
object GameIdSerializer : KSerializer<GameId> {
override fun deserialize(decoder: Decoder): GameId = GameId(UUID.fromString(decoder.decodeString()))
override fun serialize(
encoder: Encoder,
value: GameId,
) {
encoder.encodeString(value.id.toString())
}
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("GameId", PrimitiveKind.STRING)
}
object UUIDSerializer : KSerializer<UUID> {
override fun deserialize(decoder: Decoder): UUID = UUID.fromString(decoder.decodeString())
override fun serialize(
encoder: Encoder,
value: UUID,
) {
encoder.encodeString(value.toString())
}
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
}

View File

@@ -1,4 +1,4 @@
package io.github.flecomte.plugins
package eventDemo.plugins
import io.ktor.server.application.Application
import io.ktor.server.application.install

View File

@@ -1,21 +0,0 @@
package io.github.flecomte.plugins
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.response.respond
import io.ktor.server.routing.get
import io.ktor.server.routing.routing
fun Application.configureSerialization() {
install(ContentNegotiation) {
json()
}
routing {
get("/json/kotlinx-serialization") {
call.respond(mapOf("hello" to "world"))
}
}
}