I have recently released a new Haskell package called servant-rawm. It allows embedding a raw WAI
Application inside a Servant API.
How does it work?
Below I will give a simple example of how
servant-rawm can be used.
Imagine we have an API that looks like the following:
This represents a Servant API that can be accessed at
http://localhost/serve-directory-example/ and does something.
RawM represents any possible server that can be defined as a WAI
Application. Without looking at the actual server definition, we are not sure what it does, so let's take a look at the server definition:
import Control.Monad.Reader (ReaderT, ask) import Network.Wai (Application) import Servant (ServerT) import Servant.RawM (serveDirectoryWebApp) serverRoot :: ServerT Api (ReaderT FilePath IO) serverRoot = rawEndpoint rawEndpoint :: ReaderT FilePath IO Application rawEndpoint = do filePath <- ask serveDirectoryWebApp filePath
serverRoot is our main top-level Servant server. Since there is only one endpoint defined in
Api, there is only one endpoint in the body of
rawEndpoint is a fileserver. It uses
serveDirectoryWebApp to serve files from of a given directory. Note that the directory is obtained from the
serverRoot can be run with the following two functions:
import Contorl.Monad.Reader (ReaderT, runReaderT) import Control.Monad.IO.Class (liftIO) import Data.Proxy (Proxy(Proxy)) import Network.Wai (Application) import Network.Wai.Handler.Warp (run) import Servant (Handler, Serve, serve) import Servant.Utils.Enter ((:~>)(NT), enter) app :: Application app = serve (Proxy :: Proxy Api) apiServer where apiServer :: Server Api apiServer = enter (NT trans) serverRoot trans :: ReaderT FilePath IO a -> Handler a trans readerT = liftIO $ runReaderT readerT "./some/path/" main :: IO () main = do putStrLn "example server running on port 8421" run 8421 $ app config
app transforms our
serverRoot Servant API into a WAI
Application (which happens to also contain the
rawEndpoint). This uses the
enter machinery provided by Servant.1
main uses Warp's
run function to run
We can access this server with
curl. Assuming the server has been run in the current directory, and the file
./some/path/foo.txt exists, it can be fetched like the following:
RawM different from Servant's
servant-rawm is similar to, but more powerful than,
Raw provided by Servant.
Here is what the code would look like if we were using
Raw instead of
import Control.Monad.Reader (ReaderT) import Data.Tagged (Tagged(Tagged)) import Network.Wai (Application) import Servant ((:>), Raw, ServerT) type Api' = "serve-directory-example" :> Raw serverRoot' :: ServerT Api' (ReaderT FilePath IO) serverRoot' = rawEndpoint' rawEndpoint' :: Tagged (ReaderT FilePath IO) Application rawEndpoint' = Tagged $ ...
If you look at the body of the
rawEndpoint' function, you can see that it is using the
Tagged data constructor, which ends up with a type like the following type:
Tagged takes an
Application and not a
ReaderT Filepath IO Application. This means that the
ReaderT cannot be used to create the
Contrast the type of
rawEndpoint from the previous section with the type of
rawEndpoint :: ReaderT FilePath IO Application rawEndpoint' :: Tagged (ReaderT FilePath IO) Application
You can see that
RawM is strictly more powerful than
Raw, because it allows the
Application to be created monadically.
Client and Documentation
servant-rawm also provides two additional instances for
HasClient (for use with
HasDocs (for use with
More information can be found in the client example and the docs example in the source repository.
servant-rawm should be used whenever you need to use monadic effects to create an
Application in a Servant API.
Take a look at the Servant tutorial for an explanation of how
enterworks. This blog post is based on servant-0.11. Future versions of Servant will do away with
RawMwill still work in a similar fashion. See this PR for more information.↩︎