Yesod web framework/Controller

Server interface

edit

Yesod uses a Web application interface API,[1] abbreviated WAI, to isolate servlets, aka web apps., from servers, with handlers for the server protocols CGI,[2] FastCGI,[3] SCGI,[4] Warp,[5] Launch (open as local URL to the default browser, closing the server when the window is closed),[6]

The foundation type

edit

See ref.[7] Yesod requires a data type that instantiates the controller classes. This is called the foundation type. In the example below, it is named "MyApp".

The REST model identifies a web resource with a web path. Here REST resources are given names with an R suffix (like "HomeR") and are listed in a parseRoutes site map description template. From this list, route names and dispatch handler names are derived.

Yesod makes use of Template Haskell metaprogramming to generate code from templates at compile time, assuring that the names in the templates match and everything typechecks (e.g. web resource names and handler names).

By inserting a mkYesod call, this will call Template Haskell primitives to generate the code[8] corresponding to the route type members, and the instances of the dispatch controller classes as to dispatch GET calls to route HomeR to a routine named composing them both as "getHomeR", expecting an existing handler that matches the name.

Hello World

edit

"Hello world" example based on a CGI server interface (The actual handler types have changed, but the philosophy remains):

{- file wai-cgi-hello.hs -}
{-# LANGUAGE PackageImports, TypeFamilies, QuasiQuotes, MultiParamTypeClasses,
             TemplateHaskell, OverloadedStrings #-}
import "wai" Network.Wai
import "wai-extra" Network.Wai.Handler.CGI (run) -- interchangeable WAI handler

import "yesod" Yesod
import "yesod-core" Yesod.Handler (getRequest)
import "text" Data.Text (Text)
import "shakespeare" Text.Cassius (Color(..), colorBlack)

-- the Foundation type
data MyApp = MyApp

-- sitemap template, listing path, resource name and methods accepted
-- `mkYesod` takes the foundation type name as param. for name composition of dispatch functions
mkYesod "MyApp" [parseRoutes|
/ HomeR GET
|]

instance Yesod MyApp

-- indentation structured CSS template
myStyle :: [Text]  CssUrl url
myStyle paramStyle =
        [cassius|
.box
    border: 1px solid #{boxColor}
|]
        where
          boxColor = case paramStyle of
                        ["high-contrast"]  colorBlack
                        _  Color 0 0 255

-- indentation structured HTML template
myHtml :: [(Text, Text)]  HtmlUrl url
myHtml params = [hamlet|
<!-- indentation, or lack of it, under starting tags or commands ('$' prefix) 
     describe the content or sequence tree structure -->
<!-- '.' or '#' prefixes in tags introduce css styled "class" or "id" attribute values -->
<!-- interpolation of haskell expressions follow the "shakespeare templates" #{expr} syntax -->

<p>Hello World! There are <span .box>#{length params} parameters</span>:
$if null params
    <p>Nothing to list 
$else
    <ul>
         $forall param <- params
             <li>#{fst param}: #{snd param}
|]
getHomeR :: Handler RepHtml
getHomeR = do
        req <- getRequest
        let params = reqGetParams req
        paramStyle <- lookupGetParams "style"
        
        defaultLayout $ do
            -- adding widgets to the Widget monad (a ''Writer'' monad)
            setTitle "Yesod example"
            toWidgetHead $ myStyle paramStyle
            toWidgetBody $ myHtml params

-- there are ''run'' function variants for different WAI handlers

main = toWaiApp MyApp >>= run
# cgi test
export REMOTE_ADDR=127.0.0.1
export REQUEST_METHOD=GET
export PATH_INFO=/
export QUERY_STRING='p1=abc;p2=def;style=high-contrast'
./wai-cgi-hello

[7]

Resources, routes and HTTP method handlers

edit

See ref.[9][10] Yesod follows the REpresentational State Transfer model of access to web documents, identifying docs. and directories as resources with a Route constructor, named with an uppercase R suffix (for example, HomeR).

The routes table
The parseRoutes template should list the resources specifying route pieces, resource name and dispatch methods to be accepted.

URL segment capture as parameter is possible specifying a '#' prefix for single segment capture or '*' for multisegment capture, followed by the parameter type.

-- given a MyApp foundation type

mkYesod "MyApp" [parseRoutes|
/                     HomeR      -- no http methods stated: all methods accepted
/blog                 BlogR      GET POST

-- the '#' prefix specify the path segment as a route handler parameter
/article/#ArticleId   ArticleR   GET PUT

-- the '*' prefix specify the parameter as a sequence of path pieces
/branch/*Texts        BranchR    GET

-- to simplify the grammar, compound types must use an alias, eg. type Texts for ''[Text]''
|]
  • Applying the previous template generates the following route constructors:
data Route MyApp = 
    HomeR                    -- referenced in templates as: @{HomeR}
    | BlogR                  -- in templates: @{BlogR}
    | ArticleR ArticleId     -- in templates: @{ArticleR myArticleId}
    | BranchR Texts          -- in templates: @{BranchR myBranchSegments}
  • For every supported HTTP method a handler function must be created to match the dispatch names generated by mkYesod from the parseRoutes template, by prefixing the method name (or the prefix "handler" if no method stated) to the resource, as described (actual versions handler types have changed, but the philosophy remains):
-- for "/ HomeR"        -- no http methods stated ⇒ only one handler with prefix ''handler''
handlerHomeR :: HasReps t  Handler t

-- for "/blog BlogR GET POST"
getBlogR :: HasReps t  Handler t
postBlogR :: HasReps t  Handler t

-- for "/article/#ArticleId ArticleR GET PUT"
getArticleR :: HasReps t  ArticleId  Handler t
putArticleR :: HasReps t  ArticleId  Handler t

Request data, Parameters, Cookies, Languages and other Header info

edit

See ref.[9]

Authentication and authorization

edit

See ref.[11] Authentication plugins: OpenId, BrowserId, Email, GoogleEmail, HashDB, RpxNow.[12]

There is an important setting for automatic redirection after authentication.[13]

Sessions

edit

See ref.[14] Session back-ends: ClientSession[15] (it stores the session in a cookie), ServerSession[16][17] (it stores most of the session data at the server)

>> To avoid undue bandwidth overhead, production sites can serve their static content from a separate domain name to avoid the overhead of transmitting the session cookie for each request

Session messages

edit

A success, failure or indicative message can be stored (setMessage) in the Session and will be shown, if it exists, by the default_layout routine through the default_layout.hamlet template, being cleared on consultation.[18]

Subsites

edit

Common URL prefix subsites for workflows, file serving or site partitioning. See ref.[19][20]

Built-in subsites: Static,[21][22] Auth[23]

Form processing and layout generation

edit

See ref.[24]

The Form type here is an object that is used in the controller to parse and process the form fields user input and produce a (FormResult, Widget) pair were the widget holds the layout of the next rendering of the form with error messages and marks. It can also be used to generate a new form with blanks or default values.

The form type takes the shape of a function of an html snippet to be embedded in the view, that will hold security purpose hidden fields.

A form object is generated from an Applicative/Monadic composition of fields for a combined/sequential parsing of field inputs.

There are three types of forms:

  • Applicative (with tabular layout),
  • Monadic (with free layout style), both in the Yesod.Form.Functions module,
  • Input (for parsing only, no view generated) in the Yesod.Form.Input module.

The field generators, whose names are composed by the form type initial (a|m|i) followed by (req|opt){- required or optional -}, have a fieldParse component and a fieldView one.[25]

  • the function runForm{Post|Get} runs the field parsers against the form field inputs and generates a (FormResult, Widget) pair from the views offering a new form widget with the received form field values as defaults. The function suffix is the http method used in the form submission.
  • while generateForm{Post|Get} ignores inputs from the client and generates a blank or defaults form widget.[26]

The actual function parameters and types have changed through Yesod versions. Check the Yesod book and libraries signatures.

The magic is in the FormResult data type Applicative instance, where (<*>) collects the error messages for the case of FormFailure [textErrMsg] result values[27]

Monadic forms permit free form layout and better treatment of hiddenField members.[24]

A sample of an Applicative[28] form:

-- a record for our form fields
data Person = Person {personName :: Text, personAge :: Int, personLikings :: Maybe Text}

-- the Form type has an extra parameter for an html snippet to be embedded, containing a CSRF token hidden field for security
type Form sub master x = Html  MForm sub master (FormResult x, Widget)

{-
-- for messages in validation functions:
  @param master: yesod instance to use in renderMessage (return from handler's getYesod)
  @param languages: page languages to use in renderMessage

-- optional defaults record:
  @param mbPersonDefaults: Just defaults_record, or Nothing for blank form
-}

personForm :: MyFoundationType  [Text]  Maybe Person  Form sub master Person
{- ''aopt'' (optional field AForm component) for "Maybe" fields,
   ''areq'' (required fld AForm comp.) will insert the "required" attribute
-}
personForm master languages mbPersonDefaults = renderTable $ 
  Person <$> areq textField            fldSettingsName    mbNameDefault 
         <*> areq customPersonAgeField fldSettingsAge     mbAgeDefault 
         <*> aopt textareaField        fldSettingsLikings mbLikingsDefault 
  where
    mbNameDefault    = fmap personName    mbPersonDefaults
    mbAgeDefault     = fmap personAge     mbPersonDefaults
    mbLikingsDefault = fmap personLikings mbPersonDefaults

    -- "fieldSettingsLabel" returns an initial fieldSettings record
    -- recently the "FieldSettings" record can be defined from a String label since it implements IsString
    fldSettingsName = (fieldSettingsLabel MsgName) {fsAttrs = [("maxlength","20")]}
    fldSettingsAge  = fieldSettingsLabel MsgAge
    fldSettingsLikings = (fieldSettingsLabel MsgLikings) {fsAttrs = [("cols","40"),("rows","10")]}

    customPersonAgeField = check validateAge intField

    validateAge y
        | y < 18    = Left $ renderMessage master languages MsgUnderAge
        | otherwise = Right y

References

edit
  1. "The wai package". Hackage.haskell.org. Retrieved 2012-10-23.
  2. "The wai-extra package with CGI WAI handler". Hackage.haskell.org. Retrieved 2012-10-23.
  3. "The wai-handler-fastcgi package". Hackage.haskell.org. Retrieved 2012-10-23.
  4. "The wai-handler-scgi package". Hackage.haskell.org. Retrieved 2012-10-23.
  5. "The warp package". Hackage.haskell.org. Retrieved 2012-10-23.
  6. "The wai-handler-launch package". Hackage.haskell.org. Retrieved 2012-10-23.
  7. a b "book - Basics". Yesodweb.com. Retrieved 2012-10-23.
  8. The mkYesod code
  9. a b "book - Routing and Handlers". Yesodweb.com. Retrieved 2012-10-23.
  10. "Playing with Routes and Links". FPComplete.com. 2012-10-17. Retrieved 2012-10-28.
  11. "book - Authentication and Authorization". Yesodweb.com. Retrieved 2012-10-23.
  12. "The yesod-auth package". Hackage.haskell.org. Retrieved 2012-10-26.
  13. "book - Sessions - See section "Ultimate Destination"". Yesodweb.com. Retrieved 2012-11-17.
  14. "Sessions". Yesodweb.com. Retrieved 2012-10-23.
  15. "Web.ClientSession". Hackage.haskell.org. Retrieved 2012-10-25.
  16. "ServerSession: secure modular server-side sessions". Hackage.haskell.org. Retrieved 2018-10-29.
  17. "Web.ServerSession.Frontend.Yesod". Hackage.haskell.org. Retrieved 2018-10-29.
  18. "Session Messages". Yesodweb.com. Retrieved 2018-10-23.
  19. "Creating a Subsite". Yesodweb.com. Retrieved 2012-10-25.
  20. "Yesod and subsites: a no-brainer". Monoid.se. 2012-08-22. Retrieved 2012-10-28.[]
  21. "The Magic of Yesod, part 2 - See section "Static Subsite"". Yesodweb.com. 2010-12-25. Retrieved 2012-10-25.
  22. "The package yesod-static - Static Subsite". Hackage.haskell.org. Retrieved 2012-10-25.
  23. "The package yesod-auth - Auth Subsite". Hackage.haskell.org. Retrieved 2012-10-25.
  24. a b "book - Forms". Yesodweb.com. Retrieved 2012-10-23.
  25. "Yesod.Form.Fields". Hackage.haskell.org. Retrieved 2012-10-23.
  26. "Yesod.Form.Functions runFormPost". Hackage.haskell.org. Retrieved 2012-10-25.
  27. "Yesod.Form.Types". Hackage.haskell.org. Retrieved 2012-10-23.
  28. "HaskellWiki - Applicative functor". haskell.org. Retrieved 2012-10-24.