Skip to content

Port to elm/http 2.0.0 #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions elm.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
"elm-version": "0.19.0 <= v < 0.20.0",
"dependencies": {
"elm/core": "1.0.0 <= v < 2.0.0",
"elm/http": "1.0.0 <= v < 2.0.0",
"elm/http": "2.0.0 <= v < 3.0.0",
"elm/json": "1.0.0 <= v < 2.0.0",
"elm/url": "1.0.0 <= v < 2.0.0"
},
"test-dependencies": {
"elm-explorations/test": "1.0.0 <= v < 2.0.0"
}
}
}
205 changes: 50 additions & 155 deletions src/GraphQL/Client/Http.elm
Original file line number Diff line number Diff line change
@@ -1,25 +1,16 @@
module GraphQL.Client.Http
exposing
( RequestError
, DocumentLocation
, Error(..)
, RequestOptions
, sendQuery
, sendMutation
, customSendQuery
, customSendQueryRaw
, customSendMutation
, customSendMutationRaw
)
module GraphQL.Client.Http exposing (Error(..), RequestError, DocumentLocation, sendQuery, sendMutation, RequestOptions)

{-| The functions in this module let you perform HTTP requests to conventional GraphQL server endpoints.

@docs Error, RequestError, DocumentLocation, sendQuery, sendMutation, RequestOptions, customSendQuery, customSendMutation, customSendQueryRaw, customSendMutationRaw
@docs Error, RequestError, DocumentLocation, sendQuery, sendMutation, RequestOptions, customSendQuery, customSendMutation, customSendQueryRaw, customSendMutationRaw

-}

import GraphQL.Client.Http.Util as Util
import GraphQL.Request.Builder as Builder
import GraphQL.Response as Response
import Http
import Json.Decode
import Task exposing (Task)


Expand Down Expand Up @@ -51,182 +42,86 @@ type Error
sendQuery :
String
-> Builder.Request Builder.Query result
-> Task Error result
-> (Result Error result -> msg)
-> Cmd msg
sendQuery =
Util.defaultRequestOptions >> send


{-| Takes a URL and a `Query` `Request` and returns a `Task` that you can perform with `Task.attempt` which will send a `POST` request to a GraphQL server at the given endpoint and return raw `Http.Response` in Task.
-}
sendQueryRaw :
String
-> Builder.Request Builder.Query result
-> Task Error (Http.Response String)
sendQueryRaw =
Util.defaultRequestOptions >> sendExpecting rawExpect


{-| Takes a URL and a `Mutation` `Request` and returns a `Task` that you can perform with `Task.attempt` which will send a `POST` request to a GraphQL server at the given endpoint.
-}
sendMutation :
String
-> Builder.Request Builder.Mutation result
-> Task Error result
-> (Result Error result -> msg)
-> Cmd msg
sendMutation =
Util.defaultRequestOptions >> send


{-| Takes a URL and a `Mutation` `Request` and returns a `Task` that you can perform with `Task.attempt` which will send a `POST` request to a GraphQL server at the given endpoint and return raw `Http.Response` in Task.
-}
sendMutationRaw :
String
-> Builder.Request Builder.Mutation result
-> Task Error (Http.Response String)
sendMutationRaw =
Util.defaultRequestOptions >> sendExpecting rawExpect


{-| Options available for customizing GraphQL HTTP requests. `method` should be either `"GET"` or `"POST"`. For `GET` requests, the `url` is modified to include extra parameters in the query string for the GraphQL document and variables. Otherwise, the document and variables are included in the HTTP request body.
-}
type alias RequestOptions =
{ method : String
, headers : List Http.Header
, url : String
, timeout : Maybe Float
, withCredentials : Bool
}


{-| Like `sendQuery`, but takes an `RequestOptions` value instead of a URL to let you further customize the HTTP request.
-}
customSendQuery :
send :
RequestOptions
-> Builder.Request Builder.Query result
-> Task Error result
customSendQuery =
send


