Yesod Routing

As of March 2020, School of Haskell has been switched to read-only mode.

Routing Fundamentals

If you're familiar with other web frameworks, you will have a pretty good idea of the basic paradigm of URL routing, but for the uninitiated I will explain the basics here.

When you visit your favorite website the first thing your browser will request from the server a specific resource. The very first line of the connection to the server will often look like this

GET /blog/archive/2005/10/02 HTTP/1.1

The first part of the header, namely GET is known as the Method. Here' we're performing a simple GET request to retrieve the contents of the resource located at /blog/archive/2005/10/02 and using the HTTP/1.1 version of the protocol to do so. The location of the resource is known as Path which the framework maps to a Route.

Many web frameworks present some programmable way to specify a link between routes and the specific function that they should run in order to serve that route.

# Ruby (Using a DSL)
match "/patients/:id" => "patients#show"

# Flask (Using a DSL)
@app.route('/patients/<id>')
def patients_detail(id): ...

# Django (Using regex)
url(r'^patients/(?P<id>\d+)/$', views.PatientsDetailView.as_view())

When a request comes into the server, the framework interprets the path and maps it to the appropriate route and hands off control the the function associated with that URL.

The result is a function called with string arguments.

In yesod, the fundamental unit

type PatientId = Int

-- This is the URL routing definition
mkYesod "BusinessDirectory" [parseRoutes|
  /patients/#PatientId PatientDetailR GET
|]

-- The views that can be called
getPatientDetailR :: Handler RepHTML
getPatientDetailR id = undefined

This defines a single route that takes a single parameter of type PatientId and can service the GET HTTP method.


{-# LANGUAGE OverloadedStrings, FlexibleContexts, ScopedTypeVariables,
             GeneralizedNewtypeDeriving, TemplateHaskell, GADTs,
             TypeFamilies, QuasiQuotes, MultiParamTypeClasses #-}

-- As usual, a generous helping of pragmas and imports.
-- These will need to be placed at the top of your source files.

-- Everything you need to work with Yesod, exported from one package
import Yesod

-- Standard platform datatypes & functions
import Data.Text (Text)
import Data.Time (UTCTime, getCurrentTime)
import Data.Time.Format (formatTime)
import System.Locale (defaultTimeLocale)

-- Persist
import Database.Persist
import Database.Persist.Sqlite
import Database.Persist.TH

-- Monad helpers
import Control.Applicative ((<$>),(<*>))
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Logger (runNoLoggingT)
import Control.Monad.Trans.Resource (runResourceT)

data BusinessDirectory = BusinessDirectory { _db :: Connection }

instance Yesod BusinessDirectory

instance YesodPersist BusinessDirectory where
    type YesodPersistBackend BusinessDirectory = SqlPersist

    runDB action = getYesod >>= runSqlConn action . _db

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
Business
    name String
    deriving Show
Contact
    businessId BusinessId
    name String
    deriving Show
|]

mkYesod "BusinessDirectory" [parseRoutes|
  /                                         HomeR     GET
  /business/#BusinessId                     BusinessR GET
  /business/#BusinessId/contact/#ContactId  ContactR  GET
|]

blankR :: Handler RepHtml
blankR = defaultLayout [whamlet|
  <h1> This page intentionally left blank. |]

getHomeR :: Handler RepHtml
getHomeR = do
  businesses <- runDB $ selectList [] [Asc BusinessName]
  defaultLayout [whamlet|
  <h2> Businesses
  <ul>
    $forall Entity business_id business <- businesses
      <li><a href=@{ BusinessR business_id }>
        #{ businessName business } |]

getBusinessR :: BusinessId  -> Handler RepHtml
getBusinessR business_id = do
  Just business <- runDB $ get business_id
  contacts <- runDB $ selectList [ContactBusinessId ==. business_id] [Asc ContactName]
  defaultLayout [whamlet|
  <h2> Business
  <h3> Name
  <p> #{ businessName business }
  <h3> Contacts
  <ul>
    $forall Entity contact_id contact <- contacts
      <li><a href=@{ContactR business_id contact_id}>
        #{ contactName contact }
  |]

getContactR :: BusinessId -> ContactId -> Handler RepHtml
getContactR business_id contact_id = do
  Just business <- runDB $ get business_id
  Just contact  <- runDB $ get contact_id
  let bus_id_from_contact = contactBusinessId contact
  if bus_id_from_contact == business_id then
    defaultLayout [whamlet|
      <h2> Contact
      <h3> Name
      <p>  #{ contactName contact }
      <h3> Business
      <p>  #{ businessName business }
    |]
  else
    redirect $ ContactR bus_id_from_contact contact_id

main = withSqliteConn ":memory:" $ \conn -> do

  runNoLoggingT $ runResourceT $ flip runSqlConn conn $ do
    runMigration migrateAll
    b1 <- insert $ Business "Sales Direct Corp. 1"
    insert $ Contact b1 "Contact Person 1"
    insert $ Contact b1 "Contact Person 2"
    b2 <- insert $ Business "Sales Direct Corp. 2"
    insert $ Contact b2 "Contact Person 1"
    insert $ Contact b2 "Contact Person 2"

  warpEnv BusinessDirectory { _db=conn }