Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RawM combinator #1551

Merged
merged 1 commit into from
Feb 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion servant-client-core/src/Servant/Client/Core/HasClient.hs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ import Servant.API
FromSourceIO (..), Header', Headers (..), HttpVersion,
IsSecure, MimeRender (mimeRender),
MimeUnrender (mimeUnrender), NoContent (NoContent),
NoContentVerb, QueryFlag, QueryParam', QueryParams, Raw,
NoContentVerb, QueryFlag, QueryParam', QueryParams, Raw, RawM,
ReflectMethod (..), RemoteHost, ReqBody', SBoolI, Stream,
StreamBody', Summary, ToHttpApiData, ToSourceIO (..), Vault,
Verb, WithNamedContext, WithStatus (..), contentType, getHeadersHList,
Expand Down Expand Up @@ -679,6 +679,16 @@ instance RunClient m => HasClient m Raw where

hoistClientMonad _ _ f cl = \meth -> f (cl meth)

instance RunClient m => HasClient m RawM where
type Client m RawM
= H.Method -> m Response

clientWithRoute :: Proxy m -> Proxy RawM -> Request -> Client m RawM
clientWithRoute _pm Proxy req httpMethod = do
runRequest req { requestMethod = httpMethod }

hoistClientMonad _ _ f cl = \meth -> f (cl meth)

-- | If you use a 'ReqBody' in one of your endpoints in your API,
-- the corresponding querying function will automatically take
-- an additional argument of the type specified by your 'ReqBody'.
Expand Down
34 changes: 32 additions & 2 deletions servant-server/src/Servant/Server/Internal.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
Expand Down Expand Up @@ -63,7 +64,7 @@ import Network.HTTP.Types hiding
import Network.Socket
(SockAddr)
import Network.Wai
(Application, Request, httpVersion, isSecure, lazyRequestBody,
(Application, Request, Response, ResponseReceived, httpVersion, isSecure, lazyRequestBody,
queryString, remoteHost, getRequestBodyChunk, requestHeaders,
requestMethod, responseLBS, responseStream, vault)
import Prelude ()
Expand All @@ -73,7 +74,7 @@ import Servant.API
CaptureAll, Description, EmptyAPI, Fragment,
FramingRender (..), FramingUnrender (..), FromSourceIO (..),
Header', If, IsSecure (..), NoContentVerb, QueryFlag,
QueryParam', QueryParams, Raw, ReflectMethod (reflectMethod),
QueryParam', QueryParams, Raw, RawM, ReflectMethod (reflectMethod),
RemoteHost, ReqBody', SBool (..), SBoolI (..), SourceIO,
Stream, StreamBody', Summary, ToSourceIO (..), Vault, Verb,
WithNamedContext, NamedRoutes)
Expand Down Expand Up @@ -606,6 +607,35 @@ instance HasServer Raw context where
Fail a -> respond $ Fail a
FailFatal e -> respond $ FailFatal e

-- | Just pass the request to the underlying application and serve its response.
--
-- Example:
--
-- > type MyApi = "images" :> Raw
-- >
-- > server :: Server MyApi
-- > server = serveDirectory "/var/www/images"
instance HasServer RawM context where
type ServerT RawM m = Request -> (Response -> IO ResponseReceived) -> m ResponseReceived

route
:: Proxy RawM
-> Context context
-> Delayed env (Request -> (Response -> IO ResponseReceived) -> Handler ResponseReceived) -> Router env
route _ _ handleDelayed = RawRouter $ \env request respond -> runResourceT $ do
routeResult <- runDelayed handleDelayed env request
let respond' = liftIO . respond
liftIO $ case routeResult of
Route handler -> runHandler (handler request (respond . Route)) >>=
\case
Left e -> respond' $ FailFatal e
Right a -> pure a
Fail e -> respond' $ Fail e
FailFatal e -> respond' $ FailFatal e

hoistServerWithContext _ _ f srvM = \req respond -> f (srvM req respond)


-- | If you use 'ReqBody' in one of the endpoints for your API,
-- this automatically requires your server-side handler to be a function
-- that takes an argument of the type specified by 'ReqBody'.
Expand Down
51 changes: 47 additions & 4 deletions servant-server/test/Servant/ServerSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import Prelude.Compat

