atis
atis

Reputation: 921

How to properly reduce servant API path (:>) combinator trees for GET request via browser?

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.

Upvotes: 2

Views: 210

Answers (1)

atis
atis

Reputation: 921

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

Upvotes: 0

Related Questions