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 experimental Router builder API #70
Conversation
When building a `Router` one route at a time (instead of building the tree hierarchy in order), there's a need to mutably traverse the `TreeBuilder` / `NodeBuilder` structure. When lazily adding a segment, we want to ensure we're creating a segment with the correct `SegmentType`, so we need to compare `SegmentType` on traversal to ensure we don't have a false match.
Using a trait here allows us to introduce a new type for scoped routes without duplicating all the logic.
Reading the documentation for SingleRouteBuilder was an exercise in patience, with the number of generic types that were present. Extracting the important detail into a trait helps keep things clearer while keeping all the concrete types intact.
Missing rustdoc and unused imports were generating a lot of warnings. Resolve them all in one fell swoop.
This keeps the implementation closer to the function definition, rather than being kept with the data, which will hopefully improve navigability of this code.
The API had changed in Gotham `master` but this branch was still lagging behind until now.
After #60, the OrderedRegex was refactored into ConstrainedSegmentRegex to be unwind-safe, and so the original `#[derive(Clone)]` no longer works.
The changes in #60 require unwind safety in the pipeline and handlers used in the router builder API.
At the time of writing, the poll results are unanimous on removing the feature flag, so that's exactly what I've done with 626ac8f. |
src/router/builder/replace.rs
Outdated
use router::builder::SingleRouteBuilder; | ||
use router::builder::single::DefineSingleRoute; | ||
|
||
pub trait ReplacePathExtractor<T> |
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.
Will need to add documentation here
src/router/builder/replace.rs
Outdated
} | ||
} | ||
|
||
pub trait ReplaceQueryStringExtractor<T> |
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.
Will need to add documentation here
/// # } | ||
/// # fn main() { router(); } | ||
/// ``` | ||
fn to<H>(self, handler: H) |
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 is purely discussion.
I am wondering if the naming here should be to/to_handler
as opposed to to/to_new_handler
as I could easily see folks skipping over the concept of the NewHandler
type (at least initially) and this getting confusing.
Brevity in the builder is also a nice side effect of this if we decided to change.
I understand this makes the builder not quite "pure" in naming, but worth considering.
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 disagree, for the same reason you mentioned. Though, instead of calling it "pure", I'd just call it "accurate".
Handler + Copy
is the "default" here, which completely obviates the need for NewHandler
in the trait bounds for to<H>
. Having to
take a Handler + Copy
, and to_handler
take a NewHandler
seems like it would be far more confusing for people who've learned what a NewHandler
is.
For the 99.9% case, and especially new Gotham users, I see to_new_handler
not being used at all. It's just there for completeness, in being able to control the exact NewHandler
which is used for the route.
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 can live with this reasoning, consider this resolved.
/// } | ||
/// # fn main() { router(); } | ||
/// ``` | ||
pub fn build_router<C, P, F>(pipeline_chain: C, pipelines: PipelineSet<P>, f: F) -> Router |
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 appreciate this PR has not considered pipelines so I'm happy for this comment to be considered in the future if overkill now.
I was considering use cases for Gotham and it strikes me that pipelines
could very easily be something that developers who just want something simple could do without. Likewise in most of our API example documentation, where it becomes ~9 lines of boilerplate that could be left out.
How would you feel about adding a function like:
pub fn build_simple_router<F>(f: F) -> Router
That internally does the basic pipeline setup before delegating to build_router
?
Moving between the two, if developers have some future use case for pipelines come up, would be a simple exercise as well.
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.
Sounds fine to me. I assume by "basic pipeline setup" you mean with no middleware?
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.
Indeed, as bare bones as it can be, essentially "just give me routes to my functions".
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.
For posterity: Sorted in 685e0d5
So i've done a solid first read through of this PR and left some comments. In the next day or so I'll start putting some real world code against this PR, see if I can find any funky edge cases we may need to consider. |
As the `ReplacePathExtractor` and `ReplaceQueryStringExtractor` traits are not public, they weren't flagged as missing documentation by `cargo check`. They're only used in constraining / implementing Gotham, but it's useful to have a comment describing what they do anyway.
For applications which don't require any middleware, the pipeline construction was a few too many lines of boilerplate. This reduces the boilerplate considerably.
@smangelsdorf I've pushed a branch with changes to support sub-routers which can be compared at https://github.com/gotham-rs/gotham/compare/router-builder...test-helpers-and-new-tree?expand=1 (includes test helper changes due to what I am doing locally) or looked at as a standalone patch at https://gist.github.com/bradleybeddoes/698dc5212a32a919d1950800ac6402fa (does not include test helper changes). If you can consider this addition and patch your branch if you're ok with the approach here that would be very helpful. I was also looking at the support for Now that I have support for sub routers I'll continue migrating by extensive router code into the builder API and provide further feedback here. |
ping @smangelsdorf |
As suggested in review of #70 this was missing from the initial version of the router builder. The approach here keeps the delegated route as a separate piece of API to avoid going via the normal route builder API which allows path/query extractors and route matchers to be added.
@bradleybeddoes I made a slight tweak to the approach you took, though I think you'll find it's fundamentally the same idea. Rather than having the single route builder deal with delegated routes, they're done via a different piece of API like: build_router(default_pipeline_chain, pipelines, |route| {
route.delegate("/delegated").to_router(delegated_router);
}); The reasoning is that app devs are prevented at compile time from doing odd things like specifying path/query extractors and route matchers* when they're delegating to another router. Thoughts, etc welcome. If I've missed something that your version could do, happy to tweak. * There's a very small possibility that someone will want to use a route matcher to guard a subtree, but I don't think we should be tackling that piece of API now. I'd find it very odd if they were using method matchers in particular, and that's pretty much all we have. |
I'll add support for the other HTTP verbs. We've come this far, so it probably makes sense to just blow the line count out to support all the verbs 😁 |
In contrast with `get` routes which accept requests with a `GET` or `HEAD` verb, the `only_get` route will accept only `GET` requests. This allows the behaviour of the `HEAD` requests to be customised, perhaps short-circuiting the generation of a body payload.
The traits worked well to reduce the amount of clutter in the documentation, but when not exported they were not linked correctly, which obfuscated the meaning of `Self::Output` in the return type of (e.g.) `SingleRouteBuilder::with_path_extractor`. Exporting the traits helps the clarity of the documentation.
The `get` function doing both `GET` and `HEAD` routes has never sat well with me. We should start with something accurate, and then refactor to something more readable in the future if it becomes a problem.
All feedback has been addressed now. Awaiting further review. |
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 has worked out really well, I'm confident we're heading in the right direction now.
Bring on Pipeline
builders!
This is a first attempt at building the
Router
creation API that will be used by all Gotham applications.The primary goal of this branch is to get as far as we can without introducing any macros. Keeping in line with idiomatic Rust, I opted to write a "builder" API for the router. An simple example router with this API looks like:
More in-depth examples:
gotham::router::builder
cargo doc --features experimental-router
on this branchThis is an experimental API which will likely see some breaking changes before we're totally happy with it,
and so it will ship behind a Cargo feature ("experimental-router"
) to communicate that fact to app developers who might otherwise expect some stability in the API.As a future concern, once this API is bedded in and we know more about what we can't do with a builder API, we can look at adding macros. That's out of scope for this PR.
Comments/questions/feedback/review welcome from anybody who's interested in doing so.