Skip to content

Commit e8ba670

Browse files
authored
Merge pull request #614 from phadej/accept-multiple
Allow multiple content-types for single Accept
2 parents 37ec081 + da55698 commit e8ba670

File tree

5 files changed

+82
-19
lines changed

5 files changed

+82
-19
lines changed

servant-client/src/Servant/Client.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ instance (MimeRender ct a, HasClient api)
407407
clientWithRoute (Proxy :: Proxy api)
408408
(let ctProxy = Proxy :: Proxy ct
409409
in setRQBody (mimeRender ctProxy body)
410+
-- We use first contentType from the Accept list
410411
(contentType ctProxy)
411412
req
412413
)

servant-client/src/Servant/Common/Req.hs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import Prelude.Compat
1313
import Control.Exception
1414
import Control.Monad
1515
import Control.Monad.Catch (MonadThrow, MonadCatch)
16+
import Data.Foldable (toList)
1617

1718
#if MIN_VERSION_mtl(2,2,0)
1819
import Control.Monad.Except (MonadError(..))
@@ -25,7 +26,7 @@ import Control.Monad.Trans.Except
2526
import GHC.Generics
2627
import Control.Monad.IO.Class ()
2728
import Control.Monad.Reader
28-
import Data.ByteString.Lazy hiding (pack, filter, map, null, elem)
29+
import Data.ByteString.Lazy hiding (pack, filter, map, null, elem, any)
2930
import Data.String
3031
import Data.String.Conversions
3132
import Data.Proxy
@@ -215,10 +216,10 @@ performRequest reqMethod req = do
215216
performRequestCT :: MimeUnrender ct result => Proxy ct -> Method -> Req
216217
-> ClientM ([HTTP.Header], result)
217218
performRequestCT ct reqMethod req = do
218-
let acceptCT = contentType ct
219+
let acceptCTS = contentTypes ct
219220
(_status, respBody, respCT, hdrs, _response) <-
220-
performRequest reqMethod (req { reqAccept = [acceptCT] })
221-
unless (matches respCT (acceptCT)) $ throwError $ UnsupportedContentType respCT respBody
221+
performRequest reqMethod (req { reqAccept = toList acceptCTS })
222+
unless (any (matches respCT) acceptCTS) $ throwError $ UnsupportedContentType respCT respBody
222223
case mimeUnrender ct respBody of
223224
Left err -> throwError $ DecodeFailure err respCT respBody
224225
Right val -> return (hdrs, val)

servant/servant.cabal

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ library
6565
, string-conversions >= 0.3 && < 0.5
6666
, network-uri >= 2.6 && < 2.7
6767
, vault >= 0.3 && < 0.4
68+
69+
if !impl(ghc >= 8.0)
70+
build-depends:
71+
semigroups >= 0.16 && < 0.19
72+
6873
hs-source-dirs: src
6974
default-language: Haskell2010
7075
other-extensions: CPP
@@ -109,13 +114,18 @@ test-suite spec
109114
, attoparsec
110115
, bytestring
111116
, hspec == 2.*
117+
, http-media
112118
, QuickCheck
113119
, quickcheck-instances
114120
, servant
115121
, string-conversions
116122
, text
117123
, url
118124

125+
if !impl(ghc >= 8.0)
126+
build-depends:
127+
semigroups >= 0.16 && < 0.19
128+
119129
test-suite doctests
120130
build-depends: base
121131
, servant

