Reputation: 921
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
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