robx
robx

Reputation: 2329

Handling regular form posts (application/x-www-form-urlencoded) with servant

How can I handle regular form POSTs with Servant? In particular, given an HTML form like

<form action="/check" method="post">
  Solution:
  <input name="code" type="text">
  <input type="submit">
</form>

and

data CheckResult = Correct | Wrong

instance ToHtml CheckResult
    ...

checkCode :: Text -> Handler CheckResult
checkCode code = if code == "secret" then Correct else Wrong

how do I string things together?

Upvotes: 9

Views: 1524

Answers (2)

erewok
erewok

Reputation: 7845

I just wanted to add an answer for a recent version of Servant, because I had to google various things in order to assemble a complete, working version of form handling.

The above answer works well for earlier versions of Servant, but I got stuck using forms when upgrading to Servant 0.9.

Here's how I did it.

First of all, they switched from a custom Form implementation to the one in http-api-data, so you need that in your cabal file:

some-project.cabal

  build-depends:       base >= 4.7 && < 5
                     , aeson
                     , blaze-html
                     , http-api-data

Next, you can declare a form, like above, but you can use GHC.Generics to automatically derive a FromForm instance:

{-# LANGUAGE DeriveGeneric     #-}

module Html.Contact where

import           GHC.Generics
import           Servant
import           Web.FormUrlEncoded          (FromForm)

data ContactForm = ContactForm
 { cname    :: !T.Text
 , cemail   :: !T.Text
 , cmessage :: !T.Text
 } deriving (Eq, Show, Generic)

instance FromForm ContactForm

After that, you can use the regular FormUrlEncoded ContentType from Servant in your endpoint:

type ContactApi = "contact" :> ReqBody '[FormUrlEncoded] ContactForm
                                   :> Post '[HTML] Html

Almost forgot: How to render the thing

You will also need a page, probably, where you are displaying your form? Well, the "name" attributes have to match the fields in your form (here's how I did it, using Blaze):

contactForm :: H.Html
contactForm = H.section ! A.id "contact" ! A.class_ "container contact-us u-full-width u-max-full-width" $
  H.div ! A.class_ "row" $ do
    H.div ! A.class_ "eight columns contact-us-form" $
      H.form ! A.method "post" ! A.action "/contact" $ do
        H.div ! A.class_ "row" $ do
          H.div ! A.class_ "six columns" $
            H.input ! A.class_ "u-full-width" ! A.type_ "text" ! A.name "cname" ! A.placeholder "Name" ! A.id "nameInput"
          H.div ! A.class_ "six columns" $
            H.input ! A.class_ "u-full-width" ! A.type_ "text" !  A.name "cemail" ! A.placeholder "Email" ! A.id "emailInput"
        H.textarea ! A.class_ "u-full-width" ! A.name "cmessage" ! A.placeholder "Message" ! A.id "messageInput" $ ""
        H.input ! A.class_ "button u-pull-right" ! A.type_ "submit" !  A.value "Send"

Upvotes: 12

robx
robx

Reputation: 2329

Servant supports this via the data type FormUrlEncoded, and the class FromFormUrlEncoded (renamed to FromForm in Servant 0.9).

First we define a data type for the form data, and rewrite our handler to accept that.

data CheckRequest = CheckRequest { code :: Text }

checkCode :: CheckRequest -> Handler CheckResult
checkCode (CheckRequest code) = if code == "secret" then Correct else Wrong

Then we specify a POST body of type application/x-www-form-urlencoded.

type API = "check"
         :> ReqBody '[FormUrlEncoded] CheckRequest
         :> Post '[HTML] CheckResult

Now all that's required is to make CheckRequest an instance of FromFormUrlEncoded.

instance FromFormUrlEncoded CheckRequest where
  --fromFormUrlEncoded :: [(Text, Text)] -> Either String CheckRequest
  fromFormUrlEncoded [("code", c)] = Right (CheckRequest c)
  fromFormUrlEncoded _             = Left "expected a single field `code`"

Upvotes: 4

Related Questions