intrepid_core/routing/
router.rs

1use std::{collections::HashMap, sync::Arc};
2use tower::Service;
3use uuid::Uuid;
4
5use crate::{Frame, FrameFuture, Handler, MessageFrame};
6
7use super::{path_error::PathError, Endpoint, RouteId, Routes};
8
9/// A router for actions that can be invoked as services.
10#[derive(Clone, Default)]
11pub struct Router<State> {
12    routes: Arc<Routes>,
13    actions: HashMap<RouteId, Endpoint<State>>,
14}
15
16impl<State> Router<State>
17where
18    State: Clone + Send + Sync + 'static,
19{
20    /// Create a new action router.
21    pub fn new() -> Self {
22        Self {
23            routes: Arc::new(Routes::default()),
24            actions: HashMap::new(),
25        }
26    }
27
28    /// Given a path, attempt to deserialize the path fragments into a given type.
29    pub fn capture<T>(&self, path: impl AsRef<str>) -> Result<T, PathError>
30    where
31        T: serde::de::DeserializeOwned,
32    {
33        self.routes.capture(path)
34    }
35
36    /// Call an action with a given state.
37    pub fn handle_frame_with_state(&self, frame: Frame, state: State) -> FrameFuture {
38        let endpoint = match &frame.clone() {
39            Frame::Message(MessageFrame { uri, .. }) => {
40                if let Some(endpoint) = self
41                    .routes
42                    .at(uri)
43                    .map(|(found_it, _)| found_it)
44                    .and_then(|route_id| self.actions.get(route_id))
45                {
46                    endpoint
47                } else {
48                    return FrameFuture::empty();
49                }
50            }
51            _ => return FrameFuture::empty(),
52        };
53
54        let endpoint = endpoint.clone().into_inner();
55
56        endpoint.into_actionable(state).call(frame)
57    }
58
59    fn insert_endpoint(
60        &mut self,
61        route: impl Into<String>,
62        endpoint: Endpoint<State>,
63    ) -> crate::Result<()>
64    where
65        State: Clone + Send + 'static,
66    {
67        let route = {
68            let route = route.into();
69
70            match route.as_str() {
71                "" => "/".to_string(),
72                _ => route,
73            }
74        };
75
76        let initial_fetch = {
77            let key = route.as_str();
78            let fetch_by_path = self.routes.get_id(key);
79
80            fetch_by_path
81                .and_then(|route_id| self.actions.get(&route_id).map(|action| (route_id, action)))
82        };
83
84        // this is a useful dead code warning because it leads to an intended refactor
85        #[expect(
86            unused_variables,
87            reason = "we want to add some feedback to indicate that the route already exists"
88        )]
89        if let Some((route_id, action)) = initial_fetch {
90            // TODO: In the future we want to add a debug log here to indicate that the route already exists.
91            // Right now this just prevents duplication of route ids for the same path.
92            self.actions.insert(route_id, endpoint);
93        } else {
94            let route_id = Uuid::new_v4().into();
95
96            self.insert_route(&route, route_id);
97            self.actions.insert(route_id, endpoint);
98        }
99
100        Ok(())
101    }
102
103    fn insert_route(&mut self, route: &str, id: RouteId) {
104        let routes = Arc::make_mut(&mut self.routes);
105
106        routes.insert(route, id);
107    }
108
109    /// Insert a new action into the router, turning it into an [`ActionEndpoint`].
110    pub fn insert<ActionHandler, Args>(
111        &mut self,
112        route: impl Into<String>,
113        given_action: ActionHandler,
114    ) -> crate::Result<()>
115    where
116        ActionHandler: Handler<Args, State> + Clone + Send + Sync + 'static,
117        Args: Clone + Send + Sync + 'static,
118    {
119        let endpoint = Endpoint::new(given_action);
120
121        self.insert_endpoint(route, endpoint)
122    }
123
124    /// Get the routes that have been mounted so far.
125    pub fn routes(&self) -> Vec<String> {
126        self.routes.paths()
127    }
128
129    /// Get the actions that have been mounted so far, wrapped in layers that prevent their
130    /// invocation whenever called by the wrong route.
131    pub fn endpoints(&self) -> Vec<Endpoint<State>>
132    where
133        State: Clone + Send + 'static,
134    {
135        let mut endpoints = Vec::new();
136
137        for endpoint in self.actions.values() {
138            endpoints.push(endpoint.clone());
139        }
140
141        endpoints
142    }
143
144    /// Merge another system context into this one.
145    pub fn scope(&mut self, route: impl Into<String>, other: Router<State>) -> crate::Result<()>
146    where
147        State: Clone + Send + 'static,
148    {
149        let route = route.into();
150        let Router { routes, actions } = other;
151
152        for (id, action) in actions {
153            let path = routes.get_path(id).unwrap();
154            let route = {
155                let candidate = if path.starts_with('/') {
156                    format!("{route}{path}")
157                } else {
158                    format!("{route}/{path}")
159                };
160
161                match candidate.as_str() {
162                    "/" => "/".to_string(),
163                    partial if partial.ends_with('/') => partial[..partial.len() - 1].to_string(),
164                    _ => candidate,
165                }
166            };
167
168            let nested_action = action.clone();
169
170            self.insert_endpoint(route, nested_action)?;
171        }
172
173        Ok(())
174    }
175}
176
177impl<State> std::fmt::Debug for Router<State> {
178    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179        f.debug_struct("Router")
180            .field("routes", &self.routes)
181            .finish()
182    }
183}