Skip to content

Instantly share code, notes, and snippets.

@Herteby
Created January 28, 2020 12:50
Show Gist options
  • Save Herteby/020256ce5f4dd694b77b13d753f1c1e0 to your computer and use it in GitHub Desktop.
Save Herteby/020256ce5f4dd694b77b13d753f1c1e0 to your computer and use it in GitHub Desktop.
module Api exposing
( Data
, Error(..)
, ErrorResponse
, Origin
, delete
, deleteTask
, errorToNotification
, errorView
, get
, getTask
, grid
, handleResult
, patch
, patchTask
, post
, postTask
, toResult
, view
)
import FontAwesome.Solid as Solid
import Html exposing (..)
import Html.Attributes as Attributes
import Http exposing (Metadata)
import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Extra as Decode
import RemoteData exposing (RemoteData(..))
import Task exposing (Task)
import UI exposing (..)
import UI.Blocker as Blocker
import UI.Icon as Icon
import UI.Notifications as Notifications
import UI.Spinner as Spinner
import UrlBuilderPlus as UB exposing (QueryParameter)
class =
namespace "Api"
type alias Data a =
RemoteData Error a
type Error
= BadUrl String
| Timeout
| NetworkError
| BadStatus Metadata ErrorResponse
| JsonError Metadata String
{-| Custom error response unique to FörsäkringsGirot
-}
type alias ErrorResponse =
{ error : String
, exception : Maybe String
, message : String
, path : String
, status : Int
}
{-| Represents the address of a server, for example api-gateway
-}
type alias Origin =
String
{-| Display HTML which depends on Remote Data
Displays a loading spinner while waiting, and an error message in case of failure.
-}
view : Data (Html msg) -> Html msg
view stuff =
case stuff of
NotAsked ->
textSpan ""
Loading ->
Spinner.view
Failure error ->
errorView error
Success content ->
content
{-| Display a grid where rows can depend on Remote Data
Displays a loading spinner in the second column while waiting, and an error message in case of failure.
-}
grid : List (Data (List (List (Html msg)))) -> Html msg
grid =
List.concatMap
(\data ->
case data of
NotAsked ->
[]
Loading ->
[ [ none, Spinner.view ] ]
Failure err ->
[ [ none, errorView err ] ]
Success html ->
html
)
>> UI.grid
{-| Display info about an HTTP error
-}
errorView : Error -> Html msg
errorView error =
let
( header, details ) =
case error of
BadUrl message ->
( "Felaktig URL", Just message )
Timeout ->
( "Servern svarar inte", Nothing )
NetworkError ->
( "Nätverksfel", Nothing )
BadStatus metadata errorResponse ->
( String.fromInt metadata.statusCode ++ " " ++ metadata.statusText
, Just (metadata.url ++ "\n\n" ++ errorResponse.message)
)
JsonError metadata message ->
( "Servern svarade med oväntad JSON"
, Just (metadata.url ++ "\n\n" ++ message)
)
in
div [ class "error" ]
[ heading [] [ text "Ett fel inträffade" ]
, b [] [ text header ]
, textSpan "Prova att ladda om sidan, eller kontakta kundservice om felet kvarstår."
, row10
[ a [ Attributes.href "tel:08-522 529 00" ] [ Icon.view [] Solid.phone, text "08-522 529 00" ]
, a [ Attributes.href "mailto:kund@fgirot.se" ] [ Icon.view [] Solid.envelope, text "kund@fgirot.se" ]
]
, case details of
Just str ->
div [ class "errorDetails", Attributes.tabindex 0 ]
[ div [ class "showDetailsButton" ] [ text "Teknisk info" ]
, div [ class "technical" ] [ text str ]
]
Nothing ->
none
]
{-| Update a model based on a result. If the result is an Error, display an error notification.
-}
handleResult :
{ r | notifications : Notifications.Model, blocker : Blocker.Status }
-> Result Error data
-> (data -> ( { r | notifications : Notifications.Model, blocker : Blocker.Status }, Cmd msg ))
-> ( { r | notifications : Notifications.Model, blocker : Blocker.Status }, Cmd msg )
handleResult model result onSuccess =
case result of
Ok data ->
onSuccess data |> Tuple.mapFirst (\m -> { m | blocker = Blocker.decrement m.blocker })
Err error ->
( { model
| notifications = model.notifications |> Notifications.insert (errorToNotification error)
, blocker = Blocker.decrement model.blocker
}
, Cmd.none
)
{-| Convert an Error to a Notification
-}
errorToNotification : Error -> Notifications.Notification
errorToNotification error =
Notifications.Error <|
case error of
BadUrl message ->
[ message ]
Timeout ->
[ "Servern svarar inte" ]
NetworkError ->
[ "Nätverksfel" ]
BadStatus metadata errorResponse ->
[ metadata.statusText
, errorResponse.message
, metadata.url
]
JsonError metadata message ->
[ message
, metadata.url
]
toResult : Data a -> Result Error a
toResult data =
case data of
NotAsked ->
Err NetworkError
Loading ->
Err NetworkError
Failure e ->
Err e
Success a ->
Ok a
-- MAKE REQUESTS
get : { path : List String, query : List QueryParameter, decoder : Decoder a } -> (Data a -> msg) -> Origin -> Cmd msg
get args msg origin =
getTask args origin |> Task.attempt (RemoteData.fromResult >> msg)
post : { path : List String, query : List QueryParameter, body : Http.Body, decoder : Decoder a } -> (Result Error a -> msg) -> Origin -> Cmd msg
post args msg origin =
postTask args origin |> Task.attempt msg
patch : { path : List String, query : List QueryParameter, body : Http.Body, decoder : Decoder a } -> (Result Error a -> msg) -> Origin -> Cmd msg
patch args msg origin =
patchTask args origin |> Task.attempt msg
delete : { path : List String, query : List QueryParameter, body : Http.Body, decoder : Decoder a } -> (Result Error a -> msg) -> Origin -> Cmd msg
delete args msg origin =
deleteTask args origin |> Task.attempt msg
getTask : { path : List String, query : List QueryParameter, decoder : Decoder a } -> Origin -> Task Error a
getTask { path, query, decoder } =
task "GET" { path = path, query = query, decoder = decoder, body = Http.emptyBody }
postTask : { path : List String, query : List QueryParameter, body : Http.Body, decoder : Decoder a } -> Origin -> Task Error a
postTask =
task "POST"
patchTask : { path : List String, query : List QueryParameter, body : Http.Body, decoder : Decoder a } -> Origin -> Task Error a
patchTask =
task "PATCH"
deleteTask : { path : List String, query : List QueryParameter, body : Http.Body, decoder : Decoder a } -> Origin -> Task Error a
deleteTask =
task "DELETE"
-- INTERNAL STUFF
task : String -> { path : List String, query : List QueryParameter, body : Http.Body, decoder : Decoder a } -> Origin -> Task Error a
task method { path, query, body, decoder } origin =
Http.riskyTask
{ method = method
, url = UB.crossOrigin origin ("app" :: path) query
, body = body
, headers = []
, resolver = Http.stringResolver (toJsonResult decoder)
, timeout = Nothing
}
toJsonResult : Decoder a -> Http.Response String -> Result Error a
toJsonResult decoder response =
case response of
Http.BadUrl_ badUrl ->
Err (BadUrl badUrl)
Http.Timeout_ ->
Err Timeout
Http.NetworkError_ ->
Err NetworkError
Http.BadStatus_ metadata body ->
Err
(case Decode.decodeString errorResponseDecoder body of
Ok error ->
BadStatus metadata error
Err decodingError ->
JsonError metadata (Decode.errorToString decodingError)
)
Http.GoodStatus_ metadata body ->
let
-- replace empty body with "{}" so that Decode.succeed () doesn't fail
body_ =
if body == "" then
"{}"
else
body
in
case Decode.decodeString decoder body_ of
Ok value ->
Ok value
Err err ->
Err (JsonError metadata (Decode.errorToString err))
errorResponseDecoder : Decoder ErrorResponse
errorResponseDecoder =
Decode.map5 ErrorResponse
(Decode.field "error" Decode.string)
(Decode.maybe (Decode.field "exception" Decode.string))
(Decode.field "message"
(Decode.oneOf
[ Decode.doubleEncoded (Decode.field "error" Decode.string)
, Decode.string
]
)
)
(Decode.field "path" Decode.string)
(Decode.field "status" Decode.int)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment