create CommandStream and first Command
This commit is contained in:
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
40
src/main/kotlin/eventDemo/app/Command.kt
Normal file
40
src/main/kotlin/eventDemo/app/Command.kt
Normal 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,
|
||||
)
|
||||
}
|
||||
28
src/main/kotlin/eventDemo/app/CommandStream.kt
Normal file
28
src/main/kotlin/eventDemo/app/CommandStream.kt
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
52
src/main/kotlin/eventDemo/app/actions/Command.kt
Normal file
52
src/main/kotlin/eventDemo/app/actions/Command.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()))
|
||||
|
||||
|
||||
@@ -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>())
|
||||
}
|
||||
}
|
||||
|
||||
68
src/test/kotlin/eventDemo/app/actions/CommandTest.kt
Normal file
68
src/test/kotlin/eventDemo/app/actions/CommandTest.kt
Normal 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>())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user