1use indexmap::IndexMap;
2use serde::{Deserialize, Serialize};
3use serde_json::Value as JsonValue;
4
5#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
12pub struct MutateInput {
13 pub model: String,
15 pub id: Option<JsonValue>,
17 pub data: IndexMap<String, String>,
19}
20
21pub fn parse_query_params(
38 query_str: &str,
39 default_per_page: usize,
40) -> (usize, usize, IndexMap<String, String>) {
41 let mut page = 1usize;
42 let mut per_page = default_per_page;
43 let mut filters: IndexMap<String, String> = IndexMap::new();
44
45 for part in query_str.split('&').filter(|s| !s.is_empty()) {
46 let mut kv = part.splitn(2, '=');
47 let k = match kv.next() {
48 Some(k) => k,
49 None => continue,
50 };
51 let v = percent_decode(kv.next().unwrap_or(""));
52 match k {
53 "page" => { if let Ok(n) = v.parse::<usize>() { page = n.max(1); } }
54 "per_page" => { if let Ok(n) = v.parse::<usize>() { per_page = n.max(1); } }
55 _ => {
56 if !v.is_empty() {
57 filters
60 .entry(k.to_owned())
61 .and_modify(|existing| {
62 existing.push(',');
63 existing.push_str(&v);
64 })
65 .or_insert(v);
66 }
67 }
68 }
69 }
70
71 (page, per_page, filters)
72}
73
74fn percent_decode(s: &str) -> String {
75 let s = s.replace('+', " ");
76 let bytes = s.as_bytes();
77 let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
78 let mut i = 0;
79 while i < bytes.len() {
80 if bytes[i] == b'%' && i + 2 < bytes.len() {
81 if let Ok(byte) = u8::from_str_radix(
82 std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or(""),
83 16,
84 ) {
85 out.push(byte);
86 i += 3;
87 continue;
88 }
89 }
90 out.push(bytes[i]);
91 i += 1;
92 }
93 String::from_utf8_lossy(&out).into_owned()
94}
95
96#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(rename_all = "camelCase")]
98pub struct Sort {
99 pub field: String,
100 #[serde(default)]
101 pub ascending: bool,
102}
103
104#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub struct Query {
107 pub model: String,
108 #[serde(default, skip_serializing_if = "Vec::is_empty")]
109 pub select: Vec<String>,
110 #[serde(rename = "where", default, skip_serializing_if = "IndexMap::is_empty")]
111 pub where_: IndexMap<String, JsonValue>,
112 #[serde(default, skip_serializing_if = "Vec::is_empty")]
113 pub filters: Vec<Vec<String>>,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
115 pub page: Option<usize>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub per_page: Option<usize>,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
119 pub sort: Option<Sort>,
120 #[serde(default, skip_serializing_if = "Option::is_none")]
121 pub method: Option<String>,
122}
123
124impl Query {
125 pub fn new(model: impl Into<String>) -> Self {
126 Self {
127 model: model.into(),
128 ..Self::default()
129 }
130 }
131
132 pub fn select(mut self, fields: impl IntoIterator<Item = impl Into<String>>) -> Self {
133 self.select = fields.into_iter().map(Into::into).collect();
134 self
135 }
136
137 pub fn filter_layout<I, J, S>(mut self, rows: I) -> Self
138 where
139 I: IntoIterator<Item = J>,
140 J: IntoIterator<Item = S>,
141 S: Into<String>,
142 {
143 self.filters = rows
144 .into_iter()
145 .map(|row| row.into_iter().map(Into::into).collect())
146 .collect();
147 self
148 }
149
150 pub fn where_eq(mut self, field: impl Into<String>, value: JsonValue) -> Self {
151 self.where_.insert(field.into(), value);
152 self
153 }
154
155 pub fn page(mut self, page: usize, per_page: usize) -> Self {
156 self.page = Some(page);
157 self.per_page = Some(per_page);
158 self
159 }
160
161 pub fn sort_by(mut self, field: impl Into<String>, ascending: bool) -> Self {
162 self.sort = Some(Sort {
163 field: field.into(),
164 ascending,
165 });
166 self
167 }
168}
169
170#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
171#[serde(rename_all = "camelCase")]
172pub struct Manifest {
173 pub base: String,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub id: Option<JsonValue>,
176 pub fields: Vec<String>,
177 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
178 pub filter: IndexMap<String, JsonValue>,
179 #[serde(default, skip_serializing_if = "Vec::is_empty")]
180 pub lookups: Vec<String>,
181 #[serde(default, skip_serializing_if = "Vec::is_empty")]
182 pub inlines: Vec<String>,
183 #[serde(default, skip_serializing_if = "Option::is_none")]
184 pub page: Option<usize>,
185 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub per_page: Option<usize>,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub sort: Option<Sort>,
189 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub method: Option<String>,
191 #[serde(default, skip_serializing_if = "Vec::is_empty")]
193 pub filter_fields: Vec<Vec<String>>,
194}
195
196#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
203 fn parse_empty_string() {
204 let (page, per_page, filters) = parse_query_params("", 5);
205 assert_eq!(page, 1);
206 assert_eq!(per_page, 5);
207 assert!(filters.is_empty());
208 }
209
210 #[test]
211 fn parse_page_and_per_page() {
212 let (page, per_page, filters) = parse_query_params("page=3&per_page=10", 5);
213 assert_eq!(page, 3);
214 assert_eq!(per_page, 10);
215 assert!(filters.is_empty());
216 }
217
218 #[test]
219 fn parse_page_zero_clamps_to_one() {
220 let (page, _, _) = parse_query_params("page=0", 5);
221 assert_eq!(page, 1);
222 }
223
224 #[test]
225 fn parse_filters() {
226 let (page, per_page, filters) = parse_query_params("name=Ali&role=admin", 5);
227 assert_eq!(page, 1);
228 assert_eq!(per_page, 5);
229 assert_eq!(filters["name"], "Ali");
230 assert_eq!(filters["role"], "admin");
231 }
232
233 #[test]
234 fn parse_empty_value_omitted_from_filters() {
235 let (_, _, filters) = parse_query_params("name=", 5);
236 assert!(!filters.contains_key("name"));
237 }
238
239 #[test]
240 fn parse_percent_encoding() {
241 let (_, _, filters) = parse_query_params("name=Ali%20Smith", 5);
242 assert_eq!(filters["name"], "Ali Smith");
243 }
244
245 #[test]
246 fn parse_plus_as_space() {
247 let (_, _, filters) = parse_query_params("name=Ali+Smith", 5);
248 assert_eq!(filters["name"], "Ali Smith");
249 }
250
251 #[test]
252 fn parse_default_per_page_respected() {
253 let (_, per_page, _) = parse_query_params("page=2", 20);
254 assert_eq!(per_page, 20);
255 }
256
257 #[test]
258 fn parse_percent_encoding_multibyte_utf8() {
259 let (_, _, filters) = parse_query_params("name=h%C3%A9llo", 5);
261 assert_eq!(filters["name"], "héllo");
262 }
263
264 #[test]
265 fn parse_repeated_key_joins_with_comma() {
266 let (_, _, filters) = parse_query_params("tags=rust&tags=web", 5);
269 assert_eq!(filters["tags"], "rust,web");
270 }
271
272 #[test]
273 fn parse_single_array_key_not_joined() {
274 let (_, _, filters) = parse_query_params("tags=rust", 5);
276 assert_eq!(filters["tags"], "rust");
277 }
278
279 #[test]
280 fn where_serialises_as_where_not_where_underscore() {
281 use serde_json::json;
282 let q = Query::new("user").where_eq("name", json!("Alice"));
283 let serialised = serde_json::to_value(&q).unwrap();
284 assert!(serialised.get("where").is_some(), "expected 'where' key");
285 assert!(serialised.get("where_").is_none(), "unexpected 'where_' key");
286 let round_trip: Query = serde_json::from_value(serialised).unwrap();
287 assert_eq!(round_trip.where_.get("name"), Some(&json!("Alice")));
288 }
289}