{-| Like `sendQuery`, but takes an `RequestOptions` value instead of a URL to let you further customize the HTTP request. You will get a plain `Http.Response` as Task result.

Useful for things like caching, custom errors decoding, etc.

Example of response decoding:

-> Builder.Request operationType result
-> (Result Error result -> msg)
-> Cmd msg
send options request toMsg =
let
decoder =
GraphQL.Request.Builder.responseDataDecoder request
|> Json.Decode.field "data"

options =
{ method = "GET"
, headers = []
, url = "/graphql"
, timeout = Nothing
, withCredentials = False
}
in
request
|> GraphQL.Client.Http.customSendQueryRaw options
|> Task.andThen
(\response ->
case Json.Decode.decodeString decoder response.body of
Err err ->
Task.fail <| GraphQL.Client.Http.HttpError <| Http.BadPayload err response

Ok decodedValue ->
Task.succeed decodedValue
)
-}
customSendQueryRaw :
RequestOptions
-> Builder.Request Builder.Query result
-> Task Error (Http.Response String)
customSendQueryRaw =
sendExpecting rawExpect


{-| Like `sendMutation`, but takes an `RequestOptions` value instead of a URL to let you further customize the HTTP request.
-}
customSendMutation :
RequestOptions
-> Builder.Request Builder.Mutation result
-> Task Error result
customSendMutation =
send

expect =
expectGraphQL toMsg request

{-| Like `sendMutation`, but takes an `RequestOptions` value instead of a URL to let you further customize the HTTP request. You will get a plain `Http.Response` as Task result.
documentString =
Builder.requestBody request

Useful for things like custom errors decoding, etc.
variableValues =
Builder.jsonVariableValues request
in
Util.requestConfig options documentString expect variableValues
|> Http.request

Example of response decoding:

expectGraphQL :
(Result Error result -> msg)
-> Builder.Request operationType result
-> Http.Expect msg
expectGraphQL toMsg request =
let
decoder =
GraphQL.Request.Builder.responseDataDecoder mutationRequest
|> Json.Decode.field "data"

options =
{ method = "GET"
, headers = []
, url = "/graphql"
, timeout = Nothing
, withCredentials = False
}
Json.Decode.map2 (\errors data -> ( errors, data ))
(Json.Decode.maybe (Json.Decode.field "errors" Response.errorsDecoder))
(Json.Decode.field "data" (Builder.responseDataDecoder request))
in
mutationRequest
|> GraphQL.Client.Http.customSendMutationRaw options
|> Task.andThen
(\response ->
case Json.Decode.decodeString decoder response.body of
Err err ->
Task.fail <| GraphQL.Client.Http.HttpError <| Http.BadPayload err response

Ok decodedValue ->
Task.succeed decodedValue
)

-}
customSendMutationRaw :
RequestOptions
-> Builder.Request Builder.Mutation result
-> Task Error (Http.Response String)
customSendMutationRaw =
sendExpecting rawExpect
Http.expectStringResponse toMsg <|
\response ->
case response of
Http.BadUrl_ url ->
Err (HttpError (Http.BadUrl url))

Http.Timeout_ ->
Err (HttpError Http.Timeout)

rawExpect : Http.Expect (Http.Response String)
rawExpect =
Http.expectStringResponse Ok
Http.NetworkError_ ->
Err (HttpError Http.NetworkError)

Http.BadStatus_ metadata body ->
Err (HttpError (Http.BadStatus metadata.statusCode))

send :
RequestOptions
-> Builder.Request operationType result
-> Task Error result
send options request =
let
expect =
Util.defaultExpect (Builder.responseDataDecoder request)
in
sendExpecting expect options request
Http.GoodStatus_ metadata body ->
case Json.Decode.decodeString decoder body of
Ok ( Just errors, _ ) ->
Err (GraphQLError errors)

Ok ( Nothing, data ) ->
Ok data

