Skip to content

Accept multiple content-types in servant-client. #552

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

Closed
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
51 changes: 41 additions & 10 deletions servant-client/src/Servant/Client.hs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,22 @@ import Network.HTTP.Media
import qualified Network.HTTP.Types as H
import qualified Network.HTTP.Types.Header as HTTP
import Servant.API
import Servant.API.ContentTypes
import Servant.Client.Experimental.Auth
import Servant.Common.BaseUrl
import Servant.Common.BasicAuth
import Servant.Common.Req

type family FirstCT cts :: [*] where
FirstCT '[] = '[]
FirstCT (ct ': cts) = '[ct]

type family FirstCTVerb x :: * where
FirstCTVerb (Verb method statusCode cts r) = (Verb method statusCode (FirstCT cts) r)
FirstCTVerb (a :> b) = (a :> FirstCTVerb b)
FirstCTVerb (a :<|> b) = (FirstCTVerb a :<|> FirstCTVerb b)
FirstCTVerb a = a

-- * Accessing APIs as a Client

-- | 'client' allows you to produce operations to query an API from a client.
Expand All @@ -57,8 +68,28 @@ import Servant.Common.Req
-- > getAllBooks :: Manager -> BaseUrl -> ClientM [Book]
-- > postNewBook :: Book -> Manager -> BaseUrl -> ClientM Book
-- > (getAllBooks :<|> postNewBook) = client myApi
client :: HasClient api => Proxy api -> Client api
client p = clientWithRoute p defReq

client
:: forall api . HasClient (FirstCTVerb api)
=> Proxy api -> Client (FirstCTVerb api)
client _ = clientWithRoute apiWithOnlyFirstCT defReq
where
apiWithOnlyFirstCT :: Proxy (FirstCTVerb api)
apiWithOnlyFirstCT = Proxy

type family ReplaceCTs api (newCTs :: [*]) :: * where
ReplaceCTs (Verb method statusCode oldCTs r) newCTs = (Verb method statusCode newCTs r)
ReplaceCTs (a :> b) newCTs = (a :> ReplaceCTs b newCTs)
ReplaceCTs (a :<|> b) newCTs = (ReplaceCTs a newCTs :<|> ReplaceCTs b newCTs)
ReplaceCTs a newCTs = a

clientWithCTs
:: forall api cts . (HasClient (ReplaceCTs api cts))
=> Proxy cts -> Proxy api -> Client (ReplaceCTs api cts)
clientWithCTs _ _ = clientWithRoute replacedCTs defReq
where
replacedCTs :: Proxy (ReplaceCTs api cts)
replacedCTs = Proxy

-- | This class lets us define how each API combinator
-- influences the creation of an HTTP request. It's mostly
Expand Down Expand Up @@ -152,11 +183,11 @@ instance (KnownSymbol capture, ToHttpApiData a, HasClient sublayout)

instance OVERLAPPABLE_
-- Note [Non-Empty Content Types]
(MimeUnrender ct a, ReflectMethod method, cts' ~ (ct ': cts)
) => HasClient (Verb method status cts' a) where
type Client (Verb method status cts' a) = Manager -> BaseUrl -> ClientM a
( AllMime cts, AllMimeUnrender cts a, ReflectMethod method
) => HasClient (Verb method status cts a) where
type Client (Verb method status cts a) = Manager -> BaseUrl -> ClientM a
clientWithRoute Proxy req manager baseurl =
snd <$> performRequestCT (Proxy :: Proxy ct) method req manager baseurl
snd <$> performRequestCT (Proxy :: Proxy cts) method req manager baseurl
where method = reflectMethod (Proxy :: Proxy method)

instance OVERLAPPING_
Expand All @@ -169,13 +200,13 @@ instance OVERLAPPING_

instance OVERLAPPING_
-- Note [Non-Empty Content Types]
( MimeUnrender ct a, BuildHeadersTo ls, ReflectMethod method, cts' ~ (ct ': cts)
) => HasClient (Verb method status cts' (Headers ls a)) where
type Client (Verb method status cts' (Headers ls a))
( AllMime cts, AllMimeUnrender cts a, BuildHeadersTo ls, ReflectMethod method
) => HasClient (Verb method status cts (Headers ls a)) where
type Client (Verb method status cts (Headers ls a))
= Manager -> BaseUrl -> ClientM (Headers ls a)
clientWithRoute Proxy req manager baseurl = do
let method = reflectMethod (Proxy :: Proxy method)
(hdrs, resp) <- performRequestCT (Proxy :: Proxy ct) method req manager baseurl
(hdrs, resp) <- performRequestCT (Proxy :: Proxy cts) method req manager baseurl
return $ Headers { getResponse = resp
, getHeadersHList = buildHeadersTo hdrs
}
Expand Down
31 changes: 21 additions & 10 deletions servant-client/src/Servant/Common/Req.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import Control.Monad
import Control.Monad.Catch (MonadThrow)
import Control.Monad.IO.Class
import Control.Monad.Trans.Except
import Data.ByteString.Lazy hiding (pack, filter, map, null, elem)
import Data.ByteString.Lazy hiding (any, foldr, pack, filter, map, null, elem)
import Data.String
import Data.String.Conversions
import Data.Proxy
Expand Down Expand Up @@ -178,17 +178,28 @@ performRequest reqMethod req manager reqHost = do
throwE $ FailureResponse status ct body
return (status_code, body, ct, hdrs, response)

performRequestCT :: MimeUnrender ct result =>
Proxy ct -> Method -> Req -> Manager -> BaseUrl
performRequestCT :: (AllMime cts, AllMimeUnrender cts result) =>
Proxy cts -> Method -> Req -> Manager -> BaseUrl
-> ClientM ([HTTP.Header], result)
performRequestCT ct reqMethod req manager reqHost = do
let acceptCT = contentType ct
performRequestCT cts reqMethod req manager reqHost = do
let acceptCTs = allMime cts
(_status, respBody, respCT, hdrs, _response) <-
performRequest reqMethod (req { reqAccept = [acceptCT] }) manager reqHost
unless (matches respCT (acceptCT)) $ throwE $ UnsupportedContentType respCT respBody
case mimeUnrender ct respBody of
Left err -> throwE $ DecodeFailure err respCT respBody
Right val -> return (hdrs, val)
performRequest reqMethod (req { reqAccept = acceptCTs }) manager reqHost
unless (any (matches respCT) acceptCTs) $ throwE $ UnsupportedContentType respCT respBody
let unrenderedResp = allMimeUnrender cts respBody
firstSuccess = firstPred (either (const Nothing) Just) $ snd <$> unrenderedResp
case firstSuccess of
Nothing ->
let firstErr = firstPred (either Just (const Nothing)) $ snd <$> unrenderedResp
in case firstErr of
Nothing -> throwE $ DecodeFailure "failed to find the content-type render failure" respCT respBody
Just err -> throwE $ DecodeFailure err respCT respBody
Just val -> pure (hdrs, val)
where
-- In @firstPred f eitherList@, fold over a list of 'Either', returning the
-- first 'Just' value generated by the function @f@.
firstPred :: (Either e x -> Maybe a) -> [Either e x] -> Maybe a
firstPred f = foldr (\eith accum -> maybe accum Just $ f eith) Nothing

performRequestNoBody :: Method -> Req -> Manager -> BaseUrl
-> ClientM [HTTP.Header]
Expand Down