use serde::{Deserialize, Deserializer, Serialize};
use std::collections::HashMap;
const MAX_FILTER_COUNT: usize = 20;
const MAX_FILTER_STRING_LENGTH: usize = 500;
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct ListQueryParams {
pub page: Option<u64>,
pub page_size: Option<u64>,
pub search: Option<String>,
pub sort_by: Option<String>,
#[serde(default, deserialize_with = "deserialize_validated_filters")]
pub filters: HashMap<String, String>,
}
fn deserialize_validated_filters<'de, D>(
deserializer: D,
) -> Result<HashMap<String, String>, D::Error>
where
D: Deserializer<'de>,
{
let filters: HashMap<String, String> = HashMap::deserialize(deserializer)?;
if filters.len() > MAX_FILTER_COUNT {
return Err(serde::de::Error::custom(format!(
"too many filter parameters: {} (max {})",
filters.len(),
MAX_FILTER_COUNT
)));
}
for (key, value) in &filters {
if key.is_empty() {
return Err(serde::de::Error::custom("filter key must not be empty"));
}
if key.len() > MAX_FILTER_STRING_LENGTH {
return Err(serde::de::Error::custom(format!(
"filter key '{}...' exceeds maximum length of {} bytes",
&key[..32.min(key.len())],
MAX_FILTER_STRING_LENGTH
)));
}
if value.len() > MAX_FILTER_STRING_LENGTH {
return Err(serde::de::Error::custom(format!(
"filter value for '{}' exceeds maximum length of {} bytes",
key, MAX_FILTER_STRING_LENGTH
)));
}
if !key
.chars()
.all(|c| c.is_alphanumeric() || c == '_' || c == '-' || c == '.')
{
return Err(serde::de::Error::custom(format!(
"filter key '{}' contains invalid characters (allowed: alphanumeric, '_', '-', '.')",
key
)));
}
}
Ok(filters)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MutationRequest {
pub csrf_token: String,
#[serde(flatten)]
pub data: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BulkDeleteRequest {
pub csrf_token: String,
pub ids: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum ExportFormat {
#[default]
Json,
Csv,
Tsv,
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
use serde_json;
fn parse_list_query(json: &str) -> Result<ListQueryParams, serde_json::Error> {
serde_json::from_str(json)
}
#[rstest]
fn test_filters_within_limit_accepted() {
let json = r#"{"filters": {"a": "1", "b": "2", "c": "3", "d": "4", "e": "5"}}"#;
let result = parse_list_query(json);
assert!(result.is_ok());
assert_eq!(result.unwrap().filters.len(), 5);
}
#[rstest]
fn test_filters_at_exact_limit_accepted() {
let mut filters = serde_json::Map::new();
for i in 0..20 {
filters.insert(
format!("field_{}", i),
serde_json::Value::String(format!("value_{}", i)),
);
}
let json = serde_json::json!({"filters": filters}).to_string();
let result = parse_list_query(&json);
assert!(result.is_ok());
assert_eq!(result.unwrap().filters.len(), 20);
}
#[rstest]
fn test_filters_exceeding_max_count_rejected() {
let mut filters = serde_json::Map::new();
for i in 0..21 {
filters.insert(
format!("field_{}", i),
serde_json::Value::String(format!("value_{}", i)),
);
}
let json = serde_json::json!({"filters": filters}).to_string();
let result = parse_list_query(&json);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("too many filter parameters"),
"Error should mention filter count limit: {}",
err
);
}
#[rstest]
fn test_filter_key_exceeding_max_length_rejected() {
let long_key = "a".repeat(501);
let json = serde_json::json!({"filters": {long_key: "value"}}).to_string();
let result = parse_list_query(&json);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("exceeds maximum length"),
"Error should mention length limit: {}",
err
);
}
#[rstest]
fn test_filter_value_exceeding_max_length_rejected() {
let long_value = "v".repeat(501);
let json = serde_json::json!({"filters": {"field": long_value}}).to_string();
let result = parse_list_query(&json);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("exceeds maximum length"),
"Error should mention length limit: {}",
err
);
}
#[rstest]
fn test_empty_filter_key_rejected() {
let json = r#"{"filters": {"": "value"}}"#;
let result = parse_list_query(json);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("must not be empty"),
"Error should mention empty key: {}",
err
);
}
#[rstest]
#[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) {
let json = serde_json::json!({"filters": {key: "value"}}).to_string();
let result = parse_list_query(&json);
assert!(result.is_ok(), "Key '{}' should be accepted", key);
}
#[rstest]
fn test_filter_key_with_invalid_chars_rejected() {
let json = r#"{"filters": {"field;DROP TABLE users": "value"}}"#;
let result = parse_list_query(json);
assert!(result.is_err());
let err = result.unwrap_err().to_string();
assert!(
err.contains("invalid character"),
"Error should mention invalid character: {}",
err
);
}
#[rstest]
fn test_empty_filters_accepted() {
let json = r#"{"filters": {}}"#;
let result = parse_list_query(json);
assert!(result.is_ok());
assert!(result.unwrap().filters.is_empty());
}
#[rstest]
fn test_missing_filters_uses_default() {
let json = r#"{}"#;
let result = parse_list_query(json);
assert!(result.is_ok());
assert!(result.unwrap().filters.is_empty());
}
#[rstest]
#[case::zero_filters(0, true)]
#[case::nineteen_filters(19, true)]
#[case::twenty_filters(20, true)]
#[case::twentyone_filters(21, false)]
fn test_filter_count_boundary(#[case] count: usize, #[case] should_pass: bool) {
let mut filters = serde_json::Map::new();
for i in 0..count {
filters.insert(
format!("field_{}", i),
serde_json::Value::String(format!("value_{}", i)),
);
}
let json = serde_json::json!({"filters": filters}).to_string();
let result = parse_list_query(&json);
assert_eq!(
result.is_ok(),
should_pass,
"count={}, expected pass={}, got {:?}",
count,
should_pass,
result
);
}
#[rstest]
#[case::short_key(10, true)]
#[case::at_limit(500, true)]
#[case::above_limit(501, false)]
fn test_filter_key_length_boundary(#[case] length: usize, #[case] should_pass: bool) {
let key: String = "a".repeat(length);
let json = serde_json::json!({"filters": {key: "value"}}).to_string();
let result = parse_list_query(&json);
assert_eq!(
result.is_ok(),
should_pass,
"key_length={}, expected pass={}, got {:?}",
length,
should_pass,
result
);
}
#[rstest]
#[case::alphanumeric("status", true)]
#[case::with_underscore("created_at", true)]
#[case::with_hyphen("is-active", true)]
#[case::with_dot("user.name", true)]
#[case::with_semicolon("status;DROP", false)]
#[case::with_space("some field", false)]
#[case::with_quotes("field\"name", false)]
fn test_filter_key_format_equivalence(#[case] key: &str, #[case] should_pass: bool) {
let mut filters = HashMap::new();
filters.insert(key.to_string(), "value".to_string());
let json = serde_json::json!({"filters": filters}).to_string();
let result = parse_list_query(&json);
assert_eq!(
result.is_ok(),
should_pass,
"key='{}', expected pass={}, got {:?}",
key,
should_pass,
result
);
}
}