Cookbook

June 7, 2011

GravatarBy Michael Snoyman

Introduction

This is hopefully the first blog post in many with "cookbook" style answers. If you have either questions or "recipes" to submit, please . Eventually, this content will make its way into the Yesod book.

Adding a local Javascript file

There are two main functions used to include a reference to an external Javascript file: addScript and addScriptRemote. The latter is used when you want to provide the actual URL to the script, and can be especially useful for referencing CDN-hosted libraries, such as Google-hosted jQuery.

The former function is used when you want to refer to a type-safe URL. The most common case of this is with the yesod-static package; if you want to use that, the best place to see an example usage is in the scaffolded site. Here is a minimal example using the lower-level sendFile.

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
import Yesod

data Local = Local
type Handler = GHandler Local Local

mkYesod "Local" [parseRoutes|
/ RootR GET
/myfile.js MyFileJsR GET
|]

instance Yesod Local where
    approot _ = ""

getRootR = defaultLayout $ do
    addScript MyFileJsR
    addHamlet [hamlet|
<h1>HELLO WORLD!
|]

 -- type sig necessary, since sendFile is polymorphic
getMyFileJsR :: Handler ()
-- Serves "myfile.js" with text/javascript mime-type.
-- Served from /myfile.js as defined above, but your code needn't know that.
getMyFileJsR = sendFile "text/javascript" "myfile.js"

main = warpDebug 3000 Local

Virtual Hosts

Often times, we'll want to run multiple sites from a single machine, using virtual hosts. The recommended approach in general is to use a web server like Nginx to be the frontend server, and to reverse HTTP proxy to your individual sites. However, it's entirely possible to do this in 100% pure Haskell, with just a single instance running for all your sites.

To achieve this, we'll use the vhost middleware. We'll start off with some boilerplate definitions of three Yesod sites:

