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:
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:
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:
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
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 withenter
. However,RawM
will still work in a similar fashion. See this PR for more information.↩︎
tags: haskell