Closed phadej closed 4 years ago
A little note: Oleg and I have been discussing this over the past couple of days. We want to unify everything to avoid as much duplication as possible. The alternative to being able to "augment" the response information as done here would be to have all the possible "options" be type parameters to Verb
. Here's what that would look like: https://gist.github.com/alpmestan/4ad03493cb6fe87e02b514418e9f7f02
So we really want @phadej's idea to work :) This would make many adhoc things non adhoc (stream would not have to be a totally separate hierarchy of response combinators -- cc @gbaz -- while responses that provide some headers would just augment the response with those instead of having the ugly Headers '[...] resp
hack -- and it just goes on and on. We could similarly have "exhaustive endpoint descriptions" that describe the happy paths as well as the other ones, so that derived clients would basically be "more strongly typed" than they are now, because for non-happy paths you're stuck with a ServantError
value. Redirects would also work pretty easily in this setting (they're just a combination of status code + "Location" header after all).
As a user of Servant and a drive-by commentator, I really like this proposal. (I'd probably bike shed the <:
combinator, though.)
As a user of Servant and a drive-by commentator, I really like this proposal. (I'd probably bike shed the <: combinator, though.)
We want to hear from our users, maybe even more in this issue than usual. Feel free to suggest another operator and to illustrate with snippets of code you'd like to be able to write.
Thank you, @alpmestan. I should have probably kept my mouth shut until I could suggest something better. I think the reason I objected initially to <:
is that my brain wants to coerce it into the opposite of :>
whereas Verb ServantErr SomeReturnType
looks like Either a b
.
I also want to be clear that I think of this as bike-shedding: the overall improvement discussed here is excellent, in my opinion, so don't let me distract from that.
Well your point about your brain being disturbed by :<
is a good one. In fact when @phadej initially suggested this approach, I think he used :+
, which I interpreted to mean "we augment the response with more information".
One question:
Would it be possible to do something like Verb ('[HTML] ServantErr <: '[JSON] SomeReturnType)
?
I would probably never do this, just wondering.
Being able to express different "response paths" is one of the important goals of this issue. response path == status code + headers + content types + type of the response body, here. Ideally we want full flexibility, so as to assume nothing about what users want to do. Because there's always someone who will want to do that very thing that you thought was irrelevant =) It makes sense to only return errors in JSON while making the "success" response available in JSON and HTML, in some apps, so it's not all that far fetched after all.
Well, in that case then, would it possible to have different code paths depending on, for instance, objects that get submitted? I imagine something like this:
type MyApi = "a" :> ReqBody '[JSON] SomeType :> BasicAuth "api-access" User :> Verb ('[HTML] MyAppErr401 <: '[JSON] MyAppErr404 <: '[JSON] SomeReturnType)
In other words, is it more like an HList
than Either a b
? Or is it more like Alternative
, as in Error1 || SuccessOrError2
where SuccessOrError2
is Error2 || Success
...
It's the sum-type equivalent of HList that would model things accurately here, indeed. We want to model the overall response of the endpoint as being one of several possibilities. It would be up to the endpoint's code, using some dedicated function(s), to return this or that kind of response depending on its inputs and anything else you want. But at least all the possible responses (including error responses) would be declared and all of this would be documented by servant-docs, clients would with your example give something like postSomeType :: SomeType -> User -> ClientM (Either SomeReturnType (Either MyAppErr401 MyAppErr404))
, etc.
the sum-type equivalent of HList
the vinyl
records library has "co-records", which says that any value in some type can be any of a list of valid types. https://hackage.haskell.org/package/vinyl-0.6.0/docs/Data-Vinyl-CoRec.html
for example, CoRec Identity '[SomeReturnType, MyAppErr401, MyAppErr404]
.
with the type alias that implies that each value can have zero or more errors,
type Result es a = CoRec Identity (a ': es)
type MyResult = Result SomeReturnType '[MyAppErr401, MyAppErr404]
a few other record libraries have n-ary variants, like the sum-of-products package, I mention vinyl
because it's lightweight and the one I'm familiar with. and, since vinyl
wraps each field with the same functor, working with effects is easier, since you can say stuff like Rec IO '[Integer, String]
and conveniently convert it to IO (Rec Identity '[Integer, String])
with utility functions like rtraverse
.
(sorry if this isn't relevant, I only skimmed the discussion)
I have a little bit of prior work of this in my bachelor project https://github.com/arianvp/servis/raw/master/paper/paper.pdf
See the chapter about Explicit status Codes
Brainstormed about this with @rightfold yesterday. How about something like this?:
Lets ditch the idea of having a coproduct of return types
You have Verb '[200, 201, 401] GET
Signallying that an endpoint can return any of these three status codes. and some type family
type family IsElem x xs :: Constraint where
IsElem x '[] = TypeError (Text "No")
IsElem x (x ': xs) = ()
IsElem x (y ': xs) = IsElem x xs
Then the way for people implementing a server to set a status code would be something like:
setStatusCode :: IsElem x xs => Proxy x -> Server (Verb Get xs) ()
This disallows a server implementing the spec from setting a status code that is not in the spec.
You could go one step further and use something like IxMonad
to actually force the user to set the status code of the handler.
I like the direction that suggestion is going in, but (unless I'm misreading) it loses the option to specify different return types and different encodings for each return path.
To put those back in would look like:
Get '[(200, JSON, NormalType), (403, HTML, AuthError), etc.]
Edit:
Now that, I think about it, the original suggestion also had more of a combinator feel, so I could presumably do something like this:
type MyAppAuthErrors = AuthRequired401 :< AuthFailure403
Verb (MyAppAuthErrors :< Full ZonedTime)
@arianvp Well, we still need the different response types like @erewok said. They'd just be "annotations" on the status code. And this is all remarquably close to what we've been describing so far with @phadej above =)
While we're at it, we can get rid of ServantErr
right? Usually, you'd do something like err403
to give a different error code, but now that we can put the possible status codes in the type, a "Different status code" is not really an error scenario anymore, just a different code path. This would basically return our base type back to IO
which is kind of neat
Yes, we want to offer that possibility. Not sure we want to get rid of ServantErr just yet. If we solve this, we can keep ServantErr around a little bit to allow people to smoothly move to the new approach. But we're not there yet anyway :)
Content-types should be the same for all paths, since we have to resolve content type before running the code.
(We've discussed this idea a few times in the past. I think there's already an issue for it that we should use, though I'm on my phone right now so will only check later)
On Saturday, November 18, 2017, Arian van Putten notifications@github.com wrote:
While we're at it, we can get rid of ServantErr right? Usually, you'd do something like err403 to give a different error code, but now that we can put the possible status codes in the type, a "Different status code" is not really an error scenario anymore, just a different code path. This would basically return our base type back to IO which is kind of neat
— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/haskell-servant/servant/issues/841#issuecomment-345451828, or mute the thread https://github.com/notifications/unsubscribe-auth/ABlKmkX2XeGIbHfZJJNieXhNYP40PqI2ks5s3wBHgaJpZM4QCUq6 .
I like the list of tuples idea. You can verify that the combination of status code and response type exists using a constraint.
respond :: IsResponse api s r => Proxy s -> r -> Server api
respond (Proxy :: Proxy '404) (CustomerNotFound uuid)
Note this is all very much the same idea, all throughout the thread, nothing new AFAICT. But the big challenge is to make it work @phadej -style (see first post) instead of exhaustive-style (see the gist that I linked to very early in this thread).
@alpmestan pointed me to this issue in https://github.com/cdepillabout/servant-checked-exceptions/issues/4#issuecomment-378531911.
It would be really nice if servant provided some way to encode what errors are returned by an API in a flexible manner. I'm using https://github.com/cdepillabout/servant-checked-exceptions to do it in a couple projects at work (and it is working pretty well), but it would be nice if Servant provided some sort of built-in support for this.
I think this proposal of re-enginnering Verb
would enable this?
Yes, that's one of the goals. The big general goal is, now that we have a good idea of what people need from Verb
, to make it support all these use cases without the need for ugly and fragile overlapping instances all over the place, but to instead support all those things right from the start.
The first candidate approach is one where we would just add extra annotations to specify that we add response headers, want to stream the response while executing the handler, declare strongly typed error cases (what your package does), etc. See @phadej's post at the very beginning of this thread.
The second one consists in making Verb
parametrized by all those things, and to offer specialized combinators that would provide today's behaviour to try and not break things too hard, I suppose.
I'm pretty interested in seeing explicit errors land in servant.
Seems like these type of issues are being closed as dupes of this issue, so I'm just commenting here.
Here's a gist showing a way to add these to servant using a Throws e
annotation: https://gist.github.com/benweitzman/5875d3e6a8cb1470cea5c32db3180f90
It's not a change to Verb
, but it's pretty lightweight and backwards compatible.
Biggest downside I can see is mtl
issues surrounding multiple targets of throwError
, but I don't see this being avoidable in any context than throw either a ServantError
or an application specific error.
Thoughts?
I think @cdepillabout has a package for this already (servant-checked-exceptions
IIRC?). The work that @phadej has been doing and discussing here is really about enhancing Verb
to support various features that are not possible today, out of the box.
Do let us know if @cdepillabout's package doesn't quite fit what you need, in which case it is I suppose relevant to this issue. :-)
@benweitzman As alpmestan said, servant-checked-exceptions
provides a Throws
types you can use to describe the errors thrown by an endpoint. It uses a couple tricks to make Throws
as easy to use as possible (although it makes it slightly more complicated than your gist):
https://github.com/cdepillabout/servant-checked-exceptions
Feel free to open an issue if you have any problems/suggestions.
... and another one, in a very early proof-of-concept stage:
https://github.com/wireapp/servant-uverb
servant-checked-exceptions seems more mature, but it's not flexible enough for what we need, so we explored the design space a little further.
Ha, very cool! I'll take a close look later today.
Is there an introduction somewhere on how this new feature works?
@hasufell It appears there is an entry in the cookbook for this, which has been added in #1314.
Is there an introduction somewhere on how this new feature works?
The idea is that
Instead of having
we could have
which is transformed into something like
Verb
glues RHS and LHS together. This way we canHave checked exceptions (here
"date"
endpoint cannot error!)Get rid of
{-# OVERLAPPING #-}
instances, as inNoContent
doesn't overlapFull a
. Similarly we can haveStream a
andHeaders ... :< ...
.The devil in the details how to fit this into
Router
andDelayedIO
framework we have, but I don't see why it's not possible.The names of auxiliary class
HasServer'
andFull
combinator are open to bikesheding.Comments?