Skip to main content

gateway_runtime/
router.rs

1//! # Router
2//!
3//! ## Purpose
4//! The `Router` module provides the core mechanism for dispatching incoming HTTP requests to
5//! the appropriate gRPC service handlers based on path patterns and HTTP methods.
6//!
7//! ## Overview
8//! It maintains a registry of routes, where each route consists of:
9//! -   An HTTP Method (e.g., GET, POST).
10//! -   A compiled [Pattern] (from `gateway_internal::path_template`).
11//! -   A service handler (`S`) responsible for processing the request.
12//! -   [RouteMetadata], containing additional configuration like authentication requirements.
13//!
14//! ## Matching Logic
15//! When `match_request` is called, the router iterates through the registered patterns for the
16//! given HTTP method. It uses the `gateway_internal` matching engine to determine if the
17//! request path matches a pattern. If a match is found, it returns the service, any captured
18//! path variables, and the route metadata.
19//!
20//! ## Usage
21//! The router is typically populated by generated code calling `route` or `route_with_metadata`.
22//! At runtime, it is wrapped by the `Gateway` service.
23
24use crate::pattern::route_matcher;
25use alloc::collections::BTreeMap;
26use alloc::string::{String, ToString};
27use alloc::vec::Vec;
28use gateway_internal::path_template::Pattern;
29use http::Method;
30
31/// Metadata associated with a route configuration.
32///
33/// This struct holds static configuration derived from the `.proto` options, such as
34/// authentication requirements (e.g., `google.api.http` security rules).
35#[derive(Debug, Clone, Default)]
36pub struct RouteMetadata {
37    /// Configuration for API Key authentication, if required by the route.
38    pub auth_required: Option<AuthConfig>,
39}
40
41/// Configuration for API Key authentication.
42#[derive(Debug, Clone)]
43pub struct AuthConfig {
44    /// The authentication scheme (e.g., "ApiKey").
45    pub scheme: String,
46    /// The location of the API key in the request.
47    pub location: AuthLocation,
48    /// The name of the header, query parameter, or cookie.
49    pub name: String,
50}
51
52/// The location of the authentication credential.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum AuthLocation {
55    /// Credential is in an HTTP header.
56    Header,
57    /// Credential is in the URL query string.
58    Query,
59    /// Credential is in a cookie.
60    Cookie,
61}
62
63/// The request dispatcher.
64///
65/// Maps (HTTP Method) -> List of (Pattern, Service, Metadata).
66///
67/// # Type Parameters
68/// *   `S`: The type of the service handler. This is typically `BoxCloneService` or similar.
69#[derive(Clone)]
70pub struct Router<S> {
71    routes: BTreeMap<String, Vec<RouteEntry<S>>>,
72}
73
74/// A single entry in the routing table.
75#[derive(Clone)]
76struct RouteEntry<S> {
77    pattern: Pattern,
78    service: S,
79    metadata: RouteMetadata,
80}
81
82impl<S> Router<S> {
83    /// Creates a new, empty `Router`.
84    pub fn new() -> Self {
85        Self {
86            routes: BTreeMap::new(),
87        }
88    }
89
90    /// Matches an incoming request against registered routes.
91    ///
92    /// # Parameters
93    /// *   `method`: The HTTP method of the request.
94    /// *   `path`: The request path.
95    ///
96    /// # Returns
97    /// An `Option` containing a tuple if a match is found:
98    /// -   `&S`: A reference to the matched service.
99    /// -   `BTreeMap<String, String>`: A map of captured path variables (e.g., `id` from `/users/{id}`).
100    /// -   `&RouteMetadata`: Metadata associated with the matched route.
101    pub fn match_request(
102        &self,
103        method: &Method,
104        path: &str,
105    ) -> Option<(&S, BTreeMap<String, String>, &RouteMetadata)> {
106        if let Some(entries) = self.routes.get(method.as_str()) {
107            for entry in entries {
108                if let Some(captured) = route_matcher(&entry.pattern, path) {
109                    return Some((&entry.service, captured, &entry.metadata));
110                }
111            }
112        }
113        None
114    }
115}
116
117impl<S> Default for Router<S> {
118    fn default() -> Self {
119        Self::new()
120    }
121}
122
123/// Registers a service with the router using default metadata.
124///
125/// # Parameters
126/// *   `router`: The router instance.
127/// *   `method`: HTTP method.
128/// *   `pattern`: Path pattern.
129/// *   `service`: The service handler.
130pub fn route<S, C>(router: &mut Router<S>, method: Method, pattern: Pattern, service: C)
131where
132    C: Into<S>,
133{
134    route_with_metadata(router, method, pattern, service, RouteMetadata::default())
135}
136
137/// Registers a service with the router, including specific metadata.
138///
139/// # Parameters
140/// *   `router`: The router instance.
141/// *   `method`: HTTP method.
142/// *   `pattern`: Path pattern.
143/// *   `service`: The service handler.
144/// *   `metadata`: Route-specific metadata.
145pub fn route_with_metadata<S, C>(
146    router: &mut Router<S>,
147    method: Method,
148    pattern: Pattern,
149    service: C,
150    metadata: RouteMetadata,
151) where
152    C: Into<S>,
153{
154    router
155        .routes
156        .entry(method.to_string())
157        .or_default()
158        .push(RouteEntry {
159            pattern,
160            service: service.into(),
161            metadata,
162        });
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use gateway_internal::path_template::{Op, OpCode};
169
170    #[derive(Clone)]
171    struct MockService;
172    impl MockService {
173        fn new() -> Self {
174            Self
175        }
176    }
177
178    #[test]
179    fn test_router_insert_and_match() {
180        let mut router: Router<MockService> = Router::new();
181        let pattern = Pattern {
182            ops: vec![Op {
183                code: OpCode::LitPush,
184                operand: 0,
185            }],
186            pool: vec!["foo".to_string()],
187            vars: vec![],
188            stack_size: 1,
189            tail_len: 0,
190            verb: None,
191        };
192        route(&mut router, Method::GET, pattern, MockService::new());
193
194        let res = router.match_request(&Method::GET, "/foo");
195        assert!(res.is_some());
196    }
197
198    #[test]
199    fn test_router_method_mismatch() {
200        let mut router: Router<MockService> = Router::new();
201        let pattern = Pattern {
202            ops: vec![Op {
203                code: OpCode::LitPush,
204                operand: 0,
205            }],
206            pool: vec!["foo".to_string()],
207            vars: vec![],
208            stack_size: 1,
209            tail_len: 0,
210            verb: None,
211        };
212        route(&mut router, Method::GET, pattern, MockService::new());
213
214        let res = router.match_request(&Method::POST, "/foo");
215        assert!(res.is_none());
216    }
217
218    #[test]
219    fn test_router_path_mismatch() {
220        let mut router: Router<MockService> = Router::new();
221        let pattern = Pattern {
222            ops: vec![Op {
223                code: OpCode::LitPush,
224                operand: 0,
225            }],
226            pool: vec!["foo".to_string()],
227            vars: vec![],
228            stack_size: 1,
229            tail_len: 0,
230            verb: None,
231        };
232        route(&mut router, Method::GET, pattern, MockService::new());
233        assert!(router.match_request(&Method::GET, "/bar").is_none());
234    }
235
236    #[test]
237    fn test_router_multiple_routes() {
238        let mut router: Router<MockService> = Router::new();
239        let p1 = Pattern {
240            ops: vec![Op {
241                code: OpCode::LitPush,
242                operand: 0,
243            }],
244            pool: vec!["foo".to_string()],
245            vars: vec![],
246            stack_size: 1,
247            tail_len: 0,
248            verb: None,
249        };
250        let p2 = Pattern {
251            ops: vec![Op {
252                code: OpCode::LitPush,
253                operand: 0,
254            }],
255            pool: vec!["bar".to_string()],
256            vars: vec![],
257            stack_size: 1,
258            tail_len: 0,
259            verb: None,
260        };
261
262        route(&mut router, Method::GET, p1, MockService::new());
263        route(&mut router, Method::GET, p2, MockService::new());
264
265        assert!(router.match_request(&Method::GET, "/foo").is_some());
266        assert!(router.match_request(&Method::GET, "/bar").is_some());
267    }
268
269    #[test]
270    fn test_router_precedence() {
271        let mut router: Router<MockService> = Router::new();
272
273        // /foo
274        let p1 = Pattern {
275            ops: vec![Op {
276                code: OpCode::LitPush,
277                operand: 0,
278            }],
279            pool: vec!["foo".to_string()],
280            vars: vec![],
281            stack_size: 1,
282            tail_len: 0,
283            verb: None,
284        };
285        // /{var}
286        let p2 = Pattern {
287            ops: vec![
288                Op {
289                    code: OpCode::Push,
290                    operand: 0,
291                },
292                Op {
293                    code: OpCode::Capture,
294                    operand: 0,
295                },
296            ],
297            pool: vec![],
298            vars: vec!["v".to_string()],
299            stack_size: 1,
300            tail_len: 0,
301            verb: None,
302        };
303
304        // Route specific first
305        route(&mut router, Method::GET, p1, MockService::new());
306        route(&mut router, Method::GET, p2, MockService::new());
307
308        let (_s, c, _) = router.match_request(&Method::GET, "/foo").unwrap();
309        assert!(c.is_empty()); // p1 has no capture. p2 has.
310
311        let (_s, c, _) = router.match_request(&Method::GET, "/bar").unwrap();
312        assert!(!c.is_empty()); // Matches p2.
313    }
314
315    #[test]
316    fn test_router_metadata() {
317        let mut router: Router<MockService> = Router::new();
318        let pattern = Pattern {
319            ops: vec![Op {
320                code: OpCode::LitPush,
321                operand: 0,
322            }],
323            pool: vec!["foo".to_string()],
324            vars: vec![],
325            stack_size: 1,
326            tail_len: 0,
327            verb: None,
328        };
329        let meta = RouteMetadata {
330            auth_required: Some(AuthConfig {
331                scheme: "ApiKey".to_string(),
332                location: AuthLocation::Header,
333                name: "X-API-Key".to_string(),
334            }),
335        };
336
337        route_with_metadata(
338            &mut router,
339            Method::GET,
340            pattern,
341            MockService::new(),
342            meta.clone(),
343        );
344
345        let (_, _, matched_meta) = router.match_request(&Method::GET, "/foo").unwrap();
346        assert!(matched_meta.auth_required.is_some());
347        let auth = matched_meta.auth_required.as_ref().unwrap();
348        assert_eq!(auth.name, "X-API-Key");
349    }
350}