servant/src/Servant/API/ContentTypes.hs

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
{-# LANGUAGE OverloadedStrings #-}
99
{-# LANGUAGE PolyKinds #-}
1010
{-# LANGUAGE ScopedTypeVariables #-}
11+
{-# LANGUAGE TupleSections #-}
1112
{-# LANGUAGE TypeFamilies #-}
1213
{-# LANGUAGE TypeOperators #-}
1314
{-# LANGUAGE UndecidableInstances #-}
@@ -81,6 +82,7 @@ import qualified Data.ByteString as BS
8182
import Data.ByteString.Lazy (ByteString, fromStrict,
8283
toStrict)
8384
import qualified Data.ByteString.Lazy.Char8 as BC
85+
import qualified Data.List.NonEmpty as NE
8486
import Data.Maybe (isJust)
8587
import Data.String.Conversions (cs)
8688
import qualified Data.Text as TextS
@@ -119,6 +121,12 @@ data OctetStream deriving Typeable
119121
--
120122
class Accept ctype where
121123
contentType :: Proxy ctype -> M.MediaType
124+
contentType = NE.head . contentTypes
125+
126+
contentTypes :: Proxy ctype -> NE.NonEmpty M.MediaType
127+
contentTypes = (NE.:| []) . contentType
128+
129+
{-# MINIMAL contentType | contentTypes #-}
122130

123131
-- | @application/json@
124132
instance Accept JSON where
@@ -219,9 +227,10 @@ instance AllMime '[] where
219227
allMime _ = []
220228

221229
instance (Accept ctyp, AllMime ctyps) => AllMime (ctyp ': ctyps) where
222-
allMime _ = (contentType pctyp):allMime pctyps
223-
where pctyp = Proxy :: Proxy ctyp
224-
pctyps = Proxy :: Proxy ctyps
230+
allMime _ = NE.toList (contentTypes pctyp) ++ allMime pctyps
231+
where
232+
pctyp = Proxy :: Proxy ctyp
233+
pctyps = Proxy :: Proxy ctyps
225234

226235
canHandleAcceptH :: AllMime list => Proxy list -> AcceptHeader -> Bool
227236
canHandleAcceptH p (AcceptHeader h ) = isJust $ M.matchAccept (allMime p) h
@@ -235,25 +244,31 @@ class (AllMime list) => AllMimeRender (list :: [*]) a where
235244
-> [(M.MediaType, ByteString)] -- content-types/response pairs
236245

237246
instance OVERLAPPABLE_ ( MimeRender ctyp a ) => AllMimeRender '[ctyp] a where
238-
allMimeRender _ a = [(contentType pctyp, mimeRender pctyp a)]
239-
where pctyp = Proxy :: Proxy ctyp
247+
allMimeRender _ a = map (, bs) $ NE.toList $ contentTypes pctyp
248+
where
249+
bs = mimeRender pctyp a
250+
pctyp = Proxy :: Proxy ctyp
240251

241252
instance OVERLAPPABLE_
242253
( MimeRender ctyp a
243254
, AllMimeRender (ctyp' ': ctyps) a
244255
) => AllMimeRender (ctyp ': ctyp' ': ctyps) a where
245-
allMimeRender _ a = (contentType pctyp, mimeRender pctyp a)
246-
:(allMimeRender pctyps a)
247-
where pctyp = Proxy :: Proxy ctyp
248-
pctyps = Proxy :: Proxy (ctyp' ': ctyps)
256+
allMimeRender _ a =
257+
(map (, bs) $ NE.toList $ contentTypes pctyp)
258+
++ allMimeRender pctyps a
259+
where
260+
bs = mimeRender pctyp a
261+
pctyp = Proxy :: Proxy ctyp
262+
pctyps = Proxy :: Proxy (ctyp' ': ctyps)
249263

250264

251265
-- Ideally we would like to declare a 'MimeRender a NoContent' instance, and
252266
-- then this would be taken care of. However there is no more specific instance
253267
-- between that and 'MimeRender JSON a', so we do this instead
254268
instance OVERLAPPING_ ( Accept ctyp ) => AllMimeRender '[ctyp] NoContent where
255-
allMimeRender _ _ = [(contentType pctyp, "")]
256-
where pctyp = Proxy :: Proxy ctyp
269+
allMimeRender _ _ = map (, "") $ NE.toList $ contentTypes pctyp
270+
where
271+
pctyp = Proxy :: Proxy ctyp
257272

258273
instance OVERLAPPING_
259274
( AllMime (ctyp ': ctyp' ': ctyps)
@@ -274,10 +289,13 @@ instance AllMimeUnrender '[] a where
274289
instance ( MimeUnrender ctyp a
275290
, AllMimeUnrender ctyps a
276291
) => AllMimeUnrender (ctyp ': ctyps) a where
277-
allMimeUnrender _ val = (contentType pctyp, mimeUnrender pctyp val)
278-
:(allMimeUnrender pctyps val)
279-
where pctyp = Proxy :: Proxy ctyp
280-
pctyps = Proxy :: Proxy ctyps
292+
allMimeUnrender _ bs =
293+
(map (, x) $ NE.toList $ contentTypes pctyp)
294+
++ allMimeUnrender pctyps bs
295+
where
296+
x = mimeUnrender pctyp bs
297+
pctyp = Proxy :: Proxy ctyp
298+
pctyps = Proxy :: Proxy ctyps
281299

282300
--------------------------------------------------------------------------
283301
-- * MimeRender Instances

servant/test/Servant/API/ContentTypesSpec.hs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,22 @@ import Prelude.Compat
1414
import Data.Aeson
1515
import Data.ByteString.Char8 (ByteString, append, pack)
1616
import qualified Data.ByteString.Lazy as BSL
17+
import qualified Data.ByteString.Lazy.Char8 as BSL8
1718
import Data.Either
1819
import Data.Function (on)
1920
import Data.List (maximumBy)
21+
import qualified Data.List.NonEmpty as NE
2022
import Data.Maybe (fromJust, isJust, isNothing)
2123
import Data.Proxy
2224
import Data.String (IsString (..))
2325
import Data.String.Conversions (cs)
2426
import qualified Data.Text as TextS
2527
import qualified Data.Text.Lazy as TextL
2628
import GHC.Generics
29+
import qualified Network.HTTP.Media as M
2730
import Test.Hspec
2831
import Test.QuickCheck
32+
import Text.Read (readMaybe)
2933
import "quickcheck-instances" Test.QuickCheck.Instances ()
3034

3135
import Servant.API.ContentTypes
@@ -101,6 +105,14 @@ spec = describe "Servant.API.ContentTypes" $ do
101105
"application/octet-stream" ("content" :: ByteString)
102106
`shouldSatisfy` isJust
103107

108+
it "returns Just if the 'Accept' header matches, with multiple mime types" $ do
109+
handleAcceptH (Proxy :: Proxy '[JSONorText]) "application/json" (3 :: Int)
110+
`shouldSatisfy` isJust
111+
handleAcceptH (Proxy :: Proxy '[JSONorText]) "text/plain" (3 :: Int)
112+
`shouldSatisfy` isJust
113+
handleAcceptH (Proxy :: Proxy '[JSONorText]) "image/jpeg" (3 :: Int)
114+
`shouldBe` Nothing
115+
104116
it "returns the Content-Type as the first element of the tuple" $ do
105117
handleAcceptH (Proxy :: Proxy '[JSON]) "*/*" (3 :: Int)
106118
`shouldSatisfy` ((== "application/json") . fst . fromJust)
@@ -158,6 +170,15 @@ spec = describe "Servant.API.ContentTypes" $ do
158170
(encode val)
159171
`shouldBe` Just (Right val)
160172

173+
it "returns Just (Right val) if the decoding succeeds for either of multiple mime-types" $ do
174+
let val = 42 :: Int
175+
handleCTypeH (Proxy :: Proxy '[JSONorText]) "application/json"
176+
"42" `shouldBe` Just (Right val)
177+
handleCTypeH (Proxy :: Proxy '[JSONorText]) "text/plain"
178+
"42" `shouldBe` Just (Right val)
179+
handleCTypeH (Proxy :: Proxy '[JSONorText]) "image/jpeg"
180+
"42" `shouldBe` (Nothing :: Maybe (Either String Int))
181+
161182
#if MIN_VERSION_aeson(0,9,0)
162183
-- aeson >= 0.9 decodes top-level strings
163184
describe "eitherDecodeLenient" $ do
@@ -201,6 +222,18 @@ instance ToJSON ByteString where
201222
instance IsString AcceptHeader where
202223
fromString = AcceptHeader . fromString
203224

225+
-- To test multiple content types
226+
data JSONorText
227+
228+
instance Accept JSONorText where
229+
contentTypes _ = "text" M.// "plain" NE.:| [ "application" M.// "json" ]
230+
231+
instance MimeRender JSONorText Int where
232+
mimeRender _ = cs . show
233+
234+
instance MimeUnrender JSONorText Int where
235+
mimeUnrender _ = maybe (Left "") Right . readMaybe . BSL8.unpack
236+
204237
addToAccept :: Accept a => Proxy a -> ZeroToOne -> AcceptHeader -> AcceptHeader
205238
addToAccept p (ZeroToOne f) (AcceptHeader h) = AcceptHeader (cont h)
206239
where new = cs (show $ contentType p) `append` "; q=" `append` pack (show f)

0 commit comments

Comments
 (0)