Skip to main content

sofie_http/
route_path.rs

1use std::fmt;
2
3pub use http::Method;
4use sofie_core::encoding::percent;
5use sofie_db::Aggregate;
6
7/// Made by a contract's route builders, so links and tests name a route by its handler.
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct RoutePath {
10    method: Method,
11    path: String,
12}
13
14#[derive(Clone, Copy, Debug)]
15pub struct RouteName {
16    pub method: &'static str,
17    pub path: &'static str,
18    pub name: &'static str,
19}
20
21impl RoutePath {
22    #[must_use]
23    pub fn new(method: Method, path: String) -> Self {
24        Self { method, path }
25    }
26
27    /// A parameter of the query a GET reads as `Query<Q>` or `Listing<T, Q>`.
28    #[must_use]
29    pub fn with_query(mut self, name: &str, value: &impl fmt::Display) -> Self {
30        let separator = if self.path.contains('?') { '&' } else { '?' };
31        self.path.push(separator);
32        percent::push_encoded(&mut self.path, name, percent::is_unreserved);
33        self.path.push('=');
34        percent::push_encoded(&mut self.path, &value.to_string(), percent::is_unreserved);
35        self
36    }
37
38    #[must_use]
39    pub fn method(&self) -> &Method {
40        &self.method
41    }
42
43    #[must_use]
44    pub fn path(&self) -> &str {
45        &self.path
46    }
47}
48
49impl fmt::Display for RoutePath {
50    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
51        formatter.write_str(&self.path)
52    }
53}
54
55/// A `/` in `value` stays inside the segment.
56#[must_use]
57pub fn path_segment(value: &impl fmt::Display) -> String {
58    percent::encode(&value.to_string(), percent::is_unreserved)
59}
60
61/// For `{*rest}`: each part is encoded, the slashes between them kept.
62#[must_use]
63pub fn path_rest(value: &str) -> String {
64    value
65        .split('/')
66        .map(|part| percent::encode(part, percent::is_unreserved))
67        .collect::<Vec<_>>()
68        .join("/")
69}
70
71/// Called in a `const` block by a route builder, whose contract cannot name the
72/// module's aggregate: an id of any other aggregate fails the build.
73pub const fn require_aggregate<A: Aggregate>(module: &str, name: &str) {
74    assert!(
75        same_text(A::MODULE, module) && same_text(A::NAME, name),
76        "a route builder takes the id of the aggregate its path parameter is named after"
77    );
78}
79
80pub(crate) const fn same_text(left: &str, right: &str) -> bool {
81    let (left, right) = (left.as_bytes(), right.as_bytes());
82    if left.len() != right.len() {
83        return false;
84    }
85    let mut index = 0;
86    while index < left.len() {
87        if left[index] != right[index] {
88            return false;
89        }
90        index += 1;
91    }
92    true
93}
94
95#[cfg(test)]
96mod tests {
97    use super::{Method, RoutePath, path_rest, path_segment};
98
99    #[test]
100    fn a_query_parameter_is_encoded_after_the_path() {
101        let path = RoutePath::new(Method::GET, "/shops".to_owned())
102            .with_query("near", &"Ulm Mitte")
103            .with_query("radius", &5);
104
105        assert_eq!(path.path(), "/shops?near=Ulm%20Mitte&radius=5");
106    }
107
108    #[test]
109    fn a_segment_keeps_its_slashes_and_spaces_inside_it() {
110        assert_eq!(path_segment(&"a/b c"), "a%2Fb%20c");
111    }
112
113    #[test]
114    fn the_rest_of_a_path_keeps_its_slashes() {
115        assert_eq!(path_rest("a b/c?d"), "a%20b/c%3Fd");
116    }
117}