sendExpecting :
Http.Expect result
-> RequestOptions
-> Builder.Request operationType result2
-> Task Error result
sendExpecting expect requestOptions request =
let
documentString =
Builder.requestBody request

variableValues =
Builder.jsonVariableValues request
in
Util.requestConfig requestOptions documentString expect variableValues
|> Http.request
|> Http.toTask
|> Task.mapError (Util.convertHttpError HttpError GraphQLError)
Err err ->
Err (HttpError (Http.BadBody (Json.Decode.errorToString err)))
Copy link
Author

@vodik vodik Dec 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might make sense to also now flatten the Error type instead of nesting Http.Error.

58 changes: 14 additions & 44 deletions src/GraphQL/Client/Http/Util.elm
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module GraphQL.Client.Http.Util exposing (..)
module GraphQL.Client.Http.Util exposing (DocumentLocation, Error(..), RequestConfig, RequestError, RequestOptions, defaultRequestOptions, parameterizedUrl, postBody, postBodyJson, requestConfig)

import GraphQL.Response as Response
import Http
Expand All @@ -18,7 +18,7 @@ postBodyJson documentString variableValues =
|> Maybe.map (\obj -> [ ( "variables", obj ) ])
|> Maybe.withDefault []
in
Json.Encode.object ([ ( "query", documentValue ) ] ++ extraParams)
Json.Encode.object ([ ( "query", documentValue ) ] ++ extraParams)


postBody : String -> Maybe Json.Encode.Value -> Http.Body
Expand All @@ -32,6 +32,7 @@ parameterizedUrl url documentString variableValues =
firstParamPrefix =
if String.contains "?" url then
"&"

else
"?"

Expand All @@ -46,15 +47,14 @@ parameterizedUrl url documentString variableValues =
)
|> Maybe.withDefault ""
in
url ++ queryParam ++ variablesParam
url ++ queryParam ++ variablesParam


type alias RequestOptions =
{ method : String
, headers : List Http.Header
, url : String
, timeout : Maybe Float
, withCredentials : Bool
}


Expand Down Expand Up @@ -82,7 +82,7 @@ type alias RequestConfig a =
, body : Http.Body
, expect : Http.Expect a
, timeout : Maybe Float
, withCredentials : Bool
, tracker : Maybe String
}


Expand All @@ -92,7 +92,6 @@ defaultRequestOptions url =
, headers = []
, url = url
, timeout = Nothing
, withCredentials = False
}


Expand All @@ -107,44 +106,15 @@ requestConfig requestOptions documentString expect variableValues =
( url, body ) =
if requestOptions.method == "GET" then
( parameterizedUrl requestOptions.url documentString variableValues, Http.emptyBody )

else
( requestOptions.url, postBody documentString variableValues )
in
{ method = requestOptions.method
, headers = requestOptions.headers
, url = url
, body = body
, expect = expect
, timeout = requestOptions.timeout
, withCredentials = requestOptions.withCredentials
}


defaultExpect : Json.Decode.Decoder result -> Http.Expect result
defaultExpect =
Http.expectJson << Json.Decode.field "data"


errorsResponseDecoder : Json.Decode.Decoder (List RequestError)
errorsResponseDecoder =
Json.Decode.field "errors" Response.errorsDecoder


convertHttpError : (Http.Error -> err) -> (List RequestError -> err) -> Http.Error -> err
convertHttpError wrapHttpError wrapGraphQLError httpError =
let
handleErrorWithResponseBody responseBody =
responseBody
|> Json.Decode.decodeString errorsResponseDecoder
|> Result.map wrapGraphQLError
|> Result.withDefault (wrapHttpError httpError)
in
case httpError of
Http.BadStatus { body } ->
handleErrorWithResponseBody body

Http.BadPayload _ { body } ->
handleErrorWithResponseBody body

_ ->
wrapHttpError httpError
{ method = requestOptions.method
, headers = requestOptions.headers
, url = url
, body = body
, expect = expect
, timeout = requestOptions.timeout
, tracker = Nothing
}