-
Notifications
You must be signed in to change notification settings - Fork 321
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
Nail down wildcard/fallback rules #12
Comments
Maybe we can use regex to approach this. We can convert path expression into regex mechanically. Might contain some errors, but here's the rules:
This way, Bullet points:
|
A scenario I'd like us to consider is the way GitHub does routing for users / organizations. Essentially I'd like us to be able to support a similar URL structure.
|
But ins't the routing structure specific to the way the app is structured? I mean you could have a RESTful API or you could have something arbitrary right? |
I can highly recommend The problem with using pure regex is deciding the precedence when you have multiple matching routes, (EDIT: Although, I'm now remembering that it has literally the opposite order to what I mention so I was using a fork that reversed it.) |
I didn't know about |
Lookup orders
Example: https://github.com/trek-rs/path-tree/blob/master/src/lib.rs#L140-L176 |
This commit reworks Tide, with the goal of reducing the total number of concepts in the framework. The key new idea is to remove the notion of `Extractor`s, which in turn allows us to remove or simplify several other concepts in Tide. We'll first lay out the new design, and then discuss the tradeoffs made in this simplification. Here's a full list of the concepts in Tide after this commit: | Concept | Description | | ----- | ----------- | | `App` | Builder for Tide applications | | `Route` | Builder for an individual route | | `Endpoint` | Trait for actual endpoints | | `Context` | The request context for an endpoint | | `IntoResponse` | A trait for converting into a `Response` | | `Middleware` | A trait for Tide middleware | Previously, the `Endpoint` trait was treated as a somewhat magical internal abstraction, and we used a macro to provide `Endpoint` implementations for actual endpoints (with varying numbers of extractor arguments). In this commit, an `Endpoint` is just an asynchronous function from a `Context` to a `Response`: ```rust pub trait Endpoint<AppData>: Send + Sync + 'static { /// The async result of `call`. type Fut: Future<Output = Response> + Send + 'static; /// Invoke the endpoint. fn call(&self, cx: Context<AppData>) -> Self::Fut; } ``` For convenience, this trait is implemented for async functions that return any value that implements `IntoResponse`: ```rust impl<AppData, F, Fut> Endpoint<AppData> for F where F: Fn(Context<AppData>) -> Fut, Fut: Future Fut::Output: IntoResponse, // ... ``` This implementation is in contrast to the macro-generated implementations we previously had, which allowed endpoints with varying numbers of `Extractor` arguments. The intent is for endpoints to perform their own extraction directly on the `Context`, as we'll see next. The `Context` type contains all of the request and middleware context an endpoint operates on. You can think of it as wrapping an `http_service::Request` with some additional data. It's easiest to understand `Context` through the APIs it provides. First, we have methods for getting basic http request information, mirroring the `http` APIs: ```rust impl<AppData> Context<AppData> { pub fn method(&self) -> &Method; pub fn uri(&self) -> &Uri; pub fn version(&self) -> Version; pub fn headers(&self) -> &HeaderMap; } ``` The context also has a handle to application data, which typically would store database connection pools and other "application-global" state. This API replaces the old `AppData` extractor: ```rust impl<AppData> Context<AppData> { pub fn app_data(&self) -> &AppData { &self.app_data } } ``` Similarly, we provide a *direct* API for extracting any "route parameters" (i.e. placeholders in the route URL), replacing the need for `NamedSegment` and the like: ```rust impl<AppData> Context<AppData> { pub fn route_param(&self, key: &str) -> Option<&str>; } ``` Basic body extraction is likewise built in via `Context` methods, replacing the `Str`, `Bytes`, and `Json` extractors: ```rust impl<AppData> Context<AppData> { pub async fn body_bytes(&mut self) -> std::io::Result<Vec<u8>>; pub async fn body_string(&mut self) -> std::io::Result<String>; pub async fn body_json<T: serde::de::DeserializeOwned>(&mut self) -> std::io::Result<T>; } ``` Looking at the [message database example](https://github.com/rustasync/tide/blob/master/examples/messages.rs#L44), we previously had endpoints like this: ```rust async fn new_message(mut db: AppData<Database>, msg: body::Json<Message>) -> String { db.insert(msg.clone()).to_string() } async fn set_message( mut db: AppData<Database>, id: head::Path<usize>, msg: body::Json<Message>, ) -> Result<(), StatusCode> { if db.set(*id, msg.clone()) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` These endpoints would now be written something like this (where `Error` is intended as a general error type, convertible into a response): ```rust async fn new_message(cx: Context<Database>) -> Result<String, Error> { let msg = await!(cx.body_json())?; cx.app_data().insert(msg).to_string() } async fn set_message(cx: Context<Database>) -> Result<(), Error> { let msg = await!(cx.body_json())?; if cx.app_data().set(cx.route_param("id"), msg) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` The endpoint code is a bit more verbose, but also arguably easier to follow, since the extraction (and error handling) is more clear. In addition, the basic extraction approach is *more discoverable*, since it operates via normal methods on `Context`. Part of the idea of the old `Extractor` trait was that Tide would provide an *extensible* system of extractors; you could always introduce new types that implement `Extractor`. But now most of the existing extractors are built-in `Context` methods. How do we recover extensibility? Easy: we use Rust's ability to extend existing types with new methods, via traits! (Note: this is directly inspired by the Gotham framework). Let's say we want to provide cookie extraction. Previously, we'd have a `Cookies` type that you could use as an endpoint argument for extraction. Now, instead, we can introduce a `Cookies` *trait* that's used to extend `Context` with new APIs: ```rust trait Cookies { fn cookies(&self) -> CookieJar; } impl<AppData> Cookies for Context<AppData> { ... } ``` This pattern is called an "extension trait" -- a trait whose sole purpose is to extend an existing type with new methods. There are several nice properties of this approach: - The resulting extraction API is just a direct and natural as the built-in ones: just a method call on the `Context` object. - The methods that are available on `Context` are controlled by what traits are in scope. In other words, if you want to use a custom extractor from the ecosystem, you just bring its trait into scope, and then the method is available. That makes it easy to build a robust ecosystem around a small set of core Tide APIs. One of the major benefits of moving extraction into the endpoint body, rather than via `Extractor` arguments, is that it's much simpler to provide configuration. For example, we could easily provide a customized json body extractor that limited the maximum size of the body or other such options: ```rust impl<AppData> Context<AppData> { pub async fn body_json_cfg<T: serde::de::DeserializeOwned>(&mut self, cfg: JsonConfig) -> std::io::Result<T>; } ``` As a result, we can drop much of the complexity in `App` around configuration. Following the spirit of the changes to extractors, response generation for non-standard Rust types is now just done via a free function: ```rust mod response { pub fn json<T: serde::Serialize>(t: T) -> Response { ... } } ``` As before, there's a top-level `App` type for building up a Tide application. However, the API has been drastically simplified: - It no longer provides a configuration system, since extractors can now be configured directly. - It no longer allows for the middleware list to be customized per route; instead, middleware is set up only at the top level. These simplifications make the programming model much easier to understand; previously, there were inconsistencies between the way that middleware nesting and configuration nesting worked. The hope is that we can get away with this much simpler, top-level model. When actually adding routes via `at`, you get a `Route` object (which used to be `Resource`). This object now provides a *builder-style* API for adding endpoints, allowing you to chain several endpoints. Altogether, this means we can drop nested routing as well. The middleware trait is more or less as it was before, adjusted to use `Context` objects and otherwise slightly cleaned up. This commit also switches to using the route-recognizer crate, rather than the path-table crate, as the underlying routing mechanism. In addition to being more efficient, route-recognizer provides a more intuitive semantics for "ambiguous" routing situations. See issue http-rs#12 and issue http-rs#141 for more details.
This commit reworks Tide, with the goal of reducing the total number of concepts in the framework. The key new idea is to remove the notion of `Extractor`s, which in turn allows us to remove or simplify several other concepts in Tide. We'll first lay out the new design, and then discuss the tradeoffs made in this simplification. Here's a full list of the concepts in Tide after this commit: | Concept | Description | | ----- | ----------- | | `App` | Builder for Tide applications | | `Route` | Builder for an individual route | | `Endpoint` | Trait for actual endpoints | | `Context` | The request context for an endpoint | | `IntoResponse` | A trait for converting into a `Response` | | `Middleware` | A trait for Tide middleware | Previously, the `Endpoint` trait was treated as a somewhat magical internal abstraction, and we used a macro to provide `Endpoint` implementations for actual endpoints (with varying numbers of extractor arguments). In this commit, an `Endpoint` is just an asynchronous function from a `Context` to a `Response`: ```rust pub trait Endpoint<AppData>: Send + Sync + 'static { /// The async result of `call`. type Fut: Future<Output = Response> + Send + 'static; /// Invoke the endpoint. fn call(&self, cx: Context<AppData>) -> Self::Fut; } ``` For convenience, this trait is implemented for async functions that return any value that implements `IntoResponse`: ```rust impl<AppData, F, Fut> Endpoint<AppData> for F where F: Fn(Context<AppData>) -> Fut, Fut: Future Fut::Output: IntoResponse, // ... ``` This implementation is in contrast to the macro-generated implementations we previously had, which allowed endpoints with varying numbers of `Extractor` arguments. The intent is for endpoints to perform their own extraction directly on the `Context`, as we'll see next. The `Context` type contains all of the request and middleware context an endpoint operates on. You can think of it as wrapping an `http_service::Request` with some additional data. It's easiest to understand `Context` through the APIs it provides. First, we have methods for getting basic http request information, mirroring the `http` APIs: ```rust impl<AppData> Context<AppData> { pub fn method(&self) -> &Method; pub fn uri(&self) -> &Uri; pub fn version(&self) -> Version; pub fn headers(&self) -> &HeaderMap; } ``` The context also has a handle to application data, which typically would store database connection pools and other "application-global" state. This API replaces the old `AppData` extractor: ```rust impl<AppData> Context<AppData> { pub fn app_data(&self) -> &AppData { &self.app_data } } ``` Similarly, we provide a *direct* API for extracting any "route parameters" (i.e. placeholders in the route URL), replacing the need for `NamedSegment` and the like: ```rust impl<AppData> Context<AppData> { pub fn route_param(&self, key: &str) -> Option<&str>; } ``` Basic body extraction is likewise built in via `Context` methods, replacing the `Str`, `Bytes`, and `Json` extractors: ```rust impl<AppData> Context<AppData> { pub async fn body_bytes(&mut self) -> std::io::Result<Vec<u8>>; pub async fn body_string(&mut self) -> std::io::Result<String>; pub async fn body_json<T: serde::de::DeserializeOwned>(&mut self) -> std::io::Result<T>; } ``` Looking at the [message database example](https://github.com/rustasync/tide/blob/master/examples/messages.rs#L44), we previously had endpoints like this: ```rust async fn new_message(mut db: AppData<Database>, msg: body::Json<Message>) -> String { db.insert(msg.clone()).to_string() } async fn set_message( mut db: AppData<Database>, id: head::Path<usize>, msg: body::Json<Message>, ) -> Result<(), StatusCode> { if db.set(*id, msg.clone()) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` These endpoints would now be written something like this (where `Error` is intended as a general error type, convertible into a response): ```rust async fn new_message(cx: Context<Database>) -> Result<String, Error> { let msg = await!(cx.body_json())?; cx.app_data().insert(msg).to_string() } async fn set_message(cx: Context<Database>) -> Result<(), Error> { let msg = await!(cx.body_json())?; if cx.app_data().set(cx.route_param("id"), msg) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` The endpoint code is a bit more verbose, but also arguably easier to follow, since the extraction (and error handling) is more clear. In addition, the basic extraction approach is *more discoverable*, since it operates via normal methods on `Context`. Part of the idea of the old `Extractor` trait was that Tide would provide an *extensible* system of extractors; you could always introduce new types that implement `Extractor`. But now most of the existing extractors are built-in `Context` methods. How do we recover extensibility? Easy: we use Rust's ability to extend existing types with new methods, via traits! (Note: this is directly inspired by the Gotham framework). Let's say we want to provide cookie extraction. Previously, we'd have a `Cookies` type that you could use as an endpoint argument for extraction. Now, instead, we can introduce a `Cookies` *trait* that's used to extend `Context` with new APIs: ```rust trait Cookies { fn cookies(&self) -> CookieJar; } impl<AppData> Cookies for Context<AppData> { ... } ``` This pattern is called an "extension trait" -- a trait whose sole purpose is to extend an existing type with new methods. There are several nice properties of this approach: - The resulting extraction API is just a direct and natural as the built-in ones: just a method call on the `Context` object. - The methods that are available on `Context` are controlled by what traits are in scope. In other words, if you want to use a custom extractor from the ecosystem, you just bring its trait into scope, and then the method is available. That makes it easy to build a robust ecosystem around a small set of core Tide APIs. One of the major benefits of moving extraction into the endpoint body, rather than via `Extractor` arguments, is that it's much simpler to provide configuration. For example, we could easily provide a customized json body extractor that limited the maximum size of the body or other such options: ```rust impl<AppData> Context<AppData> { pub async fn body_json_cfg<T: serde::de::DeserializeOwned>(&mut self, cfg: JsonConfig) -> std::io::Result<T>; } ``` As a result, we can drop much of the complexity in `App` around configuration. Following the spirit of the changes to extractors, response generation for non-standard Rust types is now just done via a free function: ```rust mod response { pub fn json<T: serde::Serialize>(t: T) -> Response { ... } } ``` As before, there's a top-level `App` type for building up a Tide application. However, the API has been drastically simplified: - It no longer provides a configuration system, since extractors can now be configured directly. - It no longer allows for the middleware list to be customized per route; instead, middleware is set up only at the top level. These simplifications make the programming model much easier to understand; previously, there were inconsistencies between the way that middleware nesting and configuration nesting worked. The hope is that we can get away with this much simpler, top-level model. When actually adding routes via `at`, you get a `Route` object (which used to be `Resource`). This object now provides a *builder-style* API for adding endpoints, allowing you to chain several endpoints. Altogether, this means we can drop nested routing as well. The middleware trait is more or less as it was before, adjusted to use `Context` objects and otherwise slightly cleaned up. This commit also switches to using the route-recognizer crate, rather than the path-table crate, as the underlying routing mechanism. In addition to being more efficient, route-recognizer provides a more intuitive semantics for "ambiguous" routing situations. See issue http-rs#12 and issue http-rs#141 for more details.
This commit reworks Tide, with the goal of reducing the total number of concepts in the framework. The key new idea is to remove the notion of `Extractor`s, which in turn allows us to remove or simplify several other concepts in Tide. We'll first lay out the new design, and then discuss the tradeoffs made in this simplification. Here's a full list of the concepts in Tide after this commit: | Concept | Description | | ----- | ----------- | | `App` | Builder for Tide applications | | `Route` | Builder for an individual route | | `Endpoint` | Trait for actual endpoints | | `Context` | The request context for an endpoint | | `IntoResponse` | A trait for converting into a `Response` | | `Middleware` | A trait for Tide middleware | Previously, the `Endpoint` trait was treated as a somewhat magical internal abstraction, and we used a macro to provide `Endpoint` implementations for actual endpoints (with varying numbers of extractor arguments). In this commit, an `Endpoint` is just an asynchronous function from a `Context` to a `Response`: ```rust pub trait Endpoint<AppData>: Send + Sync + 'static { /// The async result of `call`. type Fut: Future<Output = Response> + Send + 'static; /// Invoke the endpoint. fn call(&self, cx: Context<AppData>) -> Self::Fut; } ``` For convenience, this trait is implemented for async functions that return any value that implements `IntoResponse`: ```rust impl<AppData, F, Fut> Endpoint<AppData> for F where F: Fn(Context<AppData>) -> Fut, Fut: Future Fut::Output: IntoResponse, // ... ``` This implementation is in contrast to the macro-generated implementations we previously had, which allowed endpoints with varying numbers of `Extractor` arguments. The intent is for endpoints to perform their own extraction directly on the `Context`, as we'll see next. The `Context` type contains all of the request and middleware context an endpoint operates on. You can think of it as wrapping an `http_service::Request` with some additional data. It's easiest to understand `Context` through the APIs it provides. First, we have methods for getting basic http request information, mirroring the `http` APIs: ```rust impl<AppData> Context<AppData> { pub fn method(&self) -> &Method; pub fn uri(&self) -> &Uri; pub fn version(&self) -> Version; pub fn headers(&self) -> &HeaderMap; } ``` The context also has a handle to application data, which typically would store database connection pools and other "application-global" state. This API replaces the old `AppData` extractor: ```rust impl<AppData> Context<AppData> { pub fn app_data(&self) -> &AppData { &self.app_data } } ``` Similarly, we provide a *direct* API for extracting any "route parameters" (i.e. placeholders in the route URL), replacing the need for `NamedSegment` and the like: ```rust impl<AppData> Context<AppData> { pub fn route_param(&self, key: &str) -> Option<&str>; } ``` Basic body extraction is likewise built in via `Context` methods, replacing the `Str`, `Bytes`, and `Json` extractors: ```rust impl<AppData> Context<AppData> { pub async fn body_bytes(&mut self) -> std::io::Result<Vec<u8>>; pub async fn body_string(&mut self) -> std::io::Result<String>; pub async fn body_json<T: serde::de::DeserializeOwned>(&mut self) -> std::io::Result<T>; } ``` Looking at the [message database example](https://github.com/rustasync/tide/blob/master/examples/messages.rs#L44), we previously had endpoints like this: ```rust async fn new_message(mut db: AppData<Database>, msg: body::Json<Message>) -> String { db.insert(msg.clone()).to_string() } async fn set_message( mut db: AppData<Database>, id: head::Path<usize>, msg: body::Json<Message>, ) -> Result<(), StatusCode> { if db.set(*id, msg.clone()) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` These endpoints would now be written something like this (where `Error` is intended as a general error type, convertible into a response): ```rust async fn new_message(cx: Context<Database>) -> Result<String, Error> { let msg = await!(cx.body_json())?; cx.app_data().insert(msg).to_string() } async fn set_message(cx: Context<Database>) -> Result<(), Error> { let msg = await!(cx.body_json())?; if cx.app_data().set(cx.route_param("id"), msg) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` The endpoint code is a bit more verbose, but also arguably easier to follow, since the extraction (and error handling) is more clear. In addition, the basic extraction approach is *more discoverable*, since it operates via normal methods on `Context`. Part of the idea of the old `Extractor` trait was that Tide would provide an *extensible* system of extractors; you could always introduce new types that implement `Extractor`. But now most of the existing extractors are built-in `Context` methods. How do we recover extensibility? Easy: we use Rust's ability to extend existing types with new methods, via traits! (Note: this is directly inspired by the Gotham framework). Let's say we want to provide cookie extraction. Previously, we'd have a `Cookies` type that you could use as an endpoint argument for extraction. Now, instead, we can introduce a `Cookies` *trait* that's used to extend `Context` with new APIs: ```rust trait Cookies { fn cookies(&self) -> CookieJar; } impl<AppData> Cookies for Context<AppData> { ... } ``` This pattern is called an "extension trait" -- a trait whose sole purpose is to extend an existing type with new methods. There are several nice properties of this approach: - The resulting extraction API is just a direct and natural as the built-in ones: just a method call on the `Context` object. - The methods that are available on `Context` are controlled by what traits are in scope. In other words, if you want to use a custom extractor from the ecosystem, you just bring its trait into scope, and then the method is available. That makes it easy to build a robust ecosystem around a small set of core Tide APIs. One of the major benefits of moving extraction into the endpoint body, rather than via `Extractor` arguments, is that it's much simpler to provide configuration. For example, we could easily provide a customized json body extractor that limited the maximum size of the body or other such options: ```rust impl<AppData> Context<AppData> { pub async fn body_json_cfg<T: serde::de::DeserializeOwned>(&mut self, cfg: JsonConfig) -> std::io::Result<T>; } ``` As a result, we can drop much of the complexity in `App` around configuration. Following the spirit of the changes to extractors, response generation for non-standard Rust types is now just done via a free function: ```rust mod response { pub fn json<T: serde::Serialize>(t: T) -> Response { ... } } ``` As before, there's a top-level `App` type for building up a Tide application. However, the API has been drastically simplified: - It no longer provides a configuration system, since extractors can now be configured directly. - It no longer allows for the middleware list to be customized per route; instead, middleware is set up only at the top level. These simplifications make the programming model much easier to understand; previously, there were inconsistencies between the way that middleware nesting and configuration nesting worked. The hope is that we can get away with this much simpler, top-level model. When actually adding routes via `at`, you get a `Route` object (which used to be `Resource`). This object now provides a *builder-style* API for adding endpoints, allowing you to chain several endpoints. Altogether, this means we can drop nested routing as well. The middleware trait is more or less as it was before, adjusted to use `Context` objects and otherwise slightly cleaned up. This commit also switches to using the route-recognizer crate, rather than the path-table crate, as the underlying routing mechanism. In addition to being more efficient, route-recognizer provides a more intuitive semantics for "ambiguous" routing situations. See issue http-rs#12 and issue http-rs#141 for more details.
This commit reworks Tide, with the goal of reducing the total number of concepts in the framework. The key new idea is to remove the notion of `Extractor`s, which in turn allows us to remove or simplify several other concepts in Tide. We'll first lay out the new design, and then discuss the tradeoffs made in this simplification. Here's a full list of the concepts in Tide after this commit: | Concept | Description | | ----- | ----------- | | `App` | Builder for Tide applications | | `Route` | Builder for an individual route | | `Endpoint` | Trait for actual endpoints | | `Context` | The request context for an endpoint | | `IntoResponse` | A trait for converting into a `Response` | | `Middleware` | A trait for Tide middleware | Previously, the `Endpoint` trait was treated as a somewhat magical internal abstraction, and we used a macro to provide `Endpoint` implementations for actual endpoints (with varying numbers of extractor arguments). In this commit, an `Endpoint` is just an asynchronous function from a `Context` to a `Response`: ```rust pub trait Endpoint<AppData>: Send + Sync + 'static { /// The async result of `call`. type Fut: Future<Output = Response> + Send + 'static; /// Invoke the endpoint. fn call(&self, cx: Context<AppData>) -> Self::Fut; } ``` For convenience, this trait is implemented for async functions that return any value that implements `IntoResponse`: ```rust impl<AppData, F, Fut> Endpoint<AppData> for F where F: Fn(Context<AppData>) -> Fut, Fut: Future Fut::Output: IntoResponse, // ... ``` This implementation is in contrast to the macro-generated implementations we previously had, which allowed endpoints with varying numbers of `Extractor` arguments. The intent is for endpoints to perform their own extraction directly on the `Context`, as we'll see next. The `Context` type contains all of the request and middleware context an endpoint operates on. You can think of it as wrapping an `http_service::Request` with some additional data. It's easiest to understand `Context` through the APIs it provides. First, we have methods for getting basic http request information, mirroring the `http` APIs: ```rust impl<AppData> Context<AppData> { pub fn method(&self) -> &Method; pub fn uri(&self) -> &Uri; pub fn version(&self) -> Version; pub fn headers(&self) -> &HeaderMap; } ``` The context also has a handle to application data, which typically would store database connection pools and other "application-global" state. This API replaces the old `AppData` extractor: ```rust impl<AppData> Context<AppData> { pub fn app_data(&self) -> &AppData { &self.app_data } } ``` Similarly, we provide a *direct* API for extracting any "route parameters" (i.e. placeholders in the route URL), replacing the need for `NamedSegment` and the like: ```rust impl<AppData> Context<AppData> { pub fn route_param(&self, key: &str) -> Option<&str>; } ``` Basic body extraction is likewise built in via `Context` methods, replacing the `Str`, `Bytes`, and `Json` extractors: ```rust impl<AppData> Context<AppData> { pub async fn body_bytes(&mut self) -> std::io::Result<Vec<u8>>; pub async fn body_string(&mut self) -> std::io::Result<String>; pub async fn body_json<T: serde::de::DeserializeOwned>(&mut self) -> std::io::Result<T>; } ``` Looking at the [message database example](https://github.com/rustasync/tide/blob/master/examples/messages.rs#L44), we previously had endpoints like this: ```rust async fn new_message(mut db: AppData<Database>, msg: body::Json<Message>) -> String { db.insert(msg.clone()).to_string() } async fn set_message( mut db: AppData<Database>, id: head::Path<usize>, msg: body::Json<Message>, ) -> Result<(), StatusCode> { if db.set(*id, msg.clone()) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` These endpoints would now be written something like this (where `Error` is intended as a general error type, convertible into a response): ```rust async fn new_message(cx: Context<Database>) -> Result<String, Error> { let msg = await!(cx.body_json())?; cx.app_data().insert(msg).to_string() } async fn set_message(cx: Context<Database>) -> Result<(), Error> { let msg = await!(cx.body_json())?; if cx.app_data().set(cx.route_param("id"), msg) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` The endpoint code is a bit more verbose, but also arguably easier to follow, since the extraction (and error handling) is more clear. In addition, the basic extraction approach is *more discoverable*, since it operates via normal methods on `Context`. Part of the idea of the old `Extractor` trait was that Tide would provide an *extensible* system of extractors; you could always introduce new types that implement `Extractor`. But now most of the existing extractors are built-in `Context` methods. How do we recover extensibility? Easy: we use Rust's ability to extend existing types with new methods, via traits! (Note: this is directly inspired by the Gotham framework). Let's say we want to provide cookie extraction. Previously, we'd have a `Cookies` type that you could use as an endpoint argument for extraction. Now, instead, we can introduce a `Cookies` *trait* that's used to extend `Context` with new APIs: ```rust trait Cookies { fn cookies(&self) -> CookieJar; } impl<AppData> Cookies for Context<AppData> { ... } ``` This pattern is called an "extension trait" -- a trait whose sole purpose is to extend an existing type with new methods. There are several nice properties of this approach: - The resulting extraction API is just a direct and natural as the built-in ones: just a method call on the `Context` object. - The methods that are available on `Context` are controlled by what traits are in scope. In other words, if you want to use a custom extractor from the ecosystem, you just bring its trait into scope, and then the method is available. That makes it easy to build a robust ecosystem around a small set of core Tide APIs. One of the major benefits of moving extraction into the endpoint body, rather than via `Extractor` arguments, is that it's much simpler to provide configuration. For example, we could easily provide a customized json body extractor that limited the maximum size of the body or other such options: ```rust impl<AppData> Context<AppData> { pub async fn body_json_cfg<T: serde::de::DeserializeOwned>(&mut self, cfg: JsonConfig) -> std::io::Result<T>; } ``` As a result, we can drop much of the complexity in `App` around configuration. Following the spirit of the changes to extractors, response generation for non-standard Rust types is now just done via a free function: ```rust mod response { pub fn json<T: serde::Serialize>(t: T) -> Response { ... } } ``` As before, there's a top-level `App` type for building up a Tide application. However, the API has been drastically simplified: - It no longer provides a configuration system, since extractors can now be configured directly. - It no longer allows for the middleware list to be customized per route; instead, middleware is set up only at the top level. These simplifications make the programming model much easier to understand; previously, there were inconsistencies between the way that middleware nesting and configuration nesting worked. The hope is that we can get away with this much simpler, top-level model. When actually adding routes via `at`, you get a `Route` object (which used to be `Resource`). This object now provides a *builder-style* API for adding endpoints, allowing you to chain several endpoints. Altogether, this means we can drop nested routing as well. The middleware trait is more or less as it was before, adjusted to use `Context` objects and otherwise slightly cleaned up. This commit also switches to using the route-recognizer crate, rather than the path-table crate, as the underlying routing mechanism. In addition to being more efficient, route-recognizer provides a more intuitive semantics for "ambiguous" routing situations. See issue http-rs#12 and issue http-rs#141 for more details.
This commit reworks Tide, with the goal of reducing the total number of concepts in the framework. The key new idea is to remove the notion of `Extractor`s, which in turn allows us to remove or simplify several other concepts in Tide. We'll first lay out the new design, and then discuss the tradeoffs made in this simplification. Here's a full list of the concepts in Tide after this commit: | Concept | Description | | ----- | ----------- | | `App` | Builder for Tide applications | | `Route` | Builder for an individual route | | `Endpoint` | Trait for actual endpoints | | `Context` | The request context for an endpoint | | `IntoResponse` | A trait for converting into a `Response` | | `Middleware` | A trait for Tide middleware | Previously, the `Endpoint` trait was treated as a somewhat magical internal abstraction, and we used a macro to provide `Endpoint` implementations for actual endpoints (with varying numbers of extractor arguments). In this commit, an `Endpoint` is just an asynchronous function from a `Context` to a `Response`: ```rust pub trait Endpoint<AppData>: Send + Sync + 'static { /// The async result of `call`. type Fut: Future<Output = Response> + Send + 'static; /// Invoke the endpoint. fn call(&self, cx: Context<AppData>) -> Self::Fut; } ``` For convenience, this trait is implemented for async functions that return any value that implements `IntoResponse`: ```rust impl<AppData, F, Fut> Endpoint<AppData> for F where F: Fn(Context<AppData>) -> Fut, Fut: Future Fut::Output: IntoResponse, // ... ``` This implementation is in contrast to the macro-generated implementations we previously had, which allowed endpoints with varying numbers of `Extractor` arguments. The intent is for endpoints to perform their own extraction directly on the `Context`, as we'll see next. The `Context` type contains all of the request and middleware context an endpoint operates on. You can think of it as wrapping an `http_service::Request` with some additional data. It's easiest to understand `Context` through the APIs it provides. First, we have methods for getting basic http request information, mirroring the `http` APIs: ```rust impl<AppData> Context<AppData> { pub fn method(&self) -> &Method; pub fn uri(&self) -> &Uri; pub fn version(&self) -> Version; pub fn headers(&self) -> &HeaderMap; } ``` The context also has a handle to application data, which typically would store database connection pools and other "application-global" state. This API replaces the old `AppData` extractor: ```rust impl<AppData> Context<AppData> { pub fn app_data(&self) -> &AppData { &self.app_data } } ``` Similarly, we provide a *direct* API for extracting any "route parameters" (i.e. placeholders in the route URL), replacing the need for `NamedSegment` and the like: ```rust impl<AppData> Context<AppData> { pub fn route_param(&self, key: &str) -> Option<&str>; } ``` Basic body extraction is likewise built in via `Context` methods, replacing the `Str`, `Bytes`, and `Json` extractors: ```rust impl<AppData> Context<AppData> { pub async fn body_bytes(&mut self) -> std::io::Result<Vec<u8>>; pub async fn body_string(&mut self) -> std::io::Result<String>; pub async fn body_json<T: serde::de::DeserializeOwned>(&mut self) -> std::io::Result<T>; } ``` Looking at the [message database example](https://github.com/rustasync/tide/blob/master/examples/messages.rs#L44), we previously had endpoints like this: ```rust async fn new_message(mut db: AppData<Database>, msg: body::Json<Message>) -> String { db.insert(msg.clone()).to_string() } async fn set_message( mut db: AppData<Database>, id: head::Path<usize>, msg: body::Json<Message>, ) -> Result<(), StatusCode> { if db.set(*id, msg.clone()) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` These endpoints would now be written something like this (where `Error` is intended as a general error type, convertible into a response): ```rust async fn new_message(cx: Context<Database>) -> Result<String, Error> { let msg = await!(cx.body_json())?; cx.app_data().insert(msg).to_string() } async fn set_message(cx: Context<Database>) -> Result<(), Error> { let msg = await!(cx.body_json())?; if cx.app_data().set(cx.route_param("id"), msg) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` The endpoint code is a bit more verbose, but also arguably easier to follow, since the extraction (and error handling) is more clear. In addition, the basic extraction approach is *more discoverable*, since it operates via normal methods on `Context`. Part of the idea of the old `Extractor` trait was that Tide would provide an *extensible* system of extractors; you could always introduce new types that implement `Extractor`. But now most of the existing extractors are built-in `Context` methods. How do we recover extensibility? Easy: we use Rust's ability to extend existing types with new methods, via traits! (Note: this is directly inspired by the Gotham framework). Let's say we want to provide cookie extraction. Previously, we'd have a `Cookies` type that you could use as an endpoint argument for extraction. Now, instead, we can introduce a `Cookies` *trait* that's used to extend `Context` with new APIs: ```rust trait Cookies { fn cookies(&self) -> CookieJar; } impl<AppData> Cookies for Context<AppData> { ... } ``` This pattern is called an "extension trait" -- a trait whose sole purpose is to extend an existing type with new methods. There are several nice properties of this approach: - The resulting extraction API is just a direct and natural as the built-in ones: just a method call on the `Context` object. - The methods that are available on `Context` are controlled by what traits are in scope. In other words, if you want to use a custom extractor from the ecosystem, you just bring its trait into scope, and then the method is available. That makes it easy to build a robust ecosystem around a small set of core Tide APIs. One of the major benefits of moving extraction into the endpoint body, rather than via `Extractor` arguments, is that it's much simpler to provide configuration. For example, we could easily provide a customized json body extractor that limited the maximum size of the body or other such options: ```rust impl<AppData> Context<AppData> { pub async fn body_json_cfg<T: serde::de::DeserializeOwned>(&mut self, cfg: JsonConfig) -> std::io::Result<T>; } ``` As a result, we can drop much of the complexity in `App` around configuration. Following the spirit of the changes to extractors, response generation for non-standard Rust types is now just done via a free function: ```rust mod response { pub fn json<T: serde::Serialize>(t: T) -> Response { ... } } ``` As before, there's a top-level `App` type for building up a Tide application. However, the API has been drastically simplified: - It no longer provides a configuration system, since extractors can now be configured directly. - It no longer allows for the middleware list to be customized per route; instead, middleware is set up only at the top level. These simplifications make the programming model much easier to understand; previously, there were inconsistencies between the way that middleware nesting and configuration nesting worked. The hope is that we can get away with this much simpler, top-level model. When actually adding routes via `at`, you get a `Route` object (which used to be `Resource`). This object now provides a *builder-style* API for adding endpoints, allowing you to chain several endpoints. Altogether, this means we can drop nested routing as well. The middleware trait is more or less as it was before, adjusted to use `Context` objects and otherwise slightly cleaned up. This commit also switches to using the route-recognizer crate, rather than the path-table crate, as the underlying routing mechanism. In addition to being more efficient, route-recognizer provides a more intuitive semantics for "ambiguous" routing situations. See issue http-rs#12 and issue http-rs#141 for more details.
This commit reworks Tide, with the goal of reducing the total number of concepts in the framework. The key new idea is to remove the notion of `Extractor`s, which in turn allows us to remove or simplify several other concepts in Tide. We'll first lay out the new design, and then discuss the tradeoffs made in this simplification. Here's a full list of the concepts in Tide after this commit: | Concept | Description | | ----- | ----------- | | `App` | Builder for Tide applications | | `Route` | Builder for an individual route | | `Endpoint` | Trait for actual endpoints | | `Context` | The request context for an endpoint | | `IntoResponse` | A trait for converting into a `Response` | | `Middleware` | A trait for Tide middleware | Previously, the `Endpoint` trait was treated as a somewhat magical internal abstraction, and we used a macro to provide `Endpoint` implementations for actual endpoints (with varying numbers of extractor arguments). In this commit, an `Endpoint` is just an asynchronous function from a `Context` to a `Response`: ```rust pub trait Endpoint<AppData>: Send + Sync + 'static { /// The async result of `call`. type Fut: Future<Output = Response> + Send + 'static; /// Invoke the endpoint. fn call(&self, cx: Context<AppData>) -> Self::Fut; } ``` For convenience, this trait is implemented for async functions that return any value that implements `IntoResponse`: ```rust impl<AppData, F, Fut> Endpoint<AppData> for F where F: Fn(Context<AppData>) -> Fut, Fut: Future Fut::Output: IntoResponse, // ... ``` This implementation is in contrast to the macro-generated implementations we previously had, which allowed endpoints with varying numbers of `Extractor` arguments. The intent is for endpoints to perform their own extraction directly on the `Context`, as we'll see next. The `Context` type contains all of the request and middleware context an endpoint operates on. You can think of it as wrapping an `http_service::Request` with some additional data. It's easiest to understand `Context` through the APIs it provides. First, we have methods for getting basic http request information, mirroring the `http` APIs: ```rust impl<AppData> Context<AppData> { pub fn method(&self) -> &Method; pub fn uri(&self) -> &Uri; pub fn version(&self) -> Version; pub fn headers(&self) -> &HeaderMap; } ``` The context also has a handle to application data, which typically would store database connection pools and other "application-global" state. This API replaces the old `AppData` extractor: ```rust impl<AppData> Context<AppData> { pub fn app_data(&self) -> &AppData { &self.app_data } } ``` Similarly, we provide a *direct* API for extracting any "route parameters" (i.e. placeholders in the route URL), replacing the need for `NamedSegment` and the like: ```rust impl<AppData> Context<AppData> { pub fn route_param(&self, key: &str) -> Option<&str>; } ``` Basic body extraction is likewise built in via `Context` methods, replacing the `Str`, `Bytes`, and `Json` extractors: ```rust impl<AppData> Context<AppData> { pub async fn body_bytes(&mut self) -> std::io::Result<Vec<u8>>; pub async fn body_string(&mut self) -> std::io::Result<String>; pub async fn body_json<T: serde::de::DeserializeOwned>(&mut self) -> std::io::Result<T>; } ``` Looking at the [message database example](https://github.com/rustasync/tide/blob/master/examples/messages.rs#L44), we previously had endpoints like this: ```rust async fn new_message(mut db: AppData<Database>, msg: body::Json<Message>) -> String { db.insert(msg.clone()).to_string() } async fn set_message( mut db: AppData<Database>, id: head::Path<usize>, msg: body::Json<Message>, ) -> Result<(), StatusCode> { if db.set(*id, msg.clone()) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` These endpoints would now be written something like this (where `Error` is intended as a general error type, convertible into a response): ```rust async fn new_message(cx: Context<Database>) -> Result<String, Error> { let msg = await!(cx.body_json())?; cx.app_data().insert(msg).to_string() } async fn set_message(cx: Context<Database>) -> Result<(), Error> { let msg = await!(cx.body_json())?; if cx.app_data().set(cx.route_param("id"), msg) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` The endpoint code is a bit more verbose, but also arguably easier to follow, since the extraction (and error handling) is more clear. In addition, the basic extraction approach is *more discoverable*, since it operates via normal methods on `Context`. Part of the idea of the old `Extractor` trait was that Tide would provide an *extensible* system of extractors; you could always introduce new types that implement `Extractor`. But now most of the existing extractors are built-in `Context` methods. How do we recover extensibility? Easy: we use Rust's ability to extend existing types with new methods, via traits! (Note: this is directly inspired by the Gotham framework). Let's say we want to provide cookie extraction. Previously, we'd have a `Cookies` type that you could use as an endpoint argument for extraction. Now, instead, we can introduce a `Cookies` *trait* that's used to extend `Context` with new APIs: ```rust trait Cookies { fn cookies(&self) -> CookieJar; } impl<AppData> Cookies for Context<AppData> { ... } ``` This pattern is called an "extension trait" -- a trait whose sole purpose is to extend an existing type with new methods. There are several nice properties of this approach: - The resulting extraction API is just a direct and natural as the built-in ones: just a method call on the `Context` object. - The methods that are available on `Context` are controlled by what traits are in scope. In other words, if you want to use a custom extractor from the ecosystem, you just bring its trait into scope, and then the method is available. That makes it easy to build a robust ecosystem around a small set of core Tide APIs. One of the major benefits of moving extraction into the endpoint body, rather than via `Extractor` arguments, is that it's much simpler to provide configuration. For example, we could easily provide a customized json body extractor that limited the maximum size of the body or other such options: ```rust impl<AppData> Context<AppData> { pub async fn body_json_cfg<T: serde::de::DeserializeOwned>(&mut self, cfg: JsonConfig) -> std::io::Result<T>; } ``` As a result, we can drop much of the complexity in `App` around configuration. Following the spirit of the changes to extractors, response generation for non-standard Rust types is now just done via a free function: ```rust mod response { pub fn json<T: serde::Serialize>(t: T) -> Response { ... } } ``` As before, there's a top-level `App` type for building up a Tide application. However, the API has been drastically simplified: - It no longer provides a configuration system, since extractors can now be configured directly. - It no longer allows for the middleware list to be customized per route; instead, middleware is set up only at the top level. These simplifications make the programming model much easier to understand; previously, there were inconsistencies between the way that middleware nesting and configuration nesting worked. The hope is that we can get away with this much simpler, top-level model. When actually adding routes via `at`, you get a `Route` object (which used to be `Resource`). This object now provides a *builder-style* API for adding endpoints, allowing you to chain several endpoints. Altogether, this means we can drop nested routing as well. The middleware trait is more or less as it was before, adjusted to use `Context` objects and otherwise slightly cleaned up. This commit also switches to using the route-recognizer crate, rather than the path-table crate, as the underlying routing mechanism. In addition to being more efficient, route-recognizer provides a more intuitive semantics for "ambiguous" routing situations. See issue http-rs#12 and issue http-rs#141 for more details.
This commit reworks Tide, with the goal of reducing the total number of concepts in the framework. The key new idea is to remove the notion of `Extractor`s, which in turn allows us to remove or simplify several other concepts in Tide. We'll first lay out the new design, and then discuss the tradeoffs made in this simplification. Here's a full list of the concepts in Tide after this commit: | Concept | Description | | ----- | ----------- | | `App` | Builder for Tide applications | | `Route` | Builder for an individual route | | `Endpoint` | Trait for actual endpoints | | `Context` | The request context for an endpoint | | `IntoResponse` | A trait for converting into a `Response` | | `Middleware` | A trait for Tide middleware | Previously, the `Endpoint` trait was treated as a somewhat magical internal abstraction, and we used a macro to provide `Endpoint` implementations for actual endpoints (with varying numbers of extractor arguments). In this commit, an `Endpoint` is just an asynchronous function from a `Context` to a `Response`: ```rust pub trait Endpoint<AppData>: Send + Sync + 'static { /// The async result of `call`. type Fut: Future<Output = Response> + Send + 'static; /// Invoke the endpoint. fn call(&self, cx: Context<AppData>) -> Self::Fut; } ``` For convenience, this trait is implemented for async functions that return any value that implements `IntoResponse`: ```rust impl<AppData, F, Fut> Endpoint<AppData> for F where F: Fn(Context<AppData>) -> Fut, Fut: Future Fut::Output: IntoResponse, // ... ``` This implementation is in contrast to the macro-generated implementations we previously had, which allowed endpoints with varying numbers of `Extractor` arguments. The intent is for endpoints to perform their own extraction directly on the `Context`, as we'll see next. The `Context` type contains all of the request and middleware context an endpoint operates on. You can think of it as wrapping an `http_service::Request` with some additional data. It's easiest to understand `Context` through the APIs it provides. First, we have methods for getting basic http request information, mirroring the `http` APIs: ```rust impl<AppData> Context<AppData> { pub fn method(&self) -> &Method; pub fn uri(&self) -> &Uri; pub fn version(&self) -> Version; pub fn headers(&self) -> &HeaderMap; } ``` The context also has a handle to application data, which typically would store database connection pools and other "application-global" state. This API replaces the old `AppData` extractor: ```rust impl<AppData> Context<AppData> { pub fn app_data(&self) -> &AppData { &self.app_data } } ``` Similarly, we provide a *direct* API for extracting any "route parameters" (i.e. placeholders in the route URL), replacing the need for `NamedSegment` and the like: ```rust impl<AppData> Context<AppData> { pub fn route_param(&self, key: &str) -> Option<&str>; } ``` Basic body extraction is likewise built in via `Context` methods, replacing the `Str`, `Bytes`, and `Json` extractors: ```rust impl<AppData> Context<AppData> { pub async fn body_bytes(&mut self) -> std::io::Result<Vec<u8>>; pub async fn body_string(&mut self) -> std::io::Result<String>; pub async fn body_json<T: serde::de::DeserializeOwned>(&mut self) -> std::io::Result<T>; } ``` Looking at the [message database example](https://github.com/rustasync/tide/blob/master/examples/messages.rs#L44), we previously had endpoints like this: ```rust async fn new_message(mut db: AppData<Database>, msg: body::Json<Message>) -> String { db.insert(msg.clone()).to_string() } async fn set_message( mut db: AppData<Database>, id: head::Path<usize>, msg: body::Json<Message>, ) -> Result<(), StatusCode> { if db.set(*id, msg.clone()) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` These endpoints would now be written something like this (where `Error` is intended as a general error type, convertible into a response): ```rust async fn new_message(cx: Context<Database>) -> Result<String, Error> { let msg = await!(cx.body_json())?; cx.app_data().insert(msg).to_string() } async fn set_message(cx: Context<Database>) -> Result<(), Error> { let msg = await!(cx.body_json())?; if cx.app_data().set(cx.route_param("id"), msg) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` The endpoint code is a bit more verbose, but also arguably easier to follow, since the extraction (and error handling) is more clear. In addition, the basic extraction approach is *more discoverable*, since it operates via normal methods on `Context`. Part of the idea of the old `Extractor` trait was that Tide would provide an *extensible* system of extractors; you could always introduce new types that implement `Extractor`. But now most of the existing extractors are built-in `Context` methods. How do we recover extensibility? Easy: we use Rust's ability to extend existing types with new methods, via traits! (Note: this is directly inspired by the Gotham framework). Let's say we want to provide cookie extraction. Previously, we'd have a `Cookies` type that you could use as an endpoint argument for extraction. Now, instead, we can introduce a `Cookies` *trait* that's used to extend `Context` with new APIs: ```rust trait Cookies { fn cookies(&self) -> CookieJar; } impl<AppData> Cookies for Context<AppData> { ... } ``` This pattern is called an "extension trait" -- a trait whose sole purpose is to extend an existing type with new methods. There are several nice properties of this approach: - The resulting extraction API is just a direct and natural as the built-in ones: just a method call on the `Context` object. - The methods that are available on `Context` are controlled by what traits are in scope. In other words, if you want to use a custom extractor from the ecosystem, you just bring its trait into scope, and then the method is available. That makes it easy to build a robust ecosystem around a small set of core Tide APIs. One of the major benefits of moving extraction into the endpoint body, rather than via `Extractor` arguments, is that it's much simpler to provide configuration. For example, we could easily provide a customized json body extractor that limited the maximum size of the body or other such options: ```rust impl<AppData> Context<AppData> { pub async fn body_json_cfg<T: serde::de::DeserializeOwned>(&mut self, cfg: JsonConfig) -> std::io::Result<T>; } ``` As a result, we can drop much of the complexity in `App` around configuration. Following the spirit of the changes to extractors, response generation for non-standard Rust types is now just done via a free function: ```rust mod response { pub fn json<T: serde::Serialize>(t: T) -> Response { ... } } ``` As before, there's a top-level `App` type for building up a Tide application. However, the API has been drastically simplified: - It no longer provides a configuration system, since extractors can now be configured directly. - It no longer allows for the middleware list to be customized per route; instead, middleware is set up only at the top level. These simplifications make the programming model much easier to understand; previously, there were inconsistencies between the way that middleware nesting and configuration nesting worked. The hope is that we can get away with this much simpler, top-level model. When actually adding routes via `at`, you get a `Route` object (which used to be `Resource`). This object now provides a *builder-style* API for adding endpoints, allowing you to chain several endpoints. Altogether, this means we can drop nested routing as well. The middleware trait is more or less as it was before, adjusted to use `Context` objects and otherwise slightly cleaned up. This commit also switches to using the route-recognizer crate, rather than the path-table crate, as the underlying routing mechanism. In addition to being more efficient, route-recognizer provides a more intuitive semantics for "ambiguous" routing situations. See issue http-rs#12 and issue http-rs#141 for more details.
This commit reworks Tide, with the goal of reducing the total number of concepts in the framework. The key new idea is to remove the notion of `Extractor`s, which in turn allows us to remove or simplify several other concepts in Tide. We'll first lay out the new design, and then discuss the tradeoffs made in this simplification. Here's a full list of the concepts in Tide after this commit: | Concept | Description | | ----- | ----------- | | `App` | Builder for Tide applications | | `Route` | Builder for an individual route | | `Endpoint` | Trait for actual endpoints | | `Context` | The request context for an endpoint | | `IntoResponse` | A trait for converting into a `Response` | | `Middleware` | A trait for Tide middleware | Previously, the `Endpoint` trait was treated as a somewhat magical internal abstraction, and we used a macro to provide `Endpoint` implementations for actual endpoints (with varying numbers of extractor arguments). In this commit, an `Endpoint` is just an asynchronous function from a `Context` to a `Response`: ```rust pub trait Endpoint<AppData>: Send + Sync + 'static { /// The async result of `call`. type Fut: Future<Output = Response> + Send + 'static; /// Invoke the endpoint. fn call(&self, cx: Context<AppData>) -> Self::Fut; } ``` For convenience, this trait is implemented for async functions that return any value that implements `IntoResponse`: ```rust impl<AppData, F, Fut> Endpoint<AppData> for F where F: Fn(Context<AppData>) -> Fut, Fut: Future Fut::Output: IntoResponse, // ... ``` This implementation is in contrast to the macro-generated implementations we previously had, which allowed endpoints with varying numbers of `Extractor` arguments. The intent is for endpoints to perform their own extraction directly on the `Context`, as we'll see next. The `Context` type contains all of the request and middleware context an endpoint operates on. You can think of it as wrapping an `http_service::Request` with some additional data. It's easiest to understand `Context` through the APIs it provides. First, we have methods for getting basic http request information, mirroring the `http` APIs: ```rust impl<AppData> Context<AppData> { pub fn method(&self) -> &Method; pub fn uri(&self) -> &Uri; pub fn version(&self) -> Version; pub fn headers(&self) -> &HeaderMap; } ``` The context also has a handle to application data, which typically would store database connection pools and other "application-global" state. This API replaces the old `AppData` extractor: ```rust impl<AppData> Context<AppData> { pub fn app_data(&self) -> &AppData { &self.app_data } } ``` Similarly, we provide a *direct* API for extracting any "route parameters" (i.e. placeholders in the route URL), replacing the need for `NamedSegment` and the like: ```rust impl<AppData> Context<AppData> { pub fn route_param(&self, key: &str) -> Option<&str>; } ``` Basic body extraction is likewise built in via `Context` methods, replacing the `Str`, `Bytes`, and `Json` extractors: ```rust impl<AppData> Context<AppData> { pub async fn body_bytes(&mut self) -> std::io::Result<Vec<u8>>; pub async fn body_string(&mut self) -> std::io::Result<String>; pub async fn body_json<T: serde::de::DeserializeOwned>(&mut self) -> std::io::Result<T>; } ``` Looking at the [message database example](https://github.com/rustasync/tide/blob/master/examples/messages.rs#L44), we previously had endpoints like this: ```rust async fn new_message(mut db: AppData<Database>, msg: body::Json<Message>) -> String { db.insert(msg.clone()).to_string() } async fn set_message( mut db: AppData<Database>, id: head::Path<usize>, msg: body::Json<Message>, ) -> Result<(), StatusCode> { if db.set(*id, msg.clone()) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` These endpoints would now be written something like this (where `Error` is intended as a general error type, convertible into a response): ```rust async fn new_message(cx: Context<Database>) -> Result<String, Error> { let msg = await!(cx.body_json())?; cx.app_data().insert(msg).to_string() } async fn set_message(cx: Context<Database>) -> Result<(), Error> { let msg = await!(cx.body_json())?; if cx.app_data().set(cx.route_param("id"), msg) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` The endpoint code is a bit more verbose, but also arguably easier to follow, since the extraction (and error handling) is more clear. In addition, the basic extraction approach is *more discoverable*, since it operates via normal methods on `Context`. Part of the idea of the old `Extractor` trait was that Tide would provide an *extensible* system of extractors; you could always introduce new types that implement `Extractor`. But now most of the existing extractors are built-in `Context` methods. How do we recover extensibility? Easy: we use Rust's ability to extend existing types with new methods, via traits! (Note: this is directly inspired by the Gotham framework). Let's say we want to provide cookie extraction. Previously, we'd have a `Cookies` type that you could use as an endpoint argument for extraction. Now, instead, we can introduce a `Cookies` *trait* that's used to extend `Context` with new APIs: ```rust trait Cookies { fn cookies(&self) -> CookieJar; } impl<AppData> Cookies for Context<AppData> { ... } ``` This pattern is called an "extension trait" -- a trait whose sole purpose is to extend an existing type with new methods. There are several nice properties of this approach: - The resulting extraction API is just a direct and natural as the built-in ones: just a method call on the `Context` object. - The methods that are available on `Context` are controlled by what traits are in scope. In other words, if you want to use a custom extractor from the ecosystem, you just bring its trait into scope, and then the method is available. That makes it easy to build a robust ecosystem around a small set of core Tide APIs. One of the major benefits of moving extraction into the endpoint body, rather than via `Extractor` arguments, is that it's much simpler to provide configuration. For example, we could easily provide a customized json body extractor that limited the maximum size of the body or other such options: ```rust impl<AppData> Context<AppData> { pub async fn body_json_cfg<T: serde::de::DeserializeOwned>(&mut self, cfg: JsonConfig) -> std::io::Result<T>; } ``` As a result, we can drop much of the complexity in `App` around configuration. Following the spirit of the changes to extractors, response generation for non-standard Rust types is now just done via a free function: ```rust mod response { pub fn json<T: serde::Serialize>(t: T) -> Response { ... } } ``` As before, there's a top-level `App` type for building up a Tide application. However, the API has been drastically simplified: - It no longer provides a configuration system, since extractors can now be configured directly. - It no longer allows for the middleware list to be customized per route; instead, middleware is set up only at the top level. These simplifications make the programming model much easier to understand; previously, there were inconsistencies between the way that middleware nesting and configuration nesting worked. The hope is that we can get away with this much simpler, top-level model. When actually adding routes via `at`, you get a `Route` object (which used to be `Resource`). This object now provides a *builder-style* API for adding endpoints, allowing you to chain several endpoints. Altogether, this means we can drop nested routing as well. The middleware trait is more or less as it was before, adjusted to use `Context` objects and otherwise slightly cleaned up. This commit also switches to using the route-recognizer crate, rather than the path-table crate, as the underlying routing mechanism. In addition to being more efficient, route-recognizer provides a more intuitive semantics for "ambiguous" routing situations. See issue http-rs#12 and issue http-rs#141 for more details.
This commit reworks Tide, with the goal of reducing the total number of concepts in the framework. The key new idea is to remove the notion of `Extractor`s, which in turn allows us to remove or simplify several other concepts in Tide. We'll first lay out the new design, and then discuss the tradeoffs made in this simplification. Here's a full list of the concepts in Tide after this commit: | Concept | Description | | ----- | ----------- | | `App` | Builder for Tide applications | | `Route` | Builder for an individual route | | `Endpoint` | Trait for actual endpoints | | `Context` | The request context for an endpoint | | `IntoResponse` | A trait for converting into a `Response` | | `Middleware` | A trait for Tide middleware | Previously, the `Endpoint` trait was treated as a somewhat magical internal abstraction, and we used a macro to provide `Endpoint` implementations for actual endpoints (with varying numbers of extractor arguments). In this commit, an `Endpoint` is just an asynchronous function from a `Context` to a `Response`: ```rust pub trait Endpoint<AppData>: Send + Sync + 'static { /// The async result of `call`. type Fut: Future<Output = Response> + Send + 'static; /// Invoke the endpoint. fn call(&self, cx: Context<AppData>) -> Self::Fut; } ``` For convenience, this trait is implemented for async functions that return any value that implements `IntoResponse`: ```rust impl<AppData, F, Fut> Endpoint<AppData> for F where F: Fn(Context<AppData>) -> Fut, Fut: Future Fut::Output: IntoResponse, // ... ``` This implementation is in contrast to the macro-generated implementations we previously had, which allowed endpoints with varying numbers of `Extractor` arguments. The intent is for endpoints to perform their own extraction directly on the `Context`, as we'll see next. The `Context` type contains all of the request and middleware context an endpoint operates on. You can think of it as wrapping an `http_service::Request` with some additional data. It's easiest to understand `Context` through the APIs it provides. First, we have methods for getting basic http request information, mirroring the `http` APIs: ```rust impl<AppData> Context<AppData> { pub fn method(&self) -> &Method; pub fn uri(&self) -> &Uri; pub fn version(&self) -> Version; pub fn headers(&self) -> &HeaderMap; } ``` The context also has a handle to application data, which typically would store database connection pools and other "application-global" state. This API replaces the old `AppData` extractor: ```rust impl<AppData> Context<AppData> { pub fn app_data(&self) -> &AppData { &self.app_data } } ``` Similarly, we provide a *direct* API for extracting any "route parameters" (i.e. placeholders in the route URL), replacing the need for `NamedSegment` and the like: ```rust impl<AppData> Context<AppData> { pub fn route_param(&self, key: &str) -> Option<&str>; } ``` Basic body extraction is likewise built in via `Context` methods, replacing the `Str`, `Bytes`, and `Json` extractors: ```rust impl<AppData> Context<AppData> { pub async fn body_bytes(&mut self) -> std::io::Result<Vec<u8>>; pub async fn body_string(&mut self) -> std::io::Result<String>; pub async fn body_json<T: serde::de::DeserializeOwned>(&mut self) -> std::io::Result<T>; } ``` Looking at the [message database example](https://github.com/rustasync/tide/blob/master/examples/messages.rs#L44), we previously had endpoints like this: ```rust async fn new_message(mut db: AppData<Database>, msg: body::Json<Message>) -> String { db.insert(msg.clone()).to_string() } async fn set_message( mut db: AppData<Database>, id: head::Path<usize>, msg: body::Json<Message>, ) -> Result<(), StatusCode> { if db.set(*id, msg.clone()) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` These endpoints would now be written something like this (where `Error` is intended as a general error type, convertible into a response): ```rust async fn new_message(cx: Context<Database>) -> Result<String, Error> { let msg = await!(cx.body_json())?; cx.app_data().insert(msg).to_string() } async fn set_message(cx: Context<Database>) -> Result<(), Error> { let msg = await!(cx.body_json())?; if cx.app_data().set(cx.route_param("id"), msg) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` The endpoint code is a bit more verbose, but also arguably easier to follow, since the extraction (and error handling) is more clear. In addition, the basic extraction approach is *more discoverable*, since it operates via normal methods on `Context`. Part of the idea of the old `Extractor` trait was that Tide would provide an *extensible* system of extractors; you could always introduce new types that implement `Extractor`. But now most of the existing extractors are built-in `Context` methods. How do we recover extensibility? Easy: we use Rust's ability to extend existing types with new methods, via traits! (Note: this is directly inspired by the Gotham framework). Let's say we want to provide cookie extraction. Previously, we'd have a `Cookies` type that you could use as an endpoint argument for extraction. Now, instead, we can introduce a `Cookies` *trait* that's used to extend `Context` with new APIs: ```rust trait Cookies { fn cookies(&self) -> CookieJar; } impl<AppData> Cookies for Context<AppData> { ... } ``` This pattern is called an "extension trait" -- a trait whose sole purpose is to extend an existing type with new methods. There are several nice properties of this approach: - The resulting extraction API is just a direct and natural as the built-in ones: just a method call on the `Context` object. - The methods that are available on `Context` are controlled by what traits are in scope. In other words, if you want to use a custom extractor from the ecosystem, you just bring its trait into scope, and then the method is available. That makes it easy to build a robust ecosystem around a small set of core Tide APIs. One of the major benefits of moving extraction into the endpoint body, rather than via `Extractor` arguments, is that it's much simpler to provide configuration. For example, we could easily provide a customized json body extractor that limited the maximum size of the body or other such options: ```rust impl<AppData> Context<AppData> { pub async fn body_json_cfg<T: serde::de::DeserializeOwned>(&mut self, cfg: JsonConfig) -> std::io::Result<T>; } ``` As a result, we can drop much of the complexity in `App` around configuration. Following the spirit of the changes to extractors, response generation for non-standard Rust types is now just done via a free function: ```rust mod response { pub fn json<T: serde::Serialize>(t: T) -> Response { ... } } ``` As before, there's a top-level `App` type for building up a Tide application. However, the API has been drastically simplified: - It no longer provides a configuration system, since extractors can now be configured directly. - It no longer allows for the middleware list to be customized per route; instead, middleware is set up only at the top level. These simplifications make the programming model much easier to understand; previously, there were inconsistencies between the way that middleware nesting and configuration nesting worked. The hope is that we can get away with this much simpler, top-level model. When actually adding routes via `at`, you get a `Route` object (which used to be `Resource`). This object now provides a *builder-style* API for adding endpoints, allowing you to chain several endpoints. Altogether, this means we can drop nested routing as well. The middleware trait is more or less as it was before, adjusted to use `Context` objects and otherwise slightly cleaned up. This commit also switches to using the route-recognizer crate, rather than the path-table crate, as the underlying routing mechanism. In addition to being more efficient, route-recognizer provides a more intuitive semantics for "ambiguous" routing situations. See issue http-rs#12 and issue http-rs#141 for more details.
This commit reworks Tide, with the goal of reducing the total number of concepts in the framework. The key new idea is to remove the notion of `Extractor`s, which in turn allows us to remove or simplify several other concepts in Tide. We'll first lay out the new design, and then discuss the tradeoffs made in this simplification. Here's a full list of the concepts in Tide after this commit: | Concept | Description | | ----- | ----------- | | `App` | Builder for Tide applications | | `Route` | Builder for an individual route | | `Endpoint` | Trait for actual endpoints | | `Context` | The request context for an endpoint | | `IntoResponse` | A trait for converting into a `Response` | | `Middleware` | A trait for Tide middleware | Previously, the `Endpoint` trait was treated as a somewhat magical internal abstraction, and we used a macro to provide `Endpoint` implementations for actual endpoints (with varying numbers of extractor arguments). In this commit, an `Endpoint` is just an asynchronous function from a `Context` to a `Response`: ```rust pub trait Endpoint<AppData>: Send + Sync + 'static { /// The async result of `call`. type Fut: Future<Output = Response> + Send + 'static; /// Invoke the endpoint. fn call(&self, cx: Context<AppData>) -> Self::Fut; } ``` For convenience, this trait is implemented for async functions that return any value that implements `IntoResponse`: ```rust impl<AppData, F, Fut> Endpoint<AppData> for F where F: Fn(Context<AppData>) -> Fut, Fut: Future Fut::Output: IntoResponse, // ... ``` This implementation is in contrast to the macro-generated implementations we previously had, which allowed endpoints with varying numbers of `Extractor` arguments. The intent is for endpoints to perform their own extraction directly on the `Context`, as we'll see next. The `Context` type contains all of the request and middleware context an endpoint operates on. You can think of it as wrapping an `http_service::Request` with some additional data. It's easiest to understand `Context` through the APIs it provides. First, we have methods for getting basic http request information, mirroring the `http` APIs: ```rust impl<AppData> Context<AppData> { pub fn method(&self) -> &Method; pub fn uri(&self) -> &Uri; pub fn version(&self) -> Version; pub fn headers(&self) -> &HeaderMap; } ``` The context also has a handle to application data, which typically would store database connection pools and other "application-global" state. This API replaces the old `AppData` extractor: ```rust impl<AppData> Context<AppData> { pub fn app_data(&self) -> &AppData { &self.app_data } } ``` Similarly, we provide a *direct* API for extracting any "route parameters" (i.e. placeholders in the route URL), replacing the need for `NamedSegment` and the like: ```rust impl<AppData> Context<AppData> { pub fn route_param(&self, key: &str) -> Option<&str>; } ``` Basic body extraction is likewise built in via `Context` methods, replacing the `Str`, `Bytes`, and `Json` extractors: ```rust impl<AppData> Context<AppData> { pub async fn body_bytes(&mut self) -> std::io::Result<Vec<u8>>; pub async fn body_string(&mut self) -> std::io::Result<String>; pub async fn body_json<T: serde::de::DeserializeOwned>(&mut self) -> std::io::Result<T>; } ``` Looking at the [message database example](https://github.com/rustasync/tide/blob/master/examples/messages.rs#L44), we previously had endpoints like this: ```rust async fn new_message(mut db: AppData<Database>, msg: body::Json<Message>) -> String { db.insert(msg.clone()).to_string() } async fn set_message( mut db: AppData<Database>, id: head::Path<usize>, msg: body::Json<Message>, ) -> Result<(), StatusCode> { if db.set(*id, msg.clone()) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` These endpoints would now be written something like this (where `Error` is intended as a general error type, convertible into a response): ```rust async fn new_message(cx: Context<Database>) -> Result<String, Error> { let msg = await!(cx.body_json())?; cx.app_data().insert(msg).to_string() } async fn set_message(cx: Context<Database>) -> Result<(), Error> { let msg = await!(cx.body_json())?; if cx.app_data().set(cx.route_param("id"), msg) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` The endpoint code is a bit more verbose, but also arguably easier to follow, since the extraction (and error handling) is more clear. In addition, the basic extraction approach is *more discoverable*, since it operates via normal methods on `Context`. Part of the idea of the old `Extractor` trait was that Tide would provide an *extensible* system of extractors; you could always introduce new types that implement `Extractor`. But now most of the existing extractors are built-in `Context` methods. How do we recover extensibility? Easy: we use Rust's ability to extend existing types with new methods, via traits! (Note: this is directly inspired by the Gotham framework). Let's say we want to provide cookie extraction. Previously, we'd have a `Cookies` type that you could use as an endpoint argument for extraction. Now, instead, we can introduce a `Cookies` *trait* that's used to extend `Context` with new APIs: ```rust trait Cookies { fn cookies(&self) -> CookieJar; } impl<AppData> Cookies for Context<AppData> { ... } ``` This pattern is called an "extension trait" -- a trait whose sole purpose is to extend an existing type with new methods. There are several nice properties of this approach: - The resulting extraction API is just a direct and natural as the built-in ones: just a method call on the `Context` object. - The methods that are available on `Context` are controlled by what traits are in scope. In other words, if you want to use a custom extractor from the ecosystem, you just bring its trait into scope, and then the method is available. That makes it easy to build a robust ecosystem around a small set of core Tide APIs. One of the major benefits of moving extraction into the endpoint body, rather than via `Extractor` arguments, is that it's much simpler to provide configuration. For example, we could easily provide a customized json body extractor that limited the maximum size of the body or other such options: ```rust impl<AppData> Context<AppData> { pub async fn body_json_cfg<T: serde::de::DeserializeOwned>(&mut self, cfg: JsonConfig) -> std::io::Result<T>; } ``` As a result, we can drop much of the complexity in `App` around configuration. Following the spirit of the changes to extractors, response generation for non-standard Rust types is now just done via a free function: ```rust mod response { pub fn json<T: serde::Serialize>(t: T) -> Response { ... } } ``` As before, there's a top-level `App` type for building up a Tide application. However, the API has been drastically simplified: - It no longer provides a configuration system, since extractors can now be configured directly. - It no longer allows for the middleware list to be customized per route; instead, middleware is set up only at the top level. These simplifications make the programming model much easier to understand; previously, there were inconsistencies between the way that middleware nesting and configuration nesting worked. The hope is that we can get away with this much simpler, top-level model. When actually adding routes via `at`, you get a `Route` object (which used to be `Resource`). This object now provides a *builder-style* API for adding endpoints, allowing you to chain several endpoints. Altogether, this means we can drop nested routing as well. The middleware trait is more or less as it was before, adjusted to use `Context` objects and otherwise slightly cleaned up. This commit also switches to using the route-recognizer crate, rather than the path-table crate, as the underlying routing mechanism. In addition to being more efficient, route-recognizer provides a more intuitive semantics for "ambiguous" routing situations. See issue http-rs#12 and issue http-rs#141 for more details.
This commit reworks Tide, with the goal of reducing the total number of concepts in the framework. The key new idea is to remove the notion of `Extractor`s, which in turn allows us to remove or simplify several other concepts in Tide. We'll first lay out the new design, and then discuss the tradeoffs made in this simplification. Here's a full list of the concepts in Tide after this commit: | Concept | Description | | ----- | ----------- | | `App` | Builder for Tide applications | | `Route` | Builder for an individual route | | `Endpoint` | Trait for actual endpoints | | `Context` | The request context for an endpoint | | `IntoResponse` | A trait for converting into a `Response` | | `Middleware` | A trait for Tide middleware | Previously, the `Endpoint` trait was treated as a somewhat magical internal abstraction, and we used a macro to provide `Endpoint` implementations for actual endpoints (with varying numbers of extractor arguments). In this commit, an `Endpoint` is just an asynchronous function from a `Context` to a `Response`: ```rust pub trait Endpoint<AppData>: Send + Sync + 'static { /// The async result of `call`. type Fut: Future<Output = Response> + Send + 'static; /// Invoke the endpoint. fn call(&self, cx: Context<AppData>) -> Self::Fut; } ``` For convenience, this trait is implemented for async functions that return any value that implements `IntoResponse`: ```rust impl<AppData, F, Fut> Endpoint<AppData> for F where F: Fn(Context<AppData>) -> Fut, Fut: Future Fut::Output: IntoResponse, // ... ``` This implementation is in contrast to the macro-generated implementations we previously had, which allowed endpoints with varying numbers of `Extractor` arguments. The intent is for endpoints to perform their own extraction directly on the `Context`, as we'll see next. The `Context` type contains all of the request and middleware context an endpoint operates on. You can think of it as wrapping an `http_service::Request` with some additional data. It's easiest to understand `Context` through the APIs it provides. First, we have methods for getting basic http request information, mirroring the `http` APIs: ```rust impl<AppData> Context<AppData> { pub fn method(&self) -> &Method; pub fn uri(&self) -> &Uri; pub fn version(&self) -> Version; pub fn headers(&self) -> &HeaderMap; } ``` The context also has a handle to application data, which typically would store database connection pools and other "application-global" state. This API replaces the old `AppData` extractor: ```rust impl<AppData> Context<AppData> { pub fn app_data(&self) -> &AppData { &self.app_data } } ``` Similarly, we provide a *direct* API for extracting any "route parameters" (i.e. placeholders in the route URL), replacing the need for `NamedSegment` and the like: ```rust impl<AppData> Context<AppData> { pub fn route_param(&self, key: &str) -> Option<&str>; } ``` Basic body extraction is likewise built in via `Context` methods, replacing the `Str`, `Bytes`, and `Json` extractors: ```rust impl<AppData> Context<AppData> { pub async fn body_bytes(&mut self) -> std::io::Result<Vec<u8>>; pub async fn body_string(&mut self) -> std::io::Result<String>; pub async fn body_json<T: serde::de::DeserializeOwned>(&mut self) -> std::io::Result<T>; } ``` Looking at the [message database example](https://github.com/rustasync/tide/blob/master/examples/messages.rs#L44), we previously had endpoints like this: ```rust async fn new_message(mut db: AppData<Database>, msg: body::Json<Message>) -> String { db.insert(msg.clone()).to_string() } async fn set_message( mut db: AppData<Database>, id: head::Path<usize>, msg: body::Json<Message>, ) -> Result<(), StatusCode> { if db.set(*id, msg.clone()) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` These endpoints would now be written something like this (where `Error` is intended as a general error type, convertible into a response): ```rust async fn new_message(cx: Context<Database>) -> Result<String, Error> { let msg = await!(cx.body_json())?; cx.app_data().insert(msg).to_string() } async fn set_message(cx: Context<Database>) -> Result<(), Error> { let msg = await!(cx.body_json())?; if cx.app_data().set(cx.route_param("id"), msg) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` The endpoint code is a bit more verbose, but also arguably easier to follow, since the extraction (and error handling) is more clear. In addition, the basic extraction approach is *more discoverable*, since it operates via normal methods on `Context`. Part of the idea of the old `Extractor` trait was that Tide would provide an *extensible* system of extractors; you could always introduce new types that implement `Extractor`. But now most of the existing extractors are built-in `Context` methods. How do we recover extensibility? Easy: we use Rust's ability to extend existing types with new methods, via traits! (Note: this is directly inspired by the Gotham framework). Let's say we want to provide cookie extraction. Previously, we'd have a `Cookies` type that you could use as an endpoint argument for extraction. Now, instead, we can introduce a `Cookies` *trait* that's used to extend `Context` with new APIs: ```rust trait Cookies { fn cookies(&self) -> CookieJar; } impl<AppData> Cookies for Context<AppData> { ... } ``` This pattern is called an "extension trait" -- a trait whose sole purpose is to extend an existing type with new methods. There are several nice properties of this approach: - The resulting extraction API is just a direct and natural as the built-in ones: just a method call on the `Context` object. - The methods that are available on `Context` are controlled by what traits are in scope. In other words, if you want to use a custom extractor from the ecosystem, you just bring its trait into scope, and then the method is available. That makes it easy to build a robust ecosystem around a small set of core Tide APIs. One of the major benefits of moving extraction into the endpoint body, rather than via `Extractor` arguments, is that it's much simpler to provide configuration. For example, we could easily provide a customized json body extractor that limited the maximum size of the body or other such options: ```rust impl<AppData> Context<AppData> { pub async fn body_json_cfg<T: serde::de::DeserializeOwned>(&mut self, cfg: JsonConfig) -> std::io::Result<T>; } ``` As a result, we can drop much of the complexity in `App` around configuration. Following the spirit of the changes to extractors, response generation for non-standard Rust types is now just done via a free function: ```rust mod response { pub fn json<T: serde::Serialize>(t: T) -> Response { ... } } ``` As before, there's a top-level `App` type for building up a Tide application. However, the API has been drastically simplified: - It no longer provides a configuration system, since extractors can now be configured directly. - It no longer allows for the middleware list to be customized per route; instead, middleware is set up only at the top level. These simplifications make the programming model much easier to understand; previously, there were inconsistencies between the way that middleware nesting and configuration nesting worked. The hope is that we can get away with this much simpler, top-level model. When actually adding routes via `at`, you get a `Route` object (which used to be `Resource`). This object now provides a *builder-style* API for adding endpoints, allowing you to chain several endpoints. Altogether, this means we can drop nested routing as well. The middleware trait is more or less as it was before, adjusted to use `Context` objects and otherwise slightly cleaned up. This commit also switches to using the route-recognizer crate, rather than the path-table crate, as the underlying routing mechanism. In addition to being more efficient, route-recognizer provides a more intuitive semantics for "ambiguous" routing situations. See issue http-rs#12 and issue http-rs#141 for more details.
This commit reworks Tide, with the goal of reducing the total number of concepts in the framework. The key new idea is to remove the notion of `Extractor`s, which in turn allows us to remove or simplify several other concepts in Tide. We'll first lay out the new design, and then discuss the tradeoffs made in this simplification. Here's a full list of the concepts in Tide after this commit: | Concept | Description | | ----- | ----------- | | `App` | Builder for Tide applications | | `Route` | Builder for an individual route | | `Endpoint` | Trait for actual endpoints | | `Context` | The request context for an endpoint | | `IntoResponse` | A trait for converting into a `Response` | | `Middleware` | A trait for Tide middleware | Previously, the `Endpoint` trait was treated as a somewhat magical internal abstraction, and we used a macro to provide `Endpoint` implementations for actual endpoints (with varying numbers of extractor arguments). In this commit, an `Endpoint` is just an asynchronous function from a `Context` to a `Response`: ```rust pub trait Endpoint<AppData>: Send + Sync + 'static { /// The async result of `call`. type Fut: Future<Output = Response> + Send + 'static; /// Invoke the endpoint. fn call(&self, cx: Context<AppData>) -> Self::Fut; } ``` For convenience, this trait is implemented for async functions that return any value that implements `IntoResponse`: ```rust impl<AppData, F, Fut> Endpoint<AppData> for F where F: Fn(Context<AppData>) -> Fut, Fut: Future Fut::Output: IntoResponse, // ... ``` This implementation is in contrast to the macro-generated implementations we previously had, which allowed endpoints with varying numbers of `Extractor` arguments. The intent is for endpoints to perform their own extraction directly on the `Context`, as we'll see next. The `Context` type contains all of the request and middleware context an endpoint operates on. You can think of it as wrapping an `http_service::Request` with some additional data. It's easiest to understand `Context` through the APIs it provides. First, we have methods for getting basic http request information, mirroring the `http` APIs: ```rust impl<AppData> Context<AppData> { pub fn method(&self) -> &Method; pub fn uri(&self) -> &Uri; pub fn version(&self) -> Version; pub fn headers(&self) -> &HeaderMap; } ``` The context also has a handle to application data, which typically would store database connection pools and other "application-global" state. This API replaces the old `AppData` extractor: ```rust impl<AppData> Context<AppData> { pub fn app_data(&self) -> &AppData { &self.app_data } } ``` Similarly, we provide a *direct* API for extracting any "route parameters" (i.e. placeholders in the route URL), replacing the need for `NamedSegment` and the like: ```rust impl<AppData> Context<AppData> { pub fn route_param(&self, key: &str) -> Option<&str>; } ``` Basic body extraction is likewise built in via `Context` methods, replacing the `Str`, `Bytes`, and `Json` extractors: ```rust impl<AppData> Context<AppData> { pub async fn body_bytes(&mut self) -> std::io::Result<Vec<u8>>; pub async fn body_string(&mut self) -> std::io::Result<String>; pub async fn body_json<T: serde::de::DeserializeOwned>(&mut self) -> std::io::Result<T>; } ``` Looking at the [message database example](https://github.com/rustasync/tide/blob/master/examples/messages.rs#L44), we previously had endpoints like this: ```rust async fn new_message(mut db: AppData<Database>, msg: body::Json<Message>) -> String { db.insert(msg.clone()).to_string() } async fn set_message( mut db: AppData<Database>, id: head::Path<usize>, msg: body::Json<Message>, ) -> Result<(), StatusCode> { if db.set(*id, msg.clone()) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` These endpoints would now be written something like this (where `Error` is intended as a general error type, convertible into a response): ```rust async fn new_message(cx: Context<Database>) -> Result<String, Error> { let msg = await!(cx.body_json())?; cx.app_data().insert(msg).to_string() } async fn set_message(cx: Context<Database>) -> Result<(), Error> { let msg = await!(cx.body_json())?; if cx.app_data().set(cx.route_param("id"), msg) { Ok(()) } else { Err(StatusCode::NOT_FOUND) } } ``` The endpoint code is a bit more verbose, but also arguably easier to follow, since the extraction (and error handling) is more clear. In addition, the basic extraction approach is *more discoverable*, since it operates via normal methods on `Context`. Part of the idea of the old `Extractor` trait was that Tide would provide an *extensible* system of extractors; you could always introduce new types that implement `Extractor`. But now most of the existing extractors are built-in `Context` methods. How do we recover extensibility? Easy: we use Rust's ability to extend existing types with new methods, via traits! (Note: this is directly inspired by the Gotham framework). Let's say we want to provide cookie extraction. Previously, we'd have a `Cookies` type that you could use as an endpoint argument for extraction. Now, instead, we can introduce a `Cookies` *trait* that's used to extend `Context` with new APIs: ```rust trait Cookies { fn cookies(&self) -> CookieJar; } impl<AppData> Cookies for Context<AppData> { ... } ``` This pattern is called an "extension trait" -- a trait whose sole purpose is to extend an existing type with new methods. There are several nice properties of this approach: - The resulting extraction API is just a direct and natural as the built-in ones: just a method call on the `Context` object. - The methods that are available on `Context` are controlled by what traits are in scope. In other words, if you want to use a custom extractor from the ecosystem, you just bring its trait into scope, and then the method is available. That makes it easy to build a robust ecosystem around a small set of core Tide APIs. One of the major benefits of moving extraction into the endpoint body, rather than via `Extractor` arguments, is that it's much simpler to provide configuration. For example, we could easily provide a customized json body extractor that limited the maximum size of the body or other such options: ```rust impl<AppData> Context<AppData> { pub async fn body_json_cfg<T: serde::de::DeserializeOwned>(&mut self, cfg: JsonConfig) -> std::io::Result<T>; } ``` As a result, we can drop much of the complexity in `App` around configuration. Following the spirit of the changes to extractors, response generation for non-standard Rust types is now just done via a free function: ```rust mod response { pub fn json<T: serde::Serialize>(t: T) -> Response { ... } } ``` As before, there's a top-level `App` type for building up a Tide application. However, the API has been drastically simplified: - It no longer provides a configuration system, since extractors can now be configured directly. - It no longer allows for the middleware list to be customized per route; instead, middleware is set up only at the top level. These simplifications make the programming model much easier to understand; previously, there were inconsistencies between the way that middleware nesting and configuration nesting worked. The hope is that we can get away with this much simpler, top-level model. When actually adding routes via `at`, you get a `Route` object (which used to be `Resource`). This object now provides a *builder-style* API for adding endpoints, allowing you to chain several endpoints. Altogether, this means we can drop nested routing as well. The middleware trait is more or less as it was before, adjusted to use `Context` objects and otherwise slightly cleaned up. This commit also switches to using the route-recognizer crate, rather than the path-table crate, as the underlying routing mechanism. In addition to being more efficient, route-recognizer provides a more intuitive semantics for "ambiguous" routing situations. See issue http-rs#12 and issue http-rs#141 for more details.
We've now moved to route-recognizer! To finish out this issue, though, we need to fix the route selection order in that crate. I'm working on getting ownership of the crate for the rustasync org so we can do that ourselves. |
I found my way back to this issue and see it is open but blocked at the moment. I've just created PR #254 which relates to this - updating the documentation to match what is actually implemented through route-recognizer. Playing around with the options it does lead to some 'interesting' routes that are supported: async fn echo_empty(cx:Context<()>) -> Result<String, tide::Error> {
let nameless: String = cx.param("").client_err()?;
Ok(nameless)
}
// snip
app.at("/echo/:/:path").get(echo_empty) I don't know if validation can be done of the path - for example maybe to panic if the above syntax is used. The other one that I tried out is a path like |
Note from triage: we want to survey routers in other languages and document their routing rules to decide how to proceed here. |
The initial implementation of the router uses greedy matching for URL segments. Imagine you have two routes:
foo/{}/baz
foo/new/bar
The URL
foo/new/baz
will fail to be routed, because thenew
component will be greedily matched (concrete segments are always preferred over wildcards) and there is no backtracking.This behavior is the simplest to implement, but it's not clear that it's the most obvious behavior. OTOH, it's not clear that the URL should match here. And in general, this kind of routing situation seems like an anti-pattern, and is perhaps something we should detect and disallow.
Thoughts welcome!
The text was updated successfully, but these errors were encountered: