quick_oxibooks_sql/
condition.rs

1use std::fmt::{Display, Write};
2
3/// Macro to create a typed where clause for a given `QuickBooks` item type
4///
5/// # Example
6/// ```rust
7/// use quick_oxibooks_sql::{qb_where, Operator, TypedWhereClause};
8/// use quickbooks_types::Customer;
9///
10/// let clause: TypedWhereClause<Customer> = qb_where!(Customer, display_name, Operator::Like);
11/// ```
12#[macro_export]
13#[cfg(feature = "macros")]
14macro_rules! qb_where {
15    ($item:ty, $first:ident$(.$nested:ident)*, $op:expr) => {
16      {
17        const _: () = {
18          fn _type_check(v: $item) {
19            let _ = v.$first$(.unwrap().$nested)*;
20          }
21        };
22        unsafe {
23          $crate::paste! {
24            TypedWhereClause::<$item>::new(
25                stringify!([<$first:camel>]$(.[<$nested:camel>])*),
26                $op,
27            )
28          }
29        }
30      }
31    };
32}
33
34// Struct representing a typed where clause in a query, for a more safe API
35#[derive(Debug, PartialEq, Clone)]
36pub struct TypedWhereClause<QB> {
37    pub field: &'static str,
38    pub operator: Operator,
39    pub values: Vec<String>,
40    _phantom: std::marker::PhantomData<QB>,
41}
42
43impl<QB> TypedWhereClause<QB> {
44    /// Create a new typed where clause
45    ///
46    /// # Safety
47    /// This function is unsafe because it accepts a raw string slice as the field name.
48    /// The caller must ensure that the field name is valid and corresponds to a field in the
49    /// `QuickBooks` entity.
50    #[must_use]
51    pub unsafe fn new(field: &'static str, operator: Operator) -> Self {
52        Self {
53            field,
54            operator,
55            values: Vec::new(),
56            _phantom: std::marker::PhantomData,
57        }
58    }
59
60    /// Add a value to the typed where clause
61    #[must_use]
62    pub fn add_value<T: Display>(mut self, value: T) -> Self {
63        self.values.push(value.to_string());
64        self
65    }
66
67    /// Add multiple values to the typed where clause from an iterator
68    #[must_use]
69    pub fn add_values<I, T>(mut self, values: I) -> Self
70    where
71        I: Iterator<Item = T>,
72        T: Display,
73    {
74        self.values.extend(values.map(|v| v.to_string()));
75        self
76    }
77}
78
79impl<T> From<TypedWhereClause<T>> for WhereClause {
80    fn from(val: TypedWhereClause<T>) -> Self {
81        WhereClause {
82            field: val.field,
83            operator: val.operator,
84            values: val.values,
85        }
86    }
87}
88
89#[cfg(test)]
90mod typed_where_tests {
91    use super::*;
92    use quickbooks_types::Customer;
93
94    #[test]
95    fn test_typed_where_clause_creation() {
96        #[cfg(feature = "macros")]
97        let clause = qb_where!(Customer, display_name, Operator::Like);
98        #[cfg(not(feature = "macros"))]
99        let clause: TypedWhereClause<Customer> =
100            unsafe { TypedWhereClause::new("DisplayName", Operator::Like) };
101        assert_eq!(clause.field, "DisplayName");
102        assert_eq!(clause.operator, Operator::Like);
103    }
104
105    #[test]
106    fn test_nested_typed_where_clause_creation() {
107        #[cfg(feature = "macros")]
108        let clause = qb_where!(Customer, primary_email_addr.address, Operator::Equal);
109        #[cfg(not(feature = "macros"))]
110        let clause: TypedWhereClause<Customer> =
111            unsafe { TypedWhereClause::new("PrimaryEmailAddr.Address", Operator::Equal) };
112        assert_eq!(clause.field, "PrimaryEmailAddr.Address");
113        assert_eq!(clause.operator, Operator::Equal);
114    }
115}
116
117/// Struct representing a where clause in a query
118#[derive(Debug, PartialEq, Clone)]
119pub struct WhereClause {
120    pub field: &'static str,
121    pub operator: Operator,
122    pub values: Vec<String>,
123}
124
125impl WhereClause {
126    /// Create a new where clause
127    #[must_use]
128    pub fn new(field: &'static str, operator: Operator) -> Self {
129        Self {
130            field,
131            operator,
132            values: Vec::new(),
133        }
134    }
135
136    /// Add a value to the where clause
137    #[must_use]
138    pub fn add_value<T: Display>(mut self, value: T) -> Self {
139        self.values.push(value.to_string());
140        self
141    }
142
143    /// Add multiple values to the where clause from an iterator
144    #[must_use]
145    pub fn add_values<I, T>(mut self, values: I) -> Self
146    where
147        I: Iterator<Item = T>,
148        T: Display,
149    {
150        self.values.extend(values.map(|v| v.to_string()));
151        self
152    }
153}
154
155impl WhereClause {
156    pub fn extend_query(&self, query: &mut String) {
157        let op_str = match self.operator {
158            Operator::In => "IN",
159            Operator::Like => "LIKE",
160            Operator::Equal => "=",
161            Operator::Less => "<",
162            Operator::Greater => ">",
163            Operator::LessEqual => "<=",
164            Operator::GreaterEqual => ">=",
165        };
166
167        if self.operator == Operator::In {
168            write!(query, " {} IN (", self.field).unwrap();
169            for (i, value) in self.values.iter().enumerate() {
170                if i > 0 {
171                    query.push_str(", ");
172                }
173                write!(query, "'{value}'").unwrap();
174            }
175            query.push(')');
176        } else {
177            write!(query, " {} {} '{}'", self.field, op_str, self.values[0]).unwrap();
178        }
179    }
180}
181
182/// Enum representing the operators used in where clauses
183#[derive(Debug, PartialEq, Clone)]
184pub enum Operator {
185    In,
186    Like,
187    Equal,
188    Less,
189    Greater,
190    LessEqual,
191    GreaterEqual,
192}