Compare commits
3 Commits
da9b96de84
...
539b20f419
Author | SHA1 | Date |
---|---|---|
|
539b20f419 | |
|
bd38a29b6d | |
|
ed6d30ec42 |
|
@ -25,53 +25,33 @@ trait Auth[F[_]] {
|
||||||
* check that room exists, password is valid call to add user to the players create session
|
* check that room exists, password is valid call to add user to the players create session
|
||||||
* mapping and return cookie
|
* mapping and return cookie
|
||||||
*/
|
*/
|
||||||
def joinRoom(
|
def joinRoom(roomId: RoomID, playerId: PlayerID): F[ResponseCookie]
|
||||||
roomName: String,
|
|
||||||
roomPassword: String,
|
|
||||||
nickName: String,
|
|
||||||
nickPassword: String
|
|
||||||
): F[Either[String, ResponseCookie]]
|
|
||||||
|
|
||||||
def deleteSession(
|
def deleteSession(sessionId: Long): F[Unit]
|
||||||
sessionId: Long
|
|
||||||
): F[Unit]
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Auth {
|
object Auth {
|
||||||
type SessionsMap = Map[Long, (RoomID, PlayerID)]
|
type SessionsMap = Map[Long, (RoomID, PlayerID)]
|
||||||
|
|
||||||
class SimpleAuth[F[_]: Sync](
|
class SimpleAuth[F[_]: Sync](sessions: Ref[F, SessionsMap]) extends Auth[F] {
|
||||||
sessions: Ref[F, SessionsMap],
|
|
||||||
roomService: RoomService[F]
|
|
||||||
) extends Auth[F] {
|
|
||||||
|
|
||||||
val authcookieName = "authcookie"
|
val authcookieName = "authcookie"
|
||||||
|
|
||||||
override def joinRoom(
|
override def joinRoom(roomId: RoomID, playerId: PlayerID): F[ResponseCookie] = {
|
||||||
roomName: String,
|
|
||||||
roomPassword: String,
|
|
||||||
nickName: String,
|
|
||||||
nickPassword: String
|
|
||||||
): F[Either[String, ResponseCookie]] = {
|
|
||||||
// TODO check for existing session for same room
|
// TODO check for existing session for same room
|
||||||
// and do i want to logout if existing session for another room? ugh
|
// and do i want to logout if existing session for another room? ugh
|
||||||
val roomId = RoomID(roomName)
|
// newSessionId = Random.nextLong() // TODO return after i stop mocking RoomService
|
||||||
val result = for {
|
val newSessionId = TestModels.testSessionId
|
||||||
playerId <- EitherT(
|
sessions
|
||||||
roomService.joinRoom(roomId, nickName, nickPassword, roomPassword)
|
.update(_.updated(newSessionId, (roomId, playerId)))
|
||||||
|
.as(
|
||||||
|
ResponseCookie(
|
||||||
|
name = authcookieName,
|
||||||
|
content = newSessionId.toString(),
|
||||||
|
secure = true
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.leftMap(_.toString())
|
|
||||||
// newSessionId = Random.nextLong() // TODO return after i stop mocking RoomService
|
|
||||||
newSessionId = TestModels.testSessionId
|
|
||||||
_ <- EitherT.liftF(sessions.update(_.updated(newSessionId, (roomId, playerId))))
|
|
||||||
} yield ResponseCookie(
|
|
||||||
name = authcookieName,
|
|
||||||
content = newSessionId.toString(),
|
|
||||||
secure = true
|
|
||||||
)
|
|
||||||
|
|
||||||
result.value
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override def authUser
|
override def authUser
|
||||||
|
@ -92,8 +72,8 @@ object Auth {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
def make[F[_]: Sync](roomsService: RoomService[F]): F[Auth[F]] =
|
def make[F[_]: Sync](): F[Auth[F]] =
|
||||||
for {
|
for {
|
||||||
sessionsMap <- Ref.of[F, SessionsMap](TestModels.testSessions)
|
sessionsMap <- Ref.of[F, SessionsMap](TestModels.testSessions)
|
||||||
} yield new SimpleAuth(sessionsMap, roomsService)
|
} yield new SimpleAuth(sessionsMap)
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,8 @@ object BackendApp extends IOApp {
|
||||||
|
|
||||||
val wiring = for {
|
val wiring = for {
|
||||||
roomService <- Resource.eval(RoomService.make[IO])
|
roomService <- Resource.eval(RoomService.make[IO])
|
||||||
auth <- Resource.eval(Auth.make(roomService))
|
auth <- Resource.eval(Auth.make[IO]())
|
||||||
httpService = MyHttpService.create(auth)
|
httpService = MyHttpService.create(auth, roomService)
|
||||||
server <- EmberServerBuilder
|
server <- EmberServerBuilder
|
||||||
.default[IO]
|
.default[IO]
|
||||||
.withHost(host)
|
.withHost(host)
|
||||||
|
@ -25,9 +25,7 @@ object BackendApp extends IOApp {
|
||||||
} yield server
|
} yield server
|
||||||
|
|
||||||
wiring.use(server =>
|
wiring.use(server =>
|
||||||
IO.delay(println(s"Server Has Started at ${server.address}")) >> IO.never
|
IO.delay(println(s"Server Has Started at ${server.address}")) >> IO.never.as(ExitCode.Success))
|
||||||
.as(ExitCode.Success)
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import cats.effect._
|
||||||
import cats.syntax.all._
|
import cats.syntax.all._
|
||||||
import org.http4s._, org.http4s.dsl.io._, org.http4s.implicits._
|
import org.http4s._, org.http4s.dsl.io._, org.http4s.implicits._
|
||||||
import org.http4s.websocket.WebSocketFrame
|
import org.http4s.websocket.WebSocketFrame
|
||||||
// import io.circe.generic.auto._
|
|
||||||
import io.circe.syntax._
|
import io.circe.syntax._
|
||||||
import org.http4s.circe.CirceEntityDecoder._
|
import org.http4s.circe.CirceEntityDecoder._
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
|
@ -16,8 +15,8 @@ import org.http4s.server.AuthMiddleware.apply
|
||||||
import org.http4s.server.AuthMiddleware
|
import org.http4s.server.AuthMiddleware
|
||||||
|
|
||||||
object MyHttpService {
|
object MyHttpService {
|
||||||
def create(auth: Auth[IO])(
|
def create(auth: Auth[IO], roomService: RoomService[IO])(
|
||||||
wsb: WebSocketBuilder[cats.effect.IO]
|
wsb: WebSocketBuilder[IO]
|
||||||
): HttpApp[cats.effect.IO] = {
|
): HttpApp[cats.effect.IO] = {
|
||||||
|
|
||||||
val authedRoomRoutes: AuthedRoutes[(PlayerID, RoomID), IO] =
|
val authedRoomRoutes: AuthedRoutes[(PlayerID, RoomID), IO] =
|
||||||
|
@ -56,26 +55,23 @@ object MyHttpService {
|
||||||
val authMiddleware = AuthMiddleware(auth.authUser)
|
val authMiddleware = AuthMiddleware(auth.authUser)
|
||||||
val aa = authMiddleware(authedRoomRoutes)
|
val aa = authMiddleware(authedRoomRoutes)
|
||||||
|
|
||||||
|
import cats.data.EitherT
|
||||||
val authenticationRoute = HttpRoutes
|
val authenticationRoute = HttpRoutes
|
||||||
.of[IO] {
|
.of[IO] {
|
||||||
case req @ POST -> Root / "login" => {
|
case req @ POST -> Root / "login" => {
|
||||||
for {
|
val responseOrError = for {
|
||||||
data <- req.as[Requests.LogIn]
|
data <- EitherT.right(req.as[Requests.LogIn])
|
||||||
authResult <- auth.joinRoom(
|
Requests.LogIn(roomName, nickName, roomPassword, nickPassword) = data
|
||||||
data.roomName,
|
roomId = RoomID(roomName)
|
||||||
data.password,
|
playerId <- EitherT(roomService.joinRoom(roomId, nickName, nickPassword, roomPassword))
|
||||||
data.nickname,
|
authCookie <- EitherT.liftF(auth.joinRoom(roomId, playerId))
|
||||||
data.nickPassword
|
_ <- EitherT.liftF(IO(println(s"> logging in $nickName to $roomName")))
|
||||||
)
|
resp <- EitherT.liftF(Ok().flatTap(resp => IO(resp.addCookie(authCookie))))
|
||||||
resp <- authResult match {
|
|
||||||
case Left(error) =>
|
|
||||||
Forbidden(error)
|
|
||||||
case Right(authCookie) => {
|
|
||||||
IO(println(s"> logging in ${data.nickname} to ${data.roomName}")) >>
|
|
||||||
Ok().map(_.addCookie(authCookie))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} yield resp
|
} yield resp
|
||||||
|
|
||||||
|
val response = responseOrError.leftSemiflatMap(error => Forbidden(error.toString())).merge
|
||||||
|
|
||||||
|
response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package industries.sunshine.planningpoker
|
package industries.sunshine.planningpoker
|
||||||
|
|
||||||
import industries.sunshine.planningpoker.common.Models.*
|
import industries.sunshine.planningpoker.common.Models.*
|
||||||
import cats.effect.{Ref, Sync}
|
import cats.effect.{Ref, Concurrent}
|
||||||
import cats.syntax.all._
|
import cats.syntax.all._
|
||||||
|
import fs2.Stream
|
||||||
|
import fs2.concurrent.Topic
|
||||||
import cats.data.EitherT
|
import cats.data.EitherT
|
||||||
|
|
||||||
enum RoomError {
|
enum RoomError {
|
||||||
|
@ -23,38 +25,69 @@ trait RoomService[F[_]] {
|
||||||
): F[Either[RoomError, PlayerID]]
|
): F[Either[RoomError, PlayerID]]
|
||||||
def deleteRoom(roomID: RoomID): F[Unit]
|
def deleteRoom(roomID: RoomID): F[Unit]
|
||||||
def getRoom(roomID: RoomID): F[Option[Room]]
|
def getRoom(roomID: RoomID): F[Option[Room]]
|
||||||
|
def subscribe(roomID: RoomID): Stream[F, Room]
|
||||||
}
|
}
|
||||||
|
|
||||||
class InMemoryRoomService[F[_]: Sync](stateRef: Ref[F, Map[RoomID, Room]]) extends RoomService[F] {
|
class InMemoryRoomService[F[_]: Concurrent](stateRef: Ref[F, Map[RoomID, (Room, Topic[F, Room])]])
|
||||||
|
extends RoomService[F] {
|
||||||
|
|
||||||
override def createRoom(newRoom: Room): F[Either[RoomError, Room]] = {
|
override def createRoom(newRoom: Room): F[Either[RoomError, Room]] = {
|
||||||
stateRef.modify { rooms =>
|
for {
|
||||||
rooms.get(newRoom.id) match {
|
updatesTopic <- Topic[F, Room]
|
||||||
case Some(_) =>
|
room <- stateRef.modify { rooms =>
|
||||||
rooms -> RoomError.RoomAlreadyExists(newRoom.id.name).asLeft[Room]
|
rooms.get(newRoom.id) match {
|
||||||
case None =>
|
case Some(_) =>
|
||||||
(rooms.updated(newRoom.id, newRoom)) -> newRoom.asRight[RoomError]
|
rooms -> RoomError.RoomAlreadyExists(newRoom.id.name).asLeft[Room]
|
||||||
|
case None =>
|
||||||
|
(rooms.updated(newRoom.id, (newRoom, updatesTopic))) -> newRoom.asRight[RoomError]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
} yield room
|
||||||
}
|
}
|
||||||
override def updateRoom(room: Room): F[Unit] = stateRef.update { state =>
|
override def updateRoom(room: Room): F[Unit] = {
|
||||||
state.get(room.id).fold(state)(oldRoom => state.updated(room.id, room))
|
for {
|
||||||
|
// modify is function to update state and compute auxillary value to return, here - topic
|
||||||
|
topic <- stateRef.modify[Topic[F, Room]] { state =>
|
||||||
|
state.get(room.id) match {
|
||||||
|
case Some((oldRoom, topic)) => state.updated(room.id, (room, topic)) -> topic
|
||||||
|
case None =>
|
||||||
|
throw new IllegalStateException(s"updateRoom with ${room.id} on nonexistent room")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ <- topic.publish1(room) // update and publish are not atomic, sadly races can happen
|
||||||
|
} yield ()
|
||||||
}
|
}
|
||||||
|
|
||||||
override def deleteRoom(roomID: RoomID): F[Unit] = stateRef.update(_.removed(roomID))
|
override def deleteRoom(roomID: RoomID): F[Unit] = {
|
||||||
|
for {
|
||||||
|
topic <- stateRef.modify[Topic[F, Room]](state =>
|
||||||
|
state.get(roomID) match {
|
||||||
|
case Some((oldRoom, topic)) => state.removed(roomID) -> topic
|
||||||
|
case None =>
|
||||||
|
throw new IllegalStateException(s"call to delete with $roomID on nonexistent room")
|
||||||
|
// TODO - i'd prefer to swallow these errors
|
||||||
|
}
|
||||||
|
)
|
||||||
|
_ <- topic.close
|
||||||
|
} yield ()
|
||||||
|
}
|
||||||
|
|
||||||
override def getRoom(roomID: RoomID): F[Option[Room]] = stateRef.get.map(_.get(roomID))
|
override def getRoom(roomID: RoomID): F[Option[Room]] = {
|
||||||
|
stateRef.get.map(_.get(roomID).map(_._1))
|
||||||
|
}
|
||||||
|
|
||||||
override def joinRoom(
|
override def joinRoom(
|
||||||
id: RoomID,
|
id: RoomID,
|
||||||
nickName: String,
|
nickName: String,
|
||||||
nickPassword: String,
|
nickPassword: String,
|
||||||
roomPassword: String
|
roomPassword: String
|
||||||
): F[Either[RoomError, PlayerID]] = stateRef.modify { rooms =>
|
): F[Either[RoomError, PlayerID]] = {
|
||||||
// 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
|
|
||||||
|
|
||||||
|
/** pure function that adds the player to the room 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) = {
|
def addPlayer(room: Room): (PlayerID, Room) = {
|
||||||
room.players.find(_.name == nickName) match {
|
room.players.find(_.name == nickName) match {
|
||||||
case Some(player) => player.id -> room
|
case Some(player) => player.id -> room
|
||||||
|
@ -71,8 +104,19 @@ class InMemoryRoomService[F[_]: Sync](stateRef: Ref[F, Map[RoomID, Room]]) exten
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val joiningWithChecks = for {
|
/** to be executed under Ref.modify (i.e with state acquired) checks of whether player can be
|
||||||
room <- rooms.get(id).toRight(RoomError.RoomMissing(id.name))
|
* added to the room:
|
||||||
|
* - room password is correct
|
||||||
|
* - nickname is either not taken, or correct password was provided
|
||||||
|
* @returns
|
||||||
|
* playerId (new or existing), updatedRoom (to be put into state), topic (to send the udpdate
|
||||||
|
* notification)
|
||||||
|
*/
|
||||||
|
def getWithChecks(
|
||||||
|
rooms: Map[RoomID, (Room, Topic[F, Room])]
|
||||||
|
): Either[RoomError, (PlayerID, Room, Topic[F, Room])] = for {
|
||||||
|
roomAndTopic <- rooms.get(id).toRight(RoomError.RoomMissing(id.name))
|
||||||
|
(room, topic) = roomAndTopic
|
||||||
_ <- Either.cond(room.password == roomPassword, (), RoomError.RoomPassIncorrect)
|
_ <- Either.cond(room.password == roomPassword, (), RoomError.RoomPassIncorrect)
|
||||||
isNickPassCorrect = room.playersPasswords
|
isNickPassCorrect = room.playersPasswords
|
||||||
.get(nickName)
|
.get(nickName)
|
||||||
|
@ -83,14 +127,54 @@ class InMemoryRoomService[F[_]: Sync](stateRef: Ref[F, Map[RoomID, Room]]) exten
|
||||||
RoomError.NickPassIncorrect
|
RoomError.NickPassIncorrect
|
||||||
)
|
)
|
||||||
(playerId, updatedRoom) = addPlayer(room)
|
(playerId, updatedRoom) = addPlayer(room)
|
||||||
} yield playerId
|
} yield (playerId, updatedRoom, topic)
|
||||||
|
|
||||||
rooms -> joiningWithChecks
|
// modify returns tuple (updatedState, valueToReturn)
|
||||||
|
// this particular update either keeps room as is, or adds the player
|
||||||
|
// and returns playerId and topic to be used outside
|
||||||
|
//
|
||||||
|
// NOTE here i have a lot of handwaving to pass topic outside
|
||||||
|
// because it's not possible to send F[Unit] update
|
||||||
|
// inside of the stateRef update (which works with pure functions?),
|
||||||
|
// so room notification change has to be returned to outside
|
||||||
|
val maybeUpdatedStateAndNotification = stateRef.modify { rooms =>
|
||||||
|
val maybeAddedUser = getWithChecks(rooms)
|
||||||
|
val updatedState =
|
||||||
|
maybeAddedUser.fold(
|
||||||
|
_ => rooms,
|
||||||
|
{ case (playerId, updatedRoom, topic) => rooms.updated(id, (updatedRoom, topic)) }
|
||||||
|
)
|
||||||
|
val toReturn = maybeAddedUser.map { case (id, updatedRoom, topic) =>
|
||||||
|
(id, topic.publish1(updatedRoom).void)
|
||||||
|
}
|
||||||
|
updatedState -> toReturn
|
||||||
|
}
|
||||||
|
|
||||||
|
// now combining the effects : getting (updatedState & notificationEffect) or error
|
||||||
|
// executing notification
|
||||||
|
// returning only playerId
|
||||||
|
val result = for {
|
||||||
|
updatedState <- EitherT(maybeUpdatedStateAndNotification)
|
||||||
|
(playerId, notification) = updatedState
|
||||||
|
_ <- EitherT.liftF(notification)
|
||||||
|
} yield (playerId)
|
||||||
|
|
||||||
|
result.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override def subscribe(roomID: RoomID): Stream[F, Room] =
|
||||||
|
Stream
|
||||||
|
.eval(stateRef.get)
|
||||||
|
.flatMap(rooms =>
|
||||||
|
rooms.get(roomID) match {
|
||||||
|
case Some((room, topic)) => topic.subscribe(10)
|
||||||
|
case None => Stream.empty
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
object RoomService {
|
object RoomService {
|
||||||
def make[F[_]: Sync]: F[RoomService[F]] = {
|
def make[F[_]: Concurrent]: F[RoomService[F]] = {
|
||||||
Ref.of[F, Map[RoomID, Room]](TestModels.testRooms).map(new InMemoryRoomService[F](_))
|
Ref.of[F, Map[RoomID, (Room, Topic[F, Room])]](Map.empty).map(new InMemoryRoomService[F](_))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,30 +20,27 @@ object Models {
|
||||||
players: List[Player],
|
players: List[Player],
|
||||||
me: PlayerID,
|
me: PlayerID,
|
||||||
allowedCards: List[String],
|
allowedCards: List[String],
|
||||||
round: RoundState,
|
round: RoundStateView,
|
||||||
canCloseRound: Boolean = false
|
canCloseRound: Boolean = false
|
||||||
) derives Codec.AsObject {
|
) derives Codec.AsObject
|
||||||
def playersCount: Int = players.size
|
|
||||||
}
|
|
||||||
|
|
||||||
object RoomStateView {
|
object RoomStateView {
|
||||||
val empty = RoomStateView(
|
val empty = RoomStateView(
|
||||||
List.empty,
|
List.empty,
|
||||||
PlayerID(0),
|
PlayerID(0),
|
||||||
List.empty,
|
List.empty,
|
||||||
RoundState.Voting(None, List.empty),
|
RoundStateView.Voting(None, List.empty),
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoundState derives Codec.AsObject:
|
enum RoundStateView derives Codec.AsObject:
|
||||||
|
|
||||||
/** view state for round before votes are open player can know their vote and who of the other
|
/** view state for round before votes are open player can know their vote and who of the other
|
||||||
* players have voted
|
* players have voted
|
||||||
*/
|
*/
|
||||||
case Voting(
|
case Voting(
|
||||||
myCard: Option[String],
|
myCard: Option[String],
|
||||||
alreadyVoted: List[Player]
|
alreadyVoted: List[PlayerID]
|
||||||
)
|
)
|
||||||
|
|
||||||
/** view state for round after opening the votes
|
/** view state for round after opening the votes
|
||||||
|
@ -51,15 +48,14 @@ object Models {
|
||||||
case Viewing(
|
case Viewing(
|
||||||
votes: List[(PlayerID, String)]
|
votes: List[(PlayerID, String)]
|
||||||
)
|
)
|
||||||
final case class PlayerID(id: Long) derives Codec.AsObject
|
|
||||||
|
|
||||||
|
final case class PlayerID(id: Long) derives Codec.AsObject
|
||||||
final case class Player(name: String, id: PlayerID) derives Codec.AsObject
|
final case class Player(name: String, id: PlayerID) derives Codec.AsObject
|
||||||
object Player {
|
object Player {
|
||||||
def create(name: String) = Player(name, PlayerID(Random.nextLong))
|
def create(name: String) = Player(name, PlayerID(Random.nextLong))
|
||||||
}
|
}
|
||||||
|
|
||||||
final case class RoomID(name: String) derives Codec.AsObject
|
final case class RoomID(name: String) derives Codec.AsObject
|
||||||
|
|
||||||
final case class Room(
|
final case class Room(
|
||||||
id: RoomID,
|
id: RoomID,
|
||||||
players: List[Player],
|
players: List[Player],
|
||||||
|
@ -67,9 +63,9 @@ object Models {
|
||||||
password: String,
|
password: String,
|
||||||
allowedCards: List[String],
|
allowedCards: List[String],
|
||||||
round: RoundState,
|
round: RoundState,
|
||||||
playersPasswords: Map[String, String] = Map.empty // nickname into password
|
playersPasswords: Map[String, String] = Map.empty, // nickname into password
|
||||||
) {
|
) {
|
||||||
def toViewFor(playerId: PlayerID): RoomStateView = {
|
def getViewFor(playerId: PlayerID): RoomStateView = {
|
||||||
players
|
players
|
||||||
.find(_.id == playerId)
|
.find(_.id == playerId)
|
||||||
.fold(ifEmpty = RoomStateView.empty)((me: Player) =>
|
.fold(ifEmpty = RoomStateView.empty)((me: Player) =>
|
||||||
|
@ -77,27 +73,28 @@ object Models {
|
||||||
players,
|
players,
|
||||||
me.id,
|
me.id,
|
||||||
allowedCards,
|
allowedCards,
|
||||||
round,
|
round.toViewFor(playerId),
|
||||||
playerId == owner
|
playerId == owner
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
object Room {
|
|
||||||
val testRoom = Room(
|
|
||||||
id = RoomID("testroom"),
|
|
||||||
players = List(
|
|
||||||
Player("me", PlayerID(1L)),
|
|
||||||
Player("horsey", PlayerID(444L)),
|
|
||||||
Player("froggy", PlayerID(555L)),
|
|
||||||
Player("owley", PlayerID(777L))
|
|
||||||
),
|
|
||||||
owner = PlayerID(1L),
|
|
||||||
password = "password",
|
|
||||||
allowedCards = List("S", "M", "L"),
|
|
||||||
// TODO - this needs to be a different hting
|
|
||||||
round = RoundState.Voting(None, List.empty)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// no need to send to the front end, no deed to derive codec, cool
|
||||||
|
sealed trait RoundState {
|
||||||
|
def toViewFor(player: PlayerID): RoundStateView
|
||||||
|
}
|
||||||
|
object RoundState {
|
||||||
|
final case class Voting(votes: Map[PlayerID, String]) extends RoundState {
|
||||||
|
def toViewFor(playerId: PlayerID): RoundStateView.Voting = RoundStateView.Voting(
|
||||||
|
myCard = votes.get(playerId),
|
||||||
|
alreadyVoted = votes.filterKeys(id => id != playerId).keys.toList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
final case class Viewing(votes: Map[PlayerID, String]) extends RoundState {
|
||||||
|
def toViewFor(player: PlayerID): RoundStateView.Viewing =
|
||||||
|
RoundStateView.Viewing(votes.toList)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,15 +9,15 @@ object TestModels {
|
||||||
val horsey = Player("horsey", PlayerID(12))
|
val horsey = Player("horsey", PlayerID(12))
|
||||||
|
|
||||||
val testRoomBackend = Room(
|
val testRoomBackend = Room(
|
||||||
id = RoomID("testroom"),
|
id = RoomID("testroom"),
|
||||||
players = List(me, birdy, pony, horsey),
|
players = List(me, birdy, pony, horsey),
|
||||||
owner = me.id,
|
owner = me.id,
|
||||||
password = "password",
|
password = "password",
|
||||||
allowedCards = List("xs", "s", "m", "l", "xl"),
|
allowedCards = List("xs", "s", "m", "l", "xl"),
|
||||||
round = RoundState.Viewing(
|
round = RoundState.Viewing(
|
||||||
List(me.id -> "xs", pony.id -> "l", birdy.id -> "s", horsey.id -> "m")
|
Map(me.id -> "xs", pony.id -> "l", birdy.id -> "s", horsey.id -> "m")
|
||||||
),
|
),
|
||||||
playersPasswords = Map("me" -> "nickpassword") // nickname into password
|
playersPasswords = Map("me" -> "nickpassword") // nickname into password
|
||||||
)
|
)
|
||||||
|
|
||||||
val testSessionId = 1L
|
val testSessionId = 1L
|
||||||
|
@ -29,70 +29,71 @@ object TestModels {
|
||||||
players = List(me),
|
players = List(me),
|
||||||
me = me.id,
|
me = me.id,
|
||||||
allowedCards = List("xs", "s", "m", "l", "xl"),
|
allowedCards = List("xs", "s", "m", "l", "xl"),
|
||||||
round = RoundState.Voting(myCard = None, alreadyVoted = List.empty),
|
round = RoundStateView.Voting(myCard = None, alreadyVoted = List.empty),
|
||||||
canCloseRound = true
|
canCloseRound = true
|
||||||
),
|
),
|
||||||
RoomStateView(
|
RoomStateView(
|
||||||
players = List(me, pony),
|
players = List(me, pony),
|
||||||
me = me.id,
|
me = me.id,
|
||||||
allowedCards = List("xs", "s", "m", "l", "xl"),
|
allowedCards = List("xs", "s", "m", "l", "xl"),
|
||||||
round = RoundState.Voting(myCard = None, alreadyVoted = List.empty),
|
round = RoundStateView.Voting(myCard = None, alreadyVoted = List.empty),
|
||||||
canCloseRound = true
|
canCloseRound = true
|
||||||
),
|
),
|
||||||
RoomStateView(
|
RoomStateView(
|
||||||
players = List(me, birdy, pony),
|
players = List(me, birdy, pony),
|
||||||
me = me.id,
|
me = me.id,
|
||||||
allowedCards = List("xs", "s", "m", "l", "xl"),
|
allowedCards = List("xs", "s", "m", "l", "xl"),
|
||||||
round = RoundState.Voting(myCard = None, alreadyVoted = List.empty),
|
round = RoundStateView.Voting(myCard = None, alreadyVoted = List.empty),
|
||||||
canCloseRound = true
|
canCloseRound = true
|
||||||
),
|
),
|
||||||
RoomStateView(
|
RoomStateView(
|
||||||
players = List(me, birdy, pony),
|
players = List(me, birdy, pony),
|
||||||
me = me.id,
|
me = me.id,
|
||||||
allowedCards = List("xs", "s", "m", "l", "xl"),
|
allowedCards = List("xs", "s", "m", "l", "xl"),
|
||||||
round = RoundState.Voting(myCard = None, alreadyVoted = List(birdy)),
|
round = RoundStateView.Voting(myCard = None, alreadyVoted = List(birdy.id)),
|
||||||
canCloseRound = true
|
canCloseRound = true
|
||||||
),
|
),
|
||||||
RoomStateView(
|
RoomStateView(
|
||||||
players = List(me, birdy, pony),
|
players = List(me, birdy, pony),
|
||||||
me = me.id,
|
me = me.id,
|
||||||
allowedCards = List("xs", "s", "m", "l", "xl"),
|
allowedCards = List("xs", "s", "m", "l", "xl"),
|
||||||
round = RoundState.Voting(myCard = Some("m"), alreadyVoted = List(birdy)),
|
round = RoundStateView.Voting(myCard = Some("m"), alreadyVoted = List(birdy.id)),
|
||||||
canCloseRound = true
|
canCloseRound = true
|
||||||
),
|
),
|
||||||
RoomStateView(
|
RoomStateView(
|
||||||
players = List(me, birdy, pony),
|
players = List(me, birdy, pony),
|
||||||
me = me.id,
|
me = me.id,
|
||||||
allowedCards = List("xs", "s", "m", "l", "xl"),
|
allowedCards = List("xs", "s", "m", "l", "xl"),
|
||||||
round = RoundState.Voting(myCard = Some("m"), alreadyVoted = List(birdy)),
|
round = RoundStateView.Voting(myCard = Some("m"), alreadyVoted = List(birdy.id)),
|
||||||
canCloseRound = true
|
canCloseRound = true
|
||||||
),
|
),
|
||||||
RoomStateView(
|
RoomStateView(
|
||||||
players = List(me, birdy, pony, horsey),
|
players = List(me, birdy, pony, horsey),
|
||||||
me = me.id,
|
me = me.id,
|
||||||
allowedCards = List("xs", "s", "m", "l", "xl"),
|
allowedCards = List("xs", "s", "m", "l", "xl"),
|
||||||
round = RoundState.Voting(myCard = Some("m"), alreadyVoted = List(birdy)),
|
round = RoundStateView.Voting(myCard = Some("m"), alreadyVoted = List(birdy.id)),
|
||||||
canCloseRound = true
|
canCloseRound = true
|
||||||
),
|
),
|
||||||
RoomStateView(
|
RoomStateView(
|
||||||
players = List(me, birdy, pony, horsey),
|
players = List(me, birdy, pony, horsey),
|
||||||
me = me.id,
|
me = me.id,
|
||||||
allowedCards = List("xs", "s", "m", "l", "xl"),
|
allowedCards = List("xs", "s", "m", "l", "xl"),
|
||||||
round = RoundState.Voting(myCard = Some("m"), alreadyVoted = List(birdy, horsey)),
|
round = RoundStateView.Voting(myCard = Some("m"), alreadyVoted = List(birdy.id, horsey.id)),
|
||||||
canCloseRound = true
|
canCloseRound = true
|
||||||
),
|
),
|
||||||
RoomStateView(
|
RoomStateView(
|
||||||
players = List(me, birdy, pony, horsey),
|
players = List(me, birdy, pony, horsey),
|
||||||
me = me.id,
|
me = me.id,
|
||||||
allowedCards = List("xs", "s", "m", "l", "xl"),
|
allowedCards = List("xs", "s", "m", "l", "xl"),
|
||||||
round = RoundState.Voting(myCard = Some("m"), alreadyVoted = List(birdy, horsey, pony)),
|
round = RoundStateView
|
||||||
|
.Voting(myCard = Some("m"), alreadyVoted = List(birdy.id, horsey.id, pony.id)),
|
||||||
canCloseRound = true
|
canCloseRound = true
|
||||||
),
|
),
|
||||||
RoomStateView(
|
RoomStateView(
|
||||||
players = List(me, birdy, pony, horsey),
|
players = List(me, birdy, pony, horsey),
|
||||||
me = me.id,
|
me = me.id,
|
||||||
allowedCards = List("xs", "s", "m", "l", "xl"),
|
allowedCards = List("xs", "s", "m", "l", "xl"),
|
||||||
round = RoundState.Viewing(
|
round = RoundStateView.Viewing(
|
||||||
List(me.id -> "m", pony.id -> "l", birdy.id -> "s", horsey.id -> "m")
|
List(me.id -> "m", pony.id -> "l", birdy.id -> "s", horsey.id -> "m")
|
||||||
),
|
),
|
||||||
canCloseRound = true
|
canCloseRound = true
|
||||||
|
|
|
@ -32,9 +32,9 @@ object OwnHandControls {
|
||||||
|
|
||||||
private def myUnselectedCards(state: RoomStateView): List[String] = {
|
private def myUnselectedCards(state: RoomStateView): List[String] = {
|
||||||
state.round match {
|
state.round match {
|
||||||
case RoundState.Voting(myCard, _) =>
|
case RoundStateView.Voting(myCard, _) =>
|
||||||
state.allowedCards.filterNot(value => myCard.contains(value))
|
state.allowedCards.filterNot(value => myCard.contains(value))
|
||||||
case RoundState.Viewing(votes) =>
|
case RoundStateView.Viewing(votes) =>
|
||||||
state.allowedCards.filterNot(value => votes.toMap.get(state.me).contains(value))
|
state.allowedCards.filterNot(value => votes.toMap.get(state.me).contains(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,21 +2,19 @@ package industries.sunshine.planningpoker
|
||||||
|
|
||||||
import scala.scalajs.js
|
import scala.scalajs.js
|
||||||
import com.raquo.laminar.api.L.{*, given}
|
import com.raquo.laminar.api.L.{*, given}
|
||||||
import industries.sunshine.planningpoker.common.Models.RoundState
|
import industries.sunshine.planningpoker.common.Models.RoundStateView
|
||||||
import com.raquo.airstream.core.Signal
|
import com.raquo.airstream.core.Signal
|
||||||
import industries.sunshine.planningpoker.common.Models.RoundState
|
|
||||||
import industries.sunshine.planningpoker.common.Models.RoundState
|
|
||||||
import io.laminext.fetch.Fetch
|
import io.laminext.fetch.Fetch
|
||||||
import scala.scalajs.js.Dynamic.{global => g}
|
import scala.scalajs.js.Dynamic.{global => g}
|
||||||
|
|
||||||
import concurrent.ExecutionContext.Implicits.global
|
import concurrent.ExecutionContext.Implicits.global
|
||||||
|
|
||||||
object TableControls {
|
object TableControls {
|
||||||
def render(roundSignal: Signal[RoundState]): Element = {
|
def render(roundSignal: Signal[RoundStateView]): Element = {
|
||||||
div(
|
div(
|
||||||
child <-- roundSignal.map {
|
child <-- roundSignal.map {
|
||||||
case RoundState.Viewing(_) => newPollButton()
|
case RoundStateView.Viewing(_) => newPollButton()
|
||||||
case RoundState.Voting(_, _) => endPollButton()
|
case RoundStateView.Voting(_, _) => endPollButton()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,16 +39,16 @@ object TableView {
|
||||||
|
|
||||||
def getPlayerCardType(
|
def getPlayerCardType(
|
||||||
id: PlayerID,
|
id: PlayerID,
|
||||||
state: RoundState,
|
state: RoundStateView,
|
||||||
name: String,
|
name: String,
|
||||||
myId: PlayerID
|
myId: PlayerID
|
||||||
): CardState = {
|
): CardState = {
|
||||||
state match {
|
state match {
|
||||||
case isOpen: RoundState.Voting =>
|
case isOpen: RoundStateView.Voting =>
|
||||||
if (myId == id) {
|
if (myId == id) {
|
||||||
isOpen.myCard.fold(NoCard(name))(vote => Open(vote))
|
isOpen.myCard.fold(NoCard(name))(vote => Open(vote))
|
||||||
} else isOpen.alreadyVoted.find(_.id == id).fold(NoCard(name))(_ => CardBack)
|
} else isOpen.alreadyVoted.find(_ == id).fold(NoCard(name))(_ => CardBack)
|
||||||
case isClosed: RoundState.Viewing =>
|
case isClosed: RoundStateView.Viewing =>
|
||||||
isClosed.votes
|
isClosed.votes
|
||||||
.find(_._1 == id)
|
.find(_._1 == id)
|
||||||
.fold {
|
.fold {
|
||||||
|
|
Loading…
Reference in New Issue