Snaplet sind eigenständiger Anwendungen. Sie bieten meißt eine bestimmte Funktionalität, z.B. ein Wiki oder ein Chat. Sie sind zudem kombinierbar, wodurch man seine Webanwendung aus zahlreichen Snaplets zusammenbauen kann. Es gibt zudem eine Snaplet API, die eine Infrastruktur für den Anwendungszustand und -umgebung, sowie Funktionen zur Initialisierung, Neuladen und Aufräumen bietet.
Mit Snap werden drei Snaplets ausgeliefert. Auf sessions und auth werden wir in diesem Kapitel noch eingehen. Die Templating Engine Heist wird dann im nächsten Kapitel behandelt. Weiterhin besteht die Möglichkeit third-party Snaplets einzubinden. Diese kann man am Besten über die Snap Website erhalten.
Die meißten Snaplets besitzen irgendeinen Zustand oder Umgebungsinformationen. Zudem sollte der Modulname mit Snap.Snaplet beginnen. Zudem kann eine Initialisierungsfunktion existieren. Diese ist aber optional.
Wollen wir nun einmal beispielhaft eine Datenbankanbindung mittels Postgres umsetzen und dabei das Snaplet postgresql-simple nutzen. Dazu erstellen wir einen Ordner namens postgrestest und rufen in ihm snap init auf, um ein Grundgerüst zu bekommen. In unserem Ordner können wir zunächst static/ und snaplets/ löschen, da wir diese Ordner nicht benötigen. Zudem müssen wir Postgres und das Snaplet selbst installieren.
1 2 3 4 5 6 | $ mkdir postgrestest
$ cd postgrestest
$ rm static/ snaplets/
# Für Debian Derivate (X.X steht für die aktuellste Version)
$ sudo apt-get install postgresql postgresql-server-dev-X.X
$ cabal install snaplet-postgresql-simple
|
Als letzte Grundlage muss noch die Abhängigkeit in die postgrestest.cabal eingetragen werden. Dazu im Abschnitt dependencies folgende Zeile anhängen:
1 | snaplet-postgresql-simple >= 0.1
|
Als Nächstes bearbeiten wir die Application.hs. Wir entfernen die unnötigen Imports und räumen die Datenstruktur auf. Außerdem fügen wir das Postgres Snaplet in die Datenstruktur ein. Abschließend fügen wir eine Instanz für HasPostgres hinzu, um Zugriff auf Postgres-relevanten Zustand zu bekommen. Das Ganze sieht dann wie folgt aus:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | {-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleInstances #-}
------------------------------------------------------------------------------
-- | This module defines our application's state type and an alias for its
-- handler monad.
module Application where
------------------------------------------------------------------------------
import Control.Lens
import Snap (get)
import Snap.Snaplet
import Snap.Snaplet.PostgresqlSimple
------------------------------------------------------------------------------
data App = App
{ _pg :: Snaplet Postgres
}
makeLenses ''App
instance HasPostgres (Handler b App) where
getPostgresState = with pg get
------------------------------------------------------------------------------
type AppHandler = Handler App App
|
Nun wollen wir die Site.hs bearbeiten. Auch hier entfernen wir die Imports des Grundgerüsts, die wir für das Beispiel nicht benötigen. Zudem entfernen wir die 4 Handles für die Authorisierung. Diese sind nicht für die Authorisierung an der Datenbank notwendig, nur wenn wir ein Benutzersystem aufbauen möchten. Auch die drei routing-Einträge login, logout und new_user können entfernt werden. Zum Abschluss definieren wir alles Nötige, um das Snaplet für Postgres in der App verfügbar zu haben. Den Code gibt es weiter unten inklusive einer Beispieldatenstruktur und den zugehörigen Handles und routing-Einträgen. Zunächst aber müssen wir noch den Zugang für die Datenbank unter ./snaplets/postgresql-simple/devel.cfg eintragen. Es ist möglich, dass der Ordner noch nicht existiert, wenn die Anwendung bisher nicht gestartet wurde. Der Eintrag sieht dann wie folgt aus:
1 2 3 4 5 | host = "localhost"
port = 5432
user = "<user>"
pass = "<password>"
db = "postgrestest"
|
Wir nehmen hier an, dass die Datenbank postgrestest bereits existiert. Für user und pass müssen entsprechend die Daten ersetzt werden.
Erzeugen wir nun in unserer Site.hs eine Datenstruktur für ein Auto mit den Eigenschaften Marke, Typ und Besitzer, alle vom Typ Text. Damit Autos auch in unserer Datenbank abgelegt werden können, benötigen wir eine entsprechende Tabelle. Dazu erzeugen wir eine .sql-Datei. Der Name und Ort sind hierbei nicht so wichtig. Ich habe die Datei genauso wie das Projekt genannt und im Unterordner sql/ platziert. Hier müssen wir nun die gleiche Datenstruktur wie in unserer Anwendung erzeugen:
1 2 3 4 5 | CREATE TABLE car (
brand TEXT NOT NULL,
carType TEXT NOT NULL,
owner TEXT NOT NULL
);
|
Diese Datei müssen wir nur noch in unsere Datenbank einspielen und schon haben wir das Grundgerüst fast fertig. Um auch entsprechende SQL-Queries ausführen zu können, müssen wir für unsere Datenstruktur noch eine Instanz für FromRow anlegen.
Nun fehlen nur noch die routing-Einträge, um Autos hinzuzufügen, zu löschen und alle existierenden aufzulisten. Die Parameter kommen über die Request-URL. Die Funktionen holen sich die Werte heraus, verarbeiten diese und machen die entsprechenden Anfragen. Nachfolgend zeige ich noch den Code der Site.hs, sowie ein paar Beispielaufrufe, die man mit der laufenden App durchführen kann:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | {-# LANGUAGE OverloadedStrings #-}
------------------------------------------------------------------------------
-- | This module is where all the routes and handlers are defined for your
-- site. The 'app' function is the initializer that combines everything
-- together and is exported by this module.
module Site
( app
) where
------------------------------------------------------------------------------
import Control.Applicative
import Data.ByteString (ByteString)
import qualified Data.Text as T
import Database.PostgreSQL.Simple.FromRow
import Snap.Core
import Snap.Snaplet
import Snap.Snaplet.PostgresqlSimple
import Snap.Util.FileServe
------------------------------------------------------------------------------
import Application
------------------------------------------------------------------------------
-- | Our example structure car with brand, type and owner
data Car = Car
{ _brand :: T.Text
, _carType :: T.Text
, _owner :: T.Text
}
------------------------------------------------------------------------------
-- | instance for FromRow to get data from our Car table
instance FromRow Car where
fromRow = Car <$> field <*> field <*> field
------------------------------------------------------------------------------
-- | instance for show for our Car data
instance Show Car where
show (Car brand carType owner) = "Car { brand: " ++ T.unpack brand ++ ", carType: " ++ T.unpack carType ++ ", owner: " ++ T.unpack owner ++ " }\n"
------------------------------------------------------------------------------
-- | The application's routes.
routes :: [(ByteString, Handler App App ())]
routes = [ ("/car/add/:brand/:cartype/:owner", addCar)
, ("/cars", getAllCars)
, ("/car/remove/:owner", deleteCarByOwner)
, ("", serveDirectory "static")
]
------------------------------------------------------------------------------
-- | Add a car to the database
addCar :: Handler App App ()
addCar = do
brand <- getParam "brand"
carType <- getParam "cartype"
owner <- getParam "owner"
execute "INSERT INTO car VALUES (?, ?, ?)" (brand, carType, owner)
redirect "/cars"
------------------------------------------------------------------------------
-- | Get all cars from the database
getAllCars :: Handler App App ()
getAllCars = do
allCars <- query_ "SELECT * FROM car"
writeText $ T.pack $ foldl (++) "" $ map show (allCars :: [Car])
------------------------------------------------------------------------------
-- | Delete a car from the database by owner
deleteCarByOwner :: Handler App App ()
deleteCarByOwner = do
owner <- getParam "owner"
execute "DELETE FROM car WHERE owner = ?" (Only owner)
redirect "/cars"
------------------------------------------------------------------------------
-- | The application initializer.
app :: SnapletInit App App
app = makeSnaplet "app" "A small postgresql test Snaplet" Nothing $ do
pgg <- nestSnaplet "pg" pg pgsInit
addRoutes routes
return $ App pgg
|
1 2 3 4 5 6 7 8 9 10 | # installieren der Anwendung (im Root der Anwendung ausführen)
$ cabal install
# starten des Servers
$ postgrestest -p 8000
#############################
# folgend URLS zum Aufrufen #
#############################
localhost:8000/cars # alle Autos auflisten
localhost:8000/car/add/VW/Golf/MaxMustermann #fügt einen VW Golf mit Besitzer MaxMustermann hinzu
localhost:8000/car/remove/MaxMustermann # entfernt alle Autos von MaxMustermann
|
Um Sessions benutzen zu können, muss das Session Snaplet der Datenstruktur hinzugefügt werden. Nun kann man die eigenen Handler mit withSession einpacken und in diesen die Funktionen des Session-Snaplets benutzen. Dazu gehören Funktionen zum Setzen und Laden von Key-Value-Paaren, das Löschen und weitere.
Man kann mit dem mitgelieferten Auth-Snaplet ganz einfach eine Anwendung mit Nutzerverwaltung inklusive Registrierung schreiben. Das Default-Gerüst von Snap (eingerichtet mit snap init) bietet hier ein sehr gutes Beispiel. Allerdings werden die Benutzer hier in einem Json-Objekt gespeichert. Dies würde man eher mit einer Datenbank im Hintergrund umsetzen, der Einfachheit halber wurde es aber ohne Datenbank gelöst.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | ------------------------------------------------------------------------------
-- | Render login form
handleLogin :: Maybe T.Text -> Handler App (AuthManager App) ()
handleLogin authError = heistLocal (I.bindSplices errs) $ render "login"
where
errs = [("loginError", I.textSplice c) | c <- maybeToList authError]
------------------------------------------------------------------------------
-- | Handle login submit
handleLoginSubmit :: Handler App (AuthManager App) ()
handleLoginSubmit =
loginUser "login" "password" Nothing
(\_ -> handleLogin err) (redirect "/")
where
err = Just "Unknown user or password"
|
handleLoginSubmit behandelt eine POST-Anfrage und meldet den User mit der Funktion loginUser an. Im HTML haben wir ein einfaches Formular mit den Feldern login und password.
1 2 3 4 | ------------------------------------------------------------------------------
-- | Logs out and redirects the user to the site index.
handleLogout :: Handler App (AuthManager App) ()
handleLogout = logout >> redirect "/"
|
Dieser Handle meldet den Benutzer ab. Dafür genügt ein einfacher Aufruf der Auth-Snaplet-Funktion logout.
1 2 3 4 5 6 7 | ------------------------------------------------------------------------------
-- | Handle new user form submit
handleNewUser :: Handler App (AuthManager App) ()
handleNewUser = method GET handleForm <|> method POST handleFormSubmit
where
handleForm = render "new_user"
handleFormSubmit = registerUser "login" "password" >> redirect "/"
|
Auch das Registrieren funktioniert mit einer einfachen Funktion des Snaplets.
Der Vollständigkeit halber noch das Routing und die Initialisierung der Anwendung.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | ------------------------------------------------------------------------------
-- | The application's routes.
routes :: [(ByteString, Handler App App ())]
routes = [ ("/login", with auth handleLoginSubmit)
, ("/logout", with auth handleLogout)
, ("/new_user", with auth handleNewUser)
, ("", serveDirectory "static")
]
------------------------------------------------------------------------------
-- | The application initializer.
app :: SnapletInit App App
app = makeSnaplet "app" "An snaplet example application." Nothing $ do
h <- nestSnaplet "" heist $ heistInit "templates"
s <- nestSnaplet "sess" sess $
initCookieSessionManager "site_key.txt" "sess" (Just 3600)
-- NOTE: We're using initJsonFileAuthManager here because it's easy and
-- doesn't require any kind of database server to run. In practice,
-- you'll probably want to change this to a more robust auth backend.
a <- nestSnaplet "auth" auth $
initJsonFileAuthManager defAuthSettings sess "users.json"
addRoutes routes
addAuthSplices h auth
return $ App h s a
|
Schlußendlich lassen sich im HTML noch die Splices
< ifLoggedIn >< /ifLoggedIn >, < ifLoggedOut >< /ifLoggedOut > und < loggedInUser > loggedInUser > nutzen. Die ersten Beiden kann man nutzen, um unterschiedliche Inhalte für angemeldete und abgemeldete Benutzer darzustellen. Der Letzte fügt einfach den Namen des eingeloggten Benutzers in den DOM-Tree ein.