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