-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Add type safe state extractor #1155
Conversation
3beae1f
to
2117c97
Compare
I'm not a fan of this change. It increases complexity of both the API surface and the implementation while only serving some use cases. Notably, the key for |
I think sharing state across all/most handlers is a very common use case. It's not specific to cookies and haven't thought about migrating that. But I still think it makes sense. Have a look at all the examples. Those got way nicer and more type safe. I do agree re adding more complexity but if/when we remove the request body type param I think that balances things out. |
I didn't want to say cookies are a special case. In general, I feel like extractors that expect some sort of "configuration" in an extension currently are not going to be easy to integrate into this new pattern. The same applies to routes not tied to a particular service (e.g. a metrics route implementation in its own library), I think.
I had a look. They certainly did get more type safe, but how are they way nicer? Thinking more about this, here is another pattern I've used before that wouldn't integrate well with this: I used to work on a webservice that integrated with a bunch of external services (i.e. stuff like S3). For some routes, having these configured was necessary for them to do their work. I set up the server such that setting specific env vars would add specific extensions to the app-level |
I actually think requiring the state used with private and signed cookies to implement
I meant the type safety made them way nicer 😛
So you just wouldn't call those routes during development because the extensions would be missing? Hm yeah not sure how to port that to this pattern 🤔 Maybe your state could have options for those configs that are But extensions aren't going away either so you could also just keep doing what you did. Any solutions I've heard for checking that extensions exist quickly dissolve in hlists which I really wanna avoid. Maybe if |
Yeah, I guess this could be a good compromise. Though it does break my proposed |
Nested routers can't inherit the state somehow, can they? That was actually something I considered a bit ugly when seeing it – |
Yep that is annoying for sure. I did actually have a previous version where the state would be lazily applied so you didn't have to pass it around. But it was pretty complicated so I abandoned it. |
…to what get, post, etc accept). Use the new Router::fallback_service for setting any Service as the fallback (#1155) Refs: tokio-rs/axum#1155
//! You should prefer using [`State`] if possible since it's more type safe. The downside is that | ||
//! its less dynamic than request extensions. |
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 was trying a similar experiment to have an AnyMap
within State
to allow using dynamic Extension
that is not type-safe. But the issue with that is that the impl crashes due to impl FromRef<T> for State
crashes with those specific implementations of FromRef
. Here separating it out seemed to work quite well.
I believe this is also more performant since we no longer have to go through a HashMap
of types (AnyMap
) and can access the fields directly. Not sure if we want to write it down but I will try to do a benchmark and see what's the difference.
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.
We already have a benchmark of extension vs state in the repo.
/// // this impl tells `SignedCookieJar` how to access the key from our state | ||
/// impl FromRef<AppState> for Key { | ||
/// fn from_ref(state: &AppState) -> Self { | ||
/// state.key.clone() | ||
/// } | ||
/// } |
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.
Is it a good idea to recommend implementing FromRef
for an owned version that requires .clone()
instead of a borrowed version? The FromRef
implementations from the docs seemed to always have .clone()
.
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.
How else would you do 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.
Maybe recommend the borrowed version like impl FromRef<AppState> for &Key
in cases where clone is heavy and user does not need the owned version? Like show an example.
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.
Have you tried that? I'm pretty damn sure it isn't going to work. The only way we could make borrowed extractors work is by adding a lifetime to FromRequest
, which would be extra complexity for little practical benefit.
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.
Yeah exactly. I don't think it'll actually work.
looking forward to the 0.6 release. this is wonderful! |
This fixes #1142 by introducing a type safe
State
extractor. Attempting toextract states that haven't seen set results in compile errors, rather than
runtime errors.
Example usage:
So the state is set with
Router::with_state
and extracted withState
.How it works
This works by changing
Router::route
to accept aMethodRouter
, rather thanany
Service
.MethodRouter
,FromRequest
,RequestParts
, andHandler
thenall have a new type parameter for the state extracted with
State
.All extractors just requires some generic state
S
butState
fixes that to besome specific type.
Most of the changes in this PR comes from having to add the new parameter
everywhere. I'm dreading all the merge conflicts but oh well...
Extracting substates
Substates can be extracted by implementing
From
:I think this gives most of the benefits of
Extension
while being type safe.State defaults to
()
By default the state type param is
()
. So unless you're explicitly using stateyou shouldn't have to worry about it:
Note that
S
does not default to()
onFromRequest
andRequestParts
. I'd be worried that library authors would accidentally bind their extractors to()
rather than a genericS
. So therefore these types requires specifying the state type.MethodRouter
andHandler
asService
Only
Router
accepts the state in its constructor so only it can implementService
directly.However
MethodRouter
andHandler
needs the state provided to implementService
. Providing that is done like so:Known limitations
No good way to provide the state automatically to nested
Router
s. You have tosomething like this:
Given all the discussions here I
think this is the best we can do.
Middleware cannot extract states since
Router::layer
just receives someL: Layer
. It doesn't know anything about the state. We cannot pull the same trickI did with
Router::route
since there are many ways to createLayer
s.This setup might not be a great fit for optional state that might only exist in
some environments. I think those use cases are rare so IMO we can live with
this.
Extension
still works well for such use cases.Same goes for routing to generic
Service
s, there is no good way to access thestate. I think thats fine because. In my experience you're either routing to
handlers (which know about state) or
Service
s likeServeDir
which don't needthe state.
Follow-ups
These are follow-up improvements I intend to make once this is merged:
#[debug_handler]
which state you use. Currently it has toassume you're using
()
.#[derive(FromRequest)]
.extensions, since its required.
#[derive(State)]
macro which adds theFrom
impls for allfields in a struct and adds
FromRequest
impls that delegate toState
. Thisshould take care of all the boilerplate if you have a lot of state.
Extension
.