gotham/router/route/
mod.rs

1//! Defines types that support individual application routes.
2//!
3//! The `Router` will identify one or more `Route` instances that match the path of a request, and
4//! iterate to find the first matching `Route` (indicated by `Route::is_match`). The request will
5//! be dispatched to the first `Route` which matches.
6
7pub mod dispatch;
8pub mod matcher;
9
10use std::marker::PhantomData;
11use std::panic::RefUnwindSafe;
12use std::pin::Pin;
13
14use hyper::{Body, Response, Uri};
15use log::debug;
16
17use crate::extractor::{self, PathExtractor, QueryStringExtractor};
18use crate::handler::HandlerFuture;
19use crate::helpers::http::request::query_string;
20use crate::router::non_match::RouteNonMatch;
21use crate::router::route::dispatch::Dispatcher;
22use crate::router::route::matcher::RouteMatcher;
23use crate::router::tree::segment::SegmentMapping;
24use crate::state::{request_id, State};
25
26#[derive(Clone, Copy, Eq, PartialEq)]
27/// Indicates whether this `Route` will dispatch the request to an inner `Router` instance. To
28/// support inner `Router` instances which handle a subtree, the `Dispatcher` stores additional
29/// context information.
30pub enum Delegation {
31    /// This `Route` is dispatching a request to a normal `NewHandler` / `Handler` and does not
32    /// need to store any additional context information.
33    Internal,
34
35    /// This `Route` is dispatching a request to another `Router` which handles a subtree. The path
36    /// segments already consumed by the current `Router` will not be processed again.
37    External,
38}
39
40/// Values of the `Route` type are used by the `Router` to conditionally dispatch a request after
41/// matching the path segments successfully. The steps taken in dispatching to a `Route` are:
42///
43/// 1. Given a list of routes that match the request path, determine the first `Route` which
44///    indicates a match via `Route::is_match`;
45/// 2. Determine whether the route's `Delegation` is `Internal` or `External`. If `External`, halt
46///    processing and dispatch to the inner `Router`;
47/// 3. Run `PathExtractor` and `QueryStringExtractor` logic to popuate `State` with the necessary
48///    request data. If either of these extractors fail, the request is halted here;
49/// 4. Dispatch the request via `Route::dispatch`.
50///
51/// `Route` exists as a trait to allow abstraction over the generic types in `RouteImpl`. This
52/// trait should not be implemented outside of Gotham.
53pub trait Route: RefUnwindSafe {
54    /// The type of the response body. The requirements of Hyper are that this implements `HttpBody`.
55    /// Almost always, it will want to be `hyper::Body`.
56    type ResBody;
57    /// Determines if this `Route` should be invoked, based on the request data in `State.
58    fn is_match(&self, state: &State) -> Result<(), RouteNonMatch>;
59
60    /// Determines if this `Route` intends to delegate requests to a secondary `Router` instance.
61    fn delegation(&self) -> Delegation;
62
63    /// Extracts dynamic components of the `Request` path and stores the `PathExtractor` in `State`.
64    fn extract_request_path<'a>(
65        &self,
66        state: &mut State,
67        params: SegmentMapping<'a>,
68    ) -> Result<(), ExtractorFailed>;
69
70    /// Extends the `Response` object when the `PathExtractor` fails.
71    fn extend_response_on_path_error(&self, state: &mut State, res: &mut Response<Self::ResBody>);
72
73    /// Extracts the query string parameters and stores the `QueryStringExtractor` in `State`.
74    fn extract_query_string(&self, state: &mut State) -> Result<(), ExtractorFailed>;
75
76    /// Extends the `Response` object when query string extraction fails.
77    fn extend_response_on_query_string_error(
78        &self,
79        state: &mut State,
80        res: &mut Response<Self::ResBody>,
81    );
82
83    /// Dispatches the request to this `Route`, which will execute the pipelines and the handler
84    /// assigned to the `Route.
85    fn dispatch(&self, state: State) -> Pin<Box<HandlerFuture>>;
86}
87
88/// Returned in the `Err` variant from `extract_query_string` or `extract_request_path`, this
89/// signals that the extractor has failed and the request should not proceed.
90pub struct ExtractorFailed;
91
92/// Concrete type for a route in a Gotham web application. Values of this type are created by the
93/// `gotham::router::builder` API and held internally in the `Router` for dispatching requests.
94pub struct RouteImpl<RM, PE, QSE>
95where
96    RM: RouteMatcher,
97    PE: PathExtractor<Body>,
98    QSE: QueryStringExtractor<Body>,
99{
100    matcher: RM,
101    dispatcher: Box<dyn Dispatcher + Send + Sync>,
102    _extractors: Extractors<PE, QSE>,
103    delegation: Delegation,
104}
105
106/// Extractors used by `RouteImpl` to acquire request data and change into a type safe form
107/// for use by `Middleware` and `Handler` implementations.
108pub struct Extractors<PE, QSE>
109where
110    PE: PathExtractor<Body>,
111    QSE: QueryStringExtractor<Body>,
112{
113    rpe_phantom: PhantomData<PE>,
114    qse_phantom: PhantomData<QSE>,
115}
116
117impl<RM, PE, QSE> RouteImpl<RM, PE, QSE>
118where
119    RM: RouteMatcher,
120    PE: PathExtractor<Body>,
121    QSE: QueryStringExtractor<Body>,
122{
123    /// Creates a new `RouteImpl` from the provided components.
124    pub fn new(
125        matcher: RM,
126        dispatcher: Box<dyn Dispatcher + Send + Sync>,
127        _extractors: Extractors<PE, QSE>,
128        delegation: Delegation,
129    ) -> Self {
130        RouteImpl {
131            matcher,
132            dispatcher,
133            _extractors,
134            delegation,
135        }
136    }
137}
138
139impl<PE, QSE> Extractors<PE, QSE>
140where
141    PE: PathExtractor<Body>,
142    QSE: QueryStringExtractor<Body>,
143{
144    /// Creates a new set of Extractors for use with a `RouteImpl`
145    pub fn new() -> Self {
146        Extractors {
147            rpe_phantom: PhantomData,
148            qse_phantom: PhantomData,
149        }
150    }
151}
152
153impl<RM, PE, QSE> Route for RouteImpl<RM, PE, QSE>
154where
155    RM: RouteMatcher,
156    PE: PathExtractor<Body>,
157    QSE: QueryStringExtractor<Body>,
158{
159    type ResBody = Body;
160
161    fn is_match(&self, state: &State) -> Result<(), RouteNonMatch> {
162        self.matcher.is_match(state)
163    }
164
165    fn delegation(&self) -> Delegation {
166        self.delegation
167    }
168
169    fn dispatch(&self, state: State) -> Pin<Box<HandlerFuture>> {
170        self.dispatcher.dispatch(state)
171    }
172
173    fn extract_request_path<'a>(
174        &self,
175        state: &mut State,
176        params: SegmentMapping<'a>,
177    ) -> Result<(), ExtractorFailed> {
178        match extractor::internal::from_segment_mapping::<PE>(params) {
179            Ok(val) => Ok(state.put(val)),
180            Err(e) => {
181                debug!("[{}] path extractor failed: {}", request_id(state), e);
182                Err(ExtractorFailed)
183            }
184        }
185    }
186
187    fn extend_response_on_path_error(&self, state: &mut State, res: &mut Response<Self::ResBody>) {
188        PE::extend(state, res)
189    }
190
191    fn extract_query_string(&self, state: &mut State) -> Result<(), ExtractorFailed> {
192        let result: Result<QSE, _> = {
193            let uri = state.borrow::<Uri>();
194            let query_string_mapping = query_string::split(uri.query());
195            extractor::internal::from_query_string_mapping(&query_string_mapping)
196        };
197
198        match result {
199            Ok(val) => Ok(state.put(val)),
200            Err(e) => {
201                debug!(
202                    "[{}] query string extractor failed: {}",
203                    request_id(state),
204                    e
205                );
206                Err(ExtractorFailed)
207            }
208        }
209    }
210
211    fn extend_response_on_query_string_error(
212        &self,
213        state: &mut State,
214        res: &mut Response<Self::ResBody>,
215    ) {
216        QSE::extend(state, res)
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    use futures_util::FutureExt;
225    use hyper::{HeaderMap, Method, StatusCode, Uri};
226    use std::str::FromStr;
227
228    use crate::extractor::{NoopPathExtractor, NoopQueryStringExtractor};
229    use crate::helpers::http::request::path::RequestPathSegments;
230    use crate::helpers::http::response::create_empty_response;
231    use crate::pipeline::{finalize_pipeline_set, new_pipeline_set};
232    use crate::router::builder::*;
233    use crate::router::route::dispatch::DispatcherImpl;
234    use crate::router::route::matcher::MethodOnlyRouteMatcher;
235    use crate::state::set_request_id;
236
237    #[test]
238    fn internal_route_tests() {
239        fn handler(state: State) -> (State, Response<Body>) {
240            let res = create_empty_response(&state, StatusCode::ACCEPTED);
241            (state, res)
242        }
243
244        let pipeline_set = finalize_pipeline_set(new_pipeline_set());
245        let methods = vec![Method::GET];
246        let matcher = MethodOnlyRouteMatcher::new(methods);
247        let dispatcher = Box::new(DispatcherImpl::new(|| Ok(handler), (), pipeline_set));
248        let extractors: Extractors<NoopPathExtractor, NoopQueryStringExtractor> = Extractors::new();
249        let route = RouteImpl::new(matcher, dispatcher, extractors, Delegation::Internal);
250
251        let mut state = State::new();
252        state.put(HeaderMap::new());
253        state.put(Method::GET);
254        set_request_id(&mut state);
255
256        match route.dispatch(state).now_or_never() {
257            Some(Ok((_state, response))) => assert_eq!(response.status(), StatusCode::ACCEPTED),
258            Some(Err((_state, e))) => panic!("error polling future: {:?}", e),
259            None => panic!("expected future to be completed already"),
260        }
261    }
262
263    #[test]
264    fn external_route_tests() {
265        fn handler(state: State) -> (State, Response<Body>) {
266            let res = create_empty_response(&state, StatusCode::ACCEPTED);
267            (state, res)
268        }
269
270        let secondary_router = build_simple_router(|route| {
271            route.get("/").to(handler);
272        });
273
274        let pipeline_set = finalize_pipeline_set(new_pipeline_set());
275        let methods = vec![Method::GET];
276        let matcher = MethodOnlyRouteMatcher::new(methods);
277        let dispatcher = Box::new(DispatcherImpl::new(secondary_router, (), pipeline_set));
278        let extractors: Extractors<NoopPathExtractor, NoopQueryStringExtractor> = Extractors::new();
279        let route = RouteImpl::new(matcher, dispatcher, extractors, Delegation::External);
280
281        let mut state = State::new();
282        state.put(Method::GET);
283        state.put(Uri::from_str("https://example.com/").unwrap());
284        state.put(HeaderMap::new());
285        state.put(RequestPathSegments::new("/"));
286        set_request_id(&mut state);
287
288        match route.dispatch(state).now_or_never() {
289            Some(Ok((_state, response))) => assert_eq!(response.status(), StatusCode::ACCEPTED),
290            Some(Err((_state, e))) => panic!("error polling future: {:?}", e),
291            None => panic!("expected future to be completed already"),
292        }
293    }
294}