From dfd1e99d1cda103b4e8cc2a4c2635fe4a9d5669b Mon Sep 17 00:00:00 2001 From: Artus Date: Tue, 5 Nov 2019 16:03:15 +0100 Subject: [PATCH] starts with a dedicated Api module, wip... --- src/Api.elm | 173 +++++++++++++++++++++++++++++++ src/Main.elm | 285 +++++++++++++-------------------------------------- 2 files changed, 243 insertions(+), 215 deletions(-) create mode 100644 src/Api.elm diff --git a/src/Api.elm b/src/Api.elm new file mode 100644 index 0000000..0a3b842 --- /dev/null +++ b/src/Api.elm @@ -0,0 +1,173 @@ +module Api exposing (..) + +import Http +import Json.Decode as D +import Json.Decode exposing (Decoder, int, string) +import Json.Encode as E + +type alias HttpResult a = (Result Http.Error a) + +type alias Response = + { value: Maybe String + , notification: Maybe String + , updates : Maybe (List Update) + , errors : Maybe String + } + + +type Update + = ItemRemoved Item + | ItemAdded Item + | WealthUpdated Wealth + | ClaimAdded () + | ClaimRemoved () + +type Msg + = GotPlayer (HttpResult Player) + | GotClaims Int (HttpResult Claims) + | GotLoot ToChest (HttpResult Loot) + | GotActionResult (HttpResult Response) + +-- PLAYERS +-- + +fetchPlayer : Int -> Cmd Msg +fetchPlayer id = + Http.get + { url = "http://localhost:8088/api/players/" ++ (String.fromInt id) ++ "/" + , expect = Http.expectJson Main.GotPlayer (valueDecoder playerDecoder ) + } + +playerDecoder : Decoder Player +playerDecoder = + D.map4 Player + (D.field "id" int) + (D.field "name" string) + (D.field "debt" int) + wealthDecoder + +wealthDecoder : Decoder Wealth +wealthDecoder = + D.map4 Wealth + (D.field "cp" int) + (D.field "sp" int) + (D.field "gp" int) + (D.field "pp" int) + +-- LOOT + +-- Location of a loot +type ToChest + = OfPlayer Int + | OfGroup + | OfShop + +itemDecoder = + D.map3 Item + (D.field "id" int) + (D.field "name" string) + (D.field "base_price" int) + +lootDecoder : Decoder Loot +lootDecoder = + Json.Decode.list itemDecoder + +fetchLoot : Main.ToChest -> Cmd Msg +fetchLoot dest = + let + url = case dest of + Main.OfPlayer id -> "http://localhost:8088/api/players/" ++ (String.fromInt id) ++ "/loot" + Main.OfShop -> "http://localhost:8088/api/items" + Main.OfGroup -> "http://localhost:8088/api/players/0/loot" + in + Http.get + { url = url + , expect = Http.expectJson (GotLoot dest) (valueDecoder lootDecoder)} + +-- CLAIMS + +claimDecoder = + D.map3 Claim + (D.field "id" int) + (D.field "player_id" int) + (D.field "loot_id" int) + +fetchClaims : Int -> Cmd Msg +fetchClaims playerId = + Http.get + { url = "http://localhost:8088/api/claims" + , expect = valueDecoder (D.list claimDecoder) + |> Http.expectJson (GotClaims playerId) + } + +-- API Response +-- +valueDecoder : Decoder a -> Decoder a +valueDecoder thenDecoder = + D.field "value" thenDecoder + +-- TODO: update server to produce better json +-- like an object with list of updates of the same type +-- { ItemRemoved : [..], Wealth : [ .. ], .. } +updatesDecoder : Decoder DbUpdate +updatesDecoder = + -- We expect one update but do not know it's kind + Json.Decode.oneOf + [ (field "ItemRemoved" (itemDecoder |> Json.Decode.andThen (\i -> succeed <| ItemRemoved i))) + , (field "ItemAdded" (itemDecoder |> Json.Decode.andThen (\i -> succeed <| ItemAdded i))) + , (field "Wealth" (wealthDecoder |> Json.Decode.andThen (\i -> succeed <| WealthUpdated i))) + , (field "ClaimRemoved" (succeed () |> Json.Decode.andThen (\i -> succeed <| ClaimRemoved i))) + , (field "ClaimAdded" (succeed () |> Json.Decode.andThen (\i -> succeed <| ClaimAdded i))) + ] + + +apiResponseDecoder : Decoder ApiResponse +apiResponseDecoder = + Json.Decode.map4 ApiResponse + (D.maybe (field "value" string)) + (Json.Decode.maybe (field "notification" string)) + (Json.Decode.maybe (field "updates" (Json.Decode.list updatesDecoder))) + (Json.Decode.maybe (field "errors" string)) + +undoLastAction id = Http.request + { url = "http://localhost:8088/api/players/" ++ String.fromInt id ++"/events/last" + , method = "DELETE" + , headers = [] + , body = Http.emptyBody + , expect = Http.expectJson GotActionResult apiResponseDecoder + , timeout = Nothing + , tracker = Nothing + } + +sendRequest : Maybe ViewMode -> Model -> Cmd Msg +sendRequest activeMode model = + case activeMode of + Nothing -> Cmd.none + Just mode -> + let + (endpoint, method) = case mode of + Add -> + ( "http://localhost:8088/api/players/" ++ (String.fromInt model.player.id) ++ "/loot" + , "POST" + ) + Buy -> + ( "http://localhost:8088/api/players/" ++ (String.fromInt model.player.id) ++ "/loot" + , "PUT" + ) + Sell -> + ( "http://localhost:8088/api/players/" ++ (String.fromInt model.player.id) ++ "/loot" + , "DELETE" + ) + Grab -> + ( "http://localhost:8088/api/players/" ++ (String.fromInt model.player.id) ++ "/claims" + , "POST") + in + Http.request + { method = method + , headers = [] + , url = endpoint + , body = Http.jsonBody <| buildPayload mode model + , expect = Http.expectJson GotActionResult apiResponseDecoder + , timeout = Nothing + , tracker = Nothing + } diff --git a/src/Main.elm b/src/Main.elm index accd18b..cc24db2 100644 --- a/src/Main.elm +++ b/src/Main.elm @@ -2,18 +2,15 @@ module Main exposing (..) import Browser import Browser.Navigation as Nav -import Platform.Cmd exposing (Cmd) import Url import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (..) import Svg.Attributes -import Http -import Json.Decode exposing (Decoder, field, list, string, int, succeed) -import Json.Encode as E import Url.Parser as P exposing (Parser, (), oneOf, s) import Set exposing (Set) +import Api -- Main @@ -32,8 +29,6 @@ main = type alias Selection = Set Int -emptySelection = [] - type alias State = { navKey : Nav.Key , route : Route @@ -75,13 +70,16 @@ fetchInitialData : Int -> Cmd Msg fetchInitialData playerId = Cmd.batch [ initPlayer playerId - , fetchShopInventory - , fetchGroupLoot + , Cmd.map ApiMsg <| Api.fetchLoot Api.OfShop + , Cmd.map ApiMsg <| Api.fetchLoot Api.OfGroup ] --- --- PLAYER +-- MODELS --- + +-- Player + type alias Player = { id: Int , name: String @@ -92,23 +90,13 @@ type alias Player = blankPlayer = Player 0 "Loot-a-lot" 0 (Wealth 0 0 0 0) + initPlayer id = - Cmd.batch [fetchPlayer id, fetchLoot id, fetchClaims id] - -fetchPlayer : Int -> Cmd Msg -fetchPlayer id = - Http.get - { url = "http://localhost:8088/api/players/" ++ (String.fromInt id) ++ "/" - , expect = Http.expectJson GotPlayer (valueDecoder playerDecoder ) - } - -playerDecoder : Decoder Player -playerDecoder = - Json.Decode.map4 Player - (field "id" int) - (field "name" string) - (field "debt" int) - wealthDecoder + Cmd.batch + [ Cmd.map ApiMsg <| Api.fetchPlayer id + , Cmd.map ApiMsg <| Api.fetchLoot (OfPlayer id) + , Cmd.map ApiMsg <| Api.fetchClaims id + ] type alias Wealth = { cp: Int @@ -117,13 +105,9 @@ type alias Wealth = , pp: Int } -wealthDecoder : Decoder Wealth -wealthDecoder = - Json.Decode.map4 Wealth - (field "cp" int) - (field "sp" int) - (field "gp" int) - (field "pp" int) +-- Loot + +type alias Loot = List Item type alias Item = { id: Int @@ -131,38 +115,7 @@ type alias Item = , base_price: Int } -itemDecoder = - Json.Decode.map3 Item - (field "id" int) - (field "name" string) - (field "base_price" int) - -type alias Loot = - List Item - -lootDecoder : Decoder Loot -lootDecoder = - Json.Decode.list itemDecoder - -fetchLoot id = - Http.get - { url = "http://localhost:8088/api/players/" ++ (String.fromInt id) ++ "/loot" - , expect = Http.expectJson (GotLoot OfPlayer) (valueDecoder lootDecoder)} - -fetchShopInventory = - Http.get - { url = "http://localhost:8088/api/items" - , expect = Http.expectJson (GotLoot OfShop) (valueDecoder lootDecoder)} - -fetchGroupLoot = - Http.get - { url = "http://localhost:8088/api/players/0/loot" - , expect = Http.expectJson (GotLoot OfGroup) (valueDecoder lootDecoder)} - -type ToChest - = OfPlayer - | OfGroup - | OfShop +-- Claims type alias Claims = List Claim @@ -172,44 +125,13 @@ type alias Claim = , loot_id: Int } -claimDecoder = - Json.Decode.map3 Claim - (field "id" int) - (field "player_id" int) - (field "loot_id" int) - -fetchClaims : Int -> Cmd Msg -fetchClaims playerId = - Http.get - { url = "http://localhost:8088/api/claims" - , expect = valueDecoder (Json.Decode.list claimDecoder) - |> Http.expectJson (GotClaims playerId) - } --- API Response --- -valueDecoder : Decoder a -> Decoder a -valueDecoder thenDecoder = - field "value" thenDecoder - -- UPDATE -type alias HttpResult a = (Result Http.Error a) - -type alias ApiResponse = - { value : Maybe String - , notification : Maybe String - , updates : Maybe (List DbUpdate) - , errors : Maybe String - } - type Msg = LinkClicked Browser.UrlRequest | UrlChanged Url.Url + | ApiMsg Api.Msg | PlayerChanged Int - | GotPlayer (HttpResult Player) - | GotClaims Int (HttpResult Claims) - | GotLoot ToChest (HttpResult Loot) - | GotActionResult (HttpResult ApiResponse) | LootViewItemSwitched Int | ModeSwitched (Maybe ViewMode) | ConfirmAction @@ -248,35 +170,50 @@ update msg model = PlayerChanged newId -> ( { model | player = blankPlayer }, initPlayer newId ) - GotPlayer result -> - case result of - Ok player -> - ( { model | player = player } - , Cmd.none - ) - Err error -> - ( setError ("Fetching player... " ++ printError error) model - , Cmd.none - ) + ApiMsg apiMsg -> case apiMsg of + GotActionResult response -> + case response of + Ok result -> + let + updates = Maybe.withDefault [] result.updates + notification = result.notification + errors = Maybe.withDefault "" result.errors + in + List.foldl applyUpdate model updates + |> setNotification notification + |> setError errors + |> update (ModeSwitched Nothing) + Err r -> (setError (Debug.toString r) model, Cmd.none) - GotClaims id result -> - case result of - Ok claims -> ( { model | claims = List.filter (\c -> c.player_id == id) claims}, Cmd.none ) - Err error -> ( setError ("Fetching claims..." ++ Debug.toString error) model, Cmd.none) + GotPlayer result -> + case result of + Ok player -> + ( { model | player = player } + , Cmd.none + ) + Err error -> + ( setError ("Fetching player... " ++ printError error) model + , Cmd.none + ) - GotLoot dest result -> - case result of - Ok loot -> - ( case dest of - OfPlayer -> { model | loot = Just loot} - OfGroup -> { model | groupLoot = Just loot} - OfShop -> { model | merchantItems = Just loot} - , Cmd.none - ) - Err error -> - ( setError ("Fetching loot... " ++ printError error) model - , Cmd.none - ) + GotClaims id result -> + case result of + Ok claims -> ( { model | claims = List.filter (\c -> c.player_id == id) claims}, Cmd.none ) + Err error -> ( setError ("Fetching claims..." ++ Debug.toString error) model, Cmd.none) + + GotLoot dest result -> + case result of + Ok loot -> + ( case dest of + Api.OfPlayer _ -> { model | loot = Just loot} + Api.OfGroup -> { model | groupLoot = Just loot} + Api.OfShop -> { model | merchantItems = Just loot} + , Cmd.none + ) + Err error -> + ( setError ("Fetching loot... " ++ printError error) model + , Cmd.none + ) LootViewItemSwitched id -> let @@ -293,17 +230,16 @@ update msg model = in ( { model | state = { state | activeMode = newMode - , selection = case newMode of - Nothing -> - Nothing + , selection = + case newMode of + Nothing -> + Nothing - Just Grab -> -- Currently claimed object are initially selected - Just ( Set.fromList - <| List.map (\c -> c.loot_id) model.claims - ) + Just Grab -> -- Currently claimed object are initially selected + Just ( Set.fromList <| List.map (\c -> c.loot_id) model.claims) - Just others -> - Just Set.empty + Just others -> + Just Set.empty }} , Cmd.none ) @@ -311,34 +247,11 @@ update msg model = let currentMode = model.state.activeMode in - (model, sendRequest currentMode model) + (model, Cmd.map ApiMsg Api.sendRequest currentMode model) UndoLastAction -> - let playerId = String.fromInt model.player.id - in - (model, Http.request - { url = "http://localhost:8088/api/players/" ++ playerId ++"/events/last" - , method = "DELETE" - , headers = [] - , body = Http.emptyBody - , expect = Http.expectJson GotActionResult apiResponseDecoder - , timeout = Nothing - , tracker = Nothing - }) + (model, Cmd.map ApiMsg Api.undoLastAction model.player.id) - GotActionResult response -> - case response of - Ok result -> - let - updates = Maybe.withDefault [] result.updates - notification = result.notification - errors = Maybe.withDefault "" result.errors - in - List.foldl applyUpdate model updates - |> setNotification notification - |> setError errors - |> update (ModeSwitched Nothing) - Err r -> (setError (Debug.toString r) model, Cmd.none) ClearNotification -> ( { model | notification = Nothing }, Cmd.none ) @@ -409,64 +322,6 @@ applyUpdate u model = ClaimAdded _ -> model ClaimRemoved _ -> model --- TODO: update server to produce better json --- like an object with list of updates of the same type --- { ItemRemoved : [..], Wealth : [ .. ], .. } -updatesDecoder : Decoder DbUpdate -updatesDecoder = - -- We expect one update but do not know it's kind - Json.Decode.oneOf - [ (field "ItemRemoved" (itemDecoder |> Json.Decode.andThen (\i -> succeed <| ItemRemoved i))) - , (field "ItemAdded" (itemDecoder |> Json.Decode.andThen (\i -> succeed <| ItemAdded i))) - , (field "Wealth" (wealthDecoder |> Json.Decode.andThen (\i -> succeed <| WealthUpdated i))) - , (field "ClaimRemoved" (succeed () |> Json.Decode.andThen (\i -> succeed <| ClaimRemoved i))) - , (field "ClaimAdded" (succeed () |> Json.Decode.andThen (\i -> succeed <| ClaimAdded i))) - ] - - -apiResponseDecoder : Decoder ApiResponse -apiResponseDecoder = - Json.Decode.map4 ApiResponse - (Json.Decode.maybe (field "value" string)) - (Json.Decode.maybe (field "notification" string)) - (Json.Decode.maybe (field "updates" (Json.Decode.list updatesDecoder))) - (Json.Decode.maybe (field "errors" string)) - - -sendRequest : Maybe ViewMode -> Model -> Cmd Msg -sendRequest activeMode model = - case activeMode of - Nothing -> Cmd.none - Just mode -> - let - (endpoint, method) = case mode of - Add -> - ( "http://localhost:8088/api/players/" ++ (String.fromInt model.player.id) ++ "/loot" - , "POST" - ) - Buy -> - ( "http://localhost:8088/api/players/" ++ (String.fromInt model.player.id) ++ "/loot" - , "PUT" - ) - Sell -> - ( "http://localhost:8088/api/players/" ++ (String.fromInt model.player.id) ++ "/loot" - , "DELETE" - ) - Grab -> - ( "http://localhost:8088/api/players/" ++ (String.fromInt model.player.id) ++ "/claims" - , "POST") - in - Http.request - { method = method - , headers = [] - , url = endpoint - , body = Http.jsonBody <| buildPayload mode model - , expect = Http.expectJson GotActionResult apiResponseDecoder - , timeout = Nothing - , tracker = Nothing - } - - -- ERRORS