2018-06-29 21:08:26 +02:00
|
|
|
{-# LANGUAGE DataKinds #-}
|
|
|
|
{-# LANGUAGE FlexibleInstances #-}
|
|
|
|
{-# LANGUAGE KindSignatures #-}
|
2017-01-18 11:17:38 +01:00
|
|
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
2018-06-29 21:08:26 +02:00
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
|
|
{-# LANGUAGE TypeFamilies #-}
|
|
|
|
{-# LANGUAGE TypeOperators #-}
|
2017-01-12 02:58:29 +01:00
|
|
|
module Servant.Server.Internal.RoutingApplicationSpec (spec) where
|
|
|
|
|
2018-06-29 21:08:26 +02:00
|
|
|
import Prelude ()
|
|
|
|
import Prelude.Compat
|
2017-01-17 21:36:28 +01:00
|
|
|
|
2020-11-18 19:57:20 +01:00
|
|
|
import Control.Exception hiding
|
2018-06-29 21:08:26 +02:00
|
|
|
(Handler)
|
|
|
|
import Control.Monad.IO.Class
|
|
|
|
import Control.Monad.Trans.Resource
|
|
|
|
(register)
|
|
|
|
import Data.IORef
|
|
|
|
import Data.Proxy
|
|
|
|
import GHC.TypeLits
|
|
|
|
(KnownSymbol, Symbol, symbolVal)
|
|
|
|
import Network.Wai
|
|
|
|
(defaultRequest)
|
|
|
|
import Servant
|
2019-02-27 12:04:33 +01:00
|
|
|
import Servant.Server.Internal
|
2018-06-29 21:08:26 +02:00
|
|
|
import Test.Hspec
|
|
|
|
import Test.Hspec.Wai
|
|
|
|
(request, shouldRespondWith, with)
|
2017-01-18 11:17:38 +01:00
|
|
|
|
2020-11-18 19:57:20 +01:00
|
|
|
import qualified Data.Text as T
|
2017-01-12 02:58:29 +01:00
|
|
|
|
2018-06-29 21:08:26 +02:00
|
|
|
import System.IO.Unsafe
|
|
|
|
(unsafePerformIO)
|
2017-01-17 21:36:28 +01:00
|
|
|
|
2017-01-18 23:55:04 +01:00
|
|
|
data TestResource x
|
|
|
|
= TestResourceNone
|
|
|
|
| TestResource x
|
|
|
|
| TestResourceFreed
|
|
|
|
| TestResourceError
|
|
|
|
deriving (Eq, Show)
|
|
|
|
|
2017-01-17 21:36:28 +01:00
|
|
|
-- Let's not write to the filesystem
|
2017-01-18 23:55:04 +01:00
|
|
|
delayedTestRef :: IORef (TestResource String)
|
|
|
|
delayedTestRef = unsafePerformIO $ newIORef TestResourceNone
|
|
|
|
|
|
|
|
fromTestResource :: a -> (b -> a) -> TestResource b -> a
|
|
|
|
fromTestResource _ f (TestResource x) = f x
|
|
|
|
fromTestResource x _ _ = x
|
|
|
|
|
|
|
|
initTestResource :: IO ()
|
|
|
|
initTestResource = writeIORef delayedTestRef TestResourceNone
|
|
|
|
|
|
|
|
writeTestResource :: String -> IO ()
|
|
|
|
writeTestResource x = modifyIORef delayedTestRef $ \r -> case r of
|
|
|
|
TestResourceNone -> TestResource x
|
|
|
|
_ -> TestResourceError
|
|
|
|
|
|
|
|
freeTestResource :: IO ()
|
|
|
|
freeTestResource = modifyIORef delayedTestRef $ \r -> case r of
|
|
|
|
TestResource _ -> TestResourceFreed
|
|
|
|
_ -> TestResourceError
|
2017-01-17 21:36:28 +01:00
|
|
|
|
2017-01-12 02:58:29 +01:00
|
|
|
delayed :: DelayedIO () -> RouteResult (Handler ()) -> Delayed () (Handler ())
|
|
|
|
delayed body srv = Delayed
|
2017-01-16 13:17:20 +01:00
|
|
|
{ capturesD = \() -> return ()
|
2017-01-18 10:37:18 +01:00
|
|
|
, methodD = return ()
|
|
|
|
, authD = return ()
|
2017-01-16 13:17:20 +01:00
|
|
|
, acceptD = return ()
|
|
|
|
, contentD = return ()
|
2016-12-12 15:17:06 +01:00
|
|
|
, paramsD = return ()
|
2017-04-06 13:59:16 +02:00
|
|
|
, headersD = return ()
|
2017-01-16 13:17:20 +01:00
|
|
|
, bodyD = \() -> do
|
|
|
|
liftIO (writeTestResource "hia" >> putStrLn "garbage created")
|
2017-01-18 23:55:04 +01:00
|
|
|
_ <- register (freeTestResource >> putStrLn "garbage collected")
|
2017-01-12 02:58:29 +01:00
|
|
|
body
|
2017-04-06 13:59:16 +02:00
|
|
|
, serverD = \() () () () _body _req -> srv
|
2017-01-12 02:58:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
simpleRun :: Delayed () (Handler ())
|
|
|
|
-> IO ()
|
|
|
|
simpleRun d = fmap (either ignoreE id) . try $
|
New combinator to return routed path in response headers
This commit introduces a new type-level combinator, `WithRoutingHeader`.
It modifies the behaviour of the following sub-API, such that all endpoint
of said API return an additional routing header in their response.
A routing header is a header that specifies which endpoint the
incoming request was routed to.
Endpoint are designated by their path, in which `Capture'` and
`CaptureAll` combinators are replaced by a capture hint.
This header can be used by downstream middlewares to gather
information about individual endpoints, since in most cases
a routing header uniquely identifies a single endpoint.
Example:
```haskell
type MyApi =
WithRoutingHeader :> "by-id" :> Capture "id" Int :> Get '[JSON] Foo
-- GET /by-id/1234 will return a response with the following header:
-- ("Servant-Routed-Path", "/by-id/<id::Int>")
```
To achieve this, two refactorings were necessary:
* Introduce a type `RouterEnv env` to encapsulate the `env` type
(as in `Router env a`), which contains a tuple-encoded list of url
pieces parsed from the incoming request.
This type makes it possible to pass more information throughout the
routing process, and the computation of the `Delayed env c` associated
with each request.
* Introduce a new kind of router, which only modifies the RouterEnv, and
doesn't affect the routing process otherwise.
`EnvRouter (RouterEnv env -> RouterEnv env) (Router' env a)`
This new router is used when encountering the `WithRoutingHeader`
combinator in an API, to notify the endpoints of the sub-API that they
must produce a routing header (this behaviour is disabled by default).
2022-03-14 14:16:54 +01:00
|
|
|
runAction d (emptyEnv ()) defaultRequest (\_ -> return ()) (\_ -> FailFatal err500)
|
2017-01-12 02:58:29 +01:00
|
|
|
|
|
|
|
where ignoreE :: SomeException -> ()
|
|
|
|
ignoreE = const ()
|
|
|
|
|
2017-01-18 11:17:38 +01:00
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- Combinator example
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
-- | This data types writes 'sym' to 'delayedTestRef'.
|
|
|
|
data Res (sym :: Symbol)
|
|
|
|
|
|
|
|
instance (KnownSymbol sym, HasServer api ctx) => HasServer (Res sym :> api) ctx where
|
2017-01-18 23:55:04 +01:00
|
|
|
type ServerT (Res sym :> api) m = IORef (TestResource String) -> ServerT api m
|
2017-09-08 18:21:16 +02:00
|
|
|
|
2017-10-01 18:20:09 +02:00
|
|
|
hoistServerWithContext _ nc nt s = hoistServerWithContext (Proxy :: Proxy api) nc nt . s
|
2017-09-08 18:21:16 +02:00
|
|
|
|
2017-01-18 11:17:38 +01:00
|
|
|
route Proxy ctx server = route (Proxy :: Proxy api) ctx $
|
2017-01-16 13:17:20 +01:00
|
|
|
addBodyCheck server (return ()) check
|
2017-01-18 11:17:38 +01:00
|
|
|
where
|
|
|
|
sym = symbolVal (Proxy :: Proxy sym)
|
2017-01-16 13:17:20 +01:00
|
|
|
check () = do
|
2017-01-18 23:55:04 +01:00
|
|
|
liftIO $ writeTestResource sym
|
|
|
|
_ <- register freeTestResource
|
2017-01-18 11:17:38 +01:00
|
|
|
return delayedTestRef
|
|
|
|
|
|
|
|
type ResApi = "foobar" :> Res "foobar" :> Get '[PlainText] T.Text
|
|
|
|
|
|
|
|
resApi :: Proxy ResApi
|
|
|
|
resApi = Proxy
|
|
|
|
|
|
|
|
resServer :: Server ResApi
|
2017-01-18 23:55:04 +01:00
|
|
|
resServer ref = liftIO $ fmap (fromTestResource "<wrong>" T.pack) $ readIORef ref
|
2017-01-18 11:17:38 +01:00
|
|
|
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
-- Spec
|
|
|
|
-------------------------------------------------------------------------------
|
|
|
|
|
2017-01-12 02:58:29 +01:00
|
|
|
spec :: Spec
|
|
|
|
spec = do
|
|
|
|
describe "Delayed" $ do
|
|
|
|
it "actually runs clean up actions" $ do
|
2017-01-18 23:55:04 +01:00
|
|
|
liftIO initTestResource
|
2017-01-12 02:58:29 +01:00
|
|
|
_ <- simpleRun $ delayed (return ()) (Route $ return ())
|
2017-01-18 23:55:04 +01:00
|
|
|
res <- readIORef delayedTestRef
|
|
|
|
res `shouldBe` TestResourceFreed
|
2017-01-12 02:58:29 +01:00
|
|
|
it "even with exceptions in serverD" $ do
|
2017-01-18 23:55:04 +01:00
|
|
|
liftIO initTestResource
|
2017-01-12 02:58:29 +01:00
|
|
|
_ <- simpleRun $ delayed (return ()) (Route $ throw DivideByZero)
|
2017-01-18 23:55:04 +01:00
|
|
|
res <- readIORef delayedTestRef
|
|
|
|
res `shouldBe` TestResourceFreed
|
2017-01-12 02:58:29 +01:00
|
|
|
it "even with routing failure in bodyD" $ do
|
2017-01-18 23:55:04 +01:00
|
|
|
liftIO initTestResource
|
2017-01-12 02:58:29 +01:00
|
|
|
_ <- simpleRun $ delayed (delayedFailFatal err500) (Route $ return ())
|
2017-01-18 23:55:04 +01:00
|
|
|
res <- readIORef delayedTestRef
|
|
|
|
res `shouldBe` TestResourceFreed
|
2017-01-12 02:58:29 +01:00
|
|
|
it "even with exceptions in bodyD" $ do
|
2017-01-18 23:55:04 +01:00
|
|
|
liftIO initTestResource
|
2017-01-12 02:58:29 +01:00
|
|
|
_ <- simpleRun $ delayed (liftIO $ throwIO DivideByZero) (Route $ return ())
|
2017-01-18 23:55:04 +01:00
|
|
|
res <- readIORef delayedTestRef
|
|
|
|
res `shouldBe` TestResourceFreed
|
2017-01-18 11:17:38 +01:00
|
|
|
describe "ResApi" $
|
|
|
|
with (return $ serve resApi resServer) $ do
|
|
|
|
it "writes and cleanups resources" $ do
|
2017-01-18 23:55:04 +01:00
|
|
|
liftIO initTestResource
|
2017-01-18 11:17:38 +01:00
|
|
|
request "GET" "foobar" [] "" `shouldRespondWith` "foobar"
|
|
|
|
liftIO $ do
|
2017-01-18 23:55:04 +01:00
|
|
|
res <- readIORef delayedTestRef
|
|
|
|
res `shouldBe` TestResourceFreed
|