use chrono::{DateTime, TimeZone};
use crate::model::Timestamp;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Op {
Eq,
Ne,
Gt,
Gte,
Lt,
Lte,
In,
Nin,
Regex,
}
impl Op {
const fn v3(self) -> &'static str {
match self {
Self::Eq => "eq",
Self::Ne => "ne",
Self::Gt => "gt",
Self::Gte => "gte",
Self::Lt => "lt",
Self::Lte => "lte",
Self::In => "in",
Self::Nin => "nin",
Self::Regex => "re",
}
}
const fn v1(self) -> &'static str {
match self {
Self::Eq => "$eq",
Self::Ne => "$ne",
Self::Gt => "$gt",
Self::Gte => "$gte",
Self::Lt => "$lt",
Self::Lte => "$lte",
Self::In => "$in",
Self::Nin => "$nin",
Self::Regex => "$regex",
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum Value {
Text(String),
Number(f64),
Bool(bool),
Time(Timestamp),
List(Vec<String>),
}
impl From<&str> for Value {
fn from(v: &str) -> Self {
Self::Text(v.to_owned())
}
}
impl From<String> for Value {
fn from(v: String) -> Self {
Self::Text(v)
}
}
impl From<&String> for Value {
fn from(v: &String) -> Self {
Self::Text(v.clone())
}
}
impl From<bool> for Value {
fn from(v: bool) -> Self {
Self::Bool(v)
}
}
impl From<Timestamp> for Value {
fn from(v: Timestamp) -> Self {
Self::Time(v)
}
}
impl<Tz: TimeZone> From<DateTime<Tz>> for Value {
fn from(v: DateTime<Tz>) -> Self {
Self::Time(v.into())
}
}
macro_rules! numeric_value {
($($t:ty),*) => {$(
impl From<$t> for Value {
fn from(v: $t) -> Self {
Self::Number(f64::from(v))
}
}
)*};
}
numeric_value!(f64, f32, i32, u32, i16, u16, i8, u8);
impl From<i64> for Value {
fn from(v: i64) -> Self {
Self::Number(v as f64)
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Condition {
pub(crate) field: String,
pub(crate) op: Op,
pub(crate) value: Value,
}
#[derive(Debug, Clone, Default, PartialEq)]
#[must_use]
pub struct Filter {
pub(crate) conditions: Vec<Condition>,
}
impl Filter {
pub const fn new() -> Self {
Self {
conditions: Vec::new(),
}
}
pub fn with(mut self, field: impl Into<String>, op: Op, value: impl Into<Value>) -> Self {
self.conditions.push(Condition {
field: field.into(),
op,
value: value.into(),
});
self
}
pub fn eq(self, field: impl Into<String>, value: impl Into<Value>) -> Self {
self.with(field, Op::Eq, value)
}
pub fn ne(self, field: impl Into<String>, value: impl Into<Value>) -> Self {
self.with(field, Op::Ne, value)
}
pub fn gt(self, field: impl Into<String>, value: impl Into<Value>) -> Self {
self.with(field, Op::Gt, value)
}
pub fn gte(self, field: impl Into<String>, value: impl Into<Value>) -> Self {
self.with(field, Op::Gte, value)
}
pub fn lt(self, field: impl Into<String>, value: impl Into<Value>) -> Self {
self.with(field, Op::Lt, value)
}
pub fn lte(self, field: impl Into<String>, value: impl Into<Value>) -> Self {
self.with(field, Op::Lte, value)
}
pub fn one_of<I, S>(self, field: impl Into<String>, values: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let list = values.into_iter().map(Into::into).collect();
self.with(field, Op::In, Value::List(list))
}
pub fn none_of<I, S>(self, field: impl Into<String>, values: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let list = values.into_iter().map(Into::into).collect();
self.with(field, Op::Nin, Value::List(list))
}
pub fn matches(self, field: impl Into<String>, pattern: impl Into<String>) -> Self {
self.with(field, Op::Regex, Value::Text(pattern.into()))
}
pub fn and(mut self, other: Self) -> Self {
self.conditions.extend(other.conditions);
self
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.conditions.is_empty()
}
pub(crate) fn mentions(&self, field: &str) -> bool {
self.conditions.iter().any(|c| c.field == field)
}
pub(crate) fn v3_pairs(&self) -> Vec<(String, String)> {
self.conditions
.iter()
.map(|c| {
let value = match &c.value {
Value::Text(s) if c.op != Op::Regex && needs_v3_quotes(s) => format!("'{s}'"),
Value::List(items) => items.join("|"),
other => encode_scalar(&c.field, other),
};
(format!("{}${}", c.field, c.op.v3()), value)
})
.collect()
}
pub(crate) fn v1_pairs(&self) -> Vec<(String, String)> {
let mut pairs = Vec::new();
for c in &self.conditions {
match (&c.op, &c.value) {
(Op::In | Op::Nin, Value::List(items)) => {
let key = format!("find[{}][{}][]", c.field, c.op.v1());
pairs.extend(items.iter().map(|item| (key.clone(), item.clone())));
}
(Op::Eq, value) => {
pairs.push((format!("find[{}]", c.field), encode_scalar(&c.field, value)));
}
(op, value) => pairs.push((
format!("find[{}][{}]", c.field, op.v1()),
encode_scalar(&c.field, value),
)),
}
}
pairs
}
}
fn is_millis_field(field: &str) -> bool {
matches!(field, "date" | "mills" | "srvModified" | "srvCreated")
}
fn encode_scalar(field: &str, value: &Value) -> String {
match value {
Value::Text(s) => s.clone(),
Value::Number(n) => format_number(*n),
Value::Bool(b) => b.to_string(),
Value::Time(t) if is_millis_field(field) => t.as_millis().to_string(),
Value::Time(t) => t.to_iso(),
Value::List(items) => items.join("|"),
}
}
pub(crate) fn format_number(n: f64) -> String {
if n.fract() == 0.0 && n.abs() < 9.0e15 {
format!("{}", n as i64)
} else {
n.to_string()
}
}
fn needs_v3_quotes(s: &str) -> bool {
s.trim().parse::<f64>().is_ok() || matches!(s, "true" | "false")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn v3_encoding() {
let f = Filter::new()
.eq("device", "xDrip")
.eq("sgv", "171")
.gte("date", Timestamp::from_millis(1000))
.lt("created_at", Timestamp::from_millis(1_714_564_800_000))
.one_of("type", ["sgv", "mbg"])
.matches("notes", "^pizza");
assert_eq!(
f.v3_pairs(),
vec![
("device$eq".into(), "xDrip".into()),
("sgv$eq".into(), "'171'".into()),
("date$gte".into(), "1000".into()),
("created_at$lt".into(), "2024-05-01T12:00:00.000Z".into()),
("type$in".into(), "sgv|mbg".into()),
("notes$re".into(), "^pizza".into()),
]
);
}
#[test]
fn v1_encoding() {
let f = Filter::new()
.eq("type", "sgv")
.gte("sgv", 180)
.none_of("device", ["a", "b"]);
assert_eq!(
f.v1_pairs(),
vec![
("find[type]".into(), "sgv".into()),
("find[sgv][$gte]".into(), "180".into()),
("find[device][$nin][]".into(), "a".into()),
("find[device][$nin][]".into(), "b".into()),
]
);
}
}