uri_routes/
lib.rs

1use http::uri;
2use ordered_float::OrderedFloat;
3
4/// Constructs URL routes from the ground up.
5/// Useful in scenarios where the need to
6/// dynamically construct routes that may have
7/// common properties.
8pub trait RouteBuilder<'a> {
9    /// New instance of a `RouteBuilder`.
10    fn new(host: &'a str) -> Self;
11    /// Tries to build a URI from path arguments
12    /// and parameters.
13    fn build(self) -> Result<uri::Uri, http::Error>;
14    /// Add a parameter key/pair to the builder.
15    fn with_param<T: ToString>(self, name: String, value: T) -> Self;
16    /// Add a path argument to the end of the
17    /// path buffer.
18    fn with_path(self, path: String) -> Self;
19    /// Inserts a path argument with the desired
20    /// weight.
21    fn with_path_weight(self, path: String, weight: f32) -> Self;
22    /// Set the protocol scheme.
23    fn with_scheme(self, scheme: String) -> Self;
24}
25
26#[derive(Clone, Eq, Ord)]
27struct ApiRoutePath {
28    path:   String,
29    weight: OrderedFloat<f32>,
30}
31
32impl ApiRoutePath {
33    pub fn new<'a>(path: String, weight: f32) -> Self {
34        Self{path: path.to_owned(), weight: OrderedFloat::from(weight)}
35    }
36}
37
38impl PartialEq for ApiRoutePath {
39    fn eq(&self, other: &Self) -> bool {
40        self.weight == other.weight && self.path == other.path
41    }
42}
43
44impl PartialEq<str> for ApiRoutePath {
45    fn eq(&self, other: &str) -> bool {
46        self.path == other
47    }
48}
49
50impl PartialOrd for ApiRoutePath {
51    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
52        self.weight.partial_cmp(&other.weight)
53    }
54}
55
56impl ToString for ApiRoutePath {
57    fn to_string(&self) -> String {
58        self.path.clone()
59    }
60}
61
62pub struct ApiRouteBuilder<'a> {
63    hostname:   &'a str,
64    parameters: Vec<String>,
65    scheme:     Option<String>,
66    sub_paths:  Vec<ApiRoutePath>,
67}
68
69impl<'a> ApiRouteBuilder<'a> {
70    fn insert_param<T: ToString>(mut self, name: String, value: T) -> Self {
71        self.parameters.push(format!("{name}={}", value.to_string()));
72        self
73    }
74
75    fn insert_path(mut self, path: String, weight: Option<f32>) -> Self {
76        let weight = weight
77            .unwrap_or(f32::MAX)
78            .clamp(0.1, f32::MAX);
79        let path = ApiRoutePath::new(path, weight);
80        self.sub_paths.push(path);
81        self.sub_paths.sort();
82        self
83    }
84
85    fn insert_scheme(mut self, scheme: Option<String>) -> Self {
86        self.scheme = scheme;
87        self
88    }
89
90    fn parse_params(&self) -> String {
91        self.parameters.join("&")
92    }
93
94    fn parse_path(&self) -> String {
95        let mut paths = self.sub_paths.clone();
96        paths.retain(|p| p != "");
97
98        let paths: Vec<_> = paths
99            .iter()
100            .map(|p| p.to_string())
101            .collect();
102        paths.join("/").replace("//", "/")
103    }
104
105    fn parse_scheme(&self) -> String {
106        self.scheme.clone().unwrap_or(String::from("https"))
107    }
108}
109
110impl<'a> RouteBuilder<'a> for ApiRouteBuilder<'a> {
111    fn new(host: &'a str) -> Self {
112        Self{
113            hostname: host,
114            parameters: vec![],
115            scheme: None,
116            sub_paths: vec![ApiRoutePath::new(String::from("/"), 0.0)]
117        }
118    }
119
120    /// Tries to build a URI from path arguments
121    /// and parameters.
122    /// ```rust
123    /// use crate::uri_routes::{RouteBuilder, ApiRouteBuilder};
124    /// let route = ApiRouteBuilder::new("google.com").build().unwrap();
125    /// assert_eq!(route, "https://google.com")
126    /// ```
127    fn build(self) -> Result<uri::Uri, http::Error> {
128        let scheme   = self.parse_scheme();
129        let hostname = self.hostname;
130        let path     = self.parse_path();
131        let params   = self.parse_params();
132
133        uri::Builder::new()
134            .scheme(scheme.as_str())
135            .authority(hostname)
136            .path_and_query(format!("{path}?{params}"))
137            .build()
138    }
139
140    /// Add a parameter key/pair to the builder.
141    /// ```rust
142    /// use crate::uri_routes::{RouteBuilder, ApiRouteBuilder};
143    /// let route = ApiRouteBuilder::new("fqdm.org")
144    ///     .with_param("page".into(), 1)
145    ///     .build()
146    ///     .unwrap();
147    /// assert_eq!(route, "https://fqdm.org?page=1")
148    /// ```
149    fn with_param<T: ToString>(self, name: String, value: T) -> Self {
150        self.insert_param(name, value)
151    }
152
153    /// Add a path argument to the end of the
154    /// path buffer.
155    /// ```rust
156    /// use crate::uri_routes::{RouteBuilder, ApiRouteBuilder};
157    /// let route = ApiRouteBuilder::new("fqdm.org")
158    ///     .with_path("resource".into())
159    ///     .build()
160    ///     .unwrap();
161    /// assert_eq!(route, "https://fqdm.org/resource")
162    /// ```
163    fn with_path(self, path: String) -> Self {
164        self.insert_path(path, None)
165    }
166
167    /// Inserts a path argument with the desired
168    /// weight.
169    /// ```rust
170    /// use crate::uri_routes::{RouteBuilder, ApiRouteBuilder};
171    /// let route = ApiRouteBuilder::new("fqdm.org")
172    ///     .with_path_weight("resource0".into(), 2.0)
173    ///     .with_path_weight("resource1".into(), 1.0)
174    ///     .build()
175    ///     .unwrap();
176    /// assert_eq!(route, "https://fqdm.org/resource1/resource0")
177    /// ```
178    fn with_path_weight(self, path: String, weight: f32) -> Self {
179        self.insert_path(path, Some(weight))
180    }
181
182    /// Tries to build a URI from path arguments
183    /// and parameters.
184    /// ```rust
185    /// use crate::uri_routes::{RouteBuilder, ApiRouteBuilder};
186    /// let route = ApiRouteBuilder::new("localhost")
187    ///     .with_scheme("file".into())
188    ///     .build()
189    ///     .unwrap();
190    /// assert_eq!(route, "file://localhost")
191    /// ```
192    fn with_scheme(self, scheme: String) -> Self {
193        self.insert_scheme(Some(scheme.to_owned()))
194    }
195}