odata_simple_client/
path.rs

1use std::{collections::HashMap, convert::TryInto};
2
3use hyper::http::uri::{InvalidUri, PathAndQuery};
4
5/// Specifies direction in which the returned results are listed. Use [`ListRequest::order_by`](`crate::ListRequest::order_by`) to change it.
6/// If nothing else is specified, it defaults to [`Direction::Ascending`]
7#[derive(Debug, Clone, Copy)]
8pub enum Direction {
9    /// List results in descending order (largest to smallest)
10    Descending,
11    /// List results in ascending order (smallest to largest)
12    Ascending,
13}
14
15/// Used by [`ListRequest::filter`](`crate::ListRequest::filter`) to apply conditional filtering to the returned results.
16///
17/// See [the OData 3.0 documentation (section 5.1.2)](https://www.odata.org/documentation/odata-version-3-0/url-conventions/) for more information.
18#[derive(Debug, Clone, Copy)]
19pub enum Comparison {
20    /// The Equal operator evaluates to true if the field is equal to the value, otherwise if evaluates to false.
21    Equal,
22    /// The NotEqual operator evaluates to true if the field is not equal to the value, otherwise if evaluates to false.
23    NotEqual,
24    /// The GreaterThan operator evaluates to true if the field is greater than the value, otherwise if evaluates to false.
25    GreaterThan,
26    /// The GreaterOrEqual operator  evaluates to true if the field is greater than or equal to the value, otherwise if evaluates to false.
27    GreaterOrEqual,
28    /// The LessThan operator evaluates to true if the field is less than the value, otherwise if evaluates to false.
29    LessThan,
30    /// LessOrEqual operator evaluates to true if the field is less than or equal to the value, otherwise if evaluates to false.
31    LessOrEqual,
32}
33
34/// Format of the returned API data. [`DataSource::fetch_paged`](`crate::DataSource::fetch_paged`) forces [`Format::Json`].
35#[derive(Debug, Clone, Copy)]
36pub enum Format {
37    /// Request that the returned API data is xml-formatted.
38    Xml,
39    /// Request that the returned API data is json-formatted.
40    Json,
41}
42
43/// Used by [`ListRequest::inline_count`](`crate::ListRequest::inline_count`) to show number of results left in a query, before all pages have been read.
44#[derive(Debug, Clone, Copy)]
45pub enum InlineCount {
46    /// Don't include an inline count.
47    None,
48    /// Include inline count on all pages.
49    AllPages,
50}
51
52#[derive(Debug, Clone)]
53pub(crate) struct PathBuilder {
54    pub(crate) base_path: String,
55    resource_type: String,
56    id: Option<usize>,
57    inner: HashMap<&'static str, String>,
58}
59
60impl PathBuilder {
61    pub fn new_with_base(base_path: String, resource_type: String) -> Self {
62        PathBuilder {
63            id: None,
64            base_path,
65            resource_type,
66            inner: HashMap::new(),
67        }
68    }
69
70    pub fn new(resource_type: String) -> Self {
71        Self::new_with_base(String::new(), resource_type)
72    }
73
74    pub fn id(mut self, id: usize) -> Self {
75        self.id = Some(id);
76        self
77    }
78
79    pub fn base_path(mut self, base_path: String) -> Self {
80        self.base_path = base_path;
81        self
82    }
83
84    pub fn order_by(mut self, field: &str, order: Direction) -> Self {
85        let order = match order {
86            Direction::Descending => "desc",
87            Direction::Ascending => "asc",
88        };
89
90        // We don't really care if the value is overwritten.
91        let _ = self.inner.insert(
92            "orderby",
93            urlencoding::encode(&format!("{field} {order}")).to_string(),
94        );
95        self
96    }
97
98    pub fn top(mut self, count: u32) -> Self {
99        // We don't really care if the value is overwritten.
100        let _ = self
101            .inner
102            .insert("top", urlencoding::encode(&count.to_string()).to_string());
103        self
104    }
105
106    pub fn format(mut self, format: Format) -> Self {
107        // We don't really care if the value is overwritten.
108        let _ = self.inner.insert(
109            "format",
110            match format {
111                Format::Xml => "xml",
112                Format::Json => "json",
113            }
114            .to_string(),
115        );
116        self
117    }
118
119    pub fn skip(mut self, count: u32) -> Self {
120        // We don't really care if the value is overwritten.
121        let _ = self
122            .inner
123            .insert("skip", urlencoding::encode(&count.to_string()).to_string());
124        self
125    }
126
127    pub fn inline_count(mut self, value: InlineCount) -> Self {
128        // We don't really care if the value is overwritten.
129        let _ = self.inner.insert(
130            "inlinecount",
131            urlencoding::encode(match value {
132                InlineCount::None => "none",
133                InlineCount::AllPages => "allpages",
134            })
135            .to_string(),
136        );
137        self
138    }
139
140    pub fn filter(mut self, field: &str, comparison: Comparison, value: &str) -> Self {
141        let comparison = match comparison {
142            Comparison::Equal => "eq",
143            Comparison::NotEqual => "ne",
144            Comparison::GreaterThan => "gt",
145            Comparison::GreaterOrEqual => "ge",
146            Comparison::LessThan => "lt",
147            Comparison::LessOrEqual => "le",
148        };
149
150        // We don't really care if the value is overwritten.
151        let _ = self.inner.insert(
152            "filter",
153            urlencoding::encode(&format!("{field} {comparison} {value}")).to_string(),
154        );
155        self
156    }
157
158    pub fn expand<'f, F>(mut self, field: F) -> Self
159    where
160        F: IntoIterator<Item = &'f str>,
161    {
162        let encoded = field
163            .into_iter()
164            .map(|field| urlencoding::encode(field).into_owned())
165            .collect::<Vec<_>>()
166            .join(",");
167
168        // We don't really care if the value is overwritten.
169        let _ = self
170            .inner
171            .entry("expand")
172            .and_modify(|current| {
173                current.push(',');
174                current.push_str(&encoded)
175            })
176            .or_insert_with(|| encoded.to_string());
177        self
178    }
179
180    pub fn build(&self) -> Result<PathAndQuery, InvalidUri> {
181        let query = {
182            let mut kv = self
183                .inner
184                .iter()
185                .map(|(key, value)| {
186                    format!(
187                        "${key}={value}",
188                        key = urlencoding::encode(key),
189                        value = value
190                    )
191                })
192                .collect::<Vec<_>>();
193            kv.sort();
194            kv
195        };
196
197        format!(
198            "{base_path}/{resource_type}{id}?{query}",
199            base_path = self.base_path,
200            resource_type = urlencoding::encode(&self.resource_type),
201            id = self
202                .id
203                .map(|id| format!("({})", urlencoding::encode(&id.to_string())))
204                .unwrap_or_default(),
205            query = query.join("&")
206        )
207        .parse()
208    }
209}
210
211impl TryInto<PathAndQuery> for PathBuilder {
212    type Error = InvalidUri;
213
214    fn try_into(self) -> Result<PathAndQuery, Self::Error> {
215        self.build()
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::PathBuilder;
222    use crate::Direction;
223
224    #[test]
225    fn test_query_builder() {
226        let query = PathBuilder::new("test_resource".into())
227            .top(2)
228            .skip(3)
229            .order_by("date", Direction::Ascending)
230            .build()
231            .unwrap();
232
233        assert_eq!("/test_resource?$orderby=date%20asc&$skip=3&$top=2", query);
234    }
235
236    #[test]
237    fn test_single_resource_expand() {
238        let query = PathBuilder::new("test_resource".into())
239            .id(100)
240            .expand(["DoThing", "What"])
241            .expand(["Hello"])
242            .build()
243            .unwrap();
244
245        assert_eq!("/test_resource(100)?$expand=DoThing,What,Hello", query);
246    }
247}