Happstack: část druhá

V první části jsme si Happstack zběžně představili, nainstalovali a napsali si triviální program. Abychom si mohli předvést něco složitějšího, musíme se nejprve podívat na zpracování URL a na různé způsoby prezentace HTML kódu.

Seriál: Happstack: Webový framework v Haskellu (3 díly)

  1. Happstack: část první 19.5.2010
  2. Happstack: část druhá 26.5.2010
  3. Happstack: část třetí 2.6.2010

Jádrem programů napsaných v Haskellu jsou typy. Ani Happstack není
výjimkou. Jak jsme si ukázali minule, funkce simpleHTTP,
pomocí které se dá jednoduše v prohlížeči vypsat text, jako jeden ze
dvou parametrů požaduje parametr typu ServerPartT IO a
(zkráceně ServerPart a), což jsme si označili jako
aplikační kód. Typ IO značí, že se jedná o vstupně-výstupní
akci a typ ServerPartT představuje součást webového
serveru. Tento pojem v sobě zahrnuje HTTP požadavky (typ Request)
a odpovědi (typ Response). Použití jedné nebo více
součástí dává dohromady celý Happstack server, což je vlastně obyčejná
serverová aplikace.

Pod tímto, na první pohled neintuitivním, konceptem si lze představit
aplikaci v Happstacku jako jednu komplexní funkci, která na základě
požadavku a dalších případných okolnostech vrátí odpověď. Hlavní součást
HTTP požadavku je URL, jenž je v Happstacku vyhodnocováno pomocí
monadických funkcí.

URL monády

Typický případ použití funkcí pro zpracování URL v Happstacku je
jejich zapsání do seznamu a jejich následné sloučení pomocí funkce msum.
Jestliže jste nikdy o této funkci neslyšeli, nalézá se v modulu Control.Monad
a funguje podobně jako standardní seznamová funkce concat,
jenom pracuje s monádami. Výsledkem její aplikace bude funkce, která
postupně prochází jednotlivé položky seznamu. V případě, že některá
položka seznamu bude vyhovovat HTTP požadavku, zpracuje se a procházení
skončí; v opačném případě se bude pokračovat v procházení seznamu dále,
dokud některá položka nebude vyhovovat. Pokud se žádná vyhovující
položka v seznamu nenalezne, zobrazí se výchozí stránka s chybou
404
(nenalezeno). Dobrým nápadem je si vždy na posledním místě
seznamu definovat vlastní chybovou stránku, jenž nahradí výchozí.

Základní funkce pro zpracování URL jsou nullDir a dir.
První uspěje, pokud je zadaná adresa prázdná (jedná se tedy o kořenový
adresář), druhá, pokud zadaná část adresy odpovídá řetězci. Jejich
použití je bohužel trochu nekonzistentní, jak ukazuje příklad:

module Main where

import Control.Monad
import Happstack.Server

main :: IO ()
main = simpleHTTP nullConf handleURL

handleURL :: ServerPart Response
handleURL = msum [ do nullDir; ok $ toResponse "Výchozí stránka"
                 , dir "prvni" . ok $ toResponse "První stránka"
                 , dir "druhy" . ok $ toResponse "Druhá stránka"
                 , dir "treti" $ msum [ do nullDir; ok $ toResponse "Třetí stránka"
                                      , dir "ctvrty" . ok $ toResponse "Čtvrtá stránka"
                                      ]
                 , notFound $ toResponse "Chyba 404"
                 ]

Námi definovaná funkce handleURL vrací typ Response,
tedy HTTP odpověď, proto je na všechny řetězce volána funkce toResponse,
která je převede na odpovídající typ. Funkce ok je
speciálním případem funkce return, ale chová se úplně
stejně — naše odpovědí obalí typovým konstruktorem IO. Jak
můžeme vidět u parametru treti, součásti URL lze zanořovat.
V našem případě se tak v prohlížeči po zadání adresy http://localhost:8000/treti/
vypíše text „Třetí stránka“ a po zadání adresy http://localhost:8000/treti/ctvrty/
text „Čtvrtá stránka“. Poslední řádek definuje pomocí funkce notFound
chybovou stránku.

