-
Notifications
You must be signed in to change notification settings - Fork 322
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
Extend extractors with an optional seed per endpoint. #126
Conversation
While reading the source I was a bit confused by how this works. I think explaining this in the source itself would be helpful! Pinging @aturon @tirr-c -- this PR seems pretty involved with the design of Tide, I think it'd be good if you could take a look. |
@yoshuawuyts I expanded the code to that section underneath, condensed to the really relevant lines, I hope that is what you meant. |
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 briefly looked through the code, and this looks quite a large change!
examples/seeded_extractor.rs
Outdated
|
||
fn main() { | ||
let mut app = tide::App::new(()); | ||
app.at("/").get_with(display_header, NamedHeader(HeaderName::from_static("user-agent"))); |
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 get_with
call would require ExtractSeed<fn(Header<HeaderValue>) -> String, NamedHeader>: Endpoint<_, _>
, but NamedHeader
is not a tuple. I think this should be (NamedHeader(...),)
, but maybe I've got something wrong.
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.
No, rather I missed something because in the macro expansion for one argument, this let
binding gets created: let ($($X),*) = …
. Which of course then expands to let (T0) =
and not a tuple as in let (T0,) = …
. And the same for the right side of that assignment.
I must however say it felt more ergonomic to write, it is much less confusing with one less pair of brackets and comma.
src/extract.rs
Outdated
@@ -3,15 +3,15 @@ use futures::prelude::*; | |||
use crate::{configuration::Store, Request, Response, RouteMatch}; | |||
|
|||
/// An extractor for an app with `Data` | |||
pub trait Extract<Data>: Send + Sized + 'static { | |||
pub trait Extract<Param, Data>: Send + Sync + Sized + 'static where Param: Send + Sized + 'static { |
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 feel like the trait signature should be Extract<Seed, Data>
. This looks quite reversed; for instance, Json
is a extractor, not ()
.
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 intended as an extractor for Json
. That sounds more plausible even, ()
extracts Param
from Data
. If we go the way of adding a trait rather than replacing one, this could of course be solved with another name. I'm open to bikeshedding.
However, Extract<(), Data> for Json
would mean that the natural &self
argument becomes &Seed
. Then the trait can no longer be boxed to create a Box<Extract<Json, Data, Fut=Box<Future<…>>>>
that can extract Json
.
src/head.rs
Outdated
|
||
pub struct Header<T>(pub T); | ||
|
||
impl<T: From<http::header::HeaderValue> + Send + 'static, S: 'static> Extract<Header<T>, S> for NamedHeader { |
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.
A seed doesn't have to be a newtype; impl Extract<Header<T>, S> for http::header::HeaderName
would work fine here! (or reversed, as of the review above.)
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 gravitated to that choice as well, but consider String
may get confusing if used as the extractor for too many argument types. Then, consistency of providing a newtype may be more important than not nesting in a newtype. It has its pros and cons, feel free to disagree.
Thanks for the PR, @HeroicKatora! There are definitely some interesting ideas here. I want to mull on it a bit, but here are some immediate thoughts:
In short, I definitely think we should have a solid story for the concerns you're raising in this PR, but I'm wondering if we can find alternative approaches that stick with Tide's original strict separation between routing concerns and endpoint/extraction concerns. |
I suppose these are related but intended was mostly a solution to the second (with a specific issue faced by third-party library authors. I'll expand on it below). The design specifically asserts all extractor seeds to be sync, and only gives immutable access. It does not assert
You are correct, and this was intentional. In the tuple model, every parameter maps very clearly to its seed. By using the standard unit type and making this the standard seed, I hoped to make
This could definitely be improved. I was just not sure where to integrate seeds idiomatically. So, I chose to put them/the one next to the convenience methods in
I'm not sure in which way this might improve the situation. Extractors seem to then need to rely on a specific type to extract from a loose typemap but that type is no longer type checked and needs to be unique to this extractor at the same time.
This seems to be at conflict with the idea of using extraced values as an easily pluggable guard to
Tl;dr: I agree that we should try to separate endpoint and routing information more cleanly in the interface, and not just its implementation. But I think that dynamic values provide the most idiomatic and most usable coupling, especially when considering how typical configurable components might be implemented in libraries. Lastly, let me note that this could solve the problem of #63 . If you want a proof of concept for this, I will gladly try to provide one. |
@HeroicKatora thanks for the detailed response! I'll try to respond fully later, but a quick question:
Can you elaborate on this a bit? One core design aspect of Tide is that routing happens fully prior to extraction, and hence extraction cannot influence which endpoint is invoked. Bailing out for an endpoint has to happen within the endpoint body (and we eventually intend to provide redirects or other similar functionality). So to clarify what I meant a bit, we could have a fn my_endpoint(segments: Segments) {
let id: u64 = segments.get("id");
let name: String = segments.get("name");
// ...
} |
Sure. In case of a denied authorization for example you might want to respond with a fully static 'Denied' error with correct Authorization header. That error response is possible for an extractor because it does not need to influence routing. And for a library, the header should most likely be set by the extractor since it contains information about the authorization method that should be used for example. That is not to say that it should not be possible to do this in the endpoint body but that for the most common cases the extractor would be much more ergonomic. |
Can I get some clarification on this? If I understood the blog post correctly and recall it, then this was sometime intended to occur not via the path, i.e. by not going through the router. Is there a reason why such a redirection would be possible for endpoints but not for extractors (especially if seeded such that the intended target can be configured at construction)? |
To address the biggest concern of @tirr-c I think I'll do a rebase of this where I leave out the changes to |
Is it okay to rebase this in progress? Or should we close and reopen, or something else entirely? |
The result looks more clunky for binding it to a route but at least the concerns are separate more strictly. This also removes the need for duplicating the router utilities for methods.
In order to makes this much more convincing, I've written a sample add-on to provide sessions: https://github.com/HeroicKatora/tide/tree/session This is exactly the use case, where the third-party library wants to provide additional extractors without the configuration living inside generic type arguments (and it should not leak into |
And another one, providing https://github.com/HeroicKatora/tide/tree/authorize |
Made irrelevant by #156 which removes the concept to be extended while simultaneously providing a much cleaner approach with core Rust semantics. Third-party crates can add parameter to the extension traits created by them to replace the concept of seeds or pass the You could still find the example code for the authentication crates useful. |
Description
The
Extract
-trait was complemented by another trait to allow one type to provide an seed for extracting multiple others. All existing self-extracting types (Body
,Json
,Path
, …) are provided with animpl Extract<T, Data> for ()
. A second impl forEndpoint
has been created:Seeded
. It is essentially a tuple of a standard endpoint function and aSync + 'static
seed tuple which provides the seeds for extracting the function arguments.The new trait looks as follows (trait bounds may be stricter than necessary), fairly closely modeled after
serde::de::DeserializeSeed
:An example
impl
for this trait (except()
for allExtract
), uses a runtime provided header name to extract the corresponding header:Similar to the other
Endpoint
impl for closures, forSeeded
we also use a closure to provide implementations up to 9 arguments. It follows the other implementation closely, only extending the required trait parameters by new ones and replacing the extraction logic with the following:Motivation and Context
This allows an extracted value to depend on information only available at runtime. Such behaviour was previously only (nicely, without
static Mutex<T>
) possible by creating a new generic type wrapping the actually intended parameter type. This generic would then take a type argument implementing a trait. However, this trait implementation must typically be deferred to an enduser who has to customize it according to their own app state type. Futhermore, this needs one type and impl per endpoint with different configuration.One problem where this would be applicable is authentication/authorization. Different endpoints may require different security realms or permissions, different user names etc. Encoding this all into types with static functions to derive different runtime values from app state quickly becomes a hassle of writing the same implementation with just slightly different constants. However, the outcome parameter into endpoint functions should mostly be very similar. A single value containing the name of the authorized entity. By implementing this via type-level generics though, the type of this value quickly becomes much much more complex. Moving the necessary information into runtime instead allows one to use a single type to guarantee the necessary invariant.
Programmers may still use newtype structs to type such values more securely but these can be expressed more succinctly without generics.
This could be a solution for #108 as
NamedComponent
may gain a seed which gets supplied with the name at runtime. This has been exemplified in the example.This solution is not intended to encourage arbitrary reconfiguration of extractor seeds at runtime although it could allow it. The seeds are hidden behind a
BoxedEndpoint
as soon as they are provided to the router and may only inspect themselves immutably while requiringSend + Sync
.How Has This Been Tested?
There is an example that extracts the
user-agent
header based on a runtime constructedHeaderName
. Another route extracts the path componentnum
wherenum
was supplied via a static string instead of an associated item on a type parameter.Other tests in
cfg(test)
will follow as implementation goes beyond Poc. No other tests had to be modified for these changes to work as theApp
interface remained unchanged.Other TODOs
Resource::get_with
SegmentName
,NamedHeader
)Types of changes
Checklist:
edit 2019-01-21 (yw): updated link to example, and added syntax highlighting.