Replies: 1 comment 3 replies
-
Hi Casey, I appreciate your detailed feedback about the migration. 0.4 and 0.6 especially included some wide-ranging changes when it comes to upgrading. I'll reply to the things I feel I can reply to in the hopes I can provide insight about intended use cases, the intention of new designs, and what we can improve on. Pain Points
When I initially designed the API for
This was kind of a hacky solution that looked cool when I designed it that, honestly, doesn't have much benefit other than being able to save typing 7 characters. The PR itself (#1008) doesn't go into too much detail, but when paired with the other PRs (#1009, #1010) it makes a bit more sense. Prior to these PRs you had to give the requests owned data. They could not (reasonably) take references due to lifetime requirements and self referential lifetimes during request execution initialization. We had to clone provided data since you can poll a Future to completion (call
The intent here is performance, similar to that of the change with
The reason for this change was that we hid the source error behind a struct because we didn't want to expose dependencies, which locks us into them. We exposed types from the HTTP client and WebSocket clients, causing us to not being able to upgrade from say 0.12 to 0.13. I'm not sure if I'm enthusiastic about it since this is an idiomatic design.
I would be in favor of unifying these naming schemes. Could you list some examples of other crates so we could create an issue justifying it? I'm having a hard time finding any right now, since most crates just expose their dependencies' errors.
Good idea. I've opened an issue: #1114
I'm not sure how I feel about this. I only really did it in the first place since we didn't have this
The problem with this is that it would expose the dependency via the public API, correct? We can't really do this. I would like it if it was easier to handle this but I don't feel Rust's Possible Mitigations
See above about exposing dependencies. The simplified explanation again is that exposing dependencies would require us doing a new major version if we want to update one. We want to be able to aim for a 1.0, and doing this would make us either begin to use consistently outdated dependencies or churn through otherwise empty major versions.
Twilight doesn't actually have the same goal as most other Discord libraries. The goal of Twilight is to expose protocol level details and not define implementations much higher than that. Other libraries tend to abstract things away and make things easier to use for the 90% of people. This is fine, most people either aren't experienced programmers and make a bot for a fun project for their server or just want to get stuff done. However, Twilight aims to cater to the 10% that do need low level access to things for either design reasons, scalability, or simple preference. Using Twilight will be more verbose than alternatives, but you can bet that it'll be faster to use and if you want to do something with it then you can do it. I hope my reply has helped and thanks again for your feedback. I may not come across as very willing to make changes purely for the intent of ergonomics, and the simple fact is that when it comes to this library I'm not for the reasons just up above. I have created #1114 based on your feedback and would like to open another issue for the |
Beta Was this translation helpful? Give feedback.
-
I recently migrated a project I'm working on from twilight 0.3 to 0.6, and wanted to provide some feedback on the process. I mentioned some of these things on Discord, but wanted to also expound on them here in more detail. I'm sorry for not following the development of twilight more closely, and that this isn't as timely as it could be.
Pain Points
Overall the migration was a bit tricky. A few pain points:
When using 0.3, we had relied on being able to call
Cluster::some_events
multiple times. For example, after bringing a cluster up, we would do:To wait for the ready event, before eventually calling
Cluster::some_events
later in the program to get events for the main loop. Adapting to getting theEvents
iterator when building theCluster
was a bit tricky due to the structure of our code. For testing, we have to have a singleCluster
instance that's shared between multiple tests, otherwise we get rate-limited when connecting the cluster. Because of this, we have to store theCluster
in a static variable, and our intialization code is pretty convoluted.Overall this wasn't so bad. Changing the initialization code was a bit tricky, but I don't anticipate further challenges.
One thing I do wonder about, is that it's now no longer possible to create multiple event streams from the same cluster that return different event types. This is totally academic, since we don't need it, but I do wonder if it's desirable functionality to have.
Instead of being able to call
.await
on a request builder, we now have to call.exec
. This isn't so bad, since it's a compile-time error to try to call.await
, so it isn't possible to forget to do it. However, it does make the code more verbose. Would it be possible to provideFuture
implementations for requests that simply forward to.exec
?Instead of returning the model that was requested, requests now return a
Response
, and it's necessary to call.model
on the response to get the model. This isn't a huge problem, since it's not possible to forget, but it does make the code more verbose. This is particularly pronounced in our test code, where we use.unwrap
instead of?
:If a request returns multiple items, it's required to call
.models
instead of.model
. This isn't a huge problem, since it's not possible to call the wrong method. However, it's easy to forgot and the error in that case is fairly inscrutable. I personally think of both aFoo
and aList<Foo>
as being a "model", so it's hard to remember that I need to call.models
. Would it be possible to make.model
work alongside.models
?Since error types are no longer enums, and
ErrorKind
fields are private, matching on specific error types is much more verbose.As an example, here's a match statement that checks for a 404 error:
We cannot match directly on the error type in the
Err(…)
pattern. And, additionally, since we need thestatus
field if it's a response, we can't put it into a conditional clause (as inPATTERN if CONDITIONAL =>
), because it's not possible to capture fields there.My suggestion would be to make the
kind
fields on errors public, so they can be matched on.Error type enums are in some places called "kinds" and in some places called "types". for example,
Error::kind
returns anErrorType
, this makes it hard to remember which to use when. I would suggest harmonizing this, and renamingErrorType
toErrorKind
, since this is the most common naming scheme I've seen in other crates.The
StatusCode
type is missing a few desirable features compared to theStatusCode
type from thehttp
cratehttp::StatusCode
allows comparison with a u16 directly, and has named associated constants for common status codes, e.g.StatusCode::NOT_FOUND
, which are convenient. This made working with status codes more challenging, which is exacerbated by the fact that in 0.6, response status codes must be checked in more scenarios.In 0.3, requests which might not return a result return
Option
to indicate that some data wasn't found. In 0.6 this is indicated with a 404 status code.On the one hand, it's nice that the 404 status is handled like every other status code. However, in practice, a 404 is often handled differently from other status codes in application client code. It is often handled not as an error that should be propagated via the error handling mechanism, but should instead trigger some special logic. This often makes a request that returns an
Option
more ergonomic than one that returns aResult::Err
on 404.Additionally, some Discord API calls return a 404 and some don't. For example,
/users/@me
never returns a404
, whereas/users/ID
may. In order know when to check for a 404 and when not to, a twilight user must consult the Discord API docs. It may be more desirable to encode this in the response type, so that the twilight types document this, and it isn't necessary to fall back on twilight or discord documentation.Source errors can now only be obtained by calling
Error::source
, which returns aOption<&(dyn Error)>
, which makes it hard to get more information on the source error. In our code, when the cluster doesn't start, we handle rate-limiting errors specially. If the cluster didn't start due to being rate-limited, we wait and try again, whereas for other errors we return the error upwards.This looks something like this:
Although this compiles, I'm not sure if this code is correct, becuase I don't know if
HttpError
is the underlying source error. (Incidentally, this match is made more verbose because the kind and status can't be matched on directly.)With 0.3, the code looked like this:
My suggestion here would be to consider using
snafu
.snafu
makes it extremely easy to add additional context to a source error, wrapping it in another error. Additionally, this preserves the concrete type of thesource
error, so that it can be matched on and inspected without calling.source
and trying to guess the type withdowncast_ref
.Possible Mitigations
I want to underscore that I'm quite grateful that Twilight exists, and, indeed, grateful for its flexibility and modularity, and will continue happily using it regardless. My intention here is just to bring attention to some sharp edges that might be sanded down.
Ideas for mitigating the above issues:
Use
snafu
for error types, and keep them as enums. This would allow matching on error variants directly, preserving strongly-typed source errors, and provide additional conveniences to the developers, such as easy conversion from source errors, easyDisplay
impls, and easily adding backtraces, if so desired.Failing that, it would be nice if
kind
fields were public, so they could be matched on. Additionally, it would be nice ifkind
was used consistently throughout the code.Use
http::StatusCode
, if possible.Add a
Future
implementation to request builders that avoids requiring boilerplate calls to.exec
, checking the response and status code, and calling.model
or.models
.The future implementation would essentially look like this:
This would have two benefits. For what I think would be the majority of users, this would cut down on boilerplate, and let them avoid consulting the discord docs in order to handle status codes correctly. They:
.exec
.model
.model
or.models
I think this would probably be a welcome addition for many users. I think many people like libraries like twilight because it frees them from thinking of protocol-level details such as status codes, single vs multiple responses, and requests that may not return a response, and instead focus on the application semantics of an API, and this would allow them to do just that.
Additionally, since the
.exec
and.model
path would still be available, users who wanted to do additional checks on the response, for example checking headers or the status code, could do so. And, since the.exec
/.model
would no longer be the only way to do things, it could be made less user friendly but more useful to users who wanted that additional flexibility. For example,.exec().await
might returnOk(Response)
in the event of a response, regardless of the status code, so that users wishing to inspect responses can do so without needing to handleErr
responses.Beta Was this translation helpful? Give feedback.
All reactions