Issue #4 - Refactored API module to give detail on errors.

This commit is contained in:
Gareth Latty
2015-12-19 02:18:58 +00:00
parent 30c9c9f0fc
commit 0c3a7fa634
16 changed files with 409 additions and 128 deletions
+175 -58
View File
@@ -1,88 +1,205 @@
module MassiveDecks.API where
import Json.Encode exposing (..)
import Json.Decode
import Json.Encode as Json
import Json.Decode exposing (succeed)
import Task
import Effects
import Http exposing (post, url, empty, send, defaultSettings, fromJson)
import Http exposing (send, defaultSettings)
import MassiveDecks.Actions.Action exposing (Action(..))
import MassiveDecks.API.Request exposing (Request, SpecificErrorDecoder, toRequest, jsonBody, specificErrorDecoder, oneArgument, twoArguments)
import MassiveDecks.Models.Player exposing (Secret, Id)
import MassiveDecks.Models.Game exposing (Lobby, LobbyAndHand)
import MassiveDecks.Models.Json.Encode exposing (..)
import MassiveDecks.Models.Json.Decode exposing (..)
createLobby : Task.Task Http.Error Lobby
createLobby = post lobbyDecoder (url "/lobbies" []) empty
headers : List (String, String)
headers = [("Content-Type", "application/json")]
newPlayer : String -> String -> Task.Task Http.Error Secret
newPlayer lobbyId name = send defaultSettings
{ verb = "POST"
, headers = [("Content-Type", "application/json")]
, url = url ("/lobbies/" ++ lobbyId ++ "/players") []
, body = Http.string ("{ \"name\": \"" ++ name ++ "\"}")
} |> fromJson playerSecretDecoder
commandBody : String -> Secret -> List (String, Json.Value) -> Http.Body
commandBody command secret data =
jsonBody (Json.object (List.append
[ ("command", Json.string command)
, ("secret", playerSecretEncoder secret)
] data))
leave : String -> Secret -> Task.Task Http.Error LobbyAndHand
leave lobbyId secret = send defaultSettings
{ verb = "POST"
, headers = [("Content-Type", "application/json")]
, url = url ("/lobbies/" ++ lobbyId ++ "/players/" ++ (toString secret.id) ++ "/leave") []
, body = Http.string ("{ \"secret\": \"" ++ secret.secret ++ "\"}")
} |> fromJson lobbyAndHandDecoder
createLobby : Request () Lobby
createLobby =
send defaultSettings
{ verb = "POST"
, headers = []
, url = "/lobbies"
, body = Http.empty
}
|> toRequest lobbyDecoder (\_ -> Nothing)
addDeck : String -> Secret -> String -> Task.Task Http.Error LobbyAndHand
addDeck lobbyId secret deckId = lobbyAction lobbyId (commandEncoder "addDeck" secret [ ("deckId", string deckId) ])
noArguments : a -> List Json.Value -> Maybe a
noArguments value _ = Just value
newAi : String -> Task.Task Http.Error ()
newAi lobbyId = send defaultSettings
{ verb = "POST"
, headers = []
, url = url ("/lobbies/" ++ lobbyId ++ "/players/newAi") []
, body = empty
} |> fromJson (Json.Decode.succeed ())
type NewPlayerError
= NameInUse
newPlayerErrorDecoder : SpecificErrorDecoder NewPlayerError
newPlayerErrorDecoder = specificErrorDecoder (List.concat
[ [ (400, "name-in-use", [], noArguments NameInUse)
]
])
newPlayer : String -> String -> Request NewPlayerError Secret
newPlayer lobbyId name =
send defaultSettings
{ verb = "POST"
, headers = headers
, url = "/lobbies/" ++ lobbyId ++ "/players"
, body = jsonBody (Json.object [ ("name", Json.string name) ])
}
|> toRequest playerSecretDecoder newPlayerErrorDecoder
newGame : String -> Secret -> Task.Task Http.Error LobbyAndHand
newGame lobbyId secret = lobbyAction lobbyId (commandEncoder "newGame" secret [])
leave : String -> Secret -> Request () LobbyAndHand
leave lobbyId secret =
send defaultSettings
{ verb = "POST"
, headers = headers
, url = "/lobbies/" ++ lobbyId ++ "/players/" ++ (toString secret.id) ++ "/leave"
, body = jsonBody (Json.object [ ("secret", Json.string secret.secret) ])
}
|> toRequest lobbyAndHandDecoder (\_ -> Nothing)
play : String -> Secret -> List Int -> Task.Task Http.Error LobbyAndHand
play lobbyId secret ids = lobbyAction lobbyId (commandEncoder "play" secret [ ("ids", list (List.map int ids)) ])
type AddDeckError
= CardCastTimeout
| DeckNotFound
addDeckErrorDecoder : SpecificErrorDecoder AddDeckError
addDeckErrorDecoder = specificErrorDecoder (List.concat
[ [ (502, "cardcast-timeout", [], noArguments CardCastTimeout)
, (400, "deck-not-found", [], noArguments DeckNotFound)
]
])
addDeck : String -> Secret -> String -> Request AddDeckError LobbyAndHand
addDeck lobbyId secret deckId =
send defaultSettings
{ verb = "POST"
, headers = headers
, url = "/lobbies/" ++ lobbyId
, body = commandBody "addDeck" secret [ ("deckId", Json.string deckId) ]
}
|> toRequest lobbyAndHandDecoder addDeckErrorDecoder
choose : String -> Secret -> Int -> Task.Task Http.Error LobbyAndHand
choose lobbyId secret winner = lobbyAction lobbyId (commandEncoder "choose" secret [ ("winner", int winner) ])
newAi : String -> Request () ()
newAi lobbyId =
send defaultSettings
{ verb = "POST"
, headers = []
, url = "/lobbies/" ++ lobbyId ++ "/players/newAi"
, body = Http.empty
}
|> toRequest (succeed ()) (\_ -> Nothing)
skip : String -> Secret -> List Id -> Task.Task Http.Error LobbyAndHand
type NewGameError
= NotEnoughPlayers Int
| GameInProgress
newGameErrorDecoder : SpecificErrorDecoder NewGameError
newGameErrorDecoder = specificErrorDecoder (List.concat
[ [ (400, "game-in-progress", [], noArguments GameInProgress)
, (400, "not-enough-players", [ "required" ], oneArgument Json.Decode.int NotEnoughPlayers)
]
])
newGame : String -> Secret -> Request NewGameError LobbyAndHand
newGame lobbyId secret =
send defaultSettings
{ verb = "POST"
, headers = headers
, url = "/lobbies/" ++ lobbyId
, body = commandBody "newGame" secret []
}
|> toRequest lobbyAndHandDecoder newGameErrorDecoder
type PlayError
= NotInRound
| AlreadyPlayed
| AlreadyJudging
| WrongNumberOfCards Int Int
playErrorDecoder : SpecificErrorDecoder PlayError
playErrorDecoder = specificErrorDecoder (List.concat
[ [ (400, "not-in-round", [], noArguments NotInRound)
, (400, "already-played", [], noArguments AlreadyPlayed)
, (400, "already-judging", [], noArguments AlreadyJudging)
, (400, "wrong-number-of-cards-played", [ "got", "expected" ]
, twoArguments (Json.Decode.int, Json.Decode.int) WrongNumberOfCards)
]
])
play : String -> Secret -> List Int -> Request PlayError LobbyAndHand
play lobbyId secret ids =
send defaultSettings
{ verb = "POST"
, headers = headers
, url = "/lobbies/" ++ lobbyId
, body = commandBody "play" secret [ ("ids", Json.list (List.map Json.int ids)) ]
}
|> toRequest lobbyAndHandDecoder playErrorDecoder
type ChooseError
= NotCzar
chooseErrorDecoder : SpecificErrorDecoder ChooseError
chooseErrorDecoder = specificErrorDecoder (List.concat
[ [ (400, "not-czar", [], noArguments NotCzar)
]
])
choose : String -> Secret -> Int -> Request ChooseError LobbyAndHand
choose lobbyId secret winner =
send defaultSettings
{ verb = "POST"
, headers = headers
, url = "/lobbies/" ++ lobbyId
, body = commandBody "choose" secret [ ("winner", Json.int winner) ]
}
|> toRequest lobbyAndHandDecoder chooseErrorDecoder
type SkipError
= NotEnoughPlayersToSkip
| PlayersNotSkippable
skipErrorDecoder : SpecificErrorDecoder SkipError
skipErrorDecoder = specificErrorDecoder (List.concat
[ [ (400, "not-enough-players-to-skip", [], noArguments NotEnoughPlayersToSkip)
, (400, "players-must-be-skippable", [], noArguments PlayersNotSkippable)
]
])
skip : String -> Secret -> List Id -> Request SkipError LobbyAndHand
skip lobbyId secret players =
lobbyAction lobbyId (commandEncoder "skip" secret [ ("players", list (List.map int players)) ])
send defaultSettings
{ verb = "POST"
, headers = headers
, url = "/lobbies/" ++ lobbyId
, body = commandBody "skip" secret [ ("players", Json.list (List.map Json.int players)) ]
}
|> toRequest lobbyAndHandDecoder skipErrorDecoder
getLobbyAndHand : String -> Secret -> Task.Task Http.Error LobbyAndHand
getLobbyAndHand : String -> Secret -> Request () LobbyAndHand
getLobbyAndHand lobbyId secret =
lobbyAction lobbyId (commandEncoder "getLobbyAndHand" secret [])
lobbyAction : String -> String -> Task.Task Http.Error LobbyAndHand
lobbyAction lobbyId content = send defaultSettings
{ verb = "POST"
, headers = [("Content-Type", "application/json")]
, url = url ("/lobbies/" ++ lobbyId) []
, body = Http.string (content)
} |> fromJson lobbyAndHandDecoder
toEffect : Task.Task Http.Error Action -> Effects.Effects Action
toEffect task = task `Task.onError` handleError |> Effects.task
handleError : Http.Error -> Task.Task b Action
handleError error = toString error |> DisplayError |> Task.succeed
send defaultSettings
{ verb = "POST"
, headers = headers
, url = "/lobbies/" ++ lobbyId
, body = commandBody "getLobbyAndHand" secret []
}
|> toRequest lobbyAndHandDecoder (\_ -> Nothing)
+130
View File
@@ -0,0 +1,130 @@
module MassiveDecks.API.Request where
import Http
import Task exposing (Task)
import Json.Decode exposing (Decoder, decodeValue, decodeString, keyValuePairs, value)
import Json.Encode as Json
import Effects exposing (Effects)
import MassiveDecks.Actions.Action exposing (Action(..))
import MassiveDecks.Util as Util
type alias Request specificErrors successfulResponse =
Task (Error specificErrors) successfulResponse
type alias SpecificErrorDecoder specificErrors =
Http.Response -> Maybe (Error specificErrors)
type Error a
= Communication Http.RawError
| Unknown Int String
| Malformed String
| Known a
specificErrorDecoder : List (Int, String, List String, (List Json.Value -> Maybe a)) -> SpecificErrorDecoder a
specificErrorDecoder errorsFormats response =
case response.value of
Http.Text rawValues ->
case decodeString (keyValuePairs value) rawValues of
Ok values ->
let
error = Util.find (\(key, value) -> key == "error") values
in
error
`Maybe.andThen`
(\(_, raw) -> decodeValue Json.Decode.string raw |> Result.toMaybe)
`Maybe.andThen`
(\respErrorName ->
Util.find (\(status, errorName, _, _) -> response.status == status && respErrorName == errorName) errorsFormats
`Maybe.andThen` (\(_, _, keys, errorConstructor) -> (errorConstructor (extractValues keys values)))
|> Maybe.map Known)
Err _ ->
Nothing
Http.Blob _ ->
Nothing
extractValues : List String -> List (String, Json.Value) -> List Json.Value
extractValues keys values =
List.filterMap (\key -> Util.find (\(potentialKey, value) -> potentialKey == key) values) keys
|> List.map snd
toArgument : Decoder a -> (a -> b) -> Json.Value -> Maybe b
toArgument decoder f value = Result.toMaybe (decodeValue decoder value) |> Maybe.map f
oneArgument : Decoder a -> (a -> b) -> List Json.Value -> Maybe b
oneArgument decoder f values = (List.head values) `Maybe.andThen` (toArgument decoder f)
twoArguments : (Decoder a, Decoder b) -> (a -> b -> c) -> List Json.Value -> Maybe c
twoArguments (decoder1, decoder2) f values =
case values of
first :: second :: [] ->
let
v1 = Result.toMaybe (decodeValue decoder1 first)
v2 = Result.toMaybe (decodeValue decoder2 second)
in
Maybe.map2 f v1 v2
_ ->
Nothing
toRequest : Decoder a -> SpecificErrorDecoder b -> Task Http.RawError Http.Response -> Request b a
toRequest successDecoder specificErrorDecoder task
= (Task.mapError Communication task)
`Task.andThen`
(\response ->
if (200 <= response.status && response.status < 300) then
wrappedSuccessDecoder successDecoder response
else
Task.fail (specificErrorDecoder response |> Maybe.withDefault (genericErrorDecoder response))
)
wrappedSuccessDecoder : Decoder a -> Http.Response -> Request b a
wrappedSuccessDecoder decoder response =
case response.value of
Http.Text rawValue ->
case decodeString decoder rawValue of
Ok value -> Task.succeed value
Err error -> Task.fail (Malformed error)
Http.Blob _ -> Task.fail (Malformed "Recieved binary data instead of expected JSON.")
genericErrorDecoder : Http.Response -> Error a
genericErrorDecoder response = Unknown response.status response.statusText
handleErrors : (a -> Action) -> Request a Action -> Task c Action
handleErrors knownErrorHandler request =
let
errorToAction error = case error of
Communication (Http.RawTimeout) ->
DisplayError "The server couldn't be reached after a long time, it may be down."
Communication (Http.RawNetworkError) ->
DisplayError "There was a network error trying to each the server."
Malformed explanation ->
DisplayError ("There was an error decoding the response the server gave: " ++ explanation)
Unknown status statusText ->
DisplayError ("Recieved an unexpected bad response from the server: " ++ (toString status) ++ " - " ++ statusText )
Known knownError ->
knownErrorHandler knownError
in
request `Task.onError` (\error -> Task.succeed (errorToAction error))
toEffect : (a -> Action) -> (b -> Action) -> Request a b -> Effects Action
toEffect errorHandler successHandler task
= Task.map successHandler task
|> handleErrors errorHandler
|> Effects.task
jsonBody : Json.Value -> Http.Body
jsonBody value = Json.encode 0 value |> Http.string
+1 -1
View File
@@ -26,7 +26,7 @@ type Action
| AddDeck
| AddGivenDeck String (APICall LobbyAndHand)
| FailAddDeck String Error
| StartGame (APICall LobbyAndHand)
| StartGame
| Pick Int
| Play
| Withdraw Int
+12 -11
View File
@@ -14,6 +14,7 @@ import MassiveDecks.Models.Notification as Notification
import MassiveDecks.Actions.Action exposing (Action(..), APICall(..), eventEffects)
import MassiveDecks.Actions.Event exposing (Event(..))
import MassiveDecks.API as API
import MassiveDecks.API.Request as Request
import MassiveDecks.States.Playing as Playing
@@ -30,9 +31,10 @@ update action global data = case action of
AddGivenDeck deckId Request ->
(model global { data | loadingDecks = List.append data.loadingDecks [ deckId ] },
((API.addDeck data.lobby.id data.secret (String.toUpper deckId))
|> Task.map (AddGivenDeck deckId << Result))
|> Request.toEffect (\error -> DisplayError (toString error)) (AddGivenDeck deckId << Result)))
{- }|> Task.map (AddGivenDeck deckId << Result))
`Task.onError` (\error -> FailAddDeck deckId error |> Task.succeed)
|> Effects.task)
|> Effects.task) -}
AddGivenDeck deckId (Result lobbyAndHand) ->
let
@@ -47,13 +49,13 @@ update action global data = case action of
AddAi ->
(model global data,
(API.newAi data.lobby.id)
|> Task.map (\_ -> NoAction)
|> API.toEffect)
|> Request.toEffect (\_ -> NoAction) (\_ -> NoAction))
StartGame Request ->
(model global data, (API.newGame data.lobby.id data.secret) |> Task.map (StartGame << Result) |> API.toEffect)
StartGame ->
(model global data, (API.newGame data.lobby.id data.secret)
|> Request.toEffect (\error -> DisplayError (toString error)) UpdateLobbyAndHand)
StartGame (Result lobbyAndHand) ->
UpdateLobbyAndHand lobbyAndHand ->
(Playing.model global (playingData lobbyAndHand.lobby lobbyAndHand.hand data.secret),
eventEffects data.lobby lobbyAndHand.lobby)
@@ -61,8 +63,8 @@ update action global data = case action of
case lobby.round of
Just _ -> (model global data,
(API.getLobbyAndHand lobby.id data.secret)
|> Task.map (\lobbyAndHand -> JoinLobby lobby.id data.secret (Result lobbyAndHand))
|> API.toEffect)
|> Request.toEffect (\error -> DisplayError (toString error))
(\lobbyAndHand -> JoinLobby lobby.id data.secret (Result lobbyAndHand)))
Nothing ->
let
(data, effects) = updateLobby data lobby
@@ -93,8 +95,7 @@ update action global data = case action of
LeaveLobby ->
({ state = SStart { name = "", lobbyId = "" }, subscription = Just Nothing, global = global },
(API.leave data.lobby.id data.secret)
|> Task.map (\_ -> NoAction)
|> API.toEffect)
|> Request.toEffect (\_ -> NoAction) (\_ -> NoAction))
GameEvent event ->
case event of
+1 -1
View File
@@ -154,7 +154,7 @@ startGameButton address enoughPlayers enoughCards = div [ id "start-game" ]
[ startGameWarning enoughPlayers
, button
[ class "mui-btn mui-btn--primary mui-btn--raised"
, onClick address (StartGame Request)
, onClick address StartGame
, disabled (not (enoughPlayers && enoughCards))
] [ text "Start Game" ]
]
+13 -8
View File
@@ -16,6 +16,7 @@ import MassiveDecks.Actions.Action exposing (Action(..), APICall(..), eventEffec
import MassiveDecks.Actions.Event exposing (Event(..))
import MassiveDecks.States.Playing.UI as UI
import MassiveDecks.API as API
import MassiveDecks.API.Request as Request
import MassiveDecks.Util as Util
@@ -38,7 +39,9 @@ update action global data = case action of
(model global { data | picked = List.filter ((/=) card) data.picked }, Effects.none)
Play ->
(model global data, (API.play data.lobby.id data.secret data.picked) |> Task.map UpdateLobbyAndHand |> API.toEffect)
(model global data,
(API.play data.lobby.id data.secret data.picked)
|> Request.toEffect (\error -> DisplayError (toString error)) UpdateLobbyAndHand)
Notification lobby ->
case lobby.round of
@@ -56,20 +59,24 @@ update action global data = case action of
(model global { data | considering = Just potentialWinner } , Effects.none)
Choose winner ->
(model global data, (API.choose data.lobby.id data.secret winner) |> Task.map UpdateLobbyAndHand |> API.toEffect)
(model global data, (API.choose data.lobby.id data.secret winner)
|> Request.toEffect (\error -> DisplayError (toString error)) UpdateLobbyAndHand)
Skip players ->
(model global data, (API.skip data.lobby.id data.secret players) |> Task.map UpdateLobbyAndHand |> API.toEffect)
(model global data, (API.skip data.lobby.id data.secret players)
|> Request.toEffect (\error -> DisplayError (toString error)) UpdateLobbyAndHand)
UpdateLobbyAndHand lobbyAndHand ->
(model global
{ data | lobby = lobbyAndHand.lobby
, hand = lobbyAndHand.hand
, picked = []
}, eventEffects data.lobby lobbyAndHand.lobby)
NextRound ->
(model global { data | lastFinishedRound = Nothing }, Effects.none)
(model global { data | lastFinishedRound = Nothing
, picked = []
, considering = Nothing
}, Effects.none)
AnimatePlayedCards toAnimate ->
let
@@ -89,9 +96,7 @@ update action global data = case action of
LeaveLobby ->
({ state = SStart { name = "", lobbyId = "" }, subscription = Just Nothing, global = global },
(API.leave data.lobby.id data.secret)
|> Task.map (\_ -> NoAction)
|> API.toEffect)
(API.leave data.lobby.id data.secret) |> Request.toEffect (\_ -> NoAction) (\_ -> NoAction))
GameEvent event ->
case event of
@@ -1,8 +1,10 @@
module MassiveDecks.States.SharedUI.General where
import Http exposing (url)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import MassiveDecks.Models.State exposing (Error)
import MassiveDecks.Actions.Action exposing (Action(..))
@@ -19,8 +21,17 @@ spinner : Html
spinner = i [ class "fa fa-circle-o-notch fa-spin" ] []
reportText : String -> String
reportText message =
("I was [a short explanation of what you were doing] when I got the following error: \n\n"
++ message)
errorMessage : Signal.Address Action -> Int -> Error -> Html
errorMessage address index error =
let
reportUrl = (url "https://github.com/Lattyware/massivedecks/issues/new" [( "body", reportText error.message ) ])
in
li
[ class "error" ]
[ div
@@ -35,6 +46,7 @@ errorMessage address index error =
]
, divider
, p [] [ text error.message ]
, p [] [ a [ href reportUrl, target "_blank" ] [ icon "bug", text " Report this as a bug." ] ]
]
]
+7 -8
View File
@@ -6,6 +6,7 @@ import Effects
import Html exposing (Html)
import MassiveDecks.API as API
import MassiveDecks.API.Request as Request
import MassiveDecks.Models.State exposing (Model, State(..))
import MassiveDecks.Actions.Action exposing (Action(..), APICall(..), catchUpEffects)
import MassiveDecks.Models.State exposing (State(..), StartData, playingData, Error, Global)
@@ -23,25 +24,23 @@ update action global data = case action of
_ -> (model global data, DisplayError "Got an update for an unknown input." |> Task.succeed |> Effects.task)
NewLobby Request ->
(model global data, API.createLobby |> Task.map (NewLobby << Result) |> API.toEffect)
(model global data, API.createLobby
|> Request.toEffect (\error -> DisplayError (toString error)) (NewLobby << Result))
NewLobby (Result lobby) ->
(model global data,
(API.newPlayer lobby.id data.name)
|> Task.map (\secret -> JoinLobby lobby.id secret Request)
|> API.toEffect)
|> Request.toEffect (\error -> DisplayError (toString error)) (\secret -> JoinLobby lobby.id secret Request))
JoinExistingLobby ->
(model global data,
(API.newPlayer data.lobbyId data.name)
|> Task.map (\secret -> JoinLobby data.lobbyId secret Request)
|> API.toEffect)
(API.newPlayer data.lobbyId data.name)
|> Request.toEffect (\error -> DisplayError (toString error)) (\secret -> JoinLobby data.lobbyId secret Request))
JoinLobby lobbyId secret Request ->
(model global data,
(API.getLobbyAndHand lobbyId secret)
|> Task.map (\lobbyAndHand -> JoinLobby lobbyId secret (Result lobbyAndHand))
|> API.toEffect)
|> Request.toEffect (\error -> DisplayError (toString error)) (\lobbyAndHand -> JoinLobby lobbyId secret (Result lobbyAndHand)))
JoinLobby lobbyId secret (Result lobbyAndHand) ->
case lobbyAndHand.lobby.round of
@@ -121,7 +121,7 @@ body.hide-scores {
margin: 0px;
}
a {
& > div > a {
display: block;
position: absolute;
top: 5px;
@@ -19,7 +19,7 @@ import controllers.massivedecks.game.Actions.Lobby.GetLobby
import controllers.massivedecks.game.Actions.Player.Formatters._
import controllers.massivedecks.game.Actions.Player.{Leave, AddAi, GetHand, NewPlayer}
import controllers.massivedecks.game.Actions.Store.{LobbyAction, NewLobby, PlayerAction}
import controllers.massivedecks.game.NotFoundException
import controllers.massivedecks.game.{BadRequestException, RequestFailedException, NotFoundException}
import models.massivedecks.Player.{Id, Secret}
class Application @Inject() (@Named("store") store: ActorRef)(implicit ec: ExecutionContext) extends Controller {
@@ -47,7 +47,7 @@ class Application @Inject() (@Named("store") store: ActorRef)(implicit ec: Execu
resultOrError(store ? LobbyAction(lobbyId, action))
case None =>
Future.successful(BadRequest("Invalid command."))
Future.successful(BadRequest("{\"error\":\"invalid-command\"}"))
}
}
@@ -57,7 +57,7 @@ class Application @Inject() (@Named("store") store: ActorRef)(implicit ec: Execu
resultOrError(store ? PlayerAction(lobbyId, action))
case None =>
Future.successful(BadRequest("Badly formed name provided."))
Future.successful(BadRequest("{\"error\":\"badly-formed-name\"}"))
}
}
@@ -67,7 +67,7 @@ class Application @Inject() (@Named("store") store: ActorRef)(implicit ec: Execu
resultOrError(store ? PlayerAction(lobbyId, GetHand(Secret(Id(playerId), secret))))
case None =>
Future.successful(BadRequest("Badly formed secret provided."))
Future.successful(BadRequest("{\"error\":\"badly-formed-secret\"}"))
}
}
@@ -81,7 +81,7 @@ class Application @Inject() (@Named("store") store: ActorRef)(implicit ec: Execu
resultOrError(store ? PlayerAction(lobbyId, Leave(Secret(Id(playerId), secret))))
case None =>
Future.successful(BadRequest("Badly formed secret provided."))
Future.successful(BadRequest("{\"error\":\"badly-formed-secret\"}"))
}
}
@@ -91,14 +91,15 @@ class Application @Inject() (@Named("store") store: ActorRef)(implicit ec: Execu
case Success(json) =>
Ok(json).as(JSON)
case Failure(error) =>
if (error.isInstanceOf[IllegalArgumentException] ||
error.isInstanceOf[IllegalStateException]) {
BadRequest(error.getMessage).as(TEXT)
} else if (error.isInstanceOf[NotFoundException]) {
NotFound
} else {
throw error
case Failure(error) => error match {
case BadRequestException(msg) =>
BadRequest(msg)
case NotFoundException(msg) =>
NotFound(msg).as(JSON)
case RequestFailedException(msg) =>
BadGateway(msg).as(JSON)
case _ =>
throw error
}
})
}
@@ -8,6 +8,8 @@ import scala.concurrent.{ExecutionContext, Future}
import scala.util.Try
import akka.pattern.after
import controllers.massivedecks.game.{BadRequestException, RequestFailedException}
import controllers.massivedecks.game.BadRequestException._
import play.api.libs.concurrent.Akka
import play.api.Play.current
import play.api.libs.json.JsValue
@@ -19,7 +21,7 @@ class CardCastAPI @Inject()(ws: WSClient)(implicit ec: ExecutionContext) {
private val apiUrl: String = "https://api.cardcastgame.com/v1"
private def deckUrl(id: String): String = {
require(id.length > 0, "An ID can't be empty.")
verify(id.length > 0, "{\"error\":\"deck-not-found\"}")
s"$apiUrl/decks/$id"
}
private def cardsUrl(id: String): String = s"${deckUrl(id)}/cards"
@@ -40,7 +42,7 @@ class CardCastAPI @Inject()(ws: WSClient)(implicit ec: ExecutionContext) {
} yield CardCastDeck(id, name, calls, responses)
val timeoutError = after(timeout, using=Akka.system.scheduler)(
Future.failed(new IllegalStateException("Timed out waiting for a response from CardCast.")))
Future.failed(new RequestFailedException("{\"error\":\"cardcast-timeout\"}")))
Future firstCompletedOf Seq(deck, timeoutError)
}
@@ -75,9 +77,9 @@ class CardCastAPI @Inject()(ws: WSClient)(implicit ec: ExecutionContext) {
private def parseError[T](error: JsValue): T = {
println(error)
(error \ "id").validate[String].asOpt match {
case Some("not_found") => throw new IllegalArgumentException("The given CardCast deck was not found.")
case Some(errorName) => throw new IllegalStateException(s"CardCast gave an unknown error ('$errorName') when trying to retrieve the deck.")
case None => throw new IllegalStateException(s"CardCast gave an error that couldn't be parsed when trying to retrieve the deck.")
case Some("not_found") => throw new BadRequestException("{\"error\":\"deck-not-found\"}")
case Some(errorName) => throw new Exception(s"CardCast gave an unknown error ('$errorName') when trying to retrieve the deck.")
case None => throw new Exception(s"CardCast gave an error that couldn't be parsed when trying to retrieve the deck.")
}
}
}
@@ -0,0 +1,10 @@
package controllers.massivedecks.game
case class BadRequestException(message: String) extends Exception
object BadRequestException {
def verify(requirement: Boolean, message: => String): Unit = {
if (!requirement) {
throw new BadRequestException(message)
}
}
}
@@ -120,7 +120,7 @@ class Game @Inject()(private val state: State, @Assisted private val id: String)
case _ =>
sender() ! Try {
throw new IllegalArgumentException("Unknown message: " + message)
throw new Exception("Unknown message: " + message)
}
}
}
@@ -0,0 +1,3 @@
package controllers.massivedecks.game
case class RequestFailedException(message: String) extends Exception
@@ -15,6 +15,7 @@ import models.massivedecks.Game._
import models.massivedecks.Lobby.Formatters._
import models.massivedecks.Lobby.{Lobby, LobbyAndHand}
import models.massivedecks.Player._
import controllers.massivedecks.game.BadRequestException._
import play.api.libs.concurrent.Akka
import play.api.libs.json.Json
import play.api.Play.current
@@ -60,7 +61,7 @@ class State @Inject()(private val cardCast: CardCastAPI, @Assisted val id: Strin
}
def newPlayer(name: String): Secret = {
require(players.forall(player => player.name != name), "The name is already in use.")
verify(players.forall(player => player.name != name), "{\"error\":\"name-in-use\"}")
lastPlayerId += 1
val id = Id(lastPlayerId)
players = players ++ List(Player(id, name, Neutral, 0, disconnected=false, left=false))
@@ -90,10 +91,10 @@ class State @Inject()(private val cardCast: CardCastAPI, @Assisted val id: Strin
def newGame(secret: Secret): Unit = {
if (numberOfPlayers < State.minimumPlayers) {
throw new IllegalStateException(s"You need a minimum of ${State.minimumPlayers} to start a game.")
throw new BadRequestException(s"""{\"error\":\"not-enough-players\",\"required\":${State.minimumPlayers}}""")
}
if (game.isDefined) {
throw new IllegalStateException(s"A game is already in progress.")
throw new BadRequestException("{\"error\":\"game-in-progress\"}")
}
val deck = Deck(decks)
val hands = (for (player <- players) yield player.id -> Hand(deck.drawResponses(Hand.size))).toMap
@@ -140,16 +141,16 @@ class State @Inject()(private val cardCast: CardCastAPI, @Assisted val id: Strin
def play(secret: Secret, ids: List[Int]): Unit = {
val id = validateSecretAndGetId(secret)
val state = validateInGameAndGetState()
require (playedInRound.get(id).isDefined, "You can't play into this round.")
verify(playedInRound.get(id).isDefined, "{\"error\":\"not-in-round\"}")
if (playedInRound(id).isDefined) {
throw new IllegalStateException("Already played into this round.")
throw new BadRequestException("{\"error\":\"already-played\"}")
}
val round = state.round
if (round.responses.revealed.isDefined) {
throw new IllegalStateException("Already judging this round, can't play into it.")
throw new BadRequestException("{\"error\":\"already-judging\"}")
}
require(ids.length == state.round.call.slots,
s"Wrong number of cards played (got ${ids.length}, expected ${state.round.call.slots}).")
verify(ids.length == state.round.call.slots,
s"""{\"error\":\"wrong-number-of-cards-played\",\"got\":${ids.length},\"expected\":${state.round.call.slots}}""")
val hand = state.hands(id).hand
val toPlay: List[Response] = ids.map(hand)
val newHand = Hand(hand.filter(response => !toPlay.contains(response)) ++ state.deck.drawResponses(toPlay.length))
@@ -170,7 +171,7 @@ class State @Inject()(private val cardCast: CardCastAPI, @Assisted val id: Strin
def choose(secret: Secret, winner: Int): Unit = {
val id = validateSecretAndGetId(secret)
val state = validateInGameAndGetState()
require(id == state.round.czar, "Only the current Czar can pick a winner.")
verify(id == state.round.czar, "{\"error\":\"not-czar\"}")
val winnerId = playedOrder.get.apply(winner)
players = players.map(player => if (player.id == winnerId) {
player.copy(score = player.score + 1)
@@ -212,10 +213,10 @@ class State @Inject()(private val cardCast: CardCastAPI, @Assisted val id: Strin
def skip(secret: Secret, unfilteredPlayers: List[Id]): Unit = {
validateSecretAndGetId(secret)
val players = unfilteredPlayers.filter(id => playerForId(id).status != Skipping)
require((numberOfPlayers - players.length) > State.minimumPlayers,
"Not enough players left in the game if the given players are skipped.")
require(players.map(id => playerForId(id)).forall(player => player.disconnected),
"Only disconnected players or players who haven't played after the round timer runs out can be skipped.")
verify((numberOfPlayers - players.length) > State.minimumPlayers,
"{\"error\":\"not-enough-players-to-skip\"}")
verify(players.map(id => playerForId(id)).forall(player => player.disconnected),
"{\"error\":\"players-must-be-skippable\"}")
for (id <- players) {
setPlayerStatus(id, Skipping)
playedInRound = playedInRound.filterKeys(pId => pId != id)
@@ -237,7 +238,7 @@ class State @Inject()(private val cardCast: CardCastAPI, @Assisted val id: Strin
def back(secret: Secret): Unit = {
val id = validateSecretAndGetId(secret)
val player = playerForId(id)
require(player.status == Skipping, "You are not being skipped.")
verify(player.status == Skipping, "{\"error\":\"not-being-skipped\"}")
setPlayerStatus(id, Neutral, force=true)
sendNotifications()
}
@@ -245,7 +246,7 @@ class State @Inject()(private val cardCast: CardCastAPI, @Assisted val id: Strin
def register(secret: Secret, socket: ActorRef): Unit = {
val id = validateSecretAndGetId(secret)
val player = playerForId(id)
require(!player.left, "You have left this game.")
verify(!player.left, "{\"error\":\"already-left-game\"}")
setPlayerDisconnected(id, disconnected=false)
connected += id
if (player.status == Skipping) {
@@ -334,7 +335,7 @@ class State @Inject()(private val cardCast: CardCastAPI, @Assisted val id: Strin
private def setPlayerDisconnected(id: Id, disconnected: Boolean): Boolean = {
var changed = false
players = players.map(player =>
if (player.id == id) {
if (player.id == id && !player.left) {
changed = true
player.copy(disconnected=disconnected)
} else {
@@ -346,7 +347,7 @@ class State @Inject()(private val cardCast: CardCastAPI, @Assisted val id: Strin
private def setPlayerLeft(id: Id, left: Boolean): Unit = {
players = players.map(player =>
if (player.id == id) {
player.copy(status=Neutral, left=left)
player.copy(status=Neutral, disconnected=if (left) { false } else { player.disconnected }, left=left)
} else {
player
})
@@ -354,13 +355,13 @@ class State @Inject()(private val cardCast: CardCastAPI, @Assisted val id: Strin
private def validateInGameAndGetState(): GameState = game match {
case Some(state) => state
case None => throw new IllegalStateException("No game in progress.")
case None => throw new BadRequestException("{\"error\":\"no-game-in-progress\"}")
}
private def validateSecretAndGetId(secret: Secret): Id = {
val id = secret.id
require(secrets.get(id).map(expected => expected.secret).contains(secret.secret),
"Secret was wrong or player doesn't exist.")
verify(secrets.get(id).map(expected => expected.secret).contains(secret.secret),
"{\"error\":\"secret-wrong-or-not-a-player\"}")
id
}
@@ -34,7 +34,7 @@ class Store @Inject() (gameFactory: Game.Factory) extends Actor with InjectedAct
private def sendActionToLobby(id: String, action: Any): Unit = {
decodeId(id).flatMap(decodedId => games.get(decodedId)) match {
case Some(game) => game.forward(action)
case None => sender() ! Failure(new NotFoundException("Lobby not found."))
case None => sender() ! Failure(new NotFoundException("{\"error\":\"lobby-not-found\"}"))
}
}