{-# LANGUAGE OverloadedStrings, QuasiQuotes, TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}

import Yesod
import Network.Wai.Middleware.Vhost
import Network.Wai.Handler.Warp
import Network.Wai

data Site1 = Site1
data Site2 = Site2
data DefaultSite = DefaultSite

mkYesod "Site1" [parseRoutes|/ Root1 GET|]
getRoot1 = return $ RepPlain "Root1"

mkYesod "Site2" [parseRoutes|/ Root2 GET|]
getRoot2 = return $ RepPlain "Root2"

mkYesod "DefaultSite" [parseRoutes|/ RootDef GET|]
getRootDef = return $ RepPlain "RootDef"

instance Yesod Site1 where approot _ = ""
instance Yesod Site2 where approot _ = ""
instance Yesod DefaultSite where approot _ = ""

Now comes the actual virtual hosting. We want to serve Site1 from "host1", Site2 from "host2", and otherwise serve DefaultSite. In order to determine which site to use, we'll compare against the serverName record of the WAI request value. The code is very straight-forward:

main = do
   app1 <- toWaiApp Site1
   app2 <- toWaiApp Site2
   appDef <- toWaiApp DefaultSite
   run 3000 $ vhost
       [ ((==) "host1" . serverName, app1)
       , ((==) "host2" . serverName, app2)
       ] appDef

The vhost function takes two arguments: a list of pairs of predicates and applications, and a final default application. This approach works very well for just a few sites, but if you have dozens of virtual hosts, it would be more efficient to use a Map. Add the following to your import list:

import qualified Data.Map as Map

And then replace your main with:

main = do
   app1 <- toWaiApp Site1
   app2 <- toWaiApp Site2
   appDef <- toWaiApp DefaultSite
   let sites = Map.fromList
                   [ ("host1", app1)
                   , ("host2", app2)
                   ]
   run 3000 $ \req ->
       case Map.lookup (serverName req) sites of
           Nothing -> appDef req
           Just app -> app req

Yesod Proxy Server

Yesod is built on top of WAI, which in turn is built on enumerators. One of the advantages of enumerators is being able to work with streams of data, in constant space. By building on a central enumerator system, multiple libraries are able to interoperate very easily. In our case, both WAI and http-enumerator share an enumerator approach. Combined with Aristid Breitkreuz's http-types, it is much easier to get these packages to interoperate.

Unfortunately, there are still a few gotchas. If you're not familiar with enumerators, it can be difficult to get the types to work out. Yesod purposely doesn't advertise the low-level functions necessary to get things to work, since they are confusing to new users. And there's a bit of a catch with Builders versus ByteStrings. We'll step through a very simple Yesod app that proxies the Yesod homepage. We'll start with our standard language extensions:

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeFamilies #-}

Next we have our import statements. Try to follow which modules/libraries and providing different functionalities.

import Yesod hiding (Request)
import Network.HTTP.Enumerator (parseUrl, withManager, http, Request)
import Network.HTTP.Types (Status, ResponseHeaders)
import Network.Wai (Response (ResponseEnumerator))
import Data.ByteString (ByteString)
import Blaze.ByteString.Builder (Builder, fromByteString)
import Data.Enumerator (Iteratee, run_, ($$), joinI)
import qualified Data.Enumerator.List as EL

And now some simple code to create a new Yesod application with a single route and is served by Warp:

data Proxy = Proxy
type Handler = GHandler Proxy Proxy

mkYesod "Proxy" [parseRoutes|
/ RootR GET
|]

instance Yesod Proxy where
   approot _ = ""

main :: IO ()
main = warpDebug 3000 Proxy

All that's left is to define our handler function. If we look at the http-enumerator package, we'll see that there are a number of functions available for running an HTTP request. However, all of them (except simpleHttp) take a Request. So we'll start by using parseUrl to generate such a value. We'll take advantage of the IO instance of Failure by using liftIO:

getRootR :: Handler ()
getRootR = do
   req <- liftIO $ parseUrl "http://www.yesodweb.com/"

For this exercise, we're going to use the http function, which has the signature:

http :: Request IO 
     -> (Status -> ResponseHeaders -> Iteratee ByteString IO a)
     -> Manager
     -> Iteratee ByteString IO a

Alright, the Manager is simply a collection of open connections. We can generate one using withManager. And we already have our request. Now we need to do something about that second argument. But it looks strangely familiar... let's look at the definition of ResponseEnumerator from WAI:

type ResponseEnumerator a = (Status -> ResponseHeaders -> Iteratee Builder IO a) -> IO a

Interesting... it's almost identical, except for the ByteString versus Builder issue. Let's take a quick break from the definition of getRootR and define a new function, blaze, to help us out here:

blaze :: (Status -> ResponseHeaders -> Iteratee Builder IO a)
      -> Status -> ResponseHeaders -> Iteratee ByteString IO a
blaze f status headers =

First, a little side note: http-enumerator returns all of the content headers from the server, including content-encoding. This can cause a bit of a problem for clients (you know, they don't like being told their data is encoded when it isn't), so we need to strip that header out:

let notEncoding ("Content-Encoding", _) = False
        notEncoding _ = True
        headers' = filter notEncoding headers

Next, we want to get apply the status and headers to f to get our Builder Iteratee:

--builderIter :: Iteratee Builder IO a
        builderIter = f status headers'

Finally, we need to turn our Builder Iteratee into a ByteString Iteratee. Let's remember that an Iteratee is a data consumer, being fed a stream of data. In order to convert an Iteratee to receive a new stream type, we need to stick an adapter at the beginning of it, called an Enumeratee. In our case, we need to convert a stream of ByteStrings to a stream of Builders. For this, we can use Data.Enumerator.List.map and fromByteString, together with joinI and $$ for some glue:

in joinI $ EL.map fromByteString $$ builderIter

Well, now that our helper function is done, we can return to http. We need to use sendWaiResponse, which will let us send a raw WAI response from our Yesod app. And we'll want to use the ResponseEnumerator constructor for Response. So our getRootR ends up looking like:

getRootR :: Handler ()
getRootR = do
   req <- liftIO $ parseUrl "http://www.yesodweb.com/"
   sendWaiResponse $ ResponseEnumerator $ inside req

And we need inside to have a type signature of:

inside :: Request IO
       -> (Status -> ResponseHeaders -> Iteratee Builder IO a)
       -> IO a

As we mentioned before, we'll use withManager to get the manager, so our function will start off as:

inside req f = withManager $ \manager ->

And the rest is just playing with the types. We know we want to call http here, and the first argument is going to be req. f is almost the right type for the second argument to http, but it needs a little help from the blaze function. And the final argument will be manager. This gives us:

http req (blaze f) manager :: Iteratee Bytestring IO a

But we need our function to return IO a. So the last step is to actually run_ the Iteratee to produce a value. Our complete definition of inside is:

inside req f = withManager $ \manager ->
   run_ (http req (blaze f) manager)

Or in point-free style, for those inclined:

inside req f = withManager $ run_ . http req (blaze f)

Comments

comments powered by Disqus

Archives