Funkce dir zpracovává adresy bez ohledu na to, zda-li
končí lomítkem nebo ne. Doporučuji toto chování změnit na přesměrování
na verzi s lomítkem, aby byla každá adresa stránky unikátní. Provedeme
to zapsáním následujících řádek do souboru .htaccess:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([a-z0-9/_-]+[^/])$ $1/ [R=301,NC]

Funkce dir zachytí pouze statické části adresy stránek.
Pro zpracování dynamických částí slouží funkce path. Ta
jako parametr očekává funkci zpracovávající řetězec nebo číslo a
vracející odpověď. Typu této funkce určuje, co se bude v adrese
očekávat. Následující kód vypíše číslo zadané v URL:

handleURL :: ServerPart Response
handleURL = msum [ do nullDir; ok $ toResponse "Výchozí stránka"
                 , path pathNumber
                 , notFound $ toResponse "Chyba 404"
                 ]

pathNumber :: Int -> ServerPart Response
pathNumber n = ok . toResponse $ "Číslo v URL: " ++ show n

Po navštívení kupříkladu adresy http://localhost:8000/42/
se vypíše odpověď „Číslo v URL: 42“. Funkce pro zpracování URL můžeme
mezi sebou libovolně kombinovat, jenom si vždy musíme dávat pozor,
abychom vraceli hodnotu správného typu.

Kromě části URL můžeme určovat i jiné parametry dotazu. Kupříkladu
funkce methodOnly určuje, kterou HTTP metodu od dotazu
očekáváme. Protože se taktéž jedná o monadickou funkci, kombinuje se
pomocí operátoru >>:

handleURL :: ServerPart Response
handleURL = msum [ do methodOnly GET >> nullDir; ok $ toResponse "Výchozí stránka"
                 , methodOnly GET >> path pathNumber
                 , notFound $ toResponse "Chyba 404"
                 ]

V tomto kódu vracíme na všechny dotazy využívající jiné metody než
GET (tedy např. POST) chybovou stránku 404.

Psaní HTML kódu

Happstack přináší tři různé způsoby zadávání HTML kódu aplikace:
přímé zadávání do řetězců, generování pomocí kombinátorů a šablony.
První způsob je nejjednodušší, ale není příliš vhodný pro netriviální
aplikace, protože pak náš kód nebývá příliš přehledný a ani bezpečný.
Kombinátorový generátor je údajně o něco rychlejší než šablonovací
systém, ten ovšem nabízí větší kontrolu nad kódem a práce s ním je
přirozenější.

Ještě než se do toho pustíte, nainstalujte si balík Happstack.Helpers
pomocí příkazu cabal install happstack-helpers, protože
obsahuje navíc instance funkcí pro generování HTML kódu.

HTML řetězce

Aby se odlišil obyčejný text od HTML, modul Text.Html
zavádí nový datový typ HtmlString. Když funkci toResponse
předáme obyčejný řetězec typu String, je automaticky
escapován — některé speciální znaky jsou převedeny na HTML entity.
Musíme tedy převést typ String na HtmlString:

module Main where

import Happstack.Server
import Happstack.Helpers
import Text.Html hiding (HtmlString)

main :: IO ()
main = simpleHTTP nullConf . ok $ toResponse ref

ref :: HtmlString
ref = HtmlString "<p><a href="http://zdrojak.cz/">Zdroják</a></p>"

Konstruktor HtmlString převede řetězec na HTML řetězec, v
prohlížeči se tedy zobrazí odkaz. Kdybychom chtěli k řetězci před jeho
převodem na HTML řetězec přiřetězit nějaký vstup, je nutné použít funkci
s relativně dlouhým názvem stringToHtmlString, jinak
nedojde k jeho escapování a my bychom si v aplikaci vytvořili
potenciální bezpečnostní díru:

