Skip to main content

alux_http/
path.rs

1//! States which path a selector matches, independently of how a router spells it.
2
3/// Names one part of a route path.
4#[derive(Debug, Clone, PartialEq, Eq, Hash)]
5pub enum PathSegment {
6    /// Matches this segment exactly.
7    Literal(String),
8    /// Binds one segment under this name.
9    Param(String),
10    /// Binds every remaining segment under this name.
11    Tail(String),
12}
13
14impl PathSegment {
15    /// Returns the name this segment binds, where it binds one.
16    pub fn name(&self) -> Option<&str> {
17        match self {
18            Self::Literal(_) => None,
19            Self::Param(name) | Self::Tail(name) => Some(name),
20        }
21    }
22}
23
24/// Carries a route path as the segments it matches.
25///
26/// Routers disagree about how a parameter is written, so a path held as a string is a path written
27/// for one framework. A path is read into segments once, here, and each interpreter spells those
28/// segments the way its own router reads them.
29#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
30pub struct RoutePath {
31    segments: Vec<PathSegment>,
32}
33
34impl RoutePath {
35    /// Reads a path written in any of the spellings the major routers accept.
36    ///
37    /// `:name` and `{name}` bind one segment, `*name` and `{*name}` bind every remaining segment,
38    /// and any other segment is matched literally. Empty segments state nothing, so leading,
39    /// trailing, and repeated separators are dropped.
40    pub fn parse(path: &str) -> Self {
41        let segments = path.split('/').filter(|segment| !segment.is_empty()).map(read_segment).collect();
42
43        Self { segments }
44    }
45
46    /// Returns the segments this path matches, in order.
47    pub fn segments(&self) -> &[PathSegment] {
48        &self.segments
49    }
50
51    /// Returns whether this path matches nothing of its own.
52    pub fn is_empty(&self) -> bool {
53        self.segments.is_empty()
54    }
55
56    /// Returns the names this path binds, in order.
57    pub fn names(&self) -> impl Iterator<Item = &str> {
58        self.segments.iter().filter_map(PathSegment::name)
59    }
60}
61
62impl From<&str> for RoutePath {
63    fn from(path: &str) -> Self {
64        Self::parse(path)
65    }
66}
67
68/// Reads one non-empty segment as what it binds.
69fn read_segment(segment: &str) -> PathSegment {
70    if let Some(name) = segment.strip_prefix(':') {
71        return PathSegment::Param(name.to_owned());
72    }
73    if let Some(name) = segment.strip_prefix('*') {
74        return PathSegment::Tail(name.to_owned());
75    }
76    if let Some(name) = segment.strip_prefix('{').and_then(|segment| segment.strip_suffix('}')) {
77        return match name.strip_prefix('*') {
78            Some(name) => PathSegment::Tail(name.to_owned()),
79            None => PathSegment::Param(name.to_owned()),
80        };
81    }
82
83    PathSegment::Literal(segment.to_owned())
84}
85
86/// Spells route-path parameters the way one router reads them.
87pub trait PathSyntaxAlg {
88    /// Returns the spelling of a parameter binding one segment.
89    fn param(&self, name: &str) -> String;
90
91    /// Returns the spelling of a parameter binding every remaining segment.
92    fn tail(&self, name: &str) -> String;
93}
94
95/// Spells parameters the way a described surface states them.
96///
97/// Every interpreter describes its composed surface in this spelling, which is what makes two
98/// interpretations of one program comparable. A router is handed its own spelling instead.
99#[derive(Debug, Default)]
100pub struct CanonicalPath;
101
102impl PathSyntaxAlg for CanonicalPath {
103    fn param(&self, name: &str) -> String {
104        format!("{{{name}}}")
105    }
106
107    fn tail(&self, name: &str) -> String {
108        format!("{{*{name}}}")
109    }
110}
111
112/// Composes one absolute path from the parts a selector holds, spelled as `syntax` reads them.
113///
114/// Selector composition concatenates path parts, so every interpreter shares one rule: each segment
115/// is preceded by exactly one separator, and a selector holding no segment matches the root.
116pub fn compose_path<'a, Parts>(parts: Parts, syntax: &impl PathSyntaxAlg) -> String
117where
118    Parts: IntoIterator<Item = &'a RoutePath>,
119{
120    let mut path = String::new();
121    for part in parts {
122        for segment in part.segments() {
123            path.push('/');
124            match segment {
125                PathSegment::Literal(value) => path.push_str(value),
126                PathSegment::Param(name) => path.push_str(&syntax.param(name)),
127                PathSegment::Tail(name) => path.push_str(&syntax.tail(name)),
128            }
129        }
130    }
131
132    if path.is_empty() {
133        path.push('/');
134    }
135
136    path
137}
138
139/// Describes one absolute path in the spelling every interpretation states it in.
140pub fn describe_path<'a, Parts>(parts: Parts) -> String
141where
142    Parts: IntoIterator<Item = &'a RoutePath>,
143{
144    compose_path(parts, &CanonicalPath)
145}
146
147#[cfg(test)]
148mod tests {
149    use super::{PathSegment, RoutePath, describe_path};
150
151    #[test]
152    fn reads_every_spelling_a_router_accepts_as_the_same_path() {
153        let poem = RoutePath::parse("/status/:id/*rest");
154        let axum = RoutePath::parse("/status/{id}/{*rest}");
155
156        assert_eq!(poem, axum);
157        assert_eq!(
158            poem.segments(),
159            [
160                PathSegment::Literal("status".to_owned()),
161                PathSegment::Param("id".to_owned()),
162                PathSegment::Tail("rest".to_owned()),
163            ]
164        );
165        assert_eq!(poem.names().collect::<Vec<_>>(), ["id", "rest"]);
166    }
167
168    #[test]
169    fn states_nothing_for_a_separator_that_states_nothing() {
170        assert!(RoutePath::parse("/").is_empty());
171        assert_eq!(RoutePath::parse("//status//"), RoutePath::parse("status"));
172        assert_eq!(describe_path([&RoutePath::parse("/")]), "/");
173    }
174
175    #[test]
176    fn composes_parts_into_one_absolute_path() {
177        let parts = [RoutePath::parse("/api"), RoutePath::parse("v1"), RoutePath::parse("/status/:id")];
178
179        assert_eq!(describe_path(&parts), "/api/v1/status/{id}");
180    }
181
182    #[test]
183    fn matches_a_segment_no_router_reads_as_a_parameter_literally() {
184        assert_eq!(RoutePath::parse("{id}.json").segments(), [PathSegment::Literal("{id}.json".to_owned())]);
185    }
186}