package industries.sunshine.planningpoker import industries.sunshine.planningpoker.common.Models.* import cats.effect.{Ref, Sync} import cats.syntax.all._ import cats.data.EitherT enum RoomError { case RoomAlreadyExists(name: String) case RoomMissing(name: String) case RoomPassIncorrect case NickPassIncorrect } trait RoomService[F[_]] { def createRoom(newRoom: Room): F[Either[RoomError, Room]] def updateRoom(room: Room): F[Unit] def joinRoom( id: RoomID, nickName: String, nickPassword: String, roomPassword: String ): F[Either[RoomError, PlayerID]] def deleteRoom(roomID: RoomID): F[Unit] def getRoom(roomID: RoomID): F[Option[Room]] } class InMemoryRoomService[F[_]: Sync](stateRef: Ref[F, Map[RoomID, Room]]) extends RoomService[F] { override def createRoom(newRoom: Room): F[Either[RoomError, Room]] = { stateRef.modify { rooms => rooms.get(newRoom.id) match { case Some(_) => rooms -> RoomError.RoomAlreadyExists(newRoom.id.name).asLeft[Room] case None => (rooms.updated(newRoom.id, newRoom)) -> newRoom.asRight[RoomError] } } } override def updateRoom(room: Room): F[Unit] = stateRef.update { state => state.get(room.id).fold(state)(oldRoom => state.updated(room.id, room)) } override def deleteRoom(roomID: RoomID): F[Unit] = stateRef.update(_.removed(roomID)) override def getRoom(roomID: RoomID): F[Option[Room]] = stateRef.get.map(_.get(roomID)) override def joinRoom( id: RoomID, nickName: String, nickPassword: String, roomPassword: String ): F[Either[RoomError, PlayerID]] = stateRef.modify { rooms => // need to cover cases: // - player already present, then return as is, i guess // - nick not known - add new player and new nick-password mapping // - nick known - add new player def addPlayer(room: Room): (PlayerID, Room) = { room.players.find(_.name == nickName) match { case Some(player) => player.id -> room case None => // player is not present, but potentially was previously val addingPlayer = Player.create(nickPassword) val roomWithPlayer = room.copy(players = addingPlayer :: room.players) room.playersPasswords.get(nickName) match { case Some(_) => addingPlayer.id -> roomWithPlayer case None => addingPlayer.id -> roomWithPlayer.copy(playersPasswords = roomWithPlayer.playersPasswords.updated(nickName, nickPassword) ) } } } val joiningWithChecks = for { room <- rooms.get(id).toRight(RoomError.RoomMissing(id.name)) _ <- Either.cond(room.password == roomPassword, (), RoomError.RoomPassIncorrect) isNickPassCorrect = room.playersPasswords .get(nickName) .fold(true)(existingPass => existingPass == nickPassword) _ <- Either.cond( isNickPassCorrect, (), RoomError.NickPassIncorrect ) (playerId, updatedRoom) = addPlayer(room) } yield playerId rooms -> joiningWithChecks } } object RoomService { def make[F[_]: Sync]: F[RoomService[F]] = { Ref.of[F, Map[RoomID, Room]](Map.empty).map(new InMemoryRoomService[F](_)) } }