use serde::{Deserialize, Deserializer, Serialize};
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use crate::types::DateHierarchySelection;
const MAX_FILTER_COUNT: usize = 20;
const MAX_FILTER_STRING_LENGTH: usize = 500;
#[derive(Debug, Clone, 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>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DateHierarchyListQueryParams {
#[serde(flatten)]
pub list: ListQueryParams,
pub date_hierarchy: Option<DateHierarchySelection>,
}
impl From<ListQueryParams> for DateHierarchyListQueryParams {
fn from(list: ListQueryParams) -> Self {
Self {
list,
date_hierarchy: None,
}
}
}
impl Deref for DateHierarchyListQueryParams {
type Target = ListQueryParams;
fn deref(&self) -> &Self::Target {
&self.list
}
}
impl DerefMut for DateHierarchyListQueryParams {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.list
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "mode", rename_all = "snake_case")]
pub enum RelationLookupRequest {
Search {
query: String,
page: Option<u64>,
page_size: Option<u64>,
},
Resolve {
id: 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, Clone, Serialize, Deserialize)]
pub struct InlineEditRequest {
pub csrf_token: String,
pub updates: Vec<InlineEditMutation>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InlineEditMutation {
pub object_id: String,
pub changes: HashMap<String, serde_json::Value>,
#[serde(default)]
pub json_fields: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BulkDeleteRequest {
pub csrf_token: String,
pub ids: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdminActionRequest {
pub csrf_token: String,
pub action: String,
pub ids: Vec<String>,
}
impl AdminActionRequest {
pub fn new(csrf_token: impl Into<String>, action: impl Into<String>, ids: Vec<String>) -> Self {
Self {
csrf_token: csrf_token.into(),
action: action.into(),
ids,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
#[derive(Default)]
pub enum ExportFormat {
#[default]
Json,
Csv,
Tsv,
}
#[cfg(all(test, server))]
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]
#[case(
RelationLookupRequest::Search {
query: "ada".to_string(),
page: Some(2),
page_size: Some(25),
},
serde_json::json!({
"mode": "search",
"query": "ada",
"page": 2,
"page_size": 25
})
)]
#[case(
RelationLookupRequest::Resolve {
id: "42".to_string(),
},
serde_json::json!({"mode": "resolve", "id": "42"})
)]
fn relation_lookup_request_round_trips(
#[case] request: RelationLookupRequest,
#[case] expected: serde_json::Value,
) {
let serialized =
serde_json::to_value(&request).expect("relation lookup request should serialize");
let deserialized: RelationLookupRequest = serde_json::from_value(serialized.clone())
.expect("relation lookup request should deserialize");
assert_eq!(serialized, expected);
assert_eq!(deserialized, request);
}
#[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
);
}
#[test]
fn date_hierarchy_query_serializes_as_flattened_list_request() {
let params = DateHierarchyListQueryParams {
list: ListQueryParams {
page: Some(2),
..Default::default()
},
date_hierarchy: Some(DateHierarchySelection {
year: Some(2024),
month: Some(2),
day: None,
}),
};
let value = serde_json::to_value(params).expect("date hierarchy request should serialize");
assert_eq!(value["page"], serde_json::json!(2));
assert_eq!(value["date_hierarchy"]["year"], serde_json::json!(2024));
assert_eq!(value["date_hierarchy"]["month"], serde_json::json!(2));
assert!(value.get("list").is_none());
}
}