servant-rawm

2017-10-15

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:

import Servant ((:>))
import Servant.RawM (RawM)

type Api = "serve-directory-example" :> RawM

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 serverRoot: rawEndpoint.

rawEndpoint is a fileserver. It uses serveDirectoryWebApp to serve files from of a given directory. Note that the directory is obtained from the ReaderT monad.

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 Application from rawEndpoint). This uses the enter machinery provided by Servant.1

main uses Warp’s run function to run app.

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:

$ curl http://localhost:8421/serve-directory-example/foo.txt

How is RawM different from Servant’s Raw type?

RawM from 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 RawM:

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 :: Application -> Tagged (ReaderT FilePath IO) Application

Notice that Tagged takes an Application and not a ReaderT Filepath IO Application. This means that the ReaderT cannot be used to create the Application.

Contrast the type of rawEndpoint from the previous section with the type of rawEndpoint':

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 RawM: HasClient (for use with servant-client) and HasDocs (for use with servant-docs).

More information can be found in the client example and the docs example in the source repository.

Conclusion

servant-rawm should be used whenever you need to use monadic effects to create an Application in a Servant API.

Footnotes


  1. Take a look at the Servant tutorial for an explanation of how enter works. This blog post is based on servant-0.11. Future versions of Servant will do away with enter. However, RawM will still work in a similar fashion. See this PR for more information.

tags: haskell