Merge pull request #614 from phadej/accept-multiple

Allow multiple content-types for single Accept
This commit is contained in:
Alp Mestanogullari 2017-01-01 11:31:00 +01:00 committed by GitHub
commit e8ba67048a
5 changed files with 82 additions and 19 deletions

View file

@ -407,6 +407,7 @@ instance (MimeRender ct a, HasClient api)
clientWithRoute (Proxy :: Proxy api) clientWithRoute (Proxy :: Proxy api)
(let ctProxy = Proxy :: Proxy ct (let ctProxy = Proxy :: Proxy ct
in setRQBody (mimeRender ctProxy body) in setRQBody (mimeRender ctProxy body)
-- We use first contentType from the Accept list
(contentType ctProxy) (contentType ctProxy)
req req
) )

View file

@ -13,6 +13,7 @@ import Prelude.Compat
import Control.Exception import Control.Exception
import Control.Monad import Control.Monad
import Control.Monad.Catch (MonadThrow, MonadCatch) import Control.Monad.Catch (MonadThrow, MonadCatch)
import Data.Foldable (toList)
#if MIN_VERSION_mtl(2,2,0) #if MIN_VERSION_mtl(2,2,0)
import Control.Monad.Except (MonadError(..)) import Control.Monad.Except (MonadError(..))
@ -25,7 +26,7 @@ import Control.Monad.Trans.Except
import GHC.Generics import GHC.Generics
import Control.Monad.IO.Class () import Control.Monad.IO.Class ()
import Control.Monad.Reader import Control.Monad.Reader
import Data.ByteString.Lazy hiding (pack, filter, map, null, elem) import Data.ByteString.Lazy hiding (pack, filter, map, null, elem, any)
import Data.String import Data.String
import Data.String.Conversions import Data.String.Conversions
import Data.Proxy import Data.Proxy
@ -215,10 +216,10 @@ performRequest reqMethod req = do
performRequestCT :: MimeUnrender ct result => Proxy ct -> Method -> Req performRequestCT :: MimeUnrender ct result => Proxy ct -> Method -> Req
-> ClientM ([HTTP.Header], result) -> ClientM ([HTTP.Header], result)
performRequestCT ct reqMethod req = do performRequestCT ct reqMethod req = do
let acceptCT = contentType ct let acceptCTS = contentTypes ct
(_status, respBody, respCT, hdrs, _response) <- (_status, respBody, respCT, hdrs, _response) <-
performRequest reqMethod (req { reqAccept = [acceptCT] }) performRequest reqMethod (req { reqAccept = toList acceptCTS })
unless (matches respCT (acceptCT)) $ throwError $ UnsupportedContentType respCT respBody unless (any (matches respCT) acceptCTS) $ throwError $ UnsupportedContentType respCT respBody
case mimeUnrender ct respBody of case mimeUnrender ct respBody of
Left err -> throwError $ DecodeFailure err respCT respBody Left err -> throwError $ DecodeFailure err respCT respBody
Right val -> return (hdrs, val) Right val -> return (hdrs, val)

View file

@ -65,6 +65,11 @@ library
, string-conversions >= 0.3 && < 0.5 , string-conversions >= 0.3 && < 0.5
, network-uri >= 2.6 && < 2.7 , network-uri >= 2.6 && < 2.7
, vault >= 0.3 && < 0.4 , vault >= 0.3 && < 0.4
if !impl(ghc >= 8.0)
build-depends:
semigroups >= 0.16 && < 0.19
hs-source-dirs: src hs-source-dirs: src
default-language: Haskell2010 default-language: Haskell2010
other-extensions: CPP other-extensions: CPP
@ -109,6 +114,7 @@ test-suite spec
, attoparsec , attoparsec
, bytestring , bytestring
, hspec == 2.* , hspec == 2.*
, http-media
, QuickCheck , QuickCheck
, quickcheck-instances , quickcheck-instances
, servant , servant
@ -116,6 +122,10 @@ test-suite spec
, text , text
, url , url
if !impl(ghc >= 8.0)
build-depends:
semigroups >= 0.16 && < 0.19
test-suite doctests test-suite doctests
build-depends: base build-depends: base
, servant , servant

View file

