use chrono::{DateTime, Utc};
use serde::Serialize;
#[derive(Debug, Default)]
pub struct QueryBuilder {
params: Vec<(String, String)>,
}
impl QueryBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn string(mut self, key: &str, value: impl Into<Option<String>>) -> Self {
if let Some(v) = value.into() {
self.params.push((key.to_string(), v));
}
self
}
pub fn string_array<I, T>(mut self, key: &str, values: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<Option<String>>,
{
for value in values {
if let Some(v) = value.into() {
self.params.push((key.to_string(), v));
}
}
self
}
pub fn int(mut self, key: &str, value: impl Into<Option<i64>>) -> Self {
if let Some(v) = value.into() {
self.params.push((key.to_string(), v.to_string()));
}
self
}
pub fn big_int(mut self, key: &str, value: impl Into<Option<num_bigint::BigInt>>) -> Self {
if let Some(v) = value.into() {
self.params.push((key.to_string(), v.to_string()));
}
self
}
pub fn int_array<I, T>(mut self, key: &str, values: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<Option<i64>>,
{
for value in values {
if let Some(v) = value.into() {
self.params.push((key.to_string(), v.to_string()));
}
}
self
}
pub fn float(mut self, key: &str, value: impl Into<Option<f64>>) -> Self {
if let Some(v) = value.into() {
self.params.push((key.to_string(), v.to_string()));
}
self
}
pub fn float_array<I, T>(mut self, key: &str, values: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<Option<f64>>,
{
for value in values {
if let Some(v) = value.into() {
self.params.push((key.to_string(), v.to_string()));
}
}
self
}
pub fn bool(mut self, key: &str, value: impl Into<Option<bool>>) -> Self {
if let Some(v) = value.into() {
self.params.push((key.to_string(), v.to_string()));
}
self
}
pub fn bool_array<I, T>(mut self, key: &str, values: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<Option<bool>>,
{
for value in values {
if let Some(v) = value.into() {
self.params.push((key.to_string(), v.to_string()));
}
}
self
}
pub fn datetime(mut self, key: &str, value: impl Into<Option<DateTime<Utc>>>) -> Self {
if let Some(v) = value.into() {
self.params.push((
key.to_string(),
v.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
));
}
self
}
pub fn uuid(mut self, key: &str, value: impl Into<Option<uuid::Uuid>>) -> Self {
if let Some(v) = value.into() {
self.params.push((key.to_string(), v.to_string()));
}
self
}
pub fn date(mut self, key: &str, value: impl Into<Option<chrono::NaiveDate>>) -> Self {
if let Some(v) = value.into() {
let datetime = v.and_hms_opt(0, 0, 0).unwrap().and_utc();
self.params.push((
key.to_string(),
datetime.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
));
}
self
}
pub fn serialize<T: Serialize>(mut self, key: &str, value: Option<T>) -> Self {
if let Some(v) = value {
if let Ok(serialized) = serde_json::to_string(&v) {
let cleaned = if serialized.starts_with('"') && serialized.ends_with('"') {
serialized.trim_matches('"').to_string()
} else {
serialized
};
self.params.push((key.to_string(), cleaned));
}
}
self
}
pub fn serialize_array<T: Serialize>(
mut self,
key: &str,
values: impl IntoIterator<Item = T>,
) -> Self {
for value in values {
if let Ok(serialized) = serde_json::to_string(&value) {
if serialized == "null" {
continue;
}
let cleaned = if serialized.starts_with('"') && serialized.ends_with('"') {
serialized.trim_matches('"').to_string()
} else {
serialized
};
self.params.push((key.to_string(), cleaned));
}
}
self
}
pub fn structured_query(mut self, key: &str, value: impl Into<Option<String>>) -> Self {
if let Some(query_str) = value.into() {
if let Ok(parsed_params) = parse_structured_query(&query_str) {
self.params.extend(parsed_params);
} else {
self.params.push((key.to_string(), query_str));
}
}
self
}
pub fn build(self) -> Option<Vec<(String, String)>> {
if self.params.is_empty() {
None
} else {
Some(self.params)
}
}
}
#[derive(Debug)]
pub enum QueryBuilderError {
InvalidQuerySyntax(String),
}
impl std::fmt::Display for QueryBuilderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
QueryBuilderError::InvalidQuerySyntax(msg) => {
write!(f, "Invalid query syntax: {}", msg)
}
}
}
}
impl std::error::Error for QueryBuilderError {}
pub fn parse_structured_query(query: &str) -> Result<Vec<(String, String)>, QueryBuilderError> {
let mut params = Vec::new();
let terms = tokenize_query(query);
for term in terms {
if let Some((key, values)) = term.split_once(':') {
for value in values.split(',') {
let clean_value = value.trim_matches('"'); params.push((key.to_string(), clean_value.to_string()));
}
} else {
return Err(QueryBuilderError::InvalidQuerySyntax(format!(
"Cannot parse term '{}' - expected 'key:value' format for structured queries",
term
)));
}
}
Ok(params)
}
fn tokenize_query(input: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut current_token = String::new();
let mut in_quotes = false;
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
match c {
'"' => {
in_quotes = !in_quotes;
current_token.push(c);
}
' ' if !in_quotes => {
if !current_token.is_empty() {
tokens.push(current_token.trim().to_string());
current_token.clear();
}
}
_ => {
current_token.push(c);
}
}
}
if !current_token.is_empty() {
tokens.push(current_token.trim().to_string());
}
tokens
}