create CommandStream and first Command

This commit is contained in:
2024-02-29 01:29:08 +01:00
parent 8beb66d8dc
commit 53cc961c62
12 changed files with 233 additions and 7 deletions

View File

@@ -1,5 +1,7 @@
package eventDemo.app
import eventDemo.plugins.GameIdSerializer
import kotlinx.serialization.Serializable
import java.util.UUID
sealed interface AggregateId {
@@ -7,6 +9,7 @@ sealed interface AggregateId {
}
@JvmInline
@Serializable(with = GameIdSerializer::class)
value class GameId(override val id: UUID = UUID.randomUUID()) : AggregateId {
constructor(id: String) : this(UUID.fromString(id))

View File

@@ -3,6 +3,17 @@ package eventDemo.app
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class Game(
val id: GameId,
) {
companion object {
fun new(): Game {
return Game(GameId())
}
}
}
@Serializable
sealed interface Card {
@Serializable

View File

@@ -0,0 +1,40 @@
package eventDemo.app
import eventDemo.plugins.CommandIdSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.util.UUID
@JvmInline
@Serializable(with = CommandIdSerializer::class)
value class CommandId(private val id: UUID = UUID.randomUUID()) {
constructor(id: String) : this(UUID.fromString(id))
override fun toString(): String = id.toString()
}
@Serializable
sealed interface Command {
val id: CommandId
val name: String
}
@Serializable
@SerialName("PlayCard")
data class PlayCardCommand(
val payload: Payload,
) : Command {
constructor(
game: Game,
card: Card,
) : this(Payload(game, card))
override val name: String = "PlayCard"
override val id: CommandId = CommandId()
@Serializable
data class Payload(
val game: Game,
val card: Card,
)
}

View File

@@ -0,0 +1,28 @@
package eventDemo.app
import io.github.oshai.kotlinlogging.KotlinLogging
class CommandStream {
private val logger = KotlinLogging.logger {}
private val commandBus: MutableList<Command> = mutableListOf()
fun sendRequest(command: Command) {
commandBus.add(command)
logger.atInfo {
message = "Command published: $command"
payload = mapOf("command" to command)
}
}
fun sendRequest(vararg commands: Command) {
commands.forEach { sendRequest(it) }
}
fun readNext(): Command? {
return commandBus.firstOrNull()
}
fun <U : Command> readNext(commandClass: Class<U>): U? {
return commandBus.filterIsInstance(commandClass).firstOrNull()
}
}

View File

@@ -41,7 +41,7 @@ fun Routing.card() {
val eventStream by inject<EventStream<GameId>>()
post<Game.Card.PutCard> {
val card = call.receive<Card.Simple>()
val card = call.receive<Card>()
eventStream.publish(PlayCardEvent(it.card.game.id, card))
call.respondNullable<Any?>(HttpStatusCode.OK, null)
}

View File

@@ -0,0 +1,52 @@
package eventDemo.app.actions
import eventDemo.app.Command
import eventDemo.app.CommandId
import eventDemo.app.CommandStream
import eventDemo.app.PlayCardCommand
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.routing.Routing
import kotlinx.serialization.Serializable
import org.koin.ktor.ext.inject
@Serializable
@Resource("/command")
class CommandRoute {
@Resource("send")
class Send(val commandRoute: CommandRoute) {
@Serializable
data class Response(
val id: CommandId,
) {
constructor(command: Command) : this(command.id)
}
}
@Resource("next")
class Next(val commandRoute: CommandRoute)
}
fun Routing.command() {
val commandStream by inject<CommandStream>()
post<CommandRoute.Send> {
val command = call.receive<PlayCardCommand>()
commandStream.sendRequest(command)
call.respond(HttpStatusCode.OK, CommandRoute.Send.Response(command))
}
get<CommandRoute.Next> {
val command = commandStream.readNext()
if (command == null) {
call.response.status(HttpStatusCode.NoContent)
} else {
call.respond(HttpStatusCode.OK, command)
}
}
}

View File

@@ -1,5 +1,6 @@
package eventDemo.plugins
import eventDemo.app.CommandStream
import eventDemo.app.EventStream
import eventDemo.app.GameId
import io.ktor.server.application.Application
@@ -19,4 +20,5 @@ fun Application.configureKoin() {
val appModule =
module {
singleOf<EventStream<GameId>>(::EventStream)
singleOf<CommandStream>(::CommandStream)
}

View File

@@ -1,6 +1,7 @@
package eventDemo.plugins
import eventDemo.app.actions.card
import eventDemo.app.actions.command
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.Application
import io.ktor.server.application.install
@@ -9,7 +10,6 @@ import io.ktor.server.plugins.statuspages.StatusPages
import io.ktor.server.resources.Resources
import io.ktor.server.response.respondText
import io.ktor.server.routing.routing
import io.ktor.util.converters.DataConversion.Configuration
fun Application.configureRouting() {
install(AutoHeadResponse)
@@ -25,5 +25,6 @@ fun Application.configureRouting() {
routing {
card()
command()
}
}

View File

@@ -1,5 +1,6 @@
package eventDemo.plugins
import eventDemo.app.CommandId
import eventDemo.app.GameId
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application
@@ -28,6 +29,19 @@ fun Application.configureSerialization() {
}
}
object CommandIdSerializer : KSerializer<CommandId> {
override fun deserialize(decoder: Decoder): CommandId = CommandId(decoder.decodeString())
override fun serialize(
encoder: Encoder,
value: CommandId,
) {
encoder.encodeString(value.toString())
}
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("CommandId", PrimitiveKind.STRING)
}
object GameIdSerializer : KSerializer<GameId> {
override fun deserialize(decoder: Decoder): GameId = GameId(UUID.fromString(decoder.decodeString()))

View File

@@ -4,6 +4,7 @@ import eventDemo.app.Card
import eventDemo.app.EventStream
import eventDemo.app.GameId
import eventDemo.app.PlayCardEvent
import eventDemo.app.read
import eventDemo.module
import io.kotest.core.spec.style.FunSpec
import io.ktor.client.call.body
@@ -11,11 +12,13 @@ import io.ktor.client.request.accept
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType.Application.Json
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import io.ktor.server.testing.testApplication
import org.koin.core.context.stopKoin
import org.koin.java.KoinJavaComponent.getKoin
import org.koin.ktor.ext.inject
import kotlin.test.assertEquals
@@ -27,13 +30,17 @@ class CardTest : FunSpec({
stopKoin()
module()
}
val id = GameId().toString()
val id = GameId()
val card: Card = Card.Simple(1, Card.Color.Blue)
client.post("/game/$id/card") {
contentType(Json)
accept(Json)
setBody(Card.Simple(1, Card.Color.Blue))
setBody(card)
}.apply {
assertEquals(status, HttpStatusCode.OK)
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
val eventStream = getKoin().get<EventStream<GameId>>()
assertEquals(PlayCardEvent(id, card), eventStream.read<PlayCardEvent, GameId>(id))
}
}
}
@@ -42,7 +49,7 @@ class CardTest : FunSpec({
testApplication {
val client = httpClient()
val id = GameId()
val card = Card.Simple(1, Card.Color.Blue)
val card: Card = Card.Simple(1, Card.Color.Blue)
application {
stopKoin()
module()
@@ -54,7 +61,7 @@ class CardTest : FunSpec({
}
client.get("/game/$id/card/last").apply {
assertEquals(HttpStatusCode.OK, status)
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
assertEquals(card, this.call.body<Card>())
}
}

View File

@@ -0,0 +1,68 @@
package eventDemo.app.actions
import eventDemo.app.Card
import eventDemo.app.Command
import eventDemo.app.CommandStream
import eventDemo.app.Game
import eventDemo.app.PlayCardCommand
import eventDemo.module
import io.kotest.core.spec.style.FunSpec
import io.ktor.client.call.body
import io.ktor.client.request.accept
import io.ktor.client.request.get
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType.Application.Json
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import io.ktor.server.testing.testApplication
import org.koin.core.context.stopKoin
import org.koin.java.KoinJavaComponent.getKoin
import org.koin.ktor.ext.inject
import kotlin.test.assertEquals
class CommandTest : FunSpec({
test("/command/send") {
testApplication {
val client = httpClient()
application {
stopKoin()
module()
}
val command = PlayCardCommand(Game.new(), Card.Simple(1, Card.Color.Blue))
client.post("/command/send") {
contentType(Json)
accept(Json)
setBody(command)
}.apply {
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
val commandStream = getKoin().get<CommandStream>()
assertEquals(command, commandStream.readNext())
}
}
}
test("/command/next") {
testApplication {
val command =
PlayCardCommand(
Game.new(),
Card.Simple(1, Card.Color.Blue),
)
application {
stopKoin()
module()
val commandStream by inject<CommandStream>()
commandStream.sendRequest(command)
}
httpClient().get("/command/next").apply {
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
assertEquals(command, this.call.body<Command>())
}
}
}
})