1use serde::{Deserialize, Deserializer, Serialize};
4use std::collections::HashMap;
5
6const MAX_FILTER_COUNT: usize = 20;
11
12const MAX_FILTER_STRING_LENGTH: usize = 500;
16
17#[derive(Debug, Serialize, Deserialize, Default)]
23pub struct ListQueryParams {
24 pub page: Option<u64>,
26 pub page_size: Option<u64>,
28 pub search: Option<String>,
30 pub sort_by: Option<String>,
32 #[serde(default, deserialize_with = "deserialize_validated_filters")]
37 pub filters: HashMap<String, String>,
38}
39
40fn deserialize_validated_filters<'de, D>(
47 deserializer: D,
48) -> Result<HashMap<String, String>, D::Error>
49where
50 D: Deserializer<'de>,
51{
52 let filters: HashMap<String, String> = HashMap::deserialize(deserializer)?;
53
54 if filters.len() > MAX_FILTER_COUNT {
55 return Err(serde::de::Error::custom(format!(
56 "too many filter parameters: {} (max {})",
57 filters.len(),
58 MAX_FILTER_COUNT
59 )));
60 }
61
62 for (key, value) in &filters {
63 if key.is_empty() {
64 return Err(serde::de::Error::custom("filter key must not be empty"));
65 }
66
67 if key.len() > MAX_FILTER_STRING_LENGTH {
68 return Err(serde::de::Error::custom(format!(
69 "filter key '{}...' exceeds maximum length of {} bytes",
70 &key[..32.min(key.len())],
71 MAX_FILTER_STRING_LENGTH
72 )));
73 }
74
75 if value.len() > MAX_FILTER_STRING_LENGTH {
76 return Err(serde::de::Error::custom(format!(
77 "filter value for '{}' exceeds maximum length of {} bytes",
78 key, MAX_FILTER_STRING_LENGTH
79 )));
80 }
81
82 if !key
84 .chars()
85 .all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
86 {
87 return Err(serde::de::Error::custom(format!(
88 "filter key '{}' contains invalid characters (allowed: alphanumeric, '_', '-', '.')",
89 key
90 )));
91 }
92 }
93
94 Ok(filters)
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct MutationRequest {
100 pub csrf_token: String,
107 #[serde(flatten)]
109 pub data: HashMap<String, serde_json::Value>,
110}
111
112#[derive(Debug, Serialize, Deserialize)]
114pub struct BulkDeleteRequest {
115 pub csrf_token: String,
122 pub ids: Vec<String>,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
128#[serde(rename_all = "lowercase")]
129#[derive(Default)]
130pub enum ExportFormat {
131 #[default]
133 Json,
134 Csv,
136 Tsv,
138}
139
140#[cfg(all(test, server))]
141mod tests {
142 use super::*;
143 use rstest::rstest;
144 use serde_json;
145
146 fn parse_list_query(json: &str) -> Result<ListQueryParams, serde_json::Error> {
148 serde_json::from_str(json)
149 }
150
151 #[rstest]
154 fn test_filters_within_limit_accepted() {
155 let json = r#"{"filters": {"a": "1", "b": "2", "c": "3", "d": "4", "e": "5"}}"#;
157
158 let result = parse_list_query(json);
160
161 assert!(result.is_ok());
163 assert_eq!(result.unwrap().filters.len(), 5);
164 }
165
166 #[rstest]
167 fn test_filters_at_exact_limit_accepted() {
168 let mut filters = serde_json::Map::new();
170 for i in 0..20 {
171 filters.insert(
172 format!("field_{}", i),
173 serde_json::Value::String(format!("value_{}", i)),
174 );
175 }
176 let json = serde_json::json!({"filters": filters}).to_string();
177
178 let result = parse_list_query(&json);
180
181 assert!(result.is_ok());
183 assert_eq!(result.unwrap().filters.len(), 20);
184 }
185
186 #[rstest]
187 fn test_filters_exceeding_max_count_rejected() {
188 let mut filters = serde_json::Map::new();
190 for i in 0..21 {
191 filters.insert(
192 format!("field_{}", i),
193 serde_json::Value::String(format!("value_{}", i)),
194 );
195 }
196 let json = serde_json::json!({"filters": filters}).to_string();
197
198 let result = parse_list_query(&json);
200
201 assert!(result.is_err());
203 let err = result.unwrap_err().to_string();
204 assert!(
205 err.contains("too many filter parameters"),
206 "Error should mention filter count limit: {}",
207 err
208 );
209 }
210
211 #[rstest]
214 fn test_filter_key_exceeding_max_length_rejected() {
215 let long_key = "a".repeat(501);
217 let json = serde_json::json!({"filters": {long_key: "value"}}).to_string();
218
219 let result = parse_list_query(&json);
221
222 assert!(result.is_err());
224 let err = result.unwrap_err().to_string();
225 assert!(
226 err.contains("exceeds maximum length"),
227 "Error should mention length limit: {}",
228 err
229 );
230 }
231
232 #[rstest]
233 fn test_filter_value_exceeding_max_length_rejected() {
234 let long_value = "v".repeat(501);
236 let json = serde_json::json!({"filters": {"field": long_value}}).to_string();
237
238 let result = parse_list_query(&json);
240
241 assert!(result.is_err());
243 let err = result.unwrap_err().to_string();
244 assert!(
245 err.contains("exceeds maximum length"),
246 "Error should mention length limit: {}",
247 err
248 );
249 }
250
251 #[rstest]
254 fn test_empty_filter_key_rejected() {
255 let json = r#"{"filters": {"": "value"}}"#;
257
258 let result = parse_list_query(json);
260
261 assert!(result.is_err());
263 let err = result.unwrap_err().to_string();
264 assert!(
265 err.contains("must not be empty"),
266 "Error should mention empty key: {}",
267 err
268 );
269 }
270
271 #[rstest]
272 #[case("field_name", true)] #[case("field-name", true)] #[case("field.name", true)] #[case("fieldName123", true)] fn test_filter_key_with_valid_chars_accepted(#[case] key: &str, #[case] _expected_valid: bool) {
277 let json = serde_json::json!({"filters": {key: "value"}}).to_string();
279
280 let result = parse_list_query(&json);
282
283 assert!(result.is_ok(), "Key '{}' should be accepted", key);
285 }
286
287 #[rstest]
288 fn test_filter_key_with_invalid_chars_rejected() {
289 let json = r#"{"filters": {"field;DROP TABLE users": "value"}}"#;
291
292 let result = parse_list_query(json);
294
295 assert!(result.is_err());
297 let err = result.unwrap_err().to_string();
298 assert!(
299 err.contains("invalid character"),
300 "Error should mention invalid character: {}",
301 err
302 );
303 }
304
305 #[rstest]
308 fn test_empty_filters_accepted() {
309 let json = r#"{"filters": {}}"#;
311
312 let result = parse_list_query(json);
314
315 assert!(result.is_ok());
317 assert!(result.unwrap().filters.is_empty());
318 }
319
320 #[rstest]
321 fn test_missing_filters_uses_default() {
322 let json = r#"{}"#;
324
325 let result = parse_list_query(json);
327
328 assert!(result.is_ok());
330 assert!(result.unwrap().filters.is_empty());
331 }
332
333 #[rstest]
336 #[case::zero_filters(0, true)]
337 #[case::nineteen_filters(19, true)]
338 #[case::twenty_filters(20, true)]
339 #[case::twentyone_filters(21, false)]
340 fn test_filter_count_boundary(#[case] count: usize, #[case] should_pass: bool) {
341 let mut filters = serde_json::Map::new();
343 for i in 0..count {
344 filters.insert(
345 format!("field_{}", i),
346 serde_json::Value::String(format!("value_{}", i)),
347 );
348 }
349 let json = serde_json::json!({"filters": filters}).to_string();
350
351 let result = parse_list_query(&json);
353
354 assert_eq!(
356 result.is_ok(),
357 should_pass,
358 "count={}, expected pass={}, got {:?}",
359 count,
360 should_pass,
361 result
362 );
363 }
364
365 #[rstest]
368 #[case::short_key(10, true)]
369 #[case::at_limit(500, true)]
370 #[case::above_limit(501, false)]
371 fn test_filter_key_length_boundary(#[case] length: usize, #[case] should_pass: bool) {
372 let key: String = "a".repeat(length);
374 let json = serde_json::json!({"filters": {key: "value"}}).to_string();
375
376 let result = parse_list_query(&json);
378
379 assert_eq!(
381 result.is_ok(),
382 should_pass,
383 "key_length={}, expected pass={}, got {:?}",
384 length,
385 should_pass,
386 result
387 );
388 }
389
390 #[rstest]
393 #[case::alphanumeric("status", true)]
394 #[case::with_underscore("created_at", true)]
395 #[case::with_hyphen("is-active", true)]
396 #[case::with_dot("user.name", true)]
397 #[case::with_semicolon("status;DROP", false)]
398 #[case::with_space("some field", false)]
399 #[case::with_quotes("field\"name", false)]
400 fn test_filter_key_format_equivalence(#[case] key: &str, #[case] should_pass: bool) {
401 let mut filters = HashMap::new();
403 filters.insert(key.to_string(), "value".to_string());
404 let json = serde_json::json!({"filters": filters}).to_string();
405
406 let result = parse_list_query(&json);
408
409 assert_eq!(
411 result.is_ok(),
412 should_pass,
413 "key='{}', expected pass={}, got {:?}",
414 key,
415 should_pass,
416 result
417 );
418 }
419}