starts with a dedicated Api module, wip...

This commit is contained in:
2019-11-05 16:03:15 +01:00
parent d56ca38dcb
commit dfd1e99d1c
2 changed files with 243 additions and 215 deletions

173
src/Api.elm Normal file
View File

@@ -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
}

View File

@@ -2,18 +2,15 @@ module Main exposing (..)
import Browser import Browser
import Browser.Navigation as Nav import Browser.Navigation as Nav
import Platform.Cmd exposing (Cmd)
import Url import Url
import Html exposing (..) import Html exposing (..)
import Html.Attributes exposing (..) import Html.Attributes exposing (..)
import Html.Events exposing (..) import Html.Events exposing (..)
import Svg.Attributes 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 Url.Parser as P exposing (Parser, (</>), oneOf, s)
import Set exposing (Set) import Set exposing (Set)
import Api
-- Main -- Main
@@ -32,8 +29,6 @@ main =
type alias Selection = Set Int type alias Selection = Set Int
emptySelection = []
type alias State = type alias State =
{ navKey : Nav.Key { navKey : Nav.Key
, route : Route , route : Route
@@ -75,13 +70,16 @@ fetchInitialData : Int -> Cmd Msg
fetchInitialData playerId = fetchInitialData playerId =
Cmd.batch Cmd.batch
[ initPlayer playerId [ initPlayer playerId
, fetchShopInventory , Cmd.map ApiMsg <| Api.fetchLoot Api.OfShop
, fetchGroupLoot , Cmd.map ApiMsg <| Api.fetchLoot Api.OfGroup
] ]
--- ---
-- PLAYER -- MODELS
--- ---
-- Player
type alias Player = type alias Player =
{ id: Int { id: Int
, name: String , name: String
@@ -92,23 +90,13 @@ type alias Player =
blankPlayer = blankPlayer =
Player 0 "Loot-a-lot" 0 (Wealth 0 0 0 0) Player 0 "Loot-a-lot" 0 (Wealth 0 0 0 0)
initPlayer id = initPlayer id =
Cmd.batch [fetchPlayer id, fetchLoot id, fetchClaims id] Cmd.batch
[ Cmd.map ApiMsg <| Api.fetchPlayer id
fetchPlayer : Int -> Cmd Msg , Cmd.map ApiMsg <| Api.fetchLoot (OfPlayer id)
fetchPlayer id = , Cmd.map ApiMsg <| Api.fetchClaims 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
type alias Wealth = type alias Wealth =
{ cp: Int { cp: Int
@@ -117,13 +105,9 @@ type alias Wealth =
, pp: Int , pp: Int
} }
wealthDecoder : Decoder Wealth -- Loot
wealthDecoder =
Json.Decode.map4 Wealth type alias Loot = List Item
(field "cp" int)
(field "sp" int)
(field "gp" int)
(field "pp" int)
type alias Item = type alias Item =
{ id: Int { id: Int
@@ -131,38 +115,7 @@ type alias Item =
, base_price: Int , base_price: Int
} }
itemDecoder = -- Claims
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
type alias Claims = List Claim type alias Claims = List Claim
@@ -172,44 +125,13 @@ type alias Claim =
, loot_id: Int , 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 -- 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 type Msg
= LinkClicked Browser.UrlRequest = LinkClicked Browser.UrlRequest
| UrlChanged Url.Url | UrlChanged Url.Url
| ApiMsg Api.Msg
| PlayerChanged Int | PlayerChanged Int
| GotPlayer (HttpResult Player)
| GotClaims Int (HttpResult Claims)
| GotLoot ToChest (HttpResult Loot)
| GotActionResult (HttpResult ApiResponse)
| LootViewItemSwitched Int | LootViewItemSwitched Int
| ModeSwitched (Maybe ViewMode) | ModeSwitched (Maybe ViewMode)
| ConfirmAction | ConfirmAction
@@ -248,35 +170,50 @@ update msg model =
PlayerChanged newId -> PlayerChanged newId ->
( { model | player = blankPlayer }, initPlayer newId ) ( { model | player = blankPlayer }, initPlayer newId )
GotPlayer result -> ApiMsg apiMsg -> case apiMsg of
case result of GotActionResult response ->
Ok player -> case response of
( { model | player = player } Ok result ->
, Cmd.none let
) updates = Maybe.withDefault [] result.updates
Err error -> notification = result.notification
( setError ("Fetching player... " ++ printError error) model errors = Maybe.withDefault "" result.errors
, Cmd.none 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 -> GotPlayer result ->
case result of case result of
Ok claims -> ( { model | claims = List.filter (\c -> c.player_id == id) claims}, Cmd.none ) Ok player ->
Err error -> ( setError ("Fetching claims..." ++ Debug.toString error) model, Cmd.none) ( { model | player = player }
, Cmd.none
)
Err error ->
( setError ("Fetching player... " ++ printError error) model
, Cmd.none
)
GotLoot dest result -> GotClaims id result ->
case result of case result of
Ok loot -> Ok claims -> ( { model | claims = List.filter (\c -> c.player_id == id) claims}, Cmd.none )
( case dest of Err error -> ( setError ("Fetching claims..." ++ Debug.toString error) model, Cmd.none)
OfPlayer -> { model | loot = Just loot}
OfGroup -> { model | groupLoot = Just loot} GotLoot dest result ->
OfShop -> { model | merchantItems = Just loot} case result of
, Cmd.none Ok loot ->
) ( case dest of
Err error -> Api.OfPlayer _ -> { model | loot = Just loot}
( setError ("Fetching loot... " ++ printError error) model Api.OfGroup -> { model | groupLoot = Just loot}
, Cmd.none Api.OfShop -> { model | merchantItems = Just loot}
) , Cmd.none
)
Err error ->
( setError ("Fetching loot... " ++ printError error) model
, Cmd.none
)
LootViewItemSwitched id -> LootViewItemSwitched id ->
let let
@@ -293,17 +230,16 @@ update msg model =
in in
( { model | state = ( { model | state =
{ state | activeMode = newMode { state | activeMode = newMode
, selection = case newMode of , selection =
Nothing -> case newMode of
Nothing Nothing ->
Nothing
Just Grab -> -- Currently claimed object are initially selected Just Grab -> -- Currently claimed object are initially selected
Just ( Set.fromList Just ( Set.fromList <| List.map (\c -> c.loot_id) model.claims)
<| List.map (\c -> c.loot_id) model.claims
)
Just others -> Just others ->
Just Set.empty Just Set.empty
}} }}
, Cmd.none ) , Cmd.none )
@@ -311,34 +247,11 @@ update msg model =
let let
currentMode = model.state.activeMode currentMode = model.state.activeMode
in in
(model, sendRequest currentMode model) (model, Cmd.map ApiMsg Api.sendRequest currentMode model)
UndoLastAction -> UndoLastAction ->
let playerId = String.fromInt model.player.id (model, Cmd.map ApiMsg Api.undoLastAction 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
})
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 -> ClearNotification ->
( { model | notification = Nothing }, Cmd.none ) ( { model | notification = Nothing }, Cmd.none )
@@ -409,64 +322,6 @@ applyUpdate u model =
ClaimAdded _ -> model ClaimAdded _ -> model
ClaimRemoved _ -> 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 -- ERRORS