git.iko.soy/typeable/servant
on master help sign in
Refactor RouteResult.

Fix rerouting tests Fix 405 > 404 issue with Capture. Remove ServantErrWithPriority and Monoid instance More tests Update auth-combinator for routing changes

commita3b5652ab9604759a62ecc5e07f02a35566a359c authorJulian K. Arni <jkarni@gmail.com> dateWed 9 Sep 2015, 23:49 (11 years ago) parent153de01
9 files changed+165−176
servant-examples/auth-combinator/auth-combinator.hs+3−2 servant-server/src/Servant/Server.hs+1−1 servant-server/src/Servant/Server/Internal.hs+16−16 servant-server/src/Servant/Server/Internal/Router.hs+12−3 servant-server/src/Servant/Server/Internal/RoutingApplication.hs+38−71 servant-server/src/Servant/Server/Internal/ServantErr.hs+1−1 servant-server/test/Servant/Server/ErrorSpec.hs+69−25 servant-server/test/Servant/ServerSpec.hs+10−57 servant/test/Servant/API/ContentTypesSpec.hs+15
servant-examples/auth-combinator/auth-combinator.hs+3−2
@@ -5,6 +5,7 @@
55{-# LANGUAGE ScopedTypeVariables #-}
66{-# LANGUAGE TypeFamilies #-}
77{-# LANGUAGE TypeOperators #-}
8+
89import Data.Aeson
910import Data.ByteString (ByteString)
1011import Data.Text (Text)
@@ -31,12 +32,12 @@ instance HasServer rest => HasServer (AuthProtected :> rest) where
3132 route Proxy a = WithRequest $ \ request ->
3233 route (Proxy :: Proxy rest) $ do
3334 case lookup "Cookie" (requestHeaders request) of
34− Nothing -> return $ failWith $ HttpError status401 (Just "Missing auth header.")
35+ Nothing -> return $! failFatallyWith err401 { errBody = "Missing auth header" }
3536 Just v -> do
3637 authGranted <- isGoodCookie v
3738 if authGranted
3839 then a
39− else return $ failWith $ HttpError status403 (Just "Invalid cookie.")
40+ else return $! failFatallyWith err403 { errBody = "Invalid cookie" }
4041
4142type PrivateAPI = Get '[JSON] [PrivateData]
4243
servant-server/src/Servant/Server.hs+1−1
@@ -103,7 +103,7 @@ import Servant.Server.Internal.Enter
103103-- > main = Network.Wai.Handler.Warp.run 8080 app
104104--
105105serve :: HasServer layout => Proxy layout -> Server layout -> Application
106−serve p server = toApplication (runRouter (route p (return (RR (Right server)))))
106+serve p server = toApplication (runRouter (route p (return (HandlerVal server))))
107107
108108
109109-- Documentation
servant-server/src/Servant/Server/Internal.hs+16−16
@@ -113,11 +113,10 @@ instance (KnownSymbol capture, FromHttpApiData a, HasServer sublayout)
113113 a -> ServerT sublayout m
114114
115115 route Proxy subserver =
116− DynamicRouter $ \ first ->
117− route (Proxy :: Proxy sublayout)
118− (case captured captureProxy first of
119− Nothing -> return $ failWith NotFound
120− Just v -> feedTo subserver v)
116+ DynamicRouter $ \ first -> case captured captureProxy first of
117+ Nothing -> LeafRouter (\_ r -> r $ failWith err404)
118+ Just v -> route (Proxy :: Proxy sublayout) (feedTo subserver v)
119+
121120 where captureProxy = Proxy :: Proxy (Capture capture a)
122121
123122allowedMethodHead :: Method -> Request -> Bool
@@ -131,7 +130,7 @@ processMethodRouter :: forall a. ConvertibleStrings a B.ByteString
131130 -> Maybe [(HeaderName, B.ByteString)]
132131 -> Request -> RouteResult Response
133132processMethodRouter handleA status method headers request = case handleA of
134− Nothing -> failWith UnsupportedMediaType
133+ Nothing -> failFatallyWith err406
135134 Just (contentT, body) -> succeedWith $ responseLBS status hdrs bdy
136135 where
137136 bdy = if allowedMethodHead method request then "" else body
@@ -150,8 +149,8 @@ methodRouter method proxy status action = LeafRouter route'
150149 handleA = handleAcceptH proxy (AcceptHeader accH) output
151150 processMethodRouter handleA status method Nothing request
152151 | pathIsEmpty request && requestMethod request /= method =
153− respond $ failWith WrongMethod
154− | otherwise = respond $ failWith NotFound
152+ respond $ failWith err405
153+ | otherwise = respond $ failWith err404
155154
156155methodRouterHeaders :: (GetHeaders (Headers h v), AllCTRender ctypes v)
157156 => Method -> Proxy ctypes -> Status
@@ -167,8 +166,8 @@ methodRouterHeaders method proxy status action = LeafRouter route'
167166 handleA = handleAcceptH proxy (AcceptHeader accH) (getResponse output)
168167 processMethodRouter handleA status method (Just headers) request
169168 | pathIsEmpty request && requestMethod request /= method =
170− respond $ failWith WrongMethod
171− | otherwise = respond $ failWith NotFound
169+ respond $ failWith err405
170+ | otherwise = respond $ failWith err404
172171
173172methodRouterEmpty :: Method
174173 -> IO (RouteResult (ExceptT ServantErr IO ()))
@@ -180,8 +179,8 @@ methodRouterEmpty method action = LeafRouter route'
180179 runAction action respond $ \ () ->
181180 succeedWith $ responseLBS noContent204 [] ""
182181 | pathIsEmpty request && requestMethod request /= method =
183− respond $ failWith WrongMethod
184− | otherwise = respond $ failWith NotFound
182+ respond $ failWith err405
183+ | otherwise = respond $ failWith err404
185184
186185-- | If you have a 'Delete' endpoint in your API,
187186-- the handler for this endpoint is meant to delete
@@ -558,8 +557,9 @@ instance HasServer Raw where
558557 route Proxy rawApplication = LeafRouter $ \ request respond -> do
559558 r <- rawApplication
560559 case r of
561− RR (Left err) -> respond $ failWith err
562− RR (Right app) -> app request (respond . succeedWith)
560+ HandlerVal app -> app request (respond . succeedWith)
561+ Retriable e -> respond $ failWith e
562+ NonRetriable e -> respond $! failFatallyWith e
563563
564564-- | If you use 'ReqBody' in one of the endpoints for your API,
565565-- this automatically requires your server-side handler to be a function
@@ -599,8 +599,8 @@ instance ( AllCTUnrender list a, HasServer sublayout
599599 mrqbody <- handleCTypeH (Proxy :: Proxy list) (cs contentTypeH)
600600 <$> lazyRequestBody request
601601 case mrqbody of
602− Nothing -> return $ failWith $ UnsupportedMediaType
603− Just (Left e) -> return $ failWith $ InvalidBody e
602+ Nothing -> return $! failFatallyWith err415
603+ Just (Left e) -> return $! failFatallyWith err400 { errBody = cs e }
604604 Just (Right v) -> feedTo subserver v
605605
606606-- | Make sure the incoming request starts with @"/path"@, strip it and
servant-server/src/Servant/Server/Internal/Router.hs+12−3
@@ -7,6 +7,8 @@ import qualified Data.Map as M
77import Data.Monoid ((<>))
88import Data.Text (Text)
99import Network.Wai (Request, Response, pathInfo)
10+import Servant.Server.Internal.ServantErr
11+import Servant.Server.Internal.PathInfo
1012import Servant.Server.Internal.RoutingApplication
1113
1214type Router = Router' RoutingApplication
@@ -63,17 +65,24 @@ runRouter (StaticRouter table) request respond =
6365 | Just router <- M.lookup first table
6466 -> let request' = request { pathInfo = rest }
6567 in runRouter router request' respond
66− _ -> respond $ failWith NotFound
68+ _ -> respond $ failWith err404
6769runRouter (DynamicRouter fun) request respond =
6870 case pathInfo request of
6971 first : rest
7072 -> let request' = request { pathInfo = rest }
7173 in runRouter (fun first) request' respond
72− _ -> respond $ failWith NotFound
74+ _ -> respond $ failWith err404
7375runRouter (LeafRouter app) request respond = app request respond
7476runRouter (Choice r1 r2) request respond =
7577 runRouter r1 request $ \ mResponse1 ->
7678 if isMismatch mResponse1
7779 then runRouter r2 request $ \ mResponse2 ->
78− respond (mResponse1 <> mResponse2)
80+ respond (highestPri mResponse1 mResponse2)
7981 else respond mResponse1
82+ where
83+ highestPri (Retriable r1) (Retriable r2) =
84+ if errHTTPCode r1 == 404 && errHTTPCode r2 /= 404
85+ then (Retriable r2)
86+ else (Retriable r1)
87+ highestPri (Retriable _) y = y
88+ highestPri x _ = x
servant-server/src/Servant/Server/Internal/RoutingApplication.hs+38−71
@@ -1,4 +1,5 @@
11{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DeriveFunctor #-}
23{-# LANGUAGE GeneralizedNewtypeDeriving #-}
34{-# LANGUAGE OverloadedStrings #-}
45{-# LANGUAGE TypeOperators #-}
@@ -6,21 +7,17 @@ module Servant.Server.Internal.RoutingApplication where
67
78#if !MIN_VERSION_base(4,8,0)
89import Control.Applicative (Applicative, (<$>))
9−import Data.Monoid (Monoid, mappend, mempty)
10+import Data.Monoid (Monoid, mappend, mempty,
11+ (<>))
1012#endif
1113import Control.Monad.Trans.Except (ExceptT, runExceptT)
1214import qualified Data.ByteString as B
1315import qualified Data.ByteString.Lazy as BL
1416import Data.IORef (newIORef, readIORef,
1517 writeIORef)
16−import Data.Maybe (fromMaybe)
17−import Data.Monoid ((<>))
18−import Data.String (fromString)
19−import Network.HTTP.Types hiding (Header,
20− ResponseHeaders)
2118import Network.Wai (Application, Request,
2219 Response, ResponseReceived,
23− requestBody, responseLBS,
20+ requestBody,
2421 strictRequestBody)
2522import Servant.API ((:<|>) (..))
2623import Servant.Server.Internal.ServantErr
@@ -30,39 +27,12 @@ type RoutingApplication =
3027 -> (RouteResult Response -> IO ResponseReceived) -> IO ResponseReceived
3128
3229-- | A wrapper around @'Either' 'RouteMismatch' a@.
33−newtype RouteResult a =
34− RR { routeResult :: Either RouteMismatch a }
35− deriving (Eq, Show, Functor, Applicative)
36−
37−-- | If we get a `Right`, it has precedence over everything else.
38−--
39−-- This in particular means that if we could get several 'Right's,
40−-- only the first we encounter would be taken into account.
41−instance Monoid (RouteResult a) where
42− mempty = RR $ Left mempty
43−
44− RR (Left x) `mappend` RR (Left y) = RR $ Left (x <> y)
45− RR (Left _) `mappend` RR (Right y) = RR $ Right y
46− r `mappend` _ = r
47−
48−-- Note that the ordering of the constructors has great significance! It
49−-- determines the Ord instance and, consequently, the monoid instance.
50−data RouteMismatch =
51− NotFound -- ^ the usual "not found" error
52− | WrongMethod -- ^ a more informative "you just got the HTTP method wrong" error
53− | UnsupportedMediaType -- ^ request body has unsupported media type
54− | InvalidBody String -- ^ an even more informative "your json request body wasn't valid" error
55− | HttpError Status (Maybe BL.ByteString) -- ^ an even even more informative arbitrary HTTP response code error.
56− deriving (Eq, Ord, Show)
57−
58−instance Monoid RouteMismatch where
59− mempty = NotFound
60− -- The following isn't great, since it picks @InvalidBody@ based on
61− -- alphabetical ordering, but any choice would be arbitrary.
62− --
63− -- "As one judge said to the other, 'Be just and if you can't be just, be
64− -- arbitrary'" -- William Burroughs
65− mappend = max
30+data RouteResult a =
31+ Retriable ServantErr -- ^ Keep trying other paths. The @ServantErr@
32+ -- should only be 404 or 405.
33+ | NonRetriable ServantErr -- ^ Stop trying.
34+ | HandlerVal a
35+ deriving (Eq, Show, Read, Functor)
6636
6737data ReqBodyState = Uncalled
6838 | Called !B.ByteString
@@ -91,55 +61,52 @@ toApplication ra request respond = do
9161 writeIORef reqBodyRef $ Called bs
9262 return B.empty
9363
94− ra request{ requestBody = memoReqBody } (routingRespond . routeResult)
64+ ra request{ requestBody = memoReqBody } routingRespond
9565 where
96− routingRespond :: Either RouteMismatch Response -> IO ResponseReceived
97− routingRespond (Left NotFound) =
98− respond $ responseLBS notFound404 [] "not found"
99− routingRespond (Left WrongMethod) =
100− respond $ responseLBS methodNotAllowed405 [] "method not allowed"
101− routingRespond (Left (InvalidBody err)) =
102− respond $ responseLBS badRequest400 [] $ fromString $ "invalid request body: " ++ err
103− routingRespond (Left UnsupportedMediaType) =
104− respond $ responseLBS unsupportedMediaType415 [] "unsupported media type"
105− routingRespond (Left (HttpError status body)) =
106− respond $ responseLBS status [] $ fromMaybe (BL.fromStrict $ statusMessage status) body
107− routingRespond (Right response) =
108− respond response
66+ routingRespond :: RouteResult Response -> IO ResponseReceived
67+ routingRespond (Retriable err) = respond $! responseServantErr err
68+ routingRespond (NonRetriable err) = respond $! responseServantErr err
69+ routingRespond (HandlerVal v) = respond v
10970
11071runAction :: IO (RouteResult (ExceptT ServantErr IO a))
11172 -> (RouteResult Response -> IO r)
11273 -> (a -> RouteResult Response)
11374 -> IO r
114−runAction action respond k = do
115− r <- action
116− go r
75+runAction action respond k = action >>= go >>= respond
11776 where
118− go (RR (Right a)) = do
77+ go (Retriable e) = return $! Retriable e
78+ go (NonRetriable e) = return . succeedWith $! responseServantErr e
79+ go (HandlerVal a) = do
11980 e <- runExceptT a
120− respond $ case e of
121− Right x -> k x
122− Left err -> succeedWith $ responseServantErr err
123− go (RR (Left err)) = respond $ failWith err
81+ case e of
82+ Left err -> return . succeedWith $! responseServantErr err
83+ Right x -> return $! k x
12484
12585feedTo :: IO (RouteResult (a -> b)) -> a -> IO (RouteResult b)
12686feedTo f x = (($ x) <$>) <$> f
12787
12888extractL :: RouteResult (a :<|> b) -> RouteResult a
129−extractL (RR (Right (a :<|> _))) = RR (Right a)
130−extractL (RR (Left err)) = RR (Left err)
89+extractL (HandlerVal (a :<|> _)) = HandlerVal a
90+extractL (Retriable x) = Retriable x
91+extractL (NonRetriable x) = NonRetriable x
13192
13293extractR :: RouteResult (a :<|> b) -> RouteResult b
133−extractR (RR (Right (_ :<|> b))) = RR (Right b)
134−extractR (RR (Left err)) = RR (Left err)
94+extractR (HandlerVal (_ :<|> b)) = HandlerVal b
95+extractR (Retriable x) = Retriable x
96+extractR (NonRetriable x) = NonRetriable x
97+
98+-- | Fail with a @ServantErr@, but keep trying other paths and.
99+failWith :: ServantErr -> RouteResult a
100+failWith = Retriable
135101
136−failWith :: RouteMismatch -> RouteResult a
137−failWith = RR . Left
102+-- | Fail with immediately @ServantErr@.
103+failFatallyWith :: ServantErr -> RouteResult a
104+failFatallyWith = NonRetriable
138105
106+-- | Return a value, and don't try other paths.
139107succeedWith :: a -> RouteResult a
140−succeedWith = RR . Right
108+succeedWith = HandlerVal
141109
142110isMismatch :: RouteResult a -> Bool
143−isMismatch (RR (Left _)) = True
111+isMismatch (Retriable _) = True
144112isMismatch _ = False
145−
servant-server/src/Servant/Server/Internal/ServantErr.hs+1−1
@@ -11,7 +11,7 @@ data ServantErr = ServantErr { errHTTPCode :: Int
1111 , errReasonPhrase :: String
1212 , errBody :: LBS.ByteString
1313 , errHeaders :: [HTTP.Header]
14− } deriving (Show, Eq)
14+ } deriving (Show, Eq, Read)
1515
1616responseServantErr :: ServantErr -> Response
1717responseServantErr ServantErr{..} = responseLBS status errHeaders errBody
servant-server/test/Servant/Server/ErrorSpec.hs+69−25
@@ -7,9 +7,9 @@ module Servant.Server.ErrorSpec (spec) where
77
88import Data.Aeson (encode)
99import qualified Data.ByteString.Lazy.Char8 as BC
10−import Control.Monad.Trans.Either (left)
1110import Data.Proxy
12−import Network.HTTP.Types (methodGet, methodPost)
11+import Network.HTTP.Types (hAccept, hContentType, methodGet,
12+ methodPost)
1313import Test.Hspec
1414import Test.Hspec.Wai
1515
@@ -31,8 +31,9 @@ import Servant
3131
3232spec :: Spec
3333spec = describe "HTTP Errors" $ do
34− errorOrder
35− errorRetry
34+ errorOrderSpec
35+ errorRetrySpec
36+ errorChoiceSpec
3637
3738------------------------------------------------------------------------------
3839-- * Error Order {{{
@@ -42,24 +43,24 @@ type ErrorOrderApi = "home"
4243 :> Capture "t" Int
4344 :> Post '[JSON] Int
4445
46+
4547errorOrderApi :: Proxy ErrorOrderApi
4648errorOrderApi = Proxy
4749
4850errorOrderServer :: Server ErrorOrderApi
49−errorOrderServer = \_ _ -> left err402
51+errorOrderServer = \_ _ -> return 5
5052
51−errorOrder :: Spec
52−errorOrder = describe "HTTP error order"
53+errorOrderSpec :: Spec
54+errorOrderSpec = describe "HTTP error order"
5355 $ with (return $ serve errorOrderApi errorOrderServer) $ do
54− let badContentType = ("Content-Type", "text/plain")
55− badAccept = ("Accept", "text/plain")
56+ let badContentType = (hContentType, "text/plain")
57+ badAccept = (hAccept, "text/plain")
5658 badMethod = methodGet
5759 badUrl = "home/nonexistent"
5860 badBody = "nonsense"
59− goodContentType = ("Content-Type", "application/json")
60− goodAccept = ("Accept", "application/json")
61+ goodContentType = (hContentType, "application/json")
6162 goodMethod = methodPost
62− goodUrl = "home/5"
63+ goodUrl = "home/2"
6364 goodBody = encode (5 :: Int)
6465
6566 it "has 404 as its highest priority error" $ do
@@ -82,10 +83,6 @@ errorOrder = describe "HTTP error order"
8283 request goodMethod goodUrl [goodContentType, badAccept] goodBody
8384 `shouldRespondWith` 406
8485
85− it "returns handler errors as its lower priority errors" $ do
86− request goodMethod goodUrl [goodContentType, goodAccept] goodBody
87− `shouldRespondWith` 402
88−
8986-- }}}
9087------------------------------------------------------------------------------
9188-- * Error Retry {{{
@@ -95,9 +92,10 @@ type ErrorRetryApi
9592 :<|> "a" :> ReqBody '[PlainText] Int :> Post '[JSON] Int -- 1
9693 :<|> "a" :> ReqBody '[JSON] Int :> Post '[PlainText] Int -- 2
9794 :<|> "a" :> ReqBody '[JSON] String :> Post '[JSON] Int -- 3
98− :<|> "a" :> ReqBody '[JSON] Int :> Get '[PlainText] Int -- 4
99− :<|> ReqBody '[JSON] Int :> Get '[JSON] Int -- 5
95+ :<|> "a" :> ReqBody '[JSON] Int :> Get '[JSON] Int -- 4
96+ :<|> "a" :> ReqBody '[JSON] Int :> Get '[PlainText] Int -- 5
10097 :<|> ReqBody '[JSON] Int :> Get '[JSON] Int -- 6
98+ :<|> ReqBody '[JSON] Int :> Post '[JSON] Int -- 7
10199
102100errorRetryApi :: Proxy ErrorRetryApi
103101errorRetryApi = Proxy
@@ -111,19 +109,21 @@ errorRetryServer
111109 :<|> (\_ -> return 4)
112110 :<|> (\_ -> return 5)
113111 :<|> (\_ -> return 6)
112+ :<|> (\_ -> return 7)
114113
115−errorRetry :: Spec
116−errorRetry = describe "Handler search"
114+errorRetrySpec :: Spec
115+errorRetrySpec = describe "Handler search"
117116 $ with (return $ serve errorRetryApi errorRetryServer) $ do
118− let plainCT = ("Content-Type", "text/plain")
119− plainAccept = ("Accept", "text/plain")
120− jsonCT = ("Content-Type", "application/json")
121− jsonAccept = ("Accept", "application/json")
117+
118+ let plainCT = (hContentType, "text/plain")
119+ plainAccept = (hAccept, "text/plain")
120+ jsonCT = (hContentType, "application/json")
121+ jsonAccept = (hAccept, "application/json")
122122 jsonBody = encode (1797 :: Int)
123123
124124 it "should continue when URLs don't match" $ do
125125 request methodPost "" [jsonCT, jsonAccept] jsonBody
126− `shouldRespondWith` 201 { matchBody = Just $ encode (5 :: Int) }
126+ `shouldRespondWith` 201 { matchBody = Just $ encode (7 :: Int) }
127127
128128 it "should continue when methods don't match" $ do
129129 request methodGet "a" [jsonCT, jsonAccept] jsonBody
@@ -141,6 +141,50 @@ errorRetry = describe "Handler search"
141141 request methodPost "a" [jsonCT, plainAccept] jsonBody
142142 `shouldRespondWith` 406
143143
144+-- }}}
145+------------------------------------------------------------------------------
146+-- * Error Choice {{{
147+
148+type ErrorChoiceApi
149+ = "path0" :> Get '[JSON] Int -- 0
150+ :<|> "path1" :> Post '[JSON] Int -- 1
151+ :<|> "path2" :> Post '[PlainText] Int -- 2
152+ :<|> "path3" :> ReqBody '[JSON] Int :> Post '[PlainText] Int -- 3
153+ :<|> "path4" :> (ReqBody '[PlainText] Int :> Post '[PlainText] Int -- 4
154+ :<|> ReqBody '[PlainText] Int :> Post '[JSON] Int) -- 5
155+
156+errorChoiceApi :: Proxy ErrorChoiceApi
157+errorChoiceApi = Proxy
158+
159+errorChoiceServer :: Server ErrorChoiceApi
160+errorChoiceServer = return 0
161+ :<|> return 1
162+ :<|> return 2
163+ :<|> (\_ -> return 3)
164+ :<|> (\_ -> return 4)
165+ :<|> (\_ -> return 5)
166+
167+
168+errorChoiceSpec :: Spec
169+errorChoiceSpec = describe "Multiple handlers return errors"
170+ $ with (return $ serve errorChoiceApi errorChoiceServer) $ do
171+
172+ it "should respond with 404 if no path matches" $ do
173+ request methodGet "" [] "" `shouldRespondWith` 404
174+
175+ it "should respond with 405 if a path but not method matches" $ do
176+ request methodGet "path2" [] "" `shouldRespondWith` 405
177+
178+ it "should respond with the corresponding error if path and method match" $ do
179+ request methodPost "path3" [(hContentType, "text/plain;charset=utf-8")] ""
180+ `shouldRespondWith` 415
181+ request methodPost "path3" [(hContentType, "application/json")] ""
182+ `shouldRespondWith` 400
183+ request methodPost "path4" [(hContentType, "text/plain;charset=utf-8"),
184+ (hAccept, "application/json")] ""
185+ `shouldRespondWith` 406
186+
187+
144188-- }}}
145189------------------------------------------------------------------------------
146190-- * Instances {{{
servant-server/test/Servant/ServerSpec.hs+10−57
@@ -17,7 +17,6 @@ import Control.Monad.Trans.Except (ExceptT, throwE)
1717import Data.Aeson (FromJSON, ToJSON, decode', encode)
1818import Data.ByteString.Conversion ()
1919import Data.Char (toUpper)
20−import Data.Monoid ((<>))
2120import Data.Proxy (Proxy (Proxy))
2221import Data.String (fromString)
2322import Data.String.Conversions (cs)
@@ -26,8 +25,7 @@ import GHC.Generics (Generic)
2625import Network.HTTP.Types (hAccept, hContentType,
2726 methodDelete, methodGet, methodHead,
2827 methodPatch, methodPost, methodPut,
29− ok200, parseQuery, status409,
30− Status(..))
28+ ok200, parseQuery, Status(..))
3129import Network.Wai (Application, Request, pathInfo,
3230 queryString, rawQueryString,
3331 responseLBS, responseBuilder)
@@ -99,7 +97,6 @@ spec = do
9997 rawSpec
10098 unionSpec
10199 prioErrorsSpec
102− errorsSpec
103100 routerSpec
104101 responseHeadersSpec
105102 miscReqCombinatorsSpec
@@ -158,9 +155,9 @@ getSpec = do
158155 it "returns 204 if the type is '()'" $ do
159156 get "/empty" `shouldRespondWith` ""{ matchStatus = 204 }
160157
161− it "returns 415 if the Accept header is not supported" $ do
158+ it "returns 406 if the Accept header is not supported" $ do
162159 Test.Hspec.Wai.request methodGet "" [(hAccept, "crazy/mime")] ""
163− `shouldRespondWith` 415
160+ `shouldRespondWith` 406
164161
165162
166163headSpec :: Spec
@@ -186,9 +183,9 @@ headSpec = do
186183 response <- Test.Hspec.Wai.request methodHead "/empty" [] ""
187184 return response `shouldRespondWith` ""{ matchStatus = 204 }
188185
189− it "returns 415 if the Accept header is not supported" $ do
186+ it "returns 406 if the Accept header is not supported" $ do
190187 Test.Hspec.Wai.request methodHead "" [(hAccept, "crazy/mime")] ""
191− `shouldRespondWith` 415
188+ `shouldRespondWith` 406
192189
193190
194191type QueryParamApi = QueryParam "name" String :> Get '[JSON] Person
@@ -311,7 +308,7 @@ postSpec = do
311308 it "returns 204 if the type is '()'" $ do
312309 post' "empty" "" `shouldRespondWith` ""{ matchStatus = 204 }
313310
314− it "responds with 415 if the requested media type is unsupported" $ do
311+ it "responds with 415 if the request body media type is unsupported" $ do
315312 let post'' x = Test.Hspec.Wai.request methodPost x [(hContentType
316313 , "application/nonsense")]
317314 post'' "/" "anything at all" `shouldRespondWith` 415
@@ -353,7 +350,7 @@ putSpec = do
353350 it "returns 204 if the type is '()'" $ do
354351 put' "empty" "" `shouldRespondWith` ""{ matchStatus = 204 }
355352
356− it "responds with 415 if the requested media type is unsupported" $ do
353+ it "responds with 415 if the request body media type is unsupported" $ do
357354 let put'' x = Test.Hspec.Wai.request methodPut x [(hContentType
358355 , "application/nonsense")]
359356 put'' "/" "anything at all" `shouldRespondWith` 415
@@ -395,7 +392,7 @@ patchSpec = do
395392 it "returns 204 if the type is '()'" $ do
396393 patch' "empty" "" `shouldRespondWith` ""{ matchStatus = 204 }
397394
398− it "responds with 415 if the requested media type is unsupported" $ do
395+ it "responds with 415 if the request body media type is unsupported" $ do
399396 let patch'' x = Test.Hspec.Wai.request methodPatch x [(hContentType
400397 , "application/nonsense")]
401398 patch'' "/" "anything at all" `shouldRespondWith` 415
@@ -524,10 +521,10 @@ responseHeadersSpec = describe "ResponseHeaders" $ do
524521 Test.Hspec.Wai.request method "blahblah" [] ""
525522 `shouldRespondWith` 404
526523
527− it "returns 415 if the Accept header is not supported" $
524+ it "returns 406 if the Accept header is not supported" $
528525 forM_ methods $ \(method,_) ->
529526 Test.Hspec.Wai.request method "" [(hAccept, "crazy/mime")] ""
530− `shouldRespondWith` 415
527+ `shouldRespondWith` 406
531528
532529type PrioErrorsApi = ReqBody '[JSON] Person :> "foo" :> Get '[JSON] Integer
533530
@@ -578,50 +575,6 @@ prioErrorsSpec = describe "PrioErrors" $ do
578575 check put' "/bar" vjson 404
579576 check put' "/foo" vjson 405
580577
581−-- | Test server error functionality.
582−errorsSpec :: Spec
583−errorsSpec = do
584− let he = HttpError status409 (Just "A custom error")
585− let ib = InvalidBody "The body is invalid"
586− let wm = WrongMethod
587− let nf = NotFound
588−
589− describe "Servant.Server.Internal.RouteMismatch" $ do
590− it "HttpError > *" $ do
591− ib <> he `shouldBe` he
592− wm <> he `shouldBe` he
593− nf <> he `shouldBe` he
594−
595− he <> ib `shouldBe` he
596− he <> wm `shouldBe` he
597− he <> nf `shouldBe` he
598−
599− it "HE > InvalidBody > (WM,NF)" $ do
600− he <> ib `shouldBe` he
601− wm <> ib `shouldBe` ib
602− nf <> ib `shouldBe` ib
603−
604− ib <> he `shouldBe` he
605− ib <> wm `shouldBe` ib
606− ib <> nf `shouldBe` ib
607−
608− it "HE > IB > WrongMethod > NF" $ do
609− he <> wm `shouldBe` he
610− ib <> wm `shouldBe` ib
611− nf <> wm `shouldBe` wm
612−
613− wm <> he `shouldBe` he
614− wm <> ib `shouldBe` ib
615− wm <> nf `shouldBe` wm
616−
617− it "* > NotFound" $ do
618− he <> nf `shouldBe` he
619− ib <> nf `shouldBe` ib
620− wm <> nf `shouldBe` wm
621−
622− nf <> he `shouldBe` he
623− nf <> ib `shouldBe` ib
624− nf <> wm `shouldBe` wm
625578
626579routerSpec :: Spec
627580routerSpec = do
servant/test/Servant/API/ContentTypesSpec.hs+15
@@ -3,6 +3,7 @@
33{-# LANGUAGE DeriveGeneric #-}
44{-# LANGUAGE MultiParamTypeClasses #-}
55{-# LANGUAGE OverloadedStrings #-}
6+{-# LANGUAGE PolyKinds #-}
67{-# OPTIONS_GHC -fno-warn-orphans #-}
78module Servant.API.ContentTypesSpec where
89
@@ -34,6 +35,20 @@ import Servant.API.ContentTypes
3435spec :: Spec
3536spec = describe "Servant.API.ContentTypes" $ do
3637
38+ describe "handleAcceptH" $ do
39+ let p = Proxy :: Proxy '[PlainText]
40+
41+ it "matches any charset if none were provided" $ do
42+ let without = handleAcceptH p (AcceptHeader "text/plain")
43+ with = handleAcceptH p (AcceptHeader "text/plain;charset=utf-8")
44+ wisdom = "ubi sub ubi" :: String
45+ without wisdom `shouldBe` with wisdom
46+
47+ it "does not match non utf-8 charsets" $ do
48+ let badCharset = handleAcceptH p (AcceptHeader "text/plain;charset=whoknows")
49+ s = "cheese" :: String
50+ badCharset s `shouldBe` Nothing
51+
3752 describe "The JSON Content-Type type" $ do
3853 let p = Proxy :: Proxy JSON
3954