1use std::fmt;
2
3pub use http::Method;
4use sofie_core::encoding::percent;
5use sofie_db::Aggregate;
6
7#[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 #[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#[must_use]
57pub fn path_segment(value: &impl fmt::Display) -> String {
58 percent::encode(&value.to_string(), percent::is_unreserved)
59}
60
61#[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
71pub 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}