2

Haskell Servant docs provide several examples for writing an API to serve some content with a type level DSL like this

type API = "position" :> Capture "x" Int :> Capture "y" Int :> Get '[JSON] Position

What I want to do is write a similar API that can take several inputs as a GET request that also works from a browser. (Following code is abridged and simplified).

type QuestionAPI = "question"
                 :> QueryParam "question1" Question
                 :> QueryParam "question2" Question
                 :> QueryParam "question3" Question
                 ...
                 ...
                 :> QueryParam "questionn" Question
                 :> Get '[JSON] [Answer]

This works just fine but the function that consumes this endpoint takes in n number of arguments

processQuestionAPI :: Maybe Question -> Maybe Question -> ... -> Handler [Answer]        
processQuestionAPI param1 param2 param3 ... paramN = ...

which makes everything more difficult to read and reason with.

The first fix I could think of was to use a record!

data LotsOfQuestions = LotsOfQuestions
                { question1 :: Maybe Question
                , question2 :: Maybe Question
                , question3 :: Maybe Question
                ...
                ...
                , questionn :: Maybe Question
                }

and rewrite the endpoint like this

type QuestionAPI = "question"
                 :> ReqBody '[FromUrlEncoded] LotsOfQuestions
                 :> Get '[JSON] [Answer]

While compiling this code GHC threw this error

• No instance for (Web.Internal.FormUrlEncoded.FromForm
                     LotsOfQuestion)
    arising from a use of ‘serve’

So I did just that wrote a custom FromForm instance for LotsOfQuestions.

instance FromForm LotsOfQuestion where
    fromForm aForm = LotsOfQuestions 
                   <$> parseMaybe "q1" aForm
                   <*> parseMaybe "q2" aForm 
                   ...
                   <*> parseMaybe "qN" aForm 

Everything compiled and the server was up and running but I couldn't connect to it using my browser.

The URL I used was this

localhost:8081/questions?q1=what&q2=where&q3=when

The odd thing was that cURL actually worked!

This

curl -d "q1=what&q2=where&q3=when" -X GET "localhost:8081/questions"

produces exactly what I want.

Looking around I found this this issue, which led me to believe that sending Request Body with a GET request isn't the recommended way of doing things.

So I have to replace ReqBody with something equivalent for GET request, but I'm not sure what that could be.

atis
  • 881
  • 5
  • 22
  • It's been a while since I last worked with Servant, but if you want to `Capture` data in a URL, I think you might need to make `LotsOfQuestions` a [FromHttpApiData](https://hackage.haskell.org/package/http-api-data/docs/Web-HttpApiData.html#t:FromHttpApiData) instance. – Mark Seemann Jun 06 '20 at 20:06
  • Thanks for the suggestion! I looked into it and saw that I only had to implement `parseQueryParam` or `parseUrlPiece` which was fairly straightforward. Now I can connect to the server via browser but the `url` it responds to now takes `&` instead of `?` at the start of the query i.e `/questions?q1=what&q2=where` to `/questions&q1=what&q2=where` (in both cases; using `Capture` and `QueryParam`) which to me seems like an undefined behavior. – atis Jun 07 '20 at 06:20
  • `Capture` is for request path, not query parameters, as you found out :) So you need to stick with `QueryParam`. That being said, have you read this part of the doc? https://docs.servant.dev/en/stable/tutorial/Server.html#the-fromhttpapidata-tohttpapidata-classes It talks about the `FromHttpApiData` typeclass which is used to translate QueryParam types, maybe it would help (I am unsure). – Sir4ur0n Jun 07 '20 at 13:53
  • From looking at the docs for `FromHttpApiData`, as far as I understand, it only requires one function like this `parseQuestions :: Text -> Either Text LotsOfQuestions` which should use `parseQueryParam` and the one I implemented works well. The problem is that it only works with this type of URLs `/questions&` and not `/question?` even though `parseQuestions` works fine on both of those. Perhaps it has something to do with how `QueryParam` works and which part of the URL is fed to it but I'm not sure. – atis Jun 07 '20 at 14:29

1 Answers1

0

This is more of a progress report than a final answer.

This main problem was endpoints like these

type QuestionAPI = "question"
                 :> QueryParam "question1" Question
                 :> QueryParam "question2" Question
                 :> QueryParam "question3" Question
                 ...
                 ...
                 :> QueryParam "questionn" Question
                 :> Get '[JSON] [Answer]

do work but the functions that consumes them often aren't as easy to work with, for example

processQuestionAPI :: Maybe Question -> Maybe Question -> ... -> Handler [Answer]        
processQuestionAPI param1 param2 param3 ... paramN = ...

My solution was to use record syntax

data LotsOfQuestions = LotsOfQuestions
                { question1 :: Maybe Question
                , question2 :: Maybe Question
                , question3 :: Maybe Question
                ...
                ...
                , questionn :: Maybe Question
                }

but I didn't know how to map that record to servant DSL.

Mark's comment gave me some insight.

What I needed to do was implement FromHttpApiData class, specifically parseQueryParam.

Because some of those questions were optional, the implementation was somewhat roundabout.

instance FromHttpApiData LotsOfQuestions where
  parseQueryParam = parseQuestions

tailMaybe :: [a] -> Maybe [a]
tailMaybe []  = Nothing
tailMaybe str = Just $ tail str

splitOnEqual :: String -> Maybe (String, Maybe String)
splitOnEqual xs = second tailMaybe . flip splitAt xs <$> elemIndex '=' xs

parseQuestions :: Text -> Either Text LotsOfQuestions
parseQuestions txt =
  LotsOfQuestions
    <$> sequence (fmap fromOrder =<< lookup "q1" txtMap)
    <*> sequence (fmap fromOrder =<< lookup "q2" txtMap)
    <*> sequence (fmap fromOrder =<< lookup "q3" txtMap)
    ...
    ...
    <*> sequence (fmap fromOrder =<< lookup "qN" txtMap)

  where txtMap = mapMaybe splitOnEqual (splitOn "&" $ unpack txt)

here fromOrder is an internal function with type Text -> Either Text Question and splitOn comes from Data.List.Split.

These are the changes I made to QuestionAPI

type QuestionAPI = "questions"
           :> QueryParam "are" LotsOfQuestions
           :> Get '[ JSON] [Answer]

and the way to interact with that API is via a link like this

http://localhost:8081/questions?are=q1=what&q2=where&q3=when
atis
  • 881
  • 5
  • 22