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

View File

@@ -3,6 +3,17 @@ package eventDemo.app
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable
data class Game(
val id: GameId,
) {
companion object {
fun new(): Game {
return Game(GameId())
}
}
}
@Serializable @Serializable
sealed interface Card { sealed interface Card {
@Serializable @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>>() val eventStream by inject<EventStream<GameId>>()
post<Game.Card.PutCard> { post<Game.Card.PutCard> {
val card = call.receive<Card.Simple>() val card = call.receive<Card>()
eventStream.publish(PlayCardEvent(it.card.game.id, card)) eventStream.publish(PlayCardEvent(it.card.game.id, card))
call.respondNullable<Any?>(HttpStatusCode.OK, null) 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 package eventDemo.plugins
import eventDemo.app.CommandStream
import eventDemo.app.EventStream import eventDemo.app.EventStream
import eventDemo.app.GameId import eventDemo.app.GameId
import io.ktor.server.application.Application import io.ktor.server.application.Application
@@ -19,4 +20,5 @@ fun Application.configureKoin() {
val appModule = val appModule =
module { module {
singleOf<EventStream<GameId>>(::EventStream) singleOf<EventStream<GameId>>(::EventStream)
singleOf<CommandStream>(::CommandStream)
} }

View File

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

View File

@@ -1,5 +1,6 @@
package eventDemo.plugins package eventDemo.plugins
import eventDemo.app.CommandId
import eventDemo.app.GameId import eventDemo.app.GameId
import io.ktor.serialization.kotlinx.json.json import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.Application 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> { object GameIdSerializer : KSerializer<GameId> {
override fun deserialize(decoder: Decoder): GameId = GameId(UUID.fromString(decoder.decodeString())) 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.EventStream
import eventDemo.app.GameId import eventDemo.app.GameId
import eventDemo.app.PlayCardEvent import eventDemo.app.PlayCardEvent
import eventDemo.app.read
import eventDemo.module import eventDemo.module
import io.kotest.core.spec.style.FunSpec import io.kotest.core.spec.style.FunSpec
import io.ktor.client.call.body 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.get
import io.ktor.client.request.post import io.ktor.client.request.post
import io.ktor.client.request.setBody import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType.Application.Json import io.ktor.http.ContentType.Application.Json
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType import io.ktor.http.contentType
import io.ktor.server.testing.testApplication import io.ktor.server.testing.testApplication
import org.koin.core.context.stopKoin import org.koin.core.context.stopKoin
import org.koin.java.KoinJavaComponent.getKoin
import org.koin.ktor.ext.inject import org.koin.ktor.ext.inject
import kotlin.test.assertEquals import kotlin.test.assertEquals
@@ -27,13 +30,17 @@ class CardTest : FunSpec({
stopKoin() stopKoin()
module() module()
} }
val id = GameId().toString() val id = GameId()
val card: Card = Card.Simple(1, Card.Color.Blue)
client.post("/game/$id/card") { client.post("/game/$id/card") {
contentType(Json) contentType(Json)
accept(Json) accept(Json)
setBody(Card.Simple(1, Card.Color.Blue)) setBody(card)
}.apply { }.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 { testApplication {
val client = httpClient() val client = httpClient()
val id = GameId() val id = GameId()
val card = Card.Simple(1, Card.Color.Blue) val card: Card = Card.Simple(1, Card.Color.Blue)
application { application {
stopKoin() stopKoin()
module() module()
@@ -54,7 +61,7 @@ class CardTest : FunSpec({
} }
client.get("/game/$id/card/last").apply { client.get("/game/$id/card/last").apply {
assertEquals(HttpStatusCode.OK, status) assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
assertEquals(card, this.call.body<Card>()) 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>())
}
}
}
})