git.iko.soy/typeable/servant
on master help sign in
Move more Content-type logic back to servant.
commite9f3341b9e4e08116f5fb4396ed2cc3c7130adc7 authorJulian K. Arni <jkarni@gmail.com> dateThu 19 Feb 2015, 19:18 (12 years ago) parent2092ddc
5 files changed+46−191
src/Servant/Server.hs+1−4 src/Servant/Server/ContentTypes.hs+5−155 src/Servant/Server/Internal.hs+29−27 test/Servant/Server/ContentTypesSpec.hs+2−1 test/Servant/ServerSpec.hs+9−4
src/Servant/Server.hs+1−4
@@ -10,16 +10,13 @@ module Servant.Server
1010 , -- * Handlers for all standard combinators
1111 HasServer(..)
1212
13− , -- * Building new Content-Types
14− Accept(..)
15− , MimeRender(..)
1613 ) where
1714
1815import Data.Proxy (Proxy)
1916import Network.Wai (Application)
2017
2118import Servant.Server.Internal
22−import Servant.Server.ContentTypes (Accept(..), MimeRender(..))
19+import Servant.Server.ContentTypes ()
2320
2421
2522-- * Implementing Servers
src/Servant/Server/ContentTypes.hs+5−155
@@ -11,7 +11,8 @@
1111module Servant.Server.ContentTypes where
1212
1313import Control.Monad (join)
14−import Data.Aeson (ToJSON(..), FromJSON(..), encode, decode)
14+import Control.Arrow (left)
15+import Data.Aeson (ToJSON(..), FromJSON(..), encode, eitherDecode)
1516import Data.ByteString.Lazy (ByteString)
1617import qualified Data.ByteString as BS
1718import Data.Proxy (Proxy(..))
@@ -23,86 +24,7 @@ import qualified Network.HTTP.Media as M
2324
2425
2526import Servant.API ( XML, HTML, JSON, JavaScript, CSS, PlainText
26− , OctetStream)
27−
28−-- * Accept class
29−
30−-- | Instances of 'Accept' represent mimetypes. They are used for matching
31−-- against the @Accept@ HTTP header of the request, and for setting the
32−-- @Content-Type@ header of the response
33−--
34−-- Example:
35−--
36−-- > instance Accept HTML where
37−-- > contentType _ = "text" // "html"
38−--
39−class Accept ctype where
40− contentType :: Proxy ctype -> M.MediaType
41−
42−-- | @text/html;charset=utf-8@
43−instance Accept HTML where
44− contentType _ = "text" M.// "html" M./: ("charset", "utf-8")
45−
46−-- | @application/json;charset=utf-8@
47−instance Accept JSON where
48− contentType _ = "application" M.// "json" M./: ("charset", "utf-8")
49−
50−-- | @application/xml;charset=utf-8@
51−instance Accept XML where
52− contentType _ = "application" M.// "xml" M./: ("charset", "utf-8")
53−
54−-- | @application/javascript;charset=utf-8@
55−instance Accept JavaScript where
56− contentType _ = "application" M.// "javascript" M./: ("charset", "utf-8")
57−
58−-- | @text/css;charset=utf-8@
59−instance Accept CSS where
60− contentType _ = "text" M.// "css" M./: ("charset", "utf-8")
61−
62−-- | @text/plain;charset=utf-8@
63−instance Accept PlainText where
64− contentType _ = "text" M.// "plain" M./: ("charset", "utf-8")
65−
66−-- | @application/octet-stream@
67−instance Accept OctetStream where
68− contentType _ = "application" M.// "octet-stream"
69−
70−newtype AcceptHeader = AcceptHeader BS.ByteString
71− deriving (Eq, Show)
72−
73−-- * Render (serializing)
74−
75−-- | Instantiate this class to register a way of serializing a type based
76−-- on the @Accept@ header.
77−--
78−-- Example:
79−--
80−-- > data MyContentType
81−-- >
82−-- > instance Accept MyContentType where
83−-- > contentType _ = "example" // "prs.me.mine" /: ("charset", "utf-8")
84−-- >
85−-- > instance Show a => MimeRender MyContentType where
86−-- > toByteString _ val = pack ("This is MINE! " ++ show val)
87−-- >
88−-- > type MyAPI = "path" :> Get '[MyContentType] Int
89−class Accept ctype => MimeRender ctype a where
90− toByteString :: Proxy ctype -> a -> ByteString
91−
92−class AllCTRender list a where
93− -- If the Accept header can be matched, returns (Just) a tuple of the
94− -- Content-Type and response (serialization of @a@ into the appropriate
95− -- mimetype).
96− handleAcceptH :: Proxy list -> AcceptHeader -> a -> Maybe (ByteString, ByteString)
97−
98−instance ( AllMimeRender ctyps a, IsNonEmpty ctyps
99− ) => AllCTRender ctyps a where
100− handleAcceptH _ (AcceptHeader accept) val = M.mapAcceptMedia lkup accept
101− where pctyps = Proxy :: Proxy ctyps
102− amrs = amr pctyps val
103− lkup = zip (map fst amrs) $ map (\(a,b) -> (cs $ show a, b)) amrs
104−
105−
27+ , OctetStream, MimeRender(..), MimeUnrender(..) )
10628
10729
10830--------------------------------------------------------------------------
@@ -116,86 +38,14 @@ instance ToJSON a => MimeRender JSON a where
11638instance MimeRender PlainText Text.Text where
11739 toByteString _ = Text.encodeUtf8
11840
119−--------------------------------------------------------------------------
120−-- * Unrender
121−class Accept ctype => MimeUnrender ctype a where
122− fromByteString :: Proxy ctype -> ByteString -> Maybe a
123−
124−class AllCTUnrender list a where
125− handleCTypeH :: Proxy list
126− -> ByteString -- Content-Type header
127− -> ByteString -- Request body
128− -> Maybe a
129−
130−instance ( AllMimeUnrender ctyps a, IsNonEmpty ctyps
131− ) => AllCTUnrender ctyps a where
132− handleCTypeH _ ctypeH body = join $ M.mapContentMedia lkup (cs ctypeH)
133− where lkup = amu (Proxy :: Proxy ctyps) body
134−
135−--------------------------------------------------------------------------
136−-- * Utils (Internal)
137−
138−
139−--------------------------------------------------------------------------
140−-- Check that all elements of list are instances of MimeRender
141−--------------------------------------------------------------------------
142−class AllMimeRender ls a where
143− amr :: Proxy ls
144− -> a -- value to serialize
145− -> [(M.MediaType, ByteString)] -- content-types/response pairs
146−
147−instance ( MimeRender ctyp a ) => AllMimeRender '[ctyp] a where
148− amr _ a = [(contentType pctyp, toByteString pctyp a)]
149− where pctyp = Proxy :: Proxy ctyp
150−
151−instance ( MimeRender ctyp a
152− , MimeRender ctyp' a -- at least two elems to avoid overlap
153− , AllMimeRender ctyps a
154− ) => AllMimeRender (ctyp ': ctyp' ': ctyps) a where
155− amr _ a = (contentType pctyp, toByteString pctyp a)
156− :(contentType pctyp', toByteString pctyp' a)
157− :(amr pctyps a)
158− where pctyp = Proxy :: Proxy ctyp
159− pctyps = Proxy :: Proxy ctyps
160− pctyp' = Proxy :: Proxy ctyp'
161−
162−
163−instance AllMimeRender '[] a where
164− amr _ _ = []
165−
166−--------------------------------------------------------------------------
167−-- Check that all elements of list are instances of MimeUnrender
168−--------------------------------------------------------------------------
169−class AllMimeUnrender ls a where
170− amu :: Proxy ls -> ByteString -> [(M.MediaType, Maybe a)]
171−
172−instance ( MimeUnrender ctyp a ) => AllMimeUnrender '[ctyp] a where
173− amu _ val = [(contentType pctyp, fromByteString pctyp val)]
174− where pctyp = Proxy :: Proxy ctyp
175−
176−instance ( MimeUnrender ctyp a
177− , MimeUnrender ctyp' a
178− , AllMimeUnrender ctyps a
179− ) => AllMimeUnrender (ctyp ': ctyp' ': ctyps) a where
180− amu _ val = (contentType pctyp, fromByteString pctyp val)
181− :(contentType pctyp', fromByteString pctyp' val)
182− :(amu pctyps val)
183− where pctyp = Proxy :: Proxy ctyp
184− pctyps = Proxy :: Proxy ctyps
185− pctyp' = Proxy :: Proxy ctyp'
186−
187−type family IsNonEmpty (ls::[*]) :: Constraint where
188− IsNonEmpty '[] = 'False ~ 'True
189− IsNonEmpty x = ()
190−
19141--------------------------------------------------------------------------
19242-- * MimeUnrender Instances
19343
19444-- | @decode@
19545instance FromJSON a => MimeUnrender JSON a where
196− fromByteString _ = decode
46+ fromByteString _ = eitherDecode
19747
19848-- | @Text.decodeUtf8'@
19949instance MimeUnrender PlainText Text.Text where
200− fromByteString _ = either (const Nothing) Just . Text.decodeUtf8'
50+ fromByteString _ = left show . Text.decodeUtf8'
20151
src/Servant/Server/Internal.hs+29−27
@@ -9,7 +9,7 @@ module Servant.Server.Internal where
99
1010import Control.Applicative ((<$>))
1111import Control.Monad.Trans.Either (EitherT, runEitherT)
12−import Data.Aeson (ToJSON, FromJSON, encode, eitherDecode')
12+import Data.Aeson (ToJSON)
1313import qualified Data.ByteString as B
1414import qualified Data.ByteString.Lazy as BL
1515import Data.IORef (newIORef, readIORef, writeIORef)
@@ -29,10 +29,10 @@ import Network.Wai ( Response, Request, ResponseReceived, Application
2929 , lazyRequestBody, requestHeaders, requestMethod,
3030 rawQueryString, responseLBS)
3131import Servant.API ( QueryParams, QueryParam, QueryFlag, ReqBody, Header
32− , MatrixParams, MatrixParam, MatrixFlag,
32+ , MatrixParams, MatrixParam, MatrixFlag
3333 , Capture, Get, Delete, Put, Post, Patch, Raw, (:>), (:<|>)(..))
34−import Servant.Server.ContentTypes ( AllCTRender(..), AcceptHeader(..)
35− , AllCTUnrender(..) )
34+import Servant.API.ContentTypes ( AllCTRender(..), AcceptHeader(..)
35+ , AllCTUnrender(..),)
3636import Servant.Common.Text (FromText, fromText)
3737
3838
@@ -72,39 +72,33 @@ toApplication ra request respond = do
7272 respond $ responseLBS methodNotAllowed405 [] "method not allowed"
7373 routingRespond (Left (InvalidBody err)) =
7474 respond $ responseLBS badRequest400 [] $ fromString $ "Invalid JSON in request body: " ++ err
75+ routingRespond (Left UnsupportedMediaType) =
76+ respond $ responseLBS unsupportedMediaType415 [] "unsupported media type"
7577 routingRespond (Left (HttpError status body)) =
7678 respond $ responseLBS status [] $ fromMaybe (BL.fromStrict $ statusMessage status) body
7779 routingRespond (Right response) =
7880 respond response
7981
82+-- Note that the ordering of the constructors has great significance! It
83+-- determines the Ord instance and, consequently, the monoid instance.
8084-- * Route mismatch
8185data RouteMismatch =
8286 NotFound -- ^ the usual "not found" error
8387 | WrongMethod -- ^ a more informative "you just got the HTTP method wrong" error
88+ | UnsupportedMediaType -- ^ request body has unsupported media type
8489 | InvalidBody String -- ^ an even more informative "your json request body wasn't valid" error
8590 | HttpError Status (Maybe BL.ByteString) -- ^ an even even more informative arbitrary HTTP response code error.
86− deriving (Eq, Show)
91+ deriving (Eq, Ord, Show)
8792
88−-- |
89−-- @
90−-- > mempty = NotFound
91−-- >
92−-- > _ `mappend` HttpError s b = HttpError s b
93−-- > HttpError s b `mappend` _ = HttpError s b
94−-- > NotFound `mappend` x = x
95−-- > WrongMethod `mappend` InvalidBody s = InvalidBody s
96−-- > WrongMethod `mappend` _ = WrongMethod
97−-- > InvalidBody s `mappend` _ = InvalidBody s
98−-- @
9993instance Monoid RouteMismatch where
10094 mempty = NotFound
95+ -- The following isn't great, since it picks @InvalidBody@ based on
96+ -- alphabetical ordering, but any choice would be arbitrary.
97+ --
98+ -- "As one judge said to the other, 'Be just and if you can't be just, be
99+ -- arbitrary'" -- William Burroughs
100+ mappend = max
101101
102− _ `mappend` HttpError s b = HttpError s b
103− HttpError s b `mappend` _ = HttpError s b
104− NotFound `mappend` x = x
105− WrongMethod `mappend` InvalidBody s = InvalidBody s
106− WrongMethod `mappend` _ = WrongMethod
107− InvalidBody s `mappend` _ = InvalidBody s
108102
109103-- | A wrapper around @'Either' 'RouteMismatch' a@.
110104newtype RouteResult a =
@@ -401,15 +395,22 @@ instance ( AllCTRender ctypes a
401395-- If successfully returning a value, we just require that its type has
402396-- a 'ToJSON' instance and servant takes care of encoding it for you,
403397-- yielding status code 201 along the way.
404−instance (Typeable a, ToJSON a) => HasServer (Patch a) where
405− type Server (Patch a) = EitherT (Int, String) IO a
398+instance ( AllCTRender ctypes a
399+ , Typeable a
400+ , ToJSON a) => HasServer (Patch ctypes a) where
401+ type Server (Patch ctypes a) = EitherT (Int, String) IO a
406402
407403 route Proxy action request respond
408404 | pathIsEmpty request && requestMethod request == methodPost = do
409405 e <- runEitherT action
410406 respond . succeedWith $ case e of
411407 Right out -> case cast out of
412− Nothing -> responseLBS status200 [("Content-Type", "application/json")] (encode out)
408+ Nothing -> do
409+ let accH = fromMaybe "*/*" $ lookup hAccept $ requestHeaders request
410+ case handleAcceptH (Proxy :: Proxy ctypes) (AcceptHeader accH) out of
411+ Nothing -> responseLBS (mkStatus 406 "") [] ""
412+ Just (contentT, body) -> responseLBS status200 [ ("Content-Type"
413+ , cs contentT)] body
413414 Just () -> responseLBS status204 [] ""
414415 Left (status, message) ->
415416 responseLBS (mkStatus status (cs message)) [] (cs message)
@@ -695,8 +696,9 @@ instance ( AllCTUnrender list a, HasServer sublayout
695696 mrqbody <- handleCTypeH (Proxy :: Proxy list) (cs contentTypeH)
696697 <$> lazyRequestBody request
697698 case mrqbody of
698− Left e -> respond . failWith $ InvalidBody e
699− Right v -> route (Proxy :: Proxy sublayout) (subserver v) request respond
699+ Nothing -> respond . failWith $ UnsupportedMediaType
700+ Just (Left e) -> respond . failWith $ InvalidBody e
701+ Just (Right v) -> route (Proxy :: Proxy sublayout) (subserver v) request respond
700702
701703-- | Make sure the incoming request starts with @"/path"@, strip it and
702704-- pass the rest of the request path to @sublayout@.
test/Servant/Server/ContentTypesSpec.hs+2−1
@@ -24,8 +24,9 @@ import Test.Hspec
2424import Test.QuickCheck
2525
2626import Servant.API
27+import Servant.API.ContentTypes
2728import Servant.Server
28−import Servant.Server.ContentTypes
29+import Servant.Server.ContentTypes ()
2930
3031
3132spec :: Spec
test/Servant/ServerSpec.hs+9−4
@@ -209,10 +209,10 @@ queryParamSpec = do
209209 name = "Alice"
210210 }
211211
212−type MatrixParamApi = "a" :> MatrixParam "name" String :> Get Person
213− :<|> "b" :> MatrixParams "names" String :> "bsub" :> MatrixParams "names" String :> Get Person
214− :<|> "c" :> MatrixFlag "capitalize" :> Get Person
215− :<|> "d" :> Capture "foo" Integer :> MatrixParam "name" String :> MatrixFlag "capitalize" :> "dsub" :> Get Person
212+type MatrixParamApi = "a" :> MatrixParam "name" String :> Get '[JSON] Person
213+ :<|> "b" :> MatrixParams "names" String :> "bsub" :> MatrixParams "names" String :> Get '[JSON] Person
214+ :<|> "c" :> MatrixFlag "capitalize" :> Get '[JSON] Person
215+ :<|> "d" :> Capture "foo" Integer :> MatrixParam "name" String :> MatrixFlag "capitalize" :> "dsub" :> Get '[JSON] Person
216216
217217matrixParamApi :: Proxy MatrixParamApi
218218matrixParamApi = Proxy
@@ -323,6 +323,11 @@ postSpec = do
323323 it "correctly rejects invalid request bodies with status 400" $ do
324324 post' "/" "some invalid body" `shouldRespondWith` 400
325325
326+ it "responds with 415 if the requested media type is unsupported" $ do
327+ let post'' x = Test.Hspec.Wai.request methodPost x [(hContentType
328+ , "application/nonsense")]
329+ post'' "/" "anything at all" `shouldRespondWith` 415
330+
326331
327332type RawApi = "foo" :> Raw
328333rawApi :: Proxy RawApi