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.
Capture
data in a URL, I think you might need to makeLotsOfQuestions
a FromHttpApiData instance. – Mark SeemannparseQueryParam
orparseUrlPiece
which was fairly straightforward. Now I can connect to the server via browser but theurl
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; usingCapture
andQueryParam
) which to me seems like an undefined behavior. – atisCapture
is for request path, not query parameters, as you found out :) So you need to stick withQueryParam
. That being said, have you read this part of the doc? docs.servant.dev/en/stable/tutorial/… It talks about theFromHttpApiData
typeclass which is used to translate QueryParam types, maybe it would help (I am unsure). – Sir4ur0nFromHttpApiData
, as far as I understand, it only requires one function like thisparseQuestions :: Text -> Either Text LotsOfQuestions
which should useparseQueryParam
and the one I implemented works well. The problem is that it only works with this type of URLs/questions&
and not/question?
even thoughparseQuestions
works fine on both of those. Perhaps it has something to do with howQueryParam
works and which part of the URL is fed to it but I'm not sure. – atis