Issue #4 - Refactored API module to give detail on errors.
This commit is contained in:
+175
-58
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -26,7 +26,7 @@ type Action
|
||||
| AddDeck
|
||||
| AddGivenDeck String (APICall LobbyAndHand)
|
||||
| FailAddDeck String Error
|
||||
| StartGame (APICall LobbyAndHand)
|
||||
| StartGame
|
||||
| Pick Int
|
||||
| Play
|
||||
| Withdraw Int
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" ]
|
||||
]
|
||||
|
||||
@@ -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." ] ]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
@@ -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\"}"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user