Skip to main content

lino_rest_api/
router.rs

1//! Route registry backing automatic `HEAD`, automatic `OPTIONS`, `405 Method Not
2//! Allowed` and the service description (specification sections 4.1 and 9).
3//!
4//! Path patterns use the `:parameter` syntax of the JavaScript and Python
5//! implementations, so the same route table is described identically in all
6//! three languages.
7
8use std::collections::BTreeMap;
9
10/// Methods that never need to be registered explicitly.
11pub const IMPLICIT_METHODS: [&str; 2] = ["HEAD", "OPTIONS"];
12
13/// Description metadata attached to one method of one route.
14#[derive(Debug, Clone, Default, PartialEq, Eq)]
15pub struct RouteMeta {
16    /// One-line summary, shown in both description documents.
17    pub summary: Option<String>,
18}
19
20impl RouteMeta {
21    /// Metadata carrying only a summary.
22    pub fn summary<S: Into<String>>(summary: S) -> Self {
23        Self {
24            summary: Some(summary.into()),
25        }
26    }
27}
28
29/// The path parameters captured by a match.
30pub type PathParams = BTreeMap<String, String>;
31
32/// Every method registered on one path pattern.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub struct RouteEntry {
35    /// The pattern the route was registered with.
36    pub pattern: String,
37    /// Methods registered on it, in registration order.
38    pub methods: Vec<(String, RouteMeta)>,
39}
40
41impl RouteEntry {
42    /// Whether a method is registered on this route.
43    pub fn has_method(&self, method: &str) -> bool {
44        self.methods.iter().any(|(name, _)| name == method)
45    }
46}
47
48/// Split a path or a pattern into its segments, ignoring one trailing slash.
49fn segments(path: &str) -> Vec<&str> {
50    let trimmed = if path.len() > 1 {
51        path.strip_suffix('/').unwrap_or(path)
52    } else {
53        path
54    };
55    trimmed.split('/').collect()
56}
57
58/// Match a concrete path against a pattern, capturing its path parameters.
59///
60/// A `:name` segment captures one non-empty segment, and a `*` segment captures
61/// everything left, including slashes.
62///
63/// # Examples
64///
65/// ```
66/// use lino_rest_api::router::match_path;
67///
68/// let params = match_path("/tasks/:id", "/tasks/7").unwrap();
69/// assert_eq!(params["id"], "7");
70/// assert!(match_path("/tasks/:id", "/tasks").is_none());
71/// ```
72pub fn match_path(pattern: &str, pathname: &str) -> Option<PathParams> {
73    let pattern_segments = segments(pattern);
74    let path_segments = segments(pathname);
75    let mut params = PathParams::new();
76
77    for (index, expected) in pattern_segments.iter().enumerate() {
78        if let Some(name) = expected.strip_prefix('*') {
79            // A wildcard swallows the rest of the path, named when it is named.
80            let rest = path_segments.get(index..).unwrap_or(&[]).join("/");
81            if !name.is_empty() {
82                params.insert(name.to_string(), rest);
83            }
84            return Some(params);
85        }
86        let actual = path_segments.get(index)?;
87        match expected.strip_prefix(':') {
88            Some(name) => {
89                if actual.is_empty() {
90                    return None;
91                }
92                params.insert(name.to_string(), (*actual).to_string());
93            }
94            None => {
95                if expected != actual {
96                    return None;
97                }
98            }
99        }
100    }
101
102    if path_segments.len() > pattern_segments.len() {
103        return None;
104    }
105    Some(params)
106}
107
108/// The set of routes registered on an application.
109#[derive(Debug, Clone, Default)]
110pub struct RouteTable {
111    routes: Vec<RouteEntry>,
112}
113
114impl RouteTable {
115    /// An empty table.
116    pub fn new() -> Self {
117        Self::default()
118    }
119
120    /// Register a method on a path.
121    ///
122    /// Registering the same method twice replaces its metadata, which is how a
123    /// route is redefined.
124    pub fn register(&mut self, method: &str, pattern: &str, meta: RouteMeta) {
125        let method = method.to_uppercase();
126        let entry = match self
127            .routes
128            .iter_mut()
129            .position(|entry| entry.pattern == pattern)
130        {
131            Some(index) => &mut self.routes[index],
132            None => {
133                self.routes.push(RouteEntry {
134                    pattern: pattern.to_string(),
135                    methods: Vec::new(),
136                });
137                self.routes.last_mut().expect("just pushed")
138            }
139        };
140        match entry.methods.iter_mut().find(|(name, _)| *name == method) {
141            Some(existing) => existing.1 = meta,
142            None => entry.methods.push((method, meta)),
143        }
144    }
145
146    /// Remove a method from a path, dropping the path when it keeps none.
147    ///
148    /// Used by [`crate::app::LinoApp::without_description`] to withdraw the
149    /// description routes the constructor registers.
150    pub fn remove(&mut self, method: &str, pattern: &str) {
151        let method = method.to_uppercase();
152        let Some(index) = self
153            .routes
154            .iter()
155            .position(|entry| entry.pattern == pattern)
156        else {
157            return;
158        };
159        self.routes[index]
160            .methods
161            .retain(|(name, _)| *name != method);
162        if self.routes[index].methods.is_empty() {
163            self.routes.remove(index);
164        }
165    }
166
167    /// Find the route entry owning a concrete path.
168    pub fn find(&self, pathname: &str) -> Option<&RouteEntry> {
169        self.match_path(pathname).map(|(entry, _)| entry)
170    }
171
172    /// Find the route entry owning a path together with its path parameters.
173    ///
174    /// Routes are tried in registration order, so an earlier registration wins.
175    pub fn match_path(&self, pathname: &str) -> Option<(&RouteEntry, PathParams)> {
176        self.routes
177            .iter()
178            .find_map(|entry| match_path(&entry.pattern, pathname).map(|params| (entry, params)))
179    }
180
181    /// List the methods allowed on a concrete path.
182    ///
183    /// `OPTIONS` is always allowed, and `HEAD` is allowed wherever `GET` is.
184    /// The answer is [`None`] when no route owns the path.
185    pub fn allowed_methods(&self, pathname: &str) -> Option<Vec<String>> {
186        let entry = self.find(pathname)?;
187        Some(self.allowed_methods_of(entry))
188    }
189
190    /// The methods allowed on a route entry, sorted.
191    pub fn allowed_methods_of(&self, entry: &RouteEntry) -> Vec<String> {
192        let mut methods: Vec<String> = entry.methods.iter().map(|(name, _)| name.clone()).collect();
193        if methods.iter().any(|method| method == "GET") && !methods.iter().any(|m| m == "HEAD") {
194            methods.push("HEAD".to_string());
195        }
196        if !methods.iter().any(|method| method == "OPTIONS") {
197            methods.push("OPTIONS".to_string());
198        }
199        methods.sort();
200        methods.dedup();
201        methods
202    }
203
204    /// The registered routes, in registration order.
205    pub fn entries(&self) -> &[RouteEntry] {
206        &self.routes
207    }
208
209    /// Render the registry as the `routes` member of a service description.
210    ///
211    /// Routes are ordered by path, so two servers describing the same routes
212    /// describe them identically.
213    pub fn describe(&self) -> Vec<RouteDescription> {
214        let mut descriptions: Vec<RouteDescription> = self
215            .routes
216            .iter()
217            .map(|entry| RouteDescription {
218                path: entry.pattern.clone(),
219                methods: self.allowed_methods_of(entry),
220                summary: entry
221                    .methods
222                    .iter()
223                    .find_map(|(_, meta)| meta.summary.clone()),
224            })
225            .collect();
226        descriptions.sort_by(|left, right| left.path.cmp(&right.path));
227        descriptions
228    }
229}
230
231/// One route of a service description.
232#[derive(Debug, Clone, PartialEq, Eq)]
233pub struct RouteDescription {
234    /// Path pattern.
235    pub path: String,
236    /// Methods allowed on it.
237    pub methods: Vec<String>,
238    /// One-line summary, when the route carries one.
239    pub summary: Option<String>,
240}