@ -8,6 +8,7 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-} {-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-} {-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE UndecidableInstances #-}
@ -81,6 +82,7 @@ import qualified Data.ByteString as BS
import Data.ByteString.Lazy (ByteString, fromStrict, import Data.ByteString.Lazy (ByteString, fromStrict,
toStrict) toStrict)
import qualified Data.ByteString.Lazy.Char8 as BC import qualified Data.ByteString.Lazy.Char8 as BC
import qualified Data.List.NonEmpty as NE
import Data.Maybe (isJust) import Data.Maybe (isJust)
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import qualified Data.Text as TextS import qualified Data.Text as TextS
@ -119,6 +121,12 @@ data OctetStream deriving Typeable
-- --
class Accept ctype where class Accept ctype where
contentType :: Proxy ctype -> M.MediaType contentType :: Proxy ctype -> M.MediaType
contentType = NE.head . contentTypes
contentTypes :: Proxy ctype -> NE.NonEmpty M.MediaType
contentTypes = (NE.:| []) . contentType
{-# MINIMAL contentType | contentTypes #-}
-- | @application/json@ -- | @application/json@
instance Accept JSON where instance Accept JSON where
@ -219,9 +227,10 @@ instance AllMime '[] where
allMime _ = [] allMime _ = []
instance (Accept ctyp, AllMime ctyps) => AllMime (ctyp ': ctyps) where instance (Accept ctyp, AllMime ctyps) => AllMime (ctyp ': ctyps) where
allMime _ = (contentType pctyp):allMime pctyps allMime _ = NE.toList (contentTypes pctyp) ++ allMime pctyps
where pctyp = Proxy :: Proxy ctyp where
pctyps = Proxy :: Proxy ctyps pctyp = Proxy :: Proxy ctyp
pctyps = Proxy :: Proxy ctyps
canHandleAcceptH :: AllMime list => Proxy list -> AcceptHeader -> Bool canHandleAcceptH :: AllMime list => Proxy list -> AcceptHeader -> Bool
canHandleAcceptH p (AcceptHeader h ) = isJust $ M.matchAccept (allMime p) h canHandleAcceptH p (AcceptHeader h ) = isJust $ M.matchAccept (allMime p) h
@ -235,25 +244,31 @@ class (AllMime list) => AllMimeRender (list :: [*]) a where
-> [(M.MediaType, ByteString)] -- content-types/response pairs -> [(M.MediaType, ByteString)] -- content-types/response pairs
instance OVERLAPPABLE_ ( MimeRender ctyp a ) => AllMimeRender '[ctyp] a where instance OVERLAPPABLE_ ( MimeRender ctyp a ) => AllMimeRender '[ctyp] a where
allMimeRender _ a = [(contentType pctyp, mimeRender pctyp a)] allMimeRender _ a = map (, bs) $ NE.toList $ contentTypes pctyp
where pctyp = Proxy :: Proxy ctyp where
bs = mimeRender pctyp a
pctyp = Proxy :: Proxy ctyp
instance OVERLAPPABLE_ instance OVERLAPPABLE_
( MimeRender ctyp a ( MimeRender ctyp a
, AllMimeRender (ctyp' ': ctyps) a , AllMimeRender (ctyp' ': ctyps) a
) => AllMimeRender (ctyp ': ctyp' ': ctyps) a where ) => AllMimeRender (ctyp ': ctyp' ': ctyps) a where
allMimeRender _ a = (contentType pctyp, mimeRender pctyp a) allMimeRender _ a =
:(allMimeRender pctyps a) (map (, bs) $ NE.toList $ contentTypes pctyp)
where pctyp = Proxy :: Proxy ctyp ++ allMimeRender pctyps a
pctyps = Proxy :: Proxy (ctyp' ': ctyps) where
bs = mimeRender pctyp a
pctyp = Proxy :: Proxy ctyp
pctyps = Proxy :: Proxy (ctyp' ': ctyps)
-- Ideally we would like to declare a 'MimeRender a NoContent' instance, and -- Ideally we would like to declare a 'MimeRender a NoContent' instance, and
-- then this would be taken care of. However there is no more specific instance -- then this would be taken care of. However there is no more specific instance
-- between that and 'MimeRender JSON a', so we do this instead -- between that and 'MimeRender JSON a', so we do this instead
instance OVERLAPPING_ ( Accept ctyp ) => AllMimeRender '[ctyp] NoContent where instance OVERLAPPING_ ( Accept ctyp ) => AllMimeRender '[ctyp] NoContent where
allMimeRender _ _ = [(contentType pctyp, "")] allMimeRender _ _ = map (, "") $ NE.toList $ contentTypes pctyp
where pctyp = Proxy :: Proxy ctyp where
pctyp = Proxy :: Proxy ctyp
instance OVERLAPPING_ instance OVERLAPPING_
( AllMime (ctyp ': ctyp' ': ctyps) ( AllMime (ctyp ': ctyp' ': ctyps)
@ -274,10 +289,13 @@ instance AllMimeUnrender '[] a where
instance ( MimeUnrender ctyp a instance ( MimeUnrender ctyp a
, AllMimeUnrender ctyps a , AllMimeUnrender ctyps a
) => AllMimeUnrender (ctyp ': ctyps) a where ) => AllMimeUnrender (ctyp ': ctyps) a where
allMimeUnrender _ val = (contentType pctyp, mimeUnrender pctyp val) allMimeUnrender _ bs =
:(allMimeUnrender pctyps val) (map (, x) $ NE.toList $ contentTypes pctyp)
where pctyp = Proxy :: Proxy ctyp ++ allMimeUnrender pctyps bs
pctyps = Proxy :: Proxy ctyps where
x = mimeUnrender pctyp bs
pctyp = Proxy :: Proxy ctyp
pctyps = Proxy :: Proxy ctyps
-------------------------------------------------------------------------- --------------------------------------------------------------------------
-- * MimeRender Instances -- * MimeRender Instances

View file

@ -14,9 +14,11 @@ import Prelude.Compat
import Data.Aeson import Data.Aeson
import Data.ByteString.Char8 (ByteString, append, pack) import Data.ByteString.Char8 (ByteString, append, pack)
import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString.Lazy as BSL
import qualified Data.ByteString.Lazy.Char8 as BSL8
import Data.Either import Data.Either
import Data.Function (on) import Data.Function (on)
import Data.List (maximumBy) import Data.List (maximumBy)
import qualified Data.List.NonEmpty as NE
import Data.Maybe (fromJust, isJust, isNothing) import Data.Maybe (fromJust, isJust, isNothing)
import Data.Proxy import Data.Proxy
import Data.String (IsString (..)) import Data.String (IsString (..))
@ -24,8 +26,10 @@ import Data.String.Conversions (cs)
import qualified Data.Text as TextS import qualified Data.Text as TextS
import qualified Data.Text.Lazy as TextL import qualified Data.Text.Lazy as TextL
import GHC.Generics import GHC.Generics
import qualified Network.HTTP.Media as M
import Test.Hspec import Test.Hspec
import Test.QuickCheck import Test.QuickCheck
import Text.Read (readMaybe)
import "quickcheck-instances" Test.QuickCheck.Instances () import "quickcheck-instances" Test.QuickCheck.Instances ()
import Servant.API.ContentTypes import Servant.API.ContentTypes
@ -101,6 +105,14 @@ spec = describe "Servant.API.ContentTypes" $ do
"application/octet-stream" ("content" :: ByteString) "application/octet-stream" ("content" :: ByteString)
`shouldSatisfy` isJust `shouldSatisfy` isJust
it "returns Just if the 'Accept' header matches, with multiple mime types" $ do
handleAcceptH (Proxy :: Proxy '[JSONorText]) "application/json" (3 :: Int)
`shouldSatisfy` isJust
handleAcceptH (Proxy :: Proxy '[JSONorText]) "text/plain" (3 :: Int)
`shouldSatisfy` isJust
handleAcceptH (Proxy :: Proxy '[JSONorText]) "image/jpeg" (3 :: Int)
`shouldBe` Nothing
it "returns the Content-Type as the first element of the tuple" $ do it "returns the Content-Type as the first element of the tuple" $ do
handleAcceptH (Proxy :: Proxy '[JSON]) "*/*" (3 :: Int) handleAcceptH (Proxy :: Proxy '[JSON]) "*/*" (3 :: Int)
`shouldSatisfy` ((== "application/json") . fst . fromJust) `shouldSatisfy` ((== "application/json") . fst . fromJust)
@ -158,6 +170,15 @@ spec = describe "Servant.API.ContentTypes" $ do
(encode val) (encode val)
`shouldBe` Just (Right val) `shouldBe` Just (Right val)
it "returns Just (Right val) if the decoding succeeds for either of multiple mime-types" $ do
let val = 42 :: Int
handleCTypeH (Proxy :: Proxy '[JSONorText]) "application/json"
"42" `shouldBe` Just (Right val)
handleCTypeH (Proxy :: Proxy '[JSONorText]) "text/plain"
"42" `shouldBe` Just (Right val)
handleCTypeH (Proxy :: Proxy '[JSONorText]) "image/jpeg"
"42" `shouldBe` (Nothing :: Maybe (Either String Int))
#if MIN_VERSION_aeson(0,9,0) #if MIN_VERSION_aeson(0,9,0)
-- aeson >= 0.9 decodes top-level strings -- aeson >= 0.9 decodes top-level strings
describe "eitherDecodeLenient" $ do describe "eitherDecodeLenient" $ do
@ -201,6 +222,18 @@ instance ToJSON ByteString where
instance IsString AcceptHeader where instance IsString AcceptHeader where
fromString = AcceptHeader . fromString fromString = AcceptHeader . fromString
-- To test multiple content types
data JSONorText
instance Accept JSONorText where
contentTypes _ = "text" M.// "plain" NE.:| [ "application" M.// "json" ]
instance MimeRender JSONorText Int where
mimeRender _ = cs . show
instance MimeUnrender JSONorText Int where
mimeUnrender _ = maybe (Left "") Right . readMaybe . BSL8.unpack
addToAccept :: Accept a => Proxy a -> ZeroToOne -> AcceptHeader -> AcceptHeader addToAccept :: Accept a => Proxy a -> ZeroToOne -> AcceptHeader -> AcceptHeader
addToAccept p (ZeroToOne f) (AcceptHeader h) = AcceptHeader (cont h) addToAccept p (ZeroToOne f) (AcceptHeader h) = AcceptHeader (cont h)
where new = cs (show $ contentType p) `append` "; q=" `append` pack (show f) where new = cs (show $ contentType p) `append` "; q=" `append` pack (show f)