1use std::{collections::HashMap, convert::TryInto};
2
3use hyper::http::uri::{InvalidUri, PathAndQuery};
4
5#[derive(Debug, Clone, Copy)]
8pub enum Direction {
9 Descending,
11 Ascending,
13}
14
15#[derive(Debug, Clone, Copy)]
19pub enum Comparison {
20 Equal,
22 NotEqual,
24 GreaterThan,
26 GreaterOrEqual,
28 LessThan,
30 LessOrEqual,
32}
33
34#[derive(Debug, Clone, Copy)]
36pub enum Format {
37 Xml,
39 Json,
41}
42
43#[derive(Debug, Clone, Copy)]
45pub enum InlineCount {
46 None,
48 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 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 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 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 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 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 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 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}