-
-
Notifications
You must be signed in to change notification settings - Fork 415
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
QueryParamForm Revisited #1047
base: master
Are you sure you want to change the base?
QueryParamForm Revisited #1047
Conversation
I ran the tests locally using GHC-8.4.3 and they all passed (same for Travis build). Travis says there was an exit code 1 for 8.2.2 (but I didn't see any failing tests?) and it didn't complete for 8.0.2? |
Thanks for the patch! I'm just leaving a quick comment right now, to try and address your question. Will do a proper review when I get a chance (I've been quite busy lately).
Great question. First, we may want to differentiate between the case where the other params are contained in the In the case where they are, one could argue your APIs are just as valid as: If we allow ourselves to assume there is no overlap (i.e if we tell users "you can mix |
I was surprised by this. To verify, I modified type QueryParamApi = QueryParam "name" String :> Get '[JSON] Person
...
:<|> "param-odd" :> QueryParam "age" Integer :> QueryParam "age" Integer :> Get '[JSON] Person
qpServer :: Server QueryParamApi
qpServer = queryParamServer :<|> qpNames :<|> qpCapitalize :<|> qpAge :<|> qpAges :<|> qpWacky
where ...
qpWacky (Just age1) (Just age2) = return alice{ age = age1 + age2}
qpWacky _ _ = return alice Then I wrote a test like this: it "parses multiple query parameters of same name" $
(flip runSession) (serve queryParamApi qpServer) $ do
let params = "?age=25"
response <- Network.Wai.Test.request defaultRequest{
rawQueryString = params,
queryString = parseQuery params,
pathInfo = ["param-odd"]
}
liftIO $
decode' (simpleBody response) `shouldBe` Just alice{
age = 50
} And, as you stated, it works. This is kind of confusing from a client standpoint: would an ideal client understand that it's the same param (which would mean looking at all params for each endpoint comparatively as a set? I think what you've said otherwise makes sense to me. I'll try to work toward that as a solution for |
Depends on whose ideals we're talking about. My opinion: I think I'd rather implement some day one old idea, which would be to run a bunch of (type level) checks on the API type before it's fed into |
@alpmestan I have another question for the server implementation that I could use your input on. Let's say I have a form like this used in an API as data BookSearchParams = BookSearchParams
{ title :: Text
, authors :: [Text]
, sort :: Maybe Bool
} deriving (Eq, Show, Generic)
instance FromForm BookSearchParams If I'm the server and there are other query params which could also be supplied, I think I need to decide when someone intended to send this form before I can say that it didn't parse successfully. For example, consider these query strings: λ> urlDecodeAsForm "title=great&authors=abe&authors=mary" :: Either Text BookSearchParams
Right (BookSearchParams {title = "great", authors = ["abe","mary"], sort = Nothing})
λ> urlDecodeAsForm "authors=abe&authors=mary" :: Either Text BookSearchParams
Left "Could not find key \"title\""
λ> urlDecodeAsForm "limit=1000" :: Either Text BookSearchParams
Left "Could not find key \"title\"" Here, for example, if either the key However, if none of these keys are presents in the query string (but others may be) and it's There's a bit of intention sniffing here that I'm hesitant about due to the fact that these are not independent query params but a collection of them in some kind of implied relationship. Should I unpack the data structure and expect that if any key not resulting in a Alternately, all fields in the record could be wrapped in When would I need to return |
I'm not sure I understand where/why you have to do what you call "intention sniffing". Aren't the |
Apologies: I think I did a poor job of explaining the problem. This relates to the previous question about having Maybe I'm missing something obvious. For example, let's say we have an API like this where there's a type SearchAPI =
"search" :> QueryParam "sort" Text :> QueryParamForm "search" BookSearchParam :> Post '[JSON] [Book] Presumably, from this definition we'd want to be able to parse the λ> urlDecodeAsForm "sort=desc" :: Either Text BookSearchParams
Left "Could not find key \"title\"" A naive server implementation (like the one I have here) would simply check if any query params had been passed and attempt to parse out the form, but then with this naive implementation in the above API example with this query string it's not really possible to get the values On the other hand, with something like this: λ> urlDecodeAsForm "sort=asc&authors=abe&authors=mary" :: Either Text BookSearchParams
Left "Could not find key \"title\"" You'd probably want to actually return a 400. |
Sorry, but I still don't understand the problem. I must be tired these days, but for me, what you want can entirely be implemented in the ToForm/FromForm instances, given the "natural" implementation of HasServer for QueryParamForm (I haven't examined closely the code for it, but I think it should be pretty close to the one for QueryParam, except that you feed it the entire query string (and the @phadej Your brain usually works a lot better, could you look at @erewok's question above when you've got a bit of time? |
Hi @alpmestan. Let me try again to explain myself. Consider this API again that I mentioned before: type SearchAPI =
"search" :> QueryParam "sort" Text :> QueryParamForm "search" BookSearchParam :> Post '[JSON] [Book] Let's assume we've got some handler function of the following form: searchBooksHandler :: Maybe Text -> Maybe BookSearchParam -> Handler [Book]
searchBooksHandler = ... Now, consider the following requests and the expected Handler args:
In the first case, the requester did not supply the first, optional However, there's a problem in that last case: if you just go with That's why I was trying to say above: it seems like you have to look for at least one of the keys from the Does that make sense? |
Yes, thanks for taking the time to spell things out properly for me. I think I see the problem. As far as I can tell, the problem basically is that we can't really make The only reasonable, non-surprising approach that I see is: if |
Thanks for being patient and working through it with me. What you suggest sounds good. I'll go for that. I will likely be unavailable for working on this over the weekend, so sometime next week I'll try to wrap it up. I'll pull master too in order to make sure CI is solid. |
Great, thanks for your work on this! |
@alpmestan I think this one is ready. I changed I was also trying to get the documentation correct (still new to the format), and I was not able to build the haddocks locally, so I had to guess a bit on the look of the documentation. |
oh wait! I forgot to add a test for |
So, let me sum it up: if there's no query string, we return (sorry, I realize you can't wait for this to land) |
No hurry from me to merge anything: I'd rather get it right. You summation is exactly what I ended up with. |
@erewok Hmm, in that case I think some docstrings need to be updated. E.g the one in the client package mentions wrapping the user's type in You might or might not want to add a few words about this either in the tutorial (if you find a good place for it) or in a dedicated cookbook recipe. This is AFAICT the best way to advertise existing or new features/combinators. But that requires time too, so it's up to you :-) |
In I'd be happy to add something in one of the tutorials about this. In fact, I thought about that a few times: it's hard to understand how to use or why it's |
Well, the tutorial is https://haskell-servant.readthedocs.io/en/stable/tutorial/index.html and is supposed to be a "servant starter kit" kind of thing. Not sure what you're saying fits the format. Maybe a dedicated cookbook recipe to modifiers, with examples using different combinators, including the new one you're introducing here, would be best? If you can make it fit in the tutorial somewhere, that'd work for me as well. Note: this doesn't have to be done in the same PR, if you'd rather split up those two things. |
Okay, sounds good. After this one is merged, I'll create a new cookbook recipe and create a new PR. For this one, it looks like I have another merge conflict to resolve, which I'll have to do this evening (California time). |
Great. Yes, the 0.15 release is getting out, some change in -client-core needs your attention. Your changes will quite likely be shipped in the very next release (AFAICT there's no breaking change, so we can just ship it in 0.15.1 if we want) -- to be confirmed with @phadej. |
Hi @alpmestan and @phadej: I think everything we discussed has been done here, so this one should be ready? Please take a look and let me know if you me to make any other changes. Otherwise, I wasn't planning to do any more to it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm happy with this now. Glad we worked it all out!
Are you planning to add a cookbook recipe about this or something like that? Or perhaps a tiny section in the tutorial or something. Just to advertise it. Not in this PR though of course.
Yeah, I think the cookbook recipe we talked about up above about modifiers in general would be cool to have and I'd be happy to do it. Feel free to give me some notes on what you'd want in there. |
Hi @alpmestan and @phadej, Hacktoberfest jogged my memory that I opened this PR last year and it's been (with apologies) a year since I thought about it. I worked on it originally because @alpmestan added I didn't totally love the way it was implemented here but it's also not something I wanted to use right away in Servant, so I didn't do the work of pushing on this PR to a resolution. I mostly did it as a way to contribute back to Servant, which is a project that I have a lot of affection for. Now that I have remembered it, and in the interests of helping out the Servant project (and hopefully closing issues and PRs helps in that effort), I wanted to know what you'd like to see done with this PR:
I respect that it takes a lot of time and effort to run a popular open-source project such as this one, so I'm happy to do whatever you both think should be done with the work already performed here. I have no qualms about abandoning this PR, for instance, if that's what you suggest. Thanks. |
Apologies for not staying on top of this as well, I've indeed been spending a bit less time on open-source stuffs. I'm personally still keen on landing this, once rebased and everything. |
Thanks for your reply, @alpmestan . It sounds like you'd vote for option 2 or 3? In that case, I'll start work on bringing it up to date with contemporary servant. Expect an update to this PR sometime soon. |
We can go for 2, and if you later feel like writing a little thing for the cookbook that'd be great. |
@phadej Looks good to you? |
-- or not (when other 'QueryParam's are present). As a result, most users | ||
-- of 'QueryParamForm' are going to implement handlers that take a value | ||
-- of type (Maybe (Either Text a)). This also means that in a server implementation | ||
-- if there as a query string of any length (even just a "?"), we'll try to parse |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems confusing to me. If QueryParamForm
is Optional
we should always match that route then, independently whether there is ?
or not.
The difference between http://example.com/
and http://example.com/?
is too subtle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand the critique about matching routes because they both match: in the first case the handler will get a Nothing
while in the second the Handler will get a Just . Left a
. The routes do match, though?
This PR has always been based on the original PR which had been abandoned in an incomplete state, so I wouldn't take personally any criticism of it. In fact, I think there's a general criticism worth making that this implementation may cause some surprise among users. One thing I thought may help is to define different, somehow more specific names for the Optional
and Required
variants.
Another idea would be for the Optional
case to return a Maybe form
instead of a nested Maybe (Either a form))
, in other words to give the user a Just form
only in the Just (Right form)
case and Nothing
everywhere else. You would probably have to surrender the Lenient
, Strict
axis in that case and just say, "it's there or not there," but that may be confusing too and lead to buggy behavior if a query param is submitted misspelled or mistyped. Actually, this goes back to that conversation @alpmestan and I had above at some point in the hazy past...
This PR remind me how hugely inconsistent (indentation) style we have in |
97ad20c
to
75da485
Compare
@phadej and @alpmestan, aside from the open-ended discussion we had above, I have made the requested changes. |
@erewok @phadej @alpmestan |
Just lack of time. I just joined the team and have no context here, but I'll try to take a look at it soon. |
@willbasky there was some dissatisfaction with the implementation here and @phadej had mentioned elsewhere wanting to go in a different direction. |
Hi all! I second the comments above, I find this feature very useful and I am looking forward to it being merged! (Also, thanks @erewok for taking care of this PR!) The natural question is: what can we do to help have it merged? By reading the thread above, I think there are mainly two issues that slow down merging:
I hope this recap can help unlock the PR (feel free to correct or add anything). It's now been more than 3 years since the first discussions on this feature 🤯 |
Hi @erewok! We are amenable to merging this PR for the next major version of Servant. Could you please address the 2nd point in Andrea's last comment? :) |
Hello @tchoutri! It's been a very long time since I've thought about this PR. Reading the comment above it rehashes the conversation we had throughout here. Oleg had an idea which he never clarified but for my own part, I was just trying to finish the feature based on a previous PR, but I don't have much for opinions about the implementation (apologies). There have been a lot of people over the years who have said that they would us this feature. Perhaps it makes sense to get this PR merged and then to refine it based on user feedback? |
aa0e24d
to
85b595c
Compare
85b595c
to
55a85e1
Compare
FWIW, sounds good to me. I think our initial efforts of fleshing out and clarifying the subtleties brought this to a good enough place for the time being. |
Any updates on this front? We've been considering moving to a |
-- > myApi :: Proxy MyApi | ||
-- > myApi = Proxy | ||
-- > | ||
-- > getBooks :: Bool -> ClientM [Book] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't it have type Maybe BookSearchParams -> ClientM [Book]
?
This looks really great, I would love to see this in Servant. |
Hi Everyone,
Included here are changes to finalize
QueryParamForm
(see Issue #728 and PR #729) which I mentioned to @alpmestan on IRC. The original request seemed like a good idea to me and thehelp-wanted
label (plus the fact that it hasn't been updated in 15 months) made me think it would be a good thing to contribute.I broke this PR description down by
Servant-
section so I could highlight solutions and questions pertinent to each section. There's atodo
section at the bottom.I've been kicking this PR around in my head for a couple of days and I think I could use some of your feedback to polish it off.
Sevant-Server
In this area, I went with something similar to the original PR but there was one issue I went back and forth on. Consider the following example:
Are all of these valid?
Consider that they can all work using
urlDecodeAsForm
:However, it's more complex to allow all of these from a server implementation. The easy answer (which I did here) is to check if there are any query params and then try to decode the
QueryParamForm sym a
, otherwise, assume there are none. That may make sense because aQueryParamForm
could be a way of wrapping up allQueryParams
into one: in other words, you wouldn't use aQueryParamForm
in combination with other query params.If we go with this simple answer, then the docs should definitely be updated to be super clear: only use one
QueryParamForm
if you aren't using any otherQuery-
things.I wasn't really satisfied with my implementation of this simple version, to be honest, but I wanted to see what the group thinks before working on a more complex implementation.
(By the way, if you look at my tests, you can see me muddling through and exploring this.)
Sevant-Client
This one seemed more straightforward: it's a
ToForm a => Maybe a
arg and then you canurlEncode
it. I need to add tests for it, though, and validate it's definitely correct.Servant-Docs
My first implementation of this just treated a
QueryParamForm
item as anotherDocQueryParam
, but I wasn't really satisfied by that.Instead, I went with a set of constraints
(ToForm a, ToSample a, Data a)
and then I created a sample and used the constructors to make a bunch ofDocQueryParam
s.It's not perfect (all
Values
are repeated), but I liked it better than the first solution I had. It ended up making docs that look like this:Would love to hear what you all think and would like to see. There are certainly other directions you could go in here as well, such as representing these things completely differently.
Servant-Foreign
Honestly, wasn't sure on this one. Please take a look and let me know it it makes sense.
To Do
servant-server
docs to make it really clear whether you can useQueryParamForm
withQueryParam
, QueryFlag`, etc.servant-client-core
.