Last week we learned the basics of the the Spock library. We saw how to set up some simple routes. Like Servant, there's a bit of dependent-type machinery with routing. But we didn't need to learn any complex operators. We just needed to match up the number of arguments to our routes. We also saw how to use an application state to persist some data between requests.
This week, we'll add a couple more complex features to our Spock application. First we'll connect to a database. Second, we'll use sessions to keep track of users.
For some more examples of useful Haskell libraries, check out our Production Checklist!
Adding a Database
Last week, we added some global application state. Even with this improvement, our vistor count doesn't persist. When we reset the server, everything goes away, and our users will see a different number. We can change this by adding a database connection to our server. We'll follow the Spock tutorial example and connect to an SQLite database by using Persistent.
If you haven't used Persistent before, take a look at this tutorial in our Haskell Web series! You can also look at our sample code on Github for any of the boilerplate you might be missing. Here's the super simple schema we'll use. Remember that Persistent will give us an auto-incrementing primary key.
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase| NameEntry json name Text deriving Show |]
Spock expects us to use a pool of connections to our database when we use it. So let's create one to an SQLite file using
createSqlitePool. We need to run this from a logging monad. While we're at it, we can migrate our database from the
main startup function. This ensures we're using an up-to-date schema:
import Database.Persist.Sqlite (createSqlitePool) ... main :: IO () main = do ref <- newIORef M.empty pool <- runStdoutLoggingT $ createSqlitePool "spock_example.db" 5 runStdoutLoggingT $ runSqlPool (runMigration migrateAll) pool ...
Now that we've created this pool, we can pass that to our configuration. We'll use the
PCPool constructor. We're now using an
SQLBackend for our server, so we'll also have to change the type of our router to reflect this:
main :: IO () main = do … spockConfig <- defaultSpockCfg EmptySession (PCPool pool) (AppState ref) runSpock 8080 (spock spockConfig app) app :: SpockM SqlBackend MySession AppState () app = ...
Now we want to update our route action to access the database instead of this map. But first, we'll write a helper function that will allow us to call any SQL action from within our
SpockM monad. It looks like this:
runSQL :: (HasSpock m, SpockConn m ~ SqlBackend) => SqlPersistT (LoggingT IO) a -> m a runSQL action = runQuery $ \conn -> runStdoutLoggingT $ runSqlConn action conn
At the core of this is the
runQuery function from the Spock library. It works since our router now uses
SpockM SqlBackend instead of
SpockM (). Now let's write a couple SQL actions we can use. We'll have one performing a lookup by name, and returning the
Key of the first entry that matches, if one exists. Then we'll also have one that will insert a new name and return its key.
fetchByName :: T.Text -> SqlPersistT (LoggingT IO) (Maybe Int64) fetchByName name = (fmap (fromSqlKey . entityKey)) <$> (listToMaybe <$> selectList [NameEntryName ==. name] ) insertAndReturnKey :: T.Text -> SqlPersistT (LoggingT IO) Int64 insertAndReturnKey name = fromSqlKey <$> insert (NameEntry name)
Now we can use these functions instead of our map!
app :: SpockM SqlBackend MySession AppState () app = do get root $ text "Hello World!" get ("hello" <//> var) $ \name -> do existingKeyMaybe <- runSQL $ fetchByName name visitorNumber <- case existingKeyMaybe of Nothing -> runSQL $ insertAndReturnKey name Just i -> return i text ("Hello " <> name <> ", you are visitor number " <> T.pack (show visitorNumber))
And voila! We can shutdown our server between runs, and we'll preserve the visitors we've seen!
Now, using a route to identify our users isn't what we want to do. Anyone can visit any route after all! So for the last modification to the server, we're going to add a small "login" functionality. We'll use the App's session to track what user is currently visiting. Our new flow will look like this:
- We'll change our entry route to
- If the user visits this, we'll show a field allowing them to enter their name and log in.
- Pressing the login button will send a post request to our server. This will update the session to match the session ID with the username.
- It will then send the user to the
/homepage, which will greet them and present a logout button.
- If they log out, we'll clear the session.
Note that using the session is different from using the app state map that we had in the first part. We share the app state across everyone who uses our server. But the session will contain user-specific references.
Adding a Session
The first step is to change our session type. Once again, we'll use a
IORef wrapper around a map. This time though, we'll use a simple type synonym to simplify things. Here's our type definition and the updated main function.
type MySession = IORef (M.Map T.Text T.Text) main :: IO () main = do ref <- newIORef M.empty -- Initialize a reference for the session sessionRef <- newIORef M.empty pool <- runStdoutLoggingT $ createSqlitePool "spock_example.db" 5 runStdoutLoggingT $ runSqlPool (runMigration migrateAll) pool -- Pass that reference! spockConfig <- defaultSpockCfg sessionRef (PCPool pool) (AppState ref) runSpock 8080 (spock spockConfig app)
Updating the Hello Page
Now let's update our "Hello" page. Check out the appendix below for what our
helloHTML looks like. It's a "login" form with a username field and a submit button.
-- Notice we use MySession! app :: SpockM SqlBackend MySession AppState () app = do get root $ text "Hello World!" get "hello" $ html helloHTML ...
Now we need to add a handler for the post request to
/hello. We'll use the
post function instead of
get. Now instead of our action taking an argument, we'll extract the post body using the
body function. If our application were more complicated, we would want to use a proper library for Form URL encoding and decoding. But for this small example, we'll use a simple helper
decodeUsername. You can view this helper in the appendix.
app :: SpockM SqlBackend MySession AppState () app = do … post "hello" $ do nameEntry <- decodeUsername <$> body ...
Now we want to save this user using our session and then redirect them to the home page. First we'll need to get the session ID and the session itself. We use the functions
readSession for this. Then we'll want to update our session by associating the name with the session ID. Finally, we'll redirect to
post "hello" $ do nameEntry <- decodeUsername <$> body sessId <- getSessionId currentSessionRef <- readSession liftIO $ modifyIORef' currentSessionRef $ M.insert sessId (nameEntryName nameEntry) redirect "home"
The Home Page
Now on the home page, we'll want to check if we've got a user associated with the session ID. If we do, we'll display some text greeting that user (and also display a logout button). Again, we need to invoke
readSession. If we have no user associated with the session, we'll bounce them back to the
get "home" $ do sessId <- getSessionId currentSessionRef <- readSession currentSession <- liftIO $ readIORef currentSessionRef case M.lookup sessId currentSession of Nothing -> redirect "hello" Just name -> html $ homeHTML name
The last piece of functionality we need is to "logout". We'll follow the familiar pattern of getting the session ID and session. This time, we'll change the session by clearing the session key. Then we'll redirect the user back to the
post "logout" $ do sessId <- getSessionId currentSessionRef <- readSession liftIO $ modifyIORef' currentSessionRef $ M.delete sessId redirect "hello"
And now our site tracks our users' sessions! We can access the same page as a different user on different sessions!
This wraps up our exploration of the Spock library! We've done a shallow but wide look at some of the different features Spock has to offer. We saw several different ways to persist information across requests on our server! Connecting to a database is the most important. But using the session is a pretty advanced feature that is quite easy in Spock!
Appendix - HTML Fragments and Helpers
helloHTML :: T.Text helloHTML = "<html>\ \<body>\ \<p>Hello! Please enter your username!\ \<form action=\"/hello\" method=\"post\">\ \Username: <input type=\"text\" name=\"username\"><br>\ \<input type=\"submit\"><br>\ \</form>\ \</body>\ \</html>" homeHTML :: T.Text -> T.Text homeHTML name = "<html><body><p>Hello " <> name <> "</p>\ \<form action=\"logout\" method=\"post\">\ \<input type=\"submit\" name=\"logout_button\"<br>\ \</form>\ \</body>\ \</html>" -- Note: 61 -> '=' in ASCII -- We expect input like "username=christopher" parseUsername :: B.ByteString -> T.Text parseUsername input = decodeUtf8 $ B.drop 1 tail_ where tail_ = B.dropWhile (/= 61) input