Skip to content

Commit

Permalink
#9 refactored REST module to be more hexagonal
Browse files Browse the repository at this point in the history
  • Loading branch information
stoerti committed Jul 8, 2024
1 parent 03ccc18 commit 541df3e
Show file tree
Hide file tree
Showing 15 changed files with 244 additions and 227 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,21 @@ import org.quizmania.game.api.*
import org.quizmania.game.common.GameConfig
import org.quizmania.game.common.GameQuestionId
import org.quizmania.game.common.GameUserId
import org.quizmania.rest.application.domain.GameEntity
import org.quizmania.rest.application.domain.GameStatus
import org.quizmania.rest.application.domain.GameUserEntity
import org.quizmania.rest.port.`in`.FindGamePort
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.bind.annotation.*
import java.util.*

/**
* GameCommandController violates hexagonal architecture by directly calling the command gateway. But adding use-cases,
* ports and command adapter seems useless since it is just a forwarding gateway without any logic
*/
@RestController
@RequestMapping(value = ["/api/game"], produces = [MediaType.APPLICATION_JSON_VALUE])
@Transactional
class GameController(
class GameCommandController(
val commandGateway: CommandGateway,
val findGamePort: FindGamePort
) {

companion object : KLogging()
Expand Down Expand Up @@ -58,26 +57,6 @@ class GameController(
return ResponseEntity.ok(gameId.toString());
}

@GetMapping("/")
fun search(
@RequestParam(
name = "gameStatus",
required = false
) gameStatus: GameStatus?
): ResponseEntity<List<GameDto>> {
val games = if (gameStatus != null) findGamePort.findByStatus(gameStatus) else findGamePort.findAll()
return ResponseEntity.ok(games.map { it.toDto() })
}

@GetMapping("/{gameId}")
fun get(@PathVariable("gameId") gameId: UUID): ResponseEntity<GameDto> {
findGamePort.findById(gameId)?.let {
return ResponseEntity.ok(it.toDto())
}

return ResponseEntity.notFound().build()
}

@PostMapping("/{gameId}/join")
fun joinGame(
@PathVariable("gameId") gameId: UUID,
Expand Down Expand Up @@ -196,36 +175,3 @@ data class AnswerOverrideDto(
val userAnswerId: UUID,
val answer: String
)

data class GameDto(
val id: UUID,
val name: String,
val maxPlayers: Int,
val numQuestions: Int,
val creator: String,
val moderator: String?,
val status: GameStatus,
val users: List<GameUserDto>,
)

data class GameUserDto(
val id: UUID,
val name: String,
)

fun GameEntity.toDto(): GameDto {
return GameDto(
id = gameId,
name = name,
maxPlayers = maxPlayers,
numQuestions = numQuestions,
creator = creator,
moderator = moderator,
status = status,
users = users.map { it.toDto() },
)
}

fun GameUserEntity.toDto(): GameUserDto {
return GameUserDto(gameUserId, username)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.quizmania.rest.adapter.`in`.rest

import mu.KLogging
import org.quizmania.rest.application.domain.Game
import org.quizmania.rest.application.domain.GameStatus
import org.quizmania.rest.application.domain.GameUser
import org.quizmania.rest.port.`in`.FindGamePort
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.bind.annotation.*
import java.util.*

@RestController
@RequestMapping(value = ["/api/game"], produces = [MediaType.APPLICATION_JSON_VALUE])
@Transactional
class GameReadController(
val findGamePort: FindGamePort
) {

companion object : KLogging()


@GetMapping("/")
fun search(
@RequestParam(
name = "gameStatus",
required = false
) gameStatus: GameStatus?
): ResponseEntity<List<GameDto>> {
val games = if (gameStatus != null) findGamePort.findByStatus(gameStatus) else findGamePort.findAll()
return ResponseEntity.ok(games.map { it.toDto() })
}

@GetMapping("/{gameId}")
fun get(@PathVariable("gameId") gameId: UUID): ResponseEntity<GameDto> {
findGamePort.findById(gameId)?.let {
return ResponseEntity.ok(it.toDto())
}

return ResponseEntity.notFound().build()
}
}

data class GameDto(
val id: UUID,
val name: String,
val maxPlayers: Int,
val numQuestions: Int,
val creator: String,
val moderator: String?,
val status: GameStatus,
val users: List<GameUserDto>,
)

data class GameUserDto(
val id: UUID,
val name: String,
)

fun Game.toDto(): GameDto {
return GameDto(
id = gameId,
name = name,
maxPlayers = maxPlayers,
numQuestions = numQuestions,
creator = creator,
moderator = moderator,
status = status,
users = users.map { it.toDto() },
)
}

fun GameUser.toDto(): GameUserDto {
return GameUserDto(gameUserId, username)
}
Original file line number Diff line number Diff line change
@@ -1,36 +1,107 @@
package org.quizmania.rest.adapter.out

import jakarta.persistence.*
import org.quizmania.game.common.GameId
import org.quizmania.rest.application.domain.GameEntity
import org.quizmania.rest.application.domain.Game
import org.quizmania.rest.application.domain.GameStatus
import org.quizmania.rest.application.domain.GameUser
import org.quizmania.rest.port.out.GameRepository
import org.springframework.data.repository.CrudRepository
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Component
import java.util.*

@Component
class GameJPARepositoryAdapter(
val repository: GameJPARepository
) : GameRepository {
override fun save(game: GameEntity) {
repository.save(game)
override fun save(game: Game) {
repository.save(GameEntity.fromModel(game))
}

override fun findById(gameId: GameId): GameEntity? {
return repository.findByIdOrNull(gameId)
override fun findById(gameId: GameId): Game? {
return repository.findByIdOrNull(gameId)?.toModel()
}

override fun findAll(): List<GameEntity> {
return repository.findAll().toList()
override fun findAll(): List<Game> {
return repository.findAll().map { it.toModel() } .toList()
}

override fun findByStatus(status: GameStatus): List<GameEntity> {
return repository.findByStatus(status)
override fun findByStatus(status: GameStatus): List<Game> {
return repository.findByStatus(status).map { it.toModel() }
}

}

interface GameJPARepository : CrudRepository<GameEntity, GameId> {
fun findByStatus(status: GameStatus): List<GameEntity>
}

@Entity(name = "GAME")
class GameEntity(
@Id
val gameId: UUID,
var name: String,
var maxPlayers: Int,
var numQuestions: Int,
var creator: String,
var moderator: String?,

var questionTimeout: Long,
@Enumerated(EnumType.STRING)
var status: GameStatus,

@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
@JoinColumn(name = "game_id")
var users: MutableList<GameUserEntity> = mutableListOf(),
) {

companion object {
fun fromModel(model: Game): GameEntity {
return GameEntity(
gameId = model.gameId,
name = model.name,
maxPlayers = model.maxPlayers,
numQuestions = model.numQuestions,
creator = model.creator,
moderator = model.moderator,
questionTimeout = model.questionTimeout,
status = model.status,
users = model.users.map { GameUserEntity.fromModel(it) }.toMutableList() // mutability needed for JPA?
)
}
}

}
fun toModel(): Game = Game(
gameId = this.gameId,
name = this.name,
maxPlayers = this.maxPlayers,
numQuestions = this.numQuestions,
creator = this.creator,
moderator = this.moderator,
questionTimeout = this.questionTimeout,
status = this.status,
users = this.users.map { it.toModel() }.toMutableList()
)
}

@Entity(name = "GAME_USER")
class GameUserEntity(
@Id
val gameUserId: UUID,
var username: String,
) {

companion object {
fun fromModel(model: GameUser): GameUserEntity {
return GameUserEntity(
gameUserId = model.gameUserId,
username = model.username,
)
}
}

fun toModel(): GameUser = GameUser(
gameUserId = this.gameUserId,
username = this.username,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ import mu.KLogging
import org.quizmania.common.EventMetaData
import org.quizmania.game.common.GameEvent
import org.quizmania.game.common.GameId
import org.quizmania.rest.adapter.`in`.rest.GameDto
import org.quizmania.rest.adapter.`in`.rest.toDto
import org.quizmania.rest.application.domain.GameEntity
import org.quizmania.rest.port.out.GameChangedEventEmitterPort
import org.springframework.messaging.simp.SimpMessagingTemplate
import org.springframework.stereotype.Component
Expand All @@ -22,15 +19,14 @@ class WebsocketGameEventEmitter(

companion object : KLogging()

override fun emitGameChangeEventToPlayers(evt: GameEvent, eventMetaData: EventMetaData, gameState: GameEntity) {
override fun emitGameChangeEventToPlayers(evt: GameEvent, eventMetaData: EventMetaData) {
val channel = "/game/${evt.gameId}"
val wrappedEvent = GameEventWrapperDto(
gameId = evt.gameId,
sequenceNumber = eventMetaData.sequenceNumber,
timestamp = eventMetaData.timestamp,
eventType = evt.javaClass.simpleName,
payload = objectMapper.writeValueAsString(evt),
game = gameState.toDto()
payload = objectMapper.writeValueAsString(evt)
)
logger.trace { "Forwarding event $wrappedEvent to websocket clients on $channel" }
template.convertAndSend(channel, wrappedEvent)
Expand All @@ -43,7 +39,6 @@ class WebsocketGameEventEmitter(
val eventType: String,
@JsonRawValue
val payload: String,
val game: GameDto
)
}

Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
package org.quizmania.rest.application.domain

import jakarta.persistence.*
import org.quizmania.game.common.*
import java.time.Instant
import java.util.*

@Entity(name = "GAME")
class GameEntity(
@Id
val gameId: UUID,
class Game(
val gameId: GameId,
var name: String,
var maxPlayers: Int,
var numQuestions: Int,
var creator: String,
var moderator: String?,

var questionTimeout: Long,
@Enumerated(EnumType.STRING)
var status: GameStatus,

@OneToMany(cascade = [CascadeType.ALL], fetch = FetchType.LAZY)
@JoinColumn(name = "game_id")
var users: MutableList<GameUserEntity> = mutableListOf(),
var users: MutableList<GameUser> = mutableListOf(),
) {

constructor(event: GameCreatedEvent) : this(
Expand All @@ -36,7 +28,7 @@ class GameEntity(
)

fun on(event: UserAddedEvent) {
users.add(GameUserEntity(event.gameUserId, event.username))
users.add(GameUser(event.gameUserId, event.username))
}

fun on(event: UserRemovedEvent) {
Expand All @@ -55,10 +47,3 @@ class GameEntity(
status = GameStatus.CANCELED
}
}

@Entity(name = "GAME_USER")
class GameUserEntity(
@Id
val gameUserId: UUID,
var username: String,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.quizmania.rest.application.domain

import java.util.*

data class GameUser(
val gameUserId: UUID,
val username: String,
)
Loading

0 comments on commit 541df3e

Please sign in to comment.