Saturday, February 3, 2018

HTML-enabled ping (2)

Last time I spent a lot of time getting something not very impressive set up - a web server that responds PONG when you request path /ping from it.

In this post I'm going to make that ping a bit prettier, by using HTML instead of plain text.

I'm going to use a package called blaze-html. It isn't the only way to generate HTML and if I was starting this project all over again I'd possibly use Heist instead, which takes an extremely different approach. But hey ho what's done is done.

If you know basic HTML (and of course you do - this isn't 1992), then you should hopefully find this not hard at all.

Get Blaze installed by by adding dependencies on blaze-html and servant-blaze (like the last post added servant-server and warp). The first of these is Blaze proper and the second is some glue that makes it work with Servant. (there are a pile of servant-* packages for gluing in other packages, some of which I'll talk about later). As you may have noticed from the previous post, I like to run stack build to install dependencies right away, but again you don't have to - they'll get done next time you run that command.

import Servant.HTML.Blaze as SB
import qualified Text.Blaze.Html5 as B

I want the server to still support the old /ping plain text endpoint, but have a new endpoint /htmlping which returns a pretty HTML version of PONG.

First modify the API to say that it has two endpoints: make a new type API which is comprised of both the old PingAPI and a new HtmlPingAPI; and change the api and server type signatures to refer to that new API type. Use the :<|> type operator to compose the two API descriptions together.

type PingAPI = "ping" :> S.Get '[S.PlainText] String

type HtmlPingAPI = "htmlping" :> S.Get '[SB.HTML] B.Html

type API = PingAPI
      :<|> HtmlPingAPI

api :: S.Proxy API
server :: S.Server API

If you try to compile this now, you should get a type error:

/home/benc/src/cwfh28/reg/app/Main.hs:32:10: error:
    * Couldn't match type `S.Handler String'
                     with `S.Handler [Char]
                           :<|> S.Handler
                                  (blaze-markup-0.8.2.0:Text.Blaze.Internal.MarkupM ())'
      Expected type: S.Server API
        Actual type: S.Handler String
    * In the expression: handlePing
      In an equation for `server': server = handlePing
   |
32 | server = handlePing
   |          ^^^^^^^^^^

... which is saying that although you claim to expose an API with both /ping returning a String, and /htmlping returning HTML, your implementation in server only handles the first case. As is the case with most non-trivial Haskell type checking errors, you'll have to stare at this for a while to put the pieces together. But it might be worth understanding how that error actually says what it says.

What needs to happen is that server needs a new implementation, to handle a regular ping and to handle an HTML ping.

server = handlePing :<|> handleHtmlPing


handleHtmlPing :: S.Handler 
handleHtmlPing = return "Some basic HTML"

You also need to add a new language extension to packages.yaml, called "OverloadedStrings" - next to where you added DataKinds and TypeOperators last time.

So with this in place, building and running the app should respond with plain text on /ping and with some very plain HTML on /htmlping - don't believe me about the second one? Well there isn't anything to see yet aside from a changed Content-type: header if you go poking down at the protocol level. Maybe your browser will display it in a different font.

So let's generate something a bit less trivial - Here's a different version of handleHtmlPing:

handleHtmlPing :: S.Handler B.Html
handleHtmlPing = return $ B.docTypeHtml $ do
  B.head $ do
    B.title "HTMLPONG"
  B.body $ do
    B.h1 "HTML Ping Response"
    B.p "It seems to work ok"

which will generate some HTML code that will render something like this:

HTML Ping Response

It seems to work ok

with raw HTML something like this: (modulo formatting)

<html>
  <head>
    <title>HTMLPONG</title>
   </head>
  <body>
    <h1>HTML Ping Response</h1>
    <p>It seems to work ok</p>
  </body>
</html>

Hopefully it's apparent that the Haskell code structure mirrors the structure of the generated HTML. Blaze provides implementations of common tags, taking the body of that tag as an argument; and multiple tags can be sequenced into a single piece of HTML using do.

It's a bit awkward using Haskell syntax to write HTML, if you're used to writing normal HTML; and in the above case there isn't much benefit. But you have access to the whole Haskell language to build the HTML now - you can use if and variables and Control.Monad and write your own functions as macros. etc.

Next, I'll put aside the web side of things and look at representing our application's registration data in Haskell and storing that data in a PostgreSQL database.

commit 8e1c944d gives the changes for this post.

No comments:

Post a Comment