git.iko.soy/typeable/servant
on master help sign in
Cleanup errorspec description of routing, changelog.

Review fixes

commitccadba81ec85222b19b4c3555bf0ecebd464762d authorJulian K. Arni <jkarni@gmail.com> dateTue 15 Sep 2015, 11:37 (11 years ago) parenta3b5652
8 files changed+154−145
servant-examples/auth-combinator/auth-combinator.hs+2−2 servant-server/CHANGELOG.md+5 servant-server/src/Servant/Server.hs+1−1 servant-server/src/Servant/Server/Internal.hs+29−29 servant-server/src/Servant/Server/Internal/Router.hs+12−14 servant-server/src/Servant/Server/Internal/RoutingApplication.hs+16−33 servant-server/test/Servant/Server/ErrorSpec.hs+70−16 servant-server/test/Servant/ServerSpec.hs+19−50
servant-examples/auth-combinator/auth-combinator.hs+2−2
@@ -32,12 +32,12 @@ instance HasServer rest => HasServer (AuthProtected :> rest) where
3232 route Proxy a = WithRequest $ \ request ->
3333 route (Proxy :: Proxy rest) $ do
3434 case lookup "Cookie" (requestHeaders request) of
35− Nothing -> return $! failFatallyWith err401 { errBody = "Missing auth header" }
35+ Nothing -> return $! FailFatal err401 { errBody = "Missing auth header" }
3636 Just v -> do
3737 authGranted <- isGoodCookie v
3838 if authGranted
3939 then a
40− else return $! failFatallyWith err403 { errBody = "Invalid cookie" }
40+ else return $ FailFatal err403 { errBody = "Invalid cookie" }
4141
4242type PrivateAPI = Get '[JSON] [PrivateData]
4343
servant-server/CHANGELOG.md+5
@@ -5,6 +5,11 @@ HEAD
55* Drop `EitherT` in favor of `ExceptT`
66* Use `http-api-data` instead of `Servant.Common.Text`
77* Remove matrix params.
8+* Remove `RouteMismatch`.
9+* Redefined constructors of `RouteResult`.
10+* Add `failFatallyWith`.
11+* Make all (framework-generated) HTTP errors except 404 and 405 not try other
12+ handlers.
813
9140.4.1
1015-----
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 (HandlerVal server))))
106+serve p server = toApplication (runRouter (route p (return (Route server))))
107107
108108
109109-- Documentation
servant-server/src/Servant/Server/Internal.hs+29−29
@@ -1,15 +1,15 @@
1−{-# LANGUAGE CPP #-}
2−{-# LANGUAGE DataKinds #-}
3−{-# LANGUAGE FlexibleContexts #-}
4−{-# LANGUAGE FlexibleInstances #-}
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE FlexibleContexts #-}
4+{-# LANGUAGE FlexibleInstances #-}
55{-# LANGUAGE GeneralizedNewtypeDeriving #-}
6−{-# LANGUAGE OverloadedStrings #-}
7−{-# LANGUAGE PolyKinds #-}
8−{-# LANGUAGE ScopedTypeVariables #-}
9−{-# LANGUAGE TypeFamilies #-}
10−{-# LANGUAGE TypeOperators #-}
6+{-# LANGUAGE OverloadedStrings #-}
7+{-# LANGUAGE PolyKinds #-}
8+{-# LANGUAGE ScopedTypeVariables #-}
9+{-# LANGUAGE TypeFamilies #-}
10+{-# LANGUAGE TypeOperators #-}
1111#if !MIN_VERSION_base(4,8,0)
12−{-# LANGUAGE OverlappingInstances #-}
12+{-# LANGUAGE OverlappingInstances #-}
1313#endif
1414
1515module Servant.Server.Internal
@@ -26,9 +26,9 @@ import Control.Monad.Trans.Except (ExceptT)
2626import qualified Data.ByteString as B
2727import qualified Data.ByteString.Lazy as BL
2828import qualified Data.Map as M
29−import Data.Maybe (mapMaybe, fromMaybe)
29+import Data.Maybe (fromMaybe, mapMaybe)
3030import Data.String (fromString)
31−import Data.String.Conversions (cs, (<>), ConvertibleStrings)
31+import Data.String.Conversions (ConvertibleStrings, cs, (<>))
3232import Data.Text (Text)
3333import Data.Typeable
3434import GHC.TypeLits (KnownSymbol, symbolVal)
@@ -47,8 +47,8 @@ import Servant.API ((:<|>) (..), (:>), Capture,
4747import Servant.API.ContentTypes (AcceptHeader (..),
4848 AllCTRender (..),
4949 AllCTUnrender (..))
50−import Servant.API.ResponseHeaders (Headers, getResponse, GetHeaders,
51− getHeaders)
50+import Servant.API.ResponseHeaders (GetHeaders, Headers, getHeaders,
51+ getResponse)
5252
5353import Servant.Server.Internal.Router
5454import Servant.Server.Internal.RoutingApplication
@@ -114,7 +114,7 @@ instance (KnownSymbol capture, FromHttpApiData a, HasServer sublayout)
114114
115115 route Proxy subserver =
116116 DynamicRouter $ \ first -> case captured captureProxy first of
117− Nothing -> LeafRouter (\_ r -> r $ failWith err404)
117+ Nothing -> LeafRouter (\_ r -> r $ Fail err404)
118118 Just v -> route (Proxy :: Proxy sublayout) (feedTo subserver v)
119119
120120 where captureProxy = Proxy :: Proxy (Capture capture a)
@@ -130,8 +130,8 @@ processMethodRouter :: forall a. ConvertibleStrings a B.ByteString
130130 -> Maybe [(HeaderName, B.ByteString)]
131131 -> Request -> RouteResult Response
132132processMethodRouter handleA status method headers request = case handleA of
133− Nothing -> failFatallyWith err406
134− Just (contentT, body) -> succeedWith $ responseLBS status hdrs bdy
133+ Nothing -> FailFatal err406
134+ Just (contentT, body) -> Route $! responseLBS status hdrs bdy
135135 where
136136 bdy = if allowedMethodHead method request then "" else body
137137 hdrs = (hContentType, cs contentT) : (fromMaybe [] headers)
@@ -149,8 +149,8 @@ methodRouter method proxy status action = LeafRouter route'
149149 handleA = handleAcceptH proxy (AcceptHeader accH) output
150150 processMethodRouter handleA status method Nothing request
151151 | pathIsEmpty request && requestMethod request /= method =
152− respond $ failWith err405
153− | otherwise = respond $ failWith err404
152+ respond $ Fail err405
153+ | otherwise = respond $ Fail err404
154154
155155methodRouterHeaders :: (GetHeaders (Headers h v), AllCTRender ctypes v)
156156 => Method -> Proxy ctypes -> Status
@@ -166,8 +166,8 @@ methodRouterHeaders method proxy status action = LeafRouter route'
166166 handleA = handleAcceptH proxy (AcceptHeader accH) (getResponse output)
167167 processMethodRouter handleA status method (Just headers) request
168168 | pathIsEmpty request && requestMethod request /= method =
169− respond $ failWith err405
170− | otherwise = respond $ failWith err404
169+ respond $ Fail err405
170+ | otherwise = respond $ Fail err404
171171
172172methodRouterEmpty :: Method
173173 -> IO (RouteResult (ExceptT ServantErr IO ()))
@@ -177,10 +177,10 @@ methodRouterEmpty method action = LeafRouter route'
177177 route' request respond
178178 | pathIsEmpty request && allowedMethod method request = do
179179 runAction action respond $ \ () ->
180− succeedWith $ responseLBS noContent204 [] ""
180+ Route $! responseLBS noContent204 [] ""
181181 | pathIsEmpty request && requestMethod request /= method =
182− respond $ failWith err405
183− | otherwise = respond $ failWith err404
182+ respond $ Fail err405
183+ | otherwise = respond $ Fail err404
184184
185185-- | If you have a 'Delete' endpoint in your API,
186186-- the handler for this endpoint is meant to delete
@@ -557,9 +557,9 @@ instance HasServer Raw where
557557 route Proxy rawApplication = LeafRouter $ \ request respond -> do
558558 r <- rawApplication
559559 case r of
560− HandlerVal app -> app request (respond . succeedWith)
561− Retriable e -> respond $ failWith e
562− NonRetriable e -> respond $! failFatallyWith e
560+ Route app -> app request (respond . Route)
561+ Fail a -> respond $ Fail a
562+ FailFatal e -> respond $ FailFatal 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 $! failFatallyWith err415
603− Just (Left e) -> return $! failFatallyWith err400 { errBody = cs e }
602+ Nothing -> return $ FailFatal err415
603+ Just (Left e) -> return $ FailFatal 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−14
@@ -1,10 +1,9 @@
11{-# LANGUAGE DeriveFunctor #-}
2−
2+{-# LANGUAGE CPP #-}
33module Servant.Server.Internal.Router where
44
55import Data.Map (Map)
66import qualified Data.Map as M
7−import Data.Monoid ((<>))
87import Data.Text (Text)
98import Network.Wai (Request, Response, pathInfo)
109import Servant.Server.Internal.ServantErr
@@ -65,24 +64,23 @@ runRouter (StaticRouter table) request respond =
6564 | Just router <- M.lookup first table
6665 -> let request' = request { pathInfo = rest }
6766 in runRouter router request' respond
68− _ -> respond $ failWith err404
67+ _ -> respond $ Fail err404
6968runRouter (DynamicRouter fun) request respond =
7069 case pathInfo request of
7170 first : rest
7271 -> let request' = request { pathInfo = rest }
7372 in runRouter (fun first) request' respond
74− _ -> respond $ failWith err404
73+ _ -> respond $ Fail err404
7574runRouter (LeafRouter app) request respond = app request respond
7675runRouter (Choice r1 r2) request respond =
77− runRouter r1 request $ \ mResponse1 ->
78− if isMismatch mResponse1
79− then runRouter r2 request $ \ mResponse2 ->
80− respond (highestPri mResponse1 mResponse2)
81− else respond mResponse1
76+ runRouter r1 request $ \ mResponse1 -> case mResponse1 of
77+ Fail _ -> runRouter r2 request $ \ mResponse2 ->
78+ respond (highestPri mResponse1 mResponse2)
79+ _ -> respond mResponse1
8280 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
81+ highestPri (Fail e1) (Fail e2) =
82+ if errHTTPCode e1 == 404 && errHTTPCode e2 /= 404
83+ then Fail e2
84+ else Fail e1
85+ highestPri (Fail _) y = y
8886 highestPri x _ = x
servant-server/src/Servant/Server/Internal/RoutingApplication.hs+16−33
@@ -1,6 +1,5 @@
11{-# LANGUAGE CPP #-}
22{-# LANGUAGE DeriveFunctor #-}
3−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
43{-# LANGUAGE OverloadedStrings #-}
54{-# LANGUAGE TypeOperators #-}
65module Servant.Server.Internal.RoutingApplication where
@@ -28,10 +27,10 @@ type RoutingApplication =
2827
2928-- | A wrapper around @'Either' 'RouteMismatch' a@.
3029data RouteResult a =
31− Retriable ServantErr -- ^ Keep trying other paths. The @ServantErr@
30+ Fail ServantErr -- ^ Keep trying other paths. The @ServantErr@
3231 -- should only be 404 or 405.
33− | NonRetriable ServantErr -- ^ Stop trying.
34− | HandlerVal a
32+ | FailFatal ServantErr -- ^ Don't other paths.
33+ | Route a
3534 deriving (Eq, Show, Read, Functor)
3635
3736data ReqBodyState = Uncalled
@@ -64,9 +63,9 @@ toApplication ra request respond = do
6463 ra request{ requestBody = memoReqBody } routingRespond
6564 where
6665 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
66+ routingRespond (Fail err) = respond $! responseServantErr err
67+ routingRespond (FailFatal err) = respond $! responseServantErr err
68+ routingRespond (Route v) = respond v
7069
7170runAction :: IO (RouteResult (ExceptT ServantErr IO a))
7271 -> (RouteResult Response -> IO r)
@@ -74,39 +73,23 @@ runAction :: IO (RouteResult (ExceptT ServantErr IO a))
7473 -> IO r
7574runAction action respond k = action >>= go >>= respond
7675 where
77− go (Retriable e) = return $! Retriable e
78− go (NonRetriable e) = return . succeedWith $! responseServantErr e
79− go (HandlerVal a) = do
76+ go (Fail e) = return $ Fail e
77+ go (FailFatal e) = return $ FailFatal e
78+ go (Route a) = do
8079 e <- runExceptT a
8180 case e of
82− Left err -> return . succeedWith $! responseServantErr err
81+ Left err -> return . Route $ responseServantErr err
8382 Right x -> return $! k x
8483
8584feedTo :: IO (RouteResult (a -> b)) -> a -> IO (RouteResult b)
8685feedTo f x = (($ x) <$>) <$> f
8786
8887extractL :: RouteResult (a :<|> b) -> RouteResult a
89−extractL (HandlerVal (a :<|> _)) = HandlerVal a
90−extractL (Retriable x) = Retriable x
91−extractL (NonRetriable x) = NonRetriable x
88+extractL (Route (a :<|> _)) = Route a
89+extractL (Fail x) = Fail x
90+extractL (FailFatal x) = FailFatal x
9291
9392extractR :: RouteResult (a :<|> b) -> RouteResult b
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
101−
102−-- | Fail with immediately @ServantErr@.
103−failFatallyWith :: ServantErr -> RouteResult a
104−failFatallyWith = NonRetriable
105−
106−-- | Return a value, and don't try other paths.
107−succeedWith :: a -> RouteResult a
108−succeedWith = HandlerVal
109−
110−isMismatch :: RouteResult a -> Bool
111−isMismatch (Retriable _) = True
112−isMismatch _ = False
93+extractR (Route (_ :<|> b)) = Route b
94+extractR (Fail x) = Fail x
95+extractR (FailFatal x) = FailFatal x
servant-server/test/Servant/Server/ErrorSpec.hs+70−16
@@ -6,32 +6,38 @@
66module Servant.Server.ErrorSpec (spec) where
77
88import Data.Aeson (encode)
9−import qualified Data.ByteString.Lazy.Char8 as BC
9+import qualified Data.ByteString.Lazy.Char8 as BCL
10+import qualified Data.ByteString.Char8 as BC
1011import Data.Proxy
1112import Network.HTTP.Types (hAccept, hContentType, methodGet,
12− methodPost)
13+ methodPost, methodPut)
1314import Test.Hspec
1415import Test.Hspec.Wai
1516
1617import Servant
1718
1819
19−-- 1) Check whether one or more endpoints have the right path. Otherwise return 404.
20−-- 2) Check whether the one of those have the right method. Otherwise return
21−-- 405. If so, pick the first. We've now commited to calling at most one handler. *
22−-- 3) Check whether the Content-Type is known. Otherwise return 415.
23−-- 4) Check whether that one deserializes the body. Otherwise return 400. If there
24−-- was no Content-Type, try the first one of the API content-type list.
25−-- 5) Check whether the request is authorized. Otherwise return a 401.
26−-- 6) Check whether the request is forbidden. If so return 403.
27−-- 7) Check whether the request has a known Accept. Otherwise return 406.
28−-- 8) Check whether Accept-Language, Accept-Charset and Accept-Encoding exist and
29−-- match. We can follow the webmachine order here.
30−-- 9) Call the handler. Whatever it returns, we return.
20+-- The semantics of routing and handling requests should be as follows:
21+--
22+-- 1) Check whether one or more endpoints have the right path. Otherwise
23+-- return 404.
24+-- 2) Check whether the one of those have the right method. Otherwise return
25+-- 405. If so, pick the first. We've now committed to calling at most one
26+-- handler.
27+-- 3) Check whether the Content-Type is known. Otherwise return 415.
28+-- 4) Check whether that one deserializes the body. Otherwise return 400. If
29+-- there was no Content-Type, try the first one of the API content-type list.
30+-- 5) Check whether the request is authorized. Otherwise return a 401.
31+-- 6) Check whether the request is forbidden. If so return 403.
32+-- 7) Check whether the request has a known Accept. Otherwise return 406.
33+-- 8) Check whether Accept-Language, Accept-Charset and Accept-Encoding
34+-- exist and match. We can follow the webmachine order here.
35+-- 9) Call the handler. Whatever it returns, we return.
3136
3237spec :: Spec
3338spec = describe "HTTP Errors" $ do
3439 errorOrderSpec
40+ prioErrorsSpec
3541 errorRetrySpec
3642 errorChoiceSpec
3743
@@ -83,6 +89,52 @@ errorOrderSpec = describe "HTTP error order"
8389 request goodMethod goodUrl [goodContentType, badAccept] goodBody
8490 `shouldRespondWith` 406
8591
92+type PrioErrorsApi = ReqBody '[JSON] Integer :> "foo" :> Get '[JSON] Integer
93+
94+prioErrorsApi :: Proxy PrioErrorsApi
95+prioErrorsApi = Proxy
96+
97+-- Check whether matching continues even if a 'ReqBody' or similar construct
98+-- is encountered early in a path. We don't want to see a complaint about the
99+-- request body unless the path actually matches.
100+prioErrorsSpec :: Spec
101+prioErrorsSpec = describe "PrioErrors" $ do
102+ let server = return
103+ with (return $ serve prioErrorsApi server) $ do
104+ let check (mdescr, method) path (cdescr, ctype, body) resp =
105+ it fulldescr $
106+ Test.Hspec.Wai.request method path [(hContentType, ctype)] body
107+ `shouldRespondWith` resp
108+ where
109+ fulldescr = "returns " ++ show (matchStatus resp) ++ " on " ++ mdescr
110+ ++ " " ++ (BC.unpack path) ++ " (" ++ cdescr ++ ")"
111+
112+ get' = ("GET", methodGet)
113+ put' = ("PUT", methodPut)
114+
115+ txt = ("text" , "text/plain;charset=utf8" , "42" )
116+ ijson = ("invalid json", "application/json;charset=utf8", "invalid" )
117+ vjson = ("valid json" , "application/json;charset=utf8", encode (5 :: Int))
118+
119+ check get' "/" txt 404
120+ check get' "/bar" txt 404
121+ check get' "/foo" txt 415
122+ check put' "/" txt 404
123+ check put' "/bar" txt 404
124+ check put' "/foo" txt 405
125+ check get' "/" ijson 404
126+ check get' "/bar" ijson 404
127+ check get' "/foo" ijson 400
128+ check put' "/" ijson 404
129+ check put' "/bar" ijson 404
130+ check put' "/foo" ijson 405
131+ check get' "/" vjson 404
132+ check get' "/bar" vjson 404
133+ check get' "/foo" vjson 200
134+ check put' "/" vjson 404
135+ check put' "/bar" vjson 404
136+ check put' "/foo" vjson 405
137+
86138-- }}}
87139------------------------------------------------------------------------------
88140-- * Error Retry {{{
@@ -190,8 +242,10 @@ errorChoiceSpec = describe "Multiple handlers return errors"
190242-- * Instances {{{
191243
192244instance MimeUnrender PlainText Int where
193− mimeUnrender _ = Right . read . BC.unpack
245+ mimeUnrender _ = Right . read . BCL.unpack
194246
195247instance MimeRender PlainText Int where
196− mimeRender _ = BC.pack . show
248+ mimeRender _ = BCL.pack . show
197249-- }}}
250+--
251+
servant-server/test/Servant/ServerSpec.hs+19−50
@@ -1,11 +1,20 @@
1+<<<<<<< HEAD
12{-# LANGUAGE CPP #-}
23{-# LANGUAGE DataKinds #-}
34{-# LANGUAGE DeriveGeneric #-}
5+=======
6+{-# LANGUAGE DataKinds #-}
7+{-# LANGUAGE DeriveGeneric #-}
8+{-# LANGUAGE FlexibleInstances #-}
9+>>>>>>> Review fixes
410{-# LANGUAGE OverloadedStrings #-}
511{-# LANGUAGE ScopedTypeVariables #-}
612{-# LANGUAGE TypeOperators #-}
713{-# LANGUAGE TypeSynonymInstances #-}
14+<<<<<<< HEAD
815{-# LANGUAGE FlexibleInstances #-}
16+=======
17+>>>>>>> Review fixes
918
1019module Servant.ServerSpec where
1120
@@ -32,6 +41,16 @@ import Network.Wai (Application, Request, pathInfo,
3241import Network.Wai.Internal (Response(ResponseBuilder))
3342import Network.Wai.Test (defaultRequest, request,
3443 runSession, simpleBody)
44+import Servant.API ((:<|>) (..), (:>), Capture, Delete,
45+ Get, Header (..), Headers,
46+ HttpVersion, IsSecure (..), JSON,
47+ MatrixFlag, MatrixParam,
48+ MatrixParams, Patch, PlainText,
49+ Post, Put, QueryFlag, QueryParam,
50+ QueryParams, Raw, RemoteHost,
51+ ReqBody, addHeader)
52+import Servant.Server (ServantErr (..), Server, err404,
53+ serve)
3554import Test.Hspec (Spec, describe, it, shouldBe)
3655import Test.Hspec.Wai (get, liftIO, matchHeaders,
3756 matchStatus, post, request,
@@ -96,7 +115,6 @@ spec = do
96115 headerSpec
97116 rawSpec
98117 unionSpec
99− prioErrorsSpec
100118 routerSpec
101119 responseHeadersSpec
102120 miscReqCombinatorsSpec
@@ -526,55 +544,6 @@ responseHeadersSpec = describe "ResponseHeaders" $ do
526544 Test.Hspec.Wai.request method "" [(hAccept, "crazy/mime")] ""
527545 `shouldRespondWith` 406
528546
529−type PrioErrorsApi = ReqBody '[JSON] Person :> "foo" :> Get '[JSON] Integer
530−
531−prioErrorsApi :: Proxy PrioErrorsApi
532−prioErrorsApi = Proxy
533−
534−-- | Test the relative priority of error responses from the server.
535−--
536−-- In particular, we check whether matching continues even if a 'ReqBody'
537−-- or similar construct is encountered early in a path. We don't want to
538−-- see a complaint about the request body unless the path actually matches.
539−--
540−prioErrorsSpec :: Spec
541−prioErrorsSpec = describe "PrioErrors" $ do
542− let server = return . age
543− with (return $ serve prioErrorsApi server) $ do
544− let check (mdescr, method) path (cdescr, ctype, body) resp =
545− it fulldescr $
546− Test.Hspec.Wai.request method path [(hContentType, ctype)] body
547− `shouldRespondWith` resp
548− where
549− fulldescr = "returns " ++ show (matchStatus resp) ++ " on " ++ mdescr
550− ++ " " ++ cs path ++ " (" ++ cdescr ++ ")"
551−
552− get' = ("GET", methodGet)
553− put' = ("PUT", methodPut)
554−
555− txt = ("text" , "text/plain;charset=utf8" , "42" )
556− ijson = ("invalid json", "application/json;charset=utf8", "invalid" )
557− vjson = ("valid json" , "application/json;charset=utf8", encode alice)
558−
559− check get' "/" txt 404
560− check get' "/bar" txt 404
561− check get' "/foo" txt 415
562− check put' "/" txt 404
563− check put' "/bar" txt 404
564− check put' "/foo" txt 405
565− check get' "/" ijson 404
566− check get' "/bar" ijson 404
567− check get' "/foo" ijson 400
568− check put' "/" ijson 404
569− check put' "/bar" ijson 404
570− check put' "/foo" ijson 405
571− check get' "/" vjson 404
572− check get' "/bar" vjson 404
573− check get' "/foo" vjson 200
574− check put' "/" vjson 404
575− check put' "/bar" vjson 404
576− check put' "/foo" vjson 405
577−
578547
579548routerSpec :: Spec
580549routerSpec = do