Create route to list all the games
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
package eventDemo.adapter.infrastructureLayer.event.projection
|
||||||
|
|
||||||
|
import eventDemo.business.entity.GameId
|
||||||
|
import eventDemo.business.event.GameEventHandler
|
||||||
|
import eventDemo.business.event.GameEventStore
|
||||||
|
import eventDemo.business.event.projection.gameList.GameList
|
||||||
|
import eventDemo.business.event.projection.gameList.GameListRepository
|
||||||
|
import eventDemo.business.event.projection.gameList.apply
|
||||||
|
import eventDemo.business.event.projection.gameState.GameState
|
||||||
|
import eventDemo.libs.event.projection.ProjectionSnapshotRepositoryInMemory
|
||||||
|
import eventDemo.libs.event.projection.SnapshotConfig
|
||||||
|
|
||||||
|
class GameListRepositoryInMemory(
|
||||||
|
eventStore: GameEventStore,
|
||||||
|
eventHandler: GameEventHandler,
|
||||||
|
snapshotConfig: SnapshotConfig = SnapshotConfig(),
|
||||||
|
) : GameListRepository {
|
||||||
|
private val projectionsSnapshot =
|
||||||
|
ProjectionSnapshotRepositoryInMemory(
|
||||||
|
eventStore = eventStore,
|
||||||
|
snapshotCacheConfig = snapshotConfig,
|
||||||
|
applyToProjection = GameList::apply,
|
||||||
|
initialStateBuilder = { aggregateId: GameId -> GameList(aggregateId) },
|
||||||
|
)
|
||||||
|
|
||||||
|
init {
|
||||||
|
eventHandler.registerProjectionBuilder { event ->
|
||||||
|
projectionsSnapshot.applyAndPutToCache(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the last version of the [GameState] from the all eventStream.
|
||||||
|
*
|
||||||
|
* It fetches it from the local cache if possible, otherwise it builds it.
|
||||||
|
*/
|
||||||
|
override fun getList(): List<GameList> =
|
||||||
|
projectionsSnapshot.getList()
|
||||||
|
}
|
||||||
28
src/main/kotlin/eventDemo/adapter/interfaceLayer/GameList.kt
Normal file
28
src/main/kotlin/eventDemo/adapter/interfaceLayer/GameList.kt
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package eventDemo.adapter.interfaceLayer
|
||||||
|
|
||||||
|
import eventDemo.business.event.projection.gameList.GameListRepository
|
||||||
|
import io.ktor.resources.Resource
|
||||||
|
import io.ktor.server.application.call
|
||||||
|
import io.ktor.server.auth.authenticate
|
||||||
|
import io.ktor.server.resources.get
|
||||||
|
import io.ktor.server.response.respond
|
||||||
|
import io.ktor.server.routing.Route
|
||||||
|
import io.ktor.server.routing.get
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@Resource("/games")
|
||||||
|
class Games
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API routes to show all games.
|
||||||
|
*/
|
||||||
|
fun Route.readGamesList(gameListRepository: GameListRepository) {
|
||||||
|
authenticate {
|
||||||
|
// Read the last played card on the game.
|
||||||
|
get<Games> {
|
||||||
|
val gameList = gameListRepository.getList()
|
||||||
|
call.respond(gameList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ import io.ktor.server.routing.Route
|
|||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@Resource("/game/{id}")
|
@Resource("/games/{id}")
|
||||||
class Game(
|
class Game(
|
||||||
@Serializable(with = GameIdSerializer::class)
|
@Serializable(with = GameIdSerializer::class)
|
||||||
val id: GameId,
|
val id: GameId,
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package eventDemo.business.event.projection.gameList
|
||||||
|
|
||||||
|
import eventDemo.business.entity.GameId
|
||||||
|
import eventDemo.business.entity.Player
|
||||||
|
import eventDemo.business.event.projection.Projection
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class GameList(
|
||||||
|
override val aggregateId: GameId,
|
||||||
|
override val lastEventVersion: Int = 0,
|
||||||
|
val status: Status = Status.OPENING,
|
||||||
|
val players: Set<Player> = emptySet(),
|
||||||
|
val winners: Set<Player> = emptySet(),
|
||||||
|
) : Projection<GameId> {
|
||||||
|
enum class Status {
|
||||||
|
OPENING,
|
||||||
|
IS_STARTED,
|
||||||
|
FINISH,
|
||||||
|
CANCELED,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package eventDemo.business.event.projection.gameList
|
||||||
|
|
||||||
|
import eventDemo.business.event.event.CardIsPlayedEvent
|
||||||
|
import eventDemo.business.event.event.GameEvent
|
||||||
|
import eventDemo.business.event.event.GameStartedEvent
|
||||||
|
import eventDemo.business.event.event.NewPlayerEvent
|
||||||
|
import eventDemo.business.event.event.PlayerChoseColorEvent
|
||||||
|
import eventDemo.business.event.event.PlayerHavePassEvent
|
||||||
|
import eventDemo.business.event.event.PlayerReadyEvent
|
||||||
|
import eventDemo.business.event.event.PlayerWinEvent
|
||||||
|
|
||||||
|
fun GameList.apply(event: GameEvent): GameList =
|
||||||
|
when (event) {
|
||||||
|
is NewPlayerEvent -> {
|
||||||
|
copy(
|
||||||
|
players = players + event.player,
|
||||||
|
status = GameList.Status.OPENING,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is GameStartedEvent -> {
|
||||||
|
copy(
|
||||||
|
status = GameList.Status.IS_STARTED,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerWinEvent -> {
|
||||||
|
copy(
|
||||||
|
winners = winners + event.player,
|
||||||
|
status = GameList.Status.FINISH,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is CardIsPlayedEvent -> {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerChoseColorEvent -> {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerHavePassEvent -> {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
is PlayerReadyEvent -> {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}.copy(
|
||||||
|
lastEventVersion = event.version,
|
||||||
|
)
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package eventDemo.business.event.projection.gameList
|
||||||
|
|
||||||
|
interface GameListRepository {
|
||||||
|
fun getList(): List<GameList>
|
||||||
|
}
|
||||||
@@ -2,9 +2,11 @@ package eventDemo.configuration.injection
|
|||||||
|
|
||||||
import eventDemo.adapter.infrastructureLayer.event.GameEventBusInMemory
|
import eventDemo.adapter.infrastructureLayer.event.GameEventBusInMemory
|
||||||
import eventDemo.adapter.infrastructureLayer.event.GameEventStoreInMemory
|
import eventDemo.adapter.infrastructureLayer.event.GameEventStoreInMemory
|
||||||
|
import eventDemo.adapter.infrastructureLayer.event.projection.GameListRepositoryInMemory
|
||||||
import eventDemo.adapter.infrastructureLayer.event.projection.GameStateRepositoryInMemory
|
import eventDemo.adapter.infrastructureLayer.event.projection.GameStateRepositoryInMemory
|
||||||
import eventDemo.business.event.GameEventBus
|
import eventDemo.business.event.GameEventBus
|
||||||
import eventDemo.business.event.GameEventStore
|
import eventDemo.business.event.GameEventStore
|
||||||
|
import eventDemo.business.event.projection.gameList.GameListRepository
|
||||||
import eventDemo.business.event.projection.gameState.GameStateRepository
|
import eventDemo.business.event.projection.gameState.GameStateRepository
|
||||||
import eventDemo.libs.event.projection.SnapshotConfig
|
import eventDemo.libs.event.projection.SnapshotConfig
|
||||||
import org.koin.core.module.Module
|
import org.koin.core.module.Module
|
||||||
@@ -22,4 +24,8 @@ fun Module.configureDIInfrastructure() {
|
|||||||
single {
|
single {
|
||||||
GameStateRepositoryInMemory(get(), get(), snapshotConfig = SnapshotConfig())
|
GameStateRepositoryInMemory(get(), get(), snapshotConfig = SnapshotConfig())
|
||||||
} bind GameStateRepository::class
|
} bind GameStateRepository::class
|
||||||
|
|
||||||
|
single {
|
||||||
|
GameListRepositoryInMemory(get(), get(), snapshotConfig = SnapshotConfig())
|
||||||
|
} bind GameListRepository::class
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eventDemo.configuration.route
|
package eventDemo.configuration.route
|
||||||
|
|
||||||
|
import eventDemo.adapter.interfaceLayer.readGamesList
|
||||||
import eventDemo.adapter.interfaceLayer.readTheGameState
|
import eventDemo.adapter.interfaceLayer.readTheGameState
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import io.ktor.server.routing.routing
|
import io.ktor.server.routing.routing
|
||||||
@@ -8,5 +9,6 @@ import org.koin.ktor.ext.get
|
|||||||
fun Application.declareHttpGameRoute() {
|
fun Application.declareHttpGameRoute() {
|
||||||
routing {
|
routing {
|
||||||
readTheGameState(get())
|
readTheGameState(get())
|
||||||
|
readGamesList(get())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class ProjectionSnapshotRepositoryInMemory<E : Event<ID>, P : Projection<ID>, ID
|
|||||||
* 6. remove old one
|
* 6. remove old one
|
||||||
*/
|
*/
|
||||||
fun applyAndPutToCache(event: E) {
|
fun applyAndPutToCache(event: E) {
|
||||||
if ((event.version % snapshotCacheConfig.modulo) == 0) {
|
if ((event.version % snapshotCacheConfig.modulo) == 1) {
|
||||||
getUntil(event)
|
getUntil(event)
|
||||||
.also {
|
.also {
|
||||||
save(it)
|
save(it)
|
||||||
@@ -51,6 +51,14 @@ class ProjectionSnapshotRepositoryInMemory<E : Event<ID>, P : Projection<ID>, ID
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the list of all [Projections][Projection]
|
||||||
|
*/
|
||||||
|
fun getList(): List<P> =
|
||||||
|
projectionsSnapshot.map { (id, b) ->
|
||||||
|
getLast(id)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the last version of the [Projection] from the cache.
|
* Build the last version of the [Projection] from the cache.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package eventDemo
|
package eventDemo.app
|
||||||
|
|
||||||
import eventDemo.business.entity.Card
|
import eventDemo.business.entity.Card
|
||||||
import eventDemo.business.entity.Deck
|
import eventDemo.business.entity.Deck
|
||||||
@@ -8,5 +8,3 @@ fun Deck.allCardCount(): Int =
|
|||||||
|
|
||||||
fun Deck.allCards(): Set<Card> =
|
fun Deck.allCards(): Set<Card> =
|
||||||
stack + discard + playersHands.values.flatten()
|
stack + discard + playersHands.values.flatten()
|
||||||
|
|
||||||
// suspend fun SendChannel<Frame>.send(command: GameCommand) = send(Frame.Text(Json.encodeToString(command)))
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
package eventDemo.app.entity
|
package eventDemo.app.entity
|
||||||
|
|
||||||
import eventDemo.allCardCount
|
import eventDemo.app.allCardCount
|
||||||
import eventDemo.allCards
|
import eventDemo.app.allCards
|
||||||
import eventDemo.business.entity.Deck
|
import eventDemo.business.entity.Deck
|
||||||
import eventDemo.business.entity.Player
|
import eventDemo.business.entity.Player
|
||||||
import eventDemo.business.entity.initHands
|
import eventDemo.business.entity.initHands
|
||||||
|
|||||||
10
src/test/kotlin/eventDemo/app/query/AuthHelper.kt
Normal file
10
src/test/kotlin/eventDemo/app/query/AuthHelper.kt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package eventDemo.app.query
|
||||||
|
|
||||||
|
import eventDemo.business.entity.Player
|
||||||
|
import eventDemo.configuration.ktor.makeJwt
|
||||||
|
import io.ktor.client.request.HttpRequestBuilder
|
||||||
|
import io.ktor.client.request.header
|
||||||
|
|
||||||
|
internal fun HttpRequestBuilder.withAuth(player: Player) {
|
||||||
|
header("Authorization", "Bearer ${player.makeJwt("secret")}")
|
||||||
|
}
|
||||||
124
src/test/kotlin/eventDemo/app/query/GameListRouteTest.kt
Normal file
124
src/test/kotlin/eventDemo/app/query/GameListRouteTest.kt
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package eventDemo.app.query
|
||||||
|
|
||||||
|
import eventDemo.business.entity.GameId
|
||||||
|
import eventDemo.business.entity.Player
|
||||||
|
import eventDemo.business.event.GameEventHandler
|
||||||
|
import eventDemo.business.event.event.GameStartedEvent
|
||||||
|
import eventDemo.business.event.event.NewPlayerEvent
|
||||||
|
import eventDemo.business.event.event.PlayerReadyEvent
|
||||||
|
import eventDemo.business.event.projection.gameList.GameList
|
||||||
|
import eventDemo.configuration.configure
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.collections.shouldContain
|
||||||
|
import io.kotest.matchers.collections.shouldHaveSize
|
||||||
|
import io.kotest.matchers.equals.shouldBeEqual
|
||||||
|
import io.ktor.client.call.body
|
||||||
|
import io.ktor.client.request.accept
|
||||||
|
import io.ktor.client.request.get
|
||||||
|
import io.ktor.client.statement.bodyAsText
|
||||||
|
import io.ktor.http.ContentType
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.server.testing.testApplication
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.koin.core.context.stopKoin
|
||||||
|
import org.koin.ktor.ext.inject
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class GameListRouteTest :
|
||||||
|
FunSpec({
|
||||||
|
test("/games with no game started") {
|
||||||
|
testApplication {
|
||||||
|
val player1 = Player(name = "Nikola")
|
||||||
|
application {
|
||||||
|
stopKoin()
|
||||||
|
configure()
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient()
|
||||||
|
.get("/games") {
|
||||||
|
withAuth(player1)
|
||||||
|
accept(ContentType.Application.Json)
|
||||||
|
}.apply {
|
||||||
|
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
|
||||||
|
val list = call.body<List<GameList>>()
|
||||||
|
assertTrue(list.isEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("/games return a game with status OPENING") {
|
||||||
|
testApplication {
|
||||||
|
val gameId = GameId()
|
||||||
|
val player1 = Player(name = "Nikola")
|
||||||
|
|
||||||
|
application {
|
||||||
|
stopKoin()
|
||||||
|
configure()
|
||||||
|
|
||||||
|
val eventHandler by inject<GameEventHandler>()
|
||||||
|
runBlocking {
|
||||||
|
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player1, it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient()
|
||||||
|
.get("/games") {
|
||||||
|
withAuth(player1)
|
||||||
|
accept(ContentType.Application.Json)
|
||||||
|
}.apply {
|
||||||
|
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
|
||||||
|
call.body<List<GameList>>().first().let {
|
||||||
|
it.status shouldBeEqual GameList.Status.OPENING
|
||||||
|
it.players shouldHaveSize 1
|
||||||
|
it.players shouldContain player1
|
||||||
|
it.winners shouldHaveSize 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("/games return a game with status IS_STARTED") {
|
||||||
|
testApplication {
|
||||||
|
val gameId = GameId()
|
||||||
|
val player1 = Player(name = "Nikola")
|
||||||
|
val player2 = Player(name = "Einstein")
|
||||||
|
|
||||||
|
application {
|
||||||
|
stopKoin()
|
||||||
|
configure()
|
||||||
|
|
||||||
|
val eventHandler by inject<GameEventHandler>()
|
||||||
|
runBlocking {
|
||||||
|
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player1, it) }
|
||||||
|
eventHandler.handle(gameId) { NewPlayerEvent(gameId, player2, it) }
|
||||||
|
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player1, it) }
|
||||||
|
eventHandler.handle(gameId) { PlayerReadyEvent(gameId, player2, it) }
|
||||||
|
eventHandler.handle(gameId) {
|
||||||
|
GameStartedEvent.new(
|
||||||
|
gameId,
|
||||||
|
setOf(player1, player2),
|
||||||
|
it,
|
||||||
|
shuffleIsDisabled = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
httpClient()
|
||||||
|
.get("/games") {
|
||||||
|
withAuth(player1)
|
||||||
|
accept(ContentType.Application.Json)
|
||||||
|
}.apply {
|
||||||
|
assertEquals(HttpStatusCode.OK, status, message = bodyAsText())
|
||||||
|
call.body<List<GameList>>().first().let {
|
||||||
|
it.status shouldBeEqual GameList.Status.IS_STARTED
|
||||||
|
it.players shouldHaveSize 2
|
||||||
|
it.players shouldContain player1
|
||||||
|
it.players shouldContain player2
|
||||||
|
it.winners shouldHaveSize 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -11,15 +11,12 @@ import eventDemo.business.event.event.PlayerReadyEvent
|
|||||||
import eventDemo.business.event.projection.gameState.GameState
|
import eventDemo.business.event.projection.gameState.GameState
|
||||||
import eventDemo.business.event.projection.gameState.GameStateRepository
|
import eventDemo.business.event.projection.gameState.GameStateRepository
|
||||||
import eventDemo.configuration.configure
|
import eventDemo.configuration.configure
|
||||||
import eventDemo.configuration.ktor.makeJwt
|
|
||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
import io.kotest.matchers.collections.shouldHaveSize
|
import io.kotest.matchers.collections.shouldHaveSize
|
||||||
import io.kotest.matchers.equals.shouldBeEqual
|
import io.kotest.matchers.equals.shouldBeEqual
|
||||||
import io.ktor.client.call.body
|
import io.ktor.client.call.body
|
||||||
import io.ktor.client.request.HttpRequestBuilder
|
|
||||||
import io.ktor.client.request.accept
|
import io.ktor.client.request.accept
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import io.ktor.client.request.header
|
|
||||||
import io.ktor.client.statement.bodyAsText
|
import io.ktor.client.statement.bodyAsText
|
||||||
import io.ktor.http.ContentType
|
import io.ktor.http.ContentType
|
||||||
import io.ktor.http.HttpStatusCode
|
import io.ktor.http.HttpStatusCode
|
||||||
@@ -34,7 +31,7 @@ import kotlin.test.assertNotNull
|
|||||||
|
|
||||||
class GameStateRouteTest :
|
class GameStateRouteTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
test("/game/{id}/state on empty game") {
|
test("/games/{id}/state on empty game") {
|
||||||
testApplication {
|
testApplication {
|
||||||
val id = GameId()
|
val id = GameId()
|
||||||
val player1 = Player(name = "Nikola")
|
val player1 = Player(name = "Nikola")
|
||||||
@@ -44,7 +41,7 @@ class GameStateRouteTest :
|
|||||||
}
|
}
|
||||||
|
|
||||||
httpClient()
|
httpClient()
|
||||||
.get("/game/$id/state") {
|
.get("/games/$id/state") {
|
||||||
withAuth(player1)
|
withAuth(player1)
|
||||||
accept(ContentType.Application.Json)
|
accept(ContentType.Application.Json)
|
||||||
}.apply {
|
}.apply {
|
||||||
@@ -57,7 +54,7 @@ class GameStateRouteTest :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test("/game/{id}/card/last") {
|
test("/games/{id}/card/last") {
|
||||||
testApplication {
|
testApplication {
|
||||||
val gameId = GameId()
|
val gameId = GameId()
|
||||||
val player1 = Player(name = "Nikola")
|
val player1 = Player(name = "Nikola")
|
||||||
@@ -105,7 +102,7 @@ class GameStateRouteTest :
|
|||||||
}
|
}
|
||||||
|
|
||||||
httpClient()
|
httpClient()
|
||||||
.get("/game/$gameId/card/last") {
|
.get("/games/$gameId/card/last") {
|
||||||
withAuth(player1)
|
withAuth(player1)
|
||||||
accept(ContentType.Application.Json)
|
accept(ContentType.Application.Json)
|
||||||
}.apply {
|
}.apply {
|
||||||
@@ -115,7 +112,3 @@ class GameStateRouteTest :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
private fun HttpRequestBuilder.withAuth(player: Player) {
|
|
||||||
header("Authorization", "Bearer ${player.makeJwt("secret")}")
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user