import Control.Monad
(forM_, unless, when)
import Control.Monad.Reader (runReaderT, ask)
import Control.Monad.Error.Class
(MonadError (..))
import Control.Monad.IO.Class (MonadIO(..))
import Data.Aeson
(FromJSON, ToJSON, decode', encode)
import qualified Data.ByteString as BS
Expand Down Expand Up @@ -52,19 +54,19 @@ import Servant.API
Delete, EmptyAPI, Fragment, Get, HasStatus (StatusOf), Header,
Headers, HttpVersion, IsSecure (..), JSON, Lenient,
NoContent (..), NoContentVerb, NoFraming, OctetStream, Patch,
PlainText, Post, Put, QueryFlag, QueryParam, QueryParams, Raw,
PlainText, Post, Put, QueryFlag, QueryParam, QueryParams, Raw, RawM,
RemoteHost, ReqBody, SourceIO, StdMethod (..), Stream, Strict,
UVerb, Union, Verb, WithStatus (..), addHeader)
import Servant.Server
(Context ((:.), EmptyContext), Handler, Server, Tagged (..),
emptyServer, err401, err403, err404, respond, serve,
(Context ((:.), EmptyContext), Handler, Server, ServerT, Tagged (..),
emptyServer, err401, err403, err404, hoistServer, respond, serve,
serveWithContext)
import Servant.Test.ComprehensiveAPI
import qualified Servant.Types.SourceT as S
import Test.Hspec
(Spec, context, describe, it, shouldBe, shouldContain)
import Test.Hspec.Wai
(get, liftIO, matchHeaders, matchStatus, shouldRespondWith,
(get, matchHeaders, matchStatus, shouldRespondWith,
with, (<:>))
import qualified Test.Hspec.Wai as THW

Expand Down Expand Up @@ -97,6 +99,7 @@ spec = do
reqBodySpec
headerSpec
rawSpec
rawMSpec
alternativeSpec
responseHeadersSpec
uverbResponseHeadersSpec
Expand Down Expand Up @@ -605,6 +608,46 @@ rawSpec = do

-- }}}
------------------------------------------------------------------------------
-- * rawMSpec {{{
------------------------------------------------------------------------------

type RawMApi = "foo" :> RawM

rawMApi :: Proxy RawMApi
rawMApi = Proxy

rawMServer :: (Monad m, MonadIO m, Show a) => (Request -> m a) -> ServerT RawMApi m
rawMServer f req resp = liftIO . resp . responseLBS ok200 [] . cs . show =<< f req

rawMSpec :: Spec
rawMSpec = do
describe "Servant.API.RawM" $ do
it "gives access to monadic context" $ do
flip runSession (serve rawMApi
(hoistServer rawMApi (flip runReaderT (42 :: Integer)) (rawMServer (const ask)))) $ do
response <- Network.Wai.Test.request defaultRequest{
pathInfo = ["foo"]
}
liftIO $ do
simpleBody response `shouldBe` "42"

it "lets users throw servant errors" $ do
flip runSession (serve rawMApi (rawMServer (const $ throwError err404 >> pure (42 :: Integer)))) $ do
response <- Network.Wai.Test.request defaultRequest{
pathInfo = ["foo"]
}
liftIO $ do
statusCode (simpleStatus response) `shouldBe` 404

it "gets the pathInfo modified" $ do
flip runSession (serve rawMApi (rawMServer (pure . pathInfo))) $ do
response <- Network.Wai.Test.request defaultRequest{
pathInfo = ["foo", "bar"]
}
liftIO $ do
simpleBody response `shouldBe` cs (show ["bar" :: String])
-- }}}
------------------------------------------------------------------------------
-- * alternativeSpec {{{
------------------------------------------------------------------------------
type AlternativeApi =
Expand Down
2 changes: 1 addition & 1 deletion servant/src/Servant/API.hs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ import Servant.API.Modifiers
import Servant.API.QueryParam
(QueryFlag, QueryParam, QueryParam', QueryParams)
import Servant.API.Raw
(Raw)
(Raw, RawM)
import Servant.API.RemoteHost
(RemoteHost)
import Servant.API.ReqBody
Expand Down
3 changes: 3 additions & 0 deletions servant/src/Servant/API/Raw.hs
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@ import Data.Typeable
-- <https://hackage.haskell.org/package/servant-server/docs/Servant-Server-StaticFiles.html Servant.Server.StaticFiles>
-- to serve static files stored in a particular directory on your filesystem
data Raw deriving Typeable

-- | Variant of 'Raw' that lets you access the underlying monadic context to process the request.
data RawM deriving Typeable
6 changes: 5 additions & 1 deletion servant/src/Servant/Links.hs
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ import Servant.API.NamedRoutes
import Servant.API.QueryParam
(QueryFlag, QueryParam', QueryParams)
import Servant.API.Raw
(Raw)
(Raw, RawM)
import Servant.API.RemoteHost
(RemoteHost)
import Servant.API.ReqBody
Expand Down Expand Up @@ -580,6 +580,10 @@ instance HasLink Raw where
type MkLink Raw a = a
toLink toA _ = toA

instance HasLink RawM where
type MkLink RawM a = a
toLink toA _ = toA

instance HasLink (Stream m status fr ct a) where
type MkLink (Stream m status fr ct a) r = r
toLink toA _ = toA
Expand Down