use chrono::{Datelike, NaiveDate};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SearchOperator {
Equals,
Exact,
Greater,
Less,
Like,
In,
Sort,
Diff,
NotLike,
NotIn,
}
impl SearchOperator {
pub fn as_str(&self) -> &'static str {
match self {
SearchOperator::Equals => "equals",
SearchOperator::Exact => "exact",
SearchOperator::Greater => "greater",
SearchOperator::Less => "less",
SearchOperator::Like => "like",
SearchOperator::In => "in",
SearchOperator::Sort => "sort",
SearchOperator::Diff => "diff",
SearchOperator::NotLike => "notlike",
SearchOperator::NotIn => "notin",
}
}
}
pub fn make_search_field(field: &str, value: &str, operator: Option<SearchOperator>) -> String {
let op = operator.unwrap_or(SearchOperator::Equals);
format!("{}[{}]={}", field, op.as_str(), value)
}
pub fn make_search_field_multi(field: &str, values: &[&str], operator: SearchOperator) -> String {
let joined = values.join(",");
format!("{}[{}]={}", field, operator.as_str(), joined)
}
pub fn make_payrix_date(date: &NaiveDate) -> String {
format!("{:04}{:02}{:02}", date.year(), date.month(), date.day())
}
pub fn parse_payrix_date(date_str: &str) -> Option<NaiveDate> {
if date_str.len() != 8 {
return None;
}
let year = date_str[0..4].parse().ok()?;
let month = date_str[4..6].parse().ok()?;
let day = date_str[6..8].parse().ok()?;
NaiveDate::from_ymd_opt(year, month, day)
}
pub fn build_expand_query(expand: &[&str]) -> String {
expand
.iter()
.map(|e| {
if e.contains('|') {
let parts: Vec<&str> = e.split('|').collect();
format!(
"expand{}",
parts.iter().map(|p| format!("[{}][]", p)).collect::<String>()
)
} else {
format!("expand[{}][]", e)
}
})
.collect::<Vec<_>>()
.join("&")
}
#[derive(Debug, Clone, Default)]
pub struct SearchBuilder {
parts: Vec<String>,
expand_fields: Vec<String>,
}
impl SearchBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn field(mut self, name: &str, value: &str) -> Self {
self.parts.push(make_search_field(name, value, None));
self
}
pub fn field_with_op(mut self, name: &str, value: &str, operator: SearchOperator) -> Self {
self.parts
.push(make_search_field(name, value, Some(operator)));
self
}
pub fn field_multi(mut self, name: &str, values: &[&str], operator: SearchOperator) -> Self {
self.parts
.push(make_search_field_multi(name, values, operator));
self
}
pub fn expand(mut self, field: &str) -> Self {
self.expand_fields.push(field.to_string());
self
}
pub fn raw(mut self, search: &str) -> Self {
self.parts.push(search.to_string());
self
}
pub fn or_group(mut self, conditions: &[(&str, &str, SearchOperator)]) -> Self {
for (field, value, operator) in conditions {
self.parts
.push(format!("[or]{}[{}]={}", field, operator.as_str(), value));
}
self
}
pub fn build(self) -> String {
let mut all_parts = self.parts;
if !self.expand_fields.is_empty() {
let expand_query = build_expand_query(
&self.expand_fields.iter().map(|s| s.as_str()).collect::<Vec<_>>()
);
all_parts.push(expand_query);
}
all_parts.join("&")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_make_search_field() {
assert_eq!(
make_search_field("merchant", "mer_123", None),
"merchant[equals]=mer_123"
);
assert_eq!(
make_search_field("created", "20240101", Some(SearchOperator::Greater)),
"created[greater]=20240101"
);
assert_eq!(
make_search_field("name", "%test%", Some(SearchOperator::Like)),
"name[like]=%test%"
);
}
#[test]
fn test_make_search_field_multi() {
assert_eq!(
make_search_field_multi("id", &["a", "b", "c"], SearchOperator::In),
"id[in]=a,b,c"
);
}
#[test]
fn test_payrix_date() {
let date = NaiveDate::from_ymd_opt(2024, 3, 15).unwrap();
assert_eq!(make_payrix_date(&date), "20240315");
assert_eq!(parse_payrix_date("20240315"), Some(date));
assert_eq!(parse_payrix_date("invalid"), None);
}
#[test]
fn test_build_expand_query() {
assert_eq!(
build_expand_query(&["token", "customer"]),
"expand[token][]&expand[customer][]"
);
assert_eq!(
build_expand_query(&["token|customer"]),
"expand[token][][customer][]"
);
}
#[test]
fn test_search_builder() {
let search = SearchBuilder::new()
.field("merchant", "mer_123")
.field_with_op("created", "20240101", SearchOperator::Greater)
.build();
assert_eq!(
search,
"merchant[equals]=mer_123&created[greater]=20240101"
);
}
#[test]
fn test_date_range_filtering() {
let start = NaiveDate::from_ymd_opt(2025, 12, 1).unwrap();
let end = NaiveDate::from_ymd_opt(2026, 1, 1).unwrap();
let search = SearchBuilder::new()
.field("merchant", "mer_123")
.field_with_op("created", &make_payrix_date(&start), SearchOperator::Greater)
.field_with_op("created", &make_payrix_date(&end), SearchOperator::Less)
.build();
assert_eq!(
search,
"merchant[equals]=mer_123&created[greater]=20251201&created[less]=20260101"
);
let date_str = make_payrix_date(&start);
assert!(!date_str.contains('-'), "Date format should not contain dashes");
assert_eq!(date_str.len(), 8, "Date format should be 8 characters (YYYYMMDD)");
}
#[test]
fn test_available_operators() {
assert_eq!(SearchOperator::Equals.as_str(), "equals");
assert_eq!(SearchOperator::Exact.as_str(), "exact");
assert_eq!(SearchOperator::Greater.as_str(), "greater");
assert_eq!(SearchOperator::Less.as_str(), "less");
assert_eq!(SearchOperator::Like.as_str(), "like");
assert_eq!(SearchOperator::In.as_str(), "in");
assert_eq!(SearchOperator::Sort.as_str(), "sort");
assert_eq!(SearchOperator::Diff.as_str(), "diff");
assert_eq!(SearchOperator::NotLike.as_str(), "notlike");
assert_eq!(SearchOperator::NotIn.as_str(), "notin");
}
#[test]
fn test_or_group() {
let search = SearchBuilder::new()
.or_group(&[
("inactive", "0", SearchOperator::Equals),
("inactive", "1", SearchOperator::Equals),
])
.build();
assert_eq!(search, "[or]inactive[equals]=0&[or]inactive[equals]=1");
}
#[test]
fn test_or_group_with_other_conditions() {
let search = SearchBuilder::new()
.field("merchant", "mer_123")
.or_group(&[
("status", "1", SearchOperator::Equals),
("status", "2", SearchOperator::Equals),
])
.build();
assert_eq!(
search,
"merchant[equals]=mer_123&[or]status[equals]=1&[or]status[equals]=2"
);
}
}