use std::collections::HashMap;
use serde::Serialize;
use umbral::migrate::Column;
use crate::config::AdminConfig;
use crate::q;
pub(crate) type ListParams = (
Option<String>,
Vec<(String, String)>,
String,
String,
usize,
usize,
);
pub(crate) fn parse_list_params(
params: &HashMap<String, String>,
cfg: Option<&AdminConfig>,
pk: &Column,
) -> ListParams {
let search_term = params
.get("search")
.filter(|s| !s.is_empty())
.or_else(|| params.get("q").filter(|s| !s.is_empty()))
.cloned();
let active_filters = parse_active_filters(params);
let sort_col = params.get("sort").cloned().unwrap_or_default();
let sort_order = params
.get("order")
.map(|o| {
if o == "desc" {
"desc".to_string()
} else {
"asc".to_string()
}
})
.unwrap_or_else(|| "asc".to_string());
let page = params
.get("page")
.and_then(|p| p.parse::<usize>().ok())
.unwrap_or(1);
let default_page_size = cfg.map(|c| c.list_per_page).unwrap_or(25);
let page_size = params
.get("page_size")
.and_then(|p| p.parse::<usize>().ok())
.unwrap_or(default_page_size)
.clamp(1, 200);
let _ = pk; (
search_term,
active_filters,
sort_col,
sort_order,
page,
page_size,
)
}
fn parse_active_filters(params: &HashMap<String, String>) -> Vec<(String, String)> {
let mut active: Vec<(String, String)> = params
.iter()
.filter_map(|(k, v)| {
k.strip_prefix("filter_")
.filter(|_| !v.is_empty())
.map(|field| (field.to_string(), v.clone()))
})
.collect();
if active.is_empty() {
if let Some(s) = params.get("filter").filter(|s| !s.is_empty()) {
let mut parts = s.splitn(2, '=');
if let (Some(field), Some(value)) = (parts.next(), parts.next()) {
active.push((field.to_string(), value.to_string()));
}
}
}
active.sort_by(|a, b| a.0.cmp(&b.0));
active
}
pub(crate) fn build_order_clause(cfg: Option<&AdminConfig>, pk: &Column) -> String {
let ordering = cfg.map(|c| c.ordering.as_slice()).unwrap_or(&[]);
if ordering.is_empty() {
return format!("\"{}\" ASC", q(&pk.name));
}
ordering
.iter()
.map(|s| {
if let Some(col) = s.strip_prefix('-') {
format!("\"{}\" DESC", q(col))
} else {
format!("\"{}\" ASC", q(s))
}
})
.collect::<Vec<_>>()
.join(", ")
}
pub(crate) fn build_order_clause_phase2(
cfg: Option<&AdminConfig>,
pk: &Column,
sort_col: &str,
sort_order: &str,
) -> String {
if !sort_col.is_empty() {
let dir = if sort_order == "desc" { "DESC" } else { "ASC" };
return format!("\"{}\" {}", q(sort_col), dir);
}
build_order_clause(cfg, pk)
}
#[derive(Debug, Clone, Serialize)]
pub(crate) struct Pagination {
pub page: usize,
pub page_size: usize,
pub total: usize,
pub total_pages: usize,
}
impl Pagination {
pub fn new(total: usize, page: usize, page_size: usize) -> Self {
let page_size = page_size.max(1);
let total_pages = total.div_ceil(page_size).max(1);
let page = page.max(1).min(total_pages);
Self {
page,
page_size,
total,
total_pages,
}
}
pub fn offset(&self) -> usize {
(self.page - 1) * self.page_size
}
}
#[cfg(test)]
mod tests {
use super::*;
fn p(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn collects_every_filter_underscore_param_sorted_by_field() {
let filters = parse_active_filters(&p(&[
("filter_status", "published"),
("filter_author", "alice"),
("filter_tag", "rust"),
]));
assert_eq!(
filters,
vec![
("author".into(), "alice".into()),
("status".into(), "published".into()),
("tag".into(), "rust".into()),
]
);
}
#[test]
fn skips_empty_filter_values() {
let filters =
parse_active_filters(&p(&[("filter_status", ""), ("filter_author", "alice")]));
assert_eq!(filters, vec![("author".into(), "alice".into())]);
}
#[test]
fn falls_back_to_legacy_single_filter_when_no_named_params() {
let filters = parse_active_filters(&p(&[("filter", "status=archived")]));
assert_eq!(filters, vec![("status".into(), "archived".into())]);
}
#[test]
fn named_filters_win_over_legacy_single_form() {
let filters = parse_active_filters(&p(&[
("filter", "status=archived"),
("filter_status", "published"),
]));
assert_eq!(filters, vec![("status".into(), "published".into())]);
}
#[test]
fn empty_params_yield_empty_filter_list() {
assert!(parse_active_filters(&HashMap::new()).is_empty());
}
}