Spock
Spock is a lightweight Haskell web framework inspired by Ruby's Sinatra. It provides a full toolbox including everything to get a quick start into web programming using Haskell.
Features
- Fast routing
- JSON
- Sessions
- Cookies
- Database helper
- CSRF protection
Hello World!
Spock-0.11.0.0
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Web.Spock
import Web.Spock.Config
import qualified Data.Text as Text
main :: IO ()
main = do
  spockCfg <- defaultSpockCfg () PCNoDatabase ()
  runSpock 8080 $ spock spockCfg $ do
    get root $
      text "Hello Spock!"
    get ("hello" <//> var) $ \name ->
      text (Text.concat ["Hello ", name, "!"])
-- Web.Spock
get root :: ActionCtxT ctx (WebStateM conn sess st) () -> SpockCtxM ctx conn sess st ()
text :: MonadIO m => Text -> ActionCtxT ctx m a
Example: Login API
Form data
To get a form data, Spock provides param function.
post "login" $ do
  maybeUsername <- param "username"
  case maybeUsername of
    Nothing       -> setStatus status400 >> text "Missing parameter."
    Just username -> text (Text.concat ["Username: ", username])
-- Web.Spock
param :: (PathPiece p, MonadIO m) => Text -> ActionCtxT ctx m (Maybe p)
$ curl -XPOST -F'username=lotz' http://localhost:8080/login
Username: lotz
First Login API
Preparing the set of username and password.
userList :: [(Text, Text)]
userList =
  [ ("lotz", "password1")
  , ("alice", "password2")
  , ("bob", "password3")
  ]
findCredential :: MonadIO m => ActionCtxT ctx m (Maybe (Text, Text))
findCredential = do
  username <- param "username"
  password <- param "password"
  pure $ (,) <$> username <*> password
Creating post "login".
post "login" $ do
  credential <- findCredential
  case credential of
    Nothing -> setStatus status400 >> text "Missing parameter."
    Just (username, password) ->
      if lookup username userList == Just password
        then text "Login succeed."
        else setStatus status400 >> text "Wrong parameter."
-- Data.List
lookup :: Eq a => a -> [(a, b)] -> Maybe b
$ curl -XPOST -F'username=lotz' -F 'password=password1' http://localhost:8080/login
Login succeed.
Of course, we need to hash password
Using bcrypt library.
-- Crypto.BCrypt
hashPasswordUsingPolicy :: HashingPolicy -> ByteString -> IO (Maybe ByteString)
validatePassword :: ByteString -> ByteString -> Bool
Hashing password in userList.
userList :: [(Text, ByteString)]
userList =
  [ ("lotz", "$2y$04$UMnRYB26AvreBv0v4efdauIIr3qTM0opEKln26tSy6XXmKV4hS56S")
  , ("alice", "$2y$04$kRpVhhxbgerHneJJ4HqmNe8MIB7WbPJPXXI3Zy0hFhWpiaJIz6t3m")
  , ("bob", "$2y$04$qbhEesNseMuIpJfFzN7F7uCN6Y5CB0vmMs7eq708CAAx8wnzxvGAm")
  ]
Create a validation function.
validateCredential :: [(Text, ByteString)] -> (Text, Text) -> Bool
validateCredential userList (username, password) =
  case lookup username userList of
    Nothing -> False
    Just passhash ->
      let password' = Text.encodeUtf8 password
      in  validatePassword passhash password'
Connect to Database
Using mysql-simple library.
Create sample database and table in MySQL.
mysql> USE login_sample;
Database changed
mysql> SHOW TABLES;
+------------------------+
| Tables_in_login_sample |
+------------------------+
| user                   |
+------------------------+
1 row in set (0.00 sec)
mysql> DESC user;
+------------+------------------+------+-----+-------------------+-----------------------------+
| Field      | Type             | Null | Key | Default           | Extra                       |
+------------+------------------+------+-----+-------------------+-----------------------------+
| id         | int(10) unsigned | NO   | PRI | NULL              | auto_increment              |
| username   | varchar(255)     | NO   |     | NULL              |                             |
| password   | varchar(255)     | NO   |     | NULL              |                             |
| created_at | timestamp        | NO   |     | CURRENT_TIMESTAMP |                             |
| updated_at | timestamp        | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+------------+------------------+------+-----+-------------------+-----------------------------+
5 rows in set (0.02 sec)
mysql> SELECT * FROM user;
+----+----------+--------------------------------------------------------------+---------------------+---------------------+
| id | username | password                                                     | created_at          | updated_at          |
+----+----------+--------------------------------------------------------------+---------------------+---------------------+
|  1 | lotz     | $2y$04$UMnRYB26AvreBv0v4efdauIIr3qTM0opEKln26tSy6XXmKV4hS56S | 2016-08-28 20:10:52 | 2016-08-28 20:10:52 |
|  2 | alice    | $2y$04$kRpVhhxbgerHneJJ4HqmNe8MIB7WbPJPXXI3Zy0hFhWpiaJIz6t3m | 2016-08-28 20:11:06 | 2016-08-28 20:11:06 |
|  3 | bob      | $2y$04$qbhEesNseMuIpJfFzN7F7uCN6Y5CB0vmMs7eq708CAAx8wnzxvGAm | 2016-08-28 20:11:22 | 2016-08-28 20:11:22 |
+----+----------+--------------------------------------------------------------+---------------------+---------------------+
3 rows in set (0.01 sec)
Preparing database connection.
main :: IO ()
main = do
  let mysqlConnect = MySQL.connect MySQL.defaultConnectInfo {MySQL.connectDatabase = "login_sample"}
      dbConn = ConnBuilder mysqlConnect MySQL.close (PoolCfg 1 1 30)
  spockCfg <- defaultSpockCfg () (PCConn dbConn) ()
  runSpock 8080 $ spock spockCfg $ do
-- Web.Spock
data ConnBuilder a
   = ConnBuilder
   { cb_createConn :: IO a
   , cb_destroyConn :: a -> IO ()
   , cb_poolConfiguration :: PoolCfg
   }
Validation function is changed as below.
validateCredential :: (Text, Text) -> ActionCtxT ctx (WebStateM MySQL.Connection sess st) (Maybe Int)
validateCredential (username, password) = do
  user <- runQuery $ \conn -> do
    results <- MySQL.query conn "SELECT id, password FROM user WHERE username = ?" (MySQL.Only (Text.encodeUtf8 username))
    pure $ (listToMaybe results :: Maybe (Int, ByteString))
  pure $ do
    (userId, passhash) <- user
    guard $ validatePassword passhash (Text.encodeUtf8 password)
    pure userId
-- Data.Maybe
listToMaybe :: [a] -> Maybe a
listToMaybe []    =  Nothing
listToMaybe (a:_) =  Just a
-- Web.Spock
runQuery :: (MySQL.Connection -> IO a) -> ActionCtxT ctx (WebStateM MySQL.Connection sess st) a
Template
Actually, Spock has no template support.
Using stache library for templates.
Preparing templates as below.
views
├── index.mustache
└── login.mustache
<p>Hello {{text}}!</p>
<form method='POST' action='/login'>
  <legend>username: </legend>
  <input type='text' name='username'/>
  <br/>
  <legend>password: </legend>
  <input type='password' name='password'/>
  <br/>
  <input type='submit'/>
</form>
Let's use templates.
main :: IO ()
main = do
  template <- compileMustacheDir "index" "views/"
  let render pname = TL.toStrict . renderMustache (template {templateActual = pname})
  ...
  spockCfg <- defaultSpockCfg () (PCConn dbConn) ()
  runSpock 8080 $ spock spockCfg $ do
    get root $ html (render "index" (object ["text" .= ("Spock" :: Text)]))
    get "login" $ html (render "login" (object []))
-- Web.Spock
html :: MonadIO m => Text -> ActionCtxT ctx m a
-- Text.Mustache
compileMustacheDir :: (MonadIO m, MonadThrow m) => PName -> FilePath -> m Template
renderMustache :: Template -> Value -> Text
Session
To use session, you just change the type of an argument of defaultSpockCfg.
spockCfg <- defaultSpockCfg (Nothing :: Maybe Int) (PCConn dbConn) ()
runSpock 8080 $ spock spockCfg $ do
  get root $ do
    maybeUserId <- readSession
    case maybeUserId of
      Nothing -> redirect "/login"
      Just userId -> html (render "index" (object ["text" .= (Text.pack (show userId))]))
  get "logout" $ writeSession Nothing >> redirect "/login"
  post "login" $ do
    credential <- findCredential
    case credential of
      Nothing -> redirect "/login"
      Just (username, password) -> do
        maybeUserId <- validateCredential (username, password)
        case maybeUserId of
          Nothing -> redirect "/login"
          Just userId -> writeSession (Just userId) >> redirect "/"
-- Web.Spock
readSession :: ActionCtxT ctx (WebStateM conn sess st) sess
writeSession :: sess -> ActionCtxT ctx (WebStateM conn sess st) ()
modifySession :: (sess -> sess) -> SpockActionCtx ctx conn sess st ()
Logging
Spock create a Middleware in WAI.
WAI is a generic web application interface like WSGI and Rack.
-- Network.Wai
type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
type Middleware = Application -> Application
-- Web.Spock
spock :: SpockCfg conn sess st -> SpockCtxM () conn sess st -> IO Middleware
You can combine any other Middleware with your Spock app.
Let's use RequestLogger in wai-extra.
spockCfg <- defaultSpockCfg (Nothing:: Maybe Int) (PCConn dbConn) ()
runSpock 8080 $ fmap (logStdoutDev.) $ spock spockCfg $ do
  get root $ do
Thank you  
This slide is used at Haskell Day 2016. For this reason it is written in English, but I use a lot of images and code to make it easier to understand  .
 .
Final example code is uploaded here.