ref :: HtmlString
ref = HtmlString $ "<p><a href="http://zdrojak.cz/">" ++ stringToHtmlString "<Zdroják>" ++ "</a></p>"

Generování HTML značek

Po nahlédnutí do dokumentace
modulu Text.Html uvidíme velké množství funkcí se stejným
názvem jako jsou některé HTML značky. To jsou již zmiňované kombinátory.
S jejich použitím a kombinací operátorů << (vložení
dovnitř prvku), +++ (přiřetězení dalšího prvku) a !
(modifikace atributu) si HTML kód lehce vygenerujeme:

ref :: Html
ref = header << thetitle << "Titulek" +++ body << p << anchor ! [href "http://zdrojak.cz/"] << "<Zdroják>"

Řetězce se automaticky escapují, takže by nemělo dojít k vložení
nějakého zákeřného kódu zvenku. Jak už název napovídá, kombinátory jsou
funkce, jež lze lehce kombinovat a spojovat dohromady, takže při dobrém
návrhu nebývá problém s opakováním kódu.

HTML šablony

Šablony mají tu výhodu, že je oddělen HTML kód od aplikačního. Lehce
tak můžeme zadat vytvoření HTML kódu někomu úplně jinému a pouze mu
poskytnout seznam proměnných, které v šablonách chceme použít. Haskell
obsahuje několik šablonovacích systémů, my se podíváme na HStringTemplate
(modul Text.StringTemplate).
Tento systém je založen na knihovně Java StringTemplate.

Nejprve si založíme adresář templates a vytvoříme v něm
hlavní šablonu body.st:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN">
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<title>$ title $</title>
</head>

<body>
$ ref() $
</body>

Tato šablona obsahuje proměnnou title a bude inkludovat
šablonu ref.st, kterou umístíme taktéž do adresáře templates:

<a href="http://zdrojak.cz/">$ link $</a>

Proměnné se v šabloně vyznačují pomocí dvou dolarových symbolů,
inkluze další šablony stejně, jenom se za identifikátor dopíšou závorky.
Kdybychom chtěli do šablony přidat komentář, stačí ho napsat mezi
dvojici znaků $! a !$. Zbývá už jen napsat
nový kód aplikace:

module Main where

import Happstack.Server
import Happstack.Helpers
import Control.Monad.Trans
import Text.StringTemplate
import Text.StringTemplate.Helpers
import Text.Html hiding (HtmlString)

main :: IO ()
main = simpleHTTP nullConf ref

escapeKeys :: [(String, String)] -> [(String, String)]
escapeKeys = map ((x, y) -> (x, stringToHtmlString y))

ref :: ServerPart Response
ref = liftIO $ do
    templates <- directoryGroup "templates"
    let keys = escapeKeys [("title", "Titulek"), ("link", "<Zdroják>")]
        body = renderTemplateGroup templates keys "body"
    return . toResponse $ HtmlString body

Tento kód je o trochu složitější. V první řadě je zde potřeba funkce liftIO
z modulu Control.Monad.Trans,
jež zařídí převod z monády IO na jinou. Ve funkci ref
jsou totiž načítány šablony, proto se celá funkce snaží zabalit do
monády IO. To je taktéž důvod, proč musíme použít obecnější
funkci return namísto funkce ok. Dále se zde
volá funkce directoryGroup sloužící k načtení adresáře s
šablonami. Funkce renderTemplateGroup vygeneruje šablonu
spolu s proměnnými v proměnné keys a zajistí i inkluzi.
Protože šablonový systém automaticky neescapuje proměnné, bylo potřeba
sem připsat vlastní funkci escapeKeys, která zajišťuje
ošetření vstupu.

Související odkazy

V následujícím dílu si vyzkoušíme zpracovávání formulářů.

Autor je dlouhodobým studentem Fakulty informatiky, webový nadšenec a programátor — nejraději programuje v jazycích Haskell a Python.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Zdroj: https://www.zdrojak.cz/?p=3239