create CommandStream and first Command
This commit is contained in:
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
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>>()
|
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)
|
||||||
}
|
}
|
||||||
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
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()))
|
||||||
|
|
||||||
|
|||||||
@@ -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>())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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