quick_oxibooks_sql_macro/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{
4    Ident, LitInt, Token, Type,
5    parse::{Parse, ParseStream},
6    punctuated::Punctuated,
7};
8
9/// Builds a type-safe QuickBooks Online query at compile time.
10///
11/// This macro parses SQL-like syntax and generates a `Query<T>` struct that can be used to query
12/// the QuickBooks Online API. Field names are automatically validated at compile time and converted
13/// from snake_case to CamelCase to match QuickBooks naming conventions.
14///
15/// # Syntax
16///
17/// ```text
18/// qb_sql!(
19///     select [* | field1, field2, ...]
20///     from EntityType
21///     [where condition [and condition ...]]
22///     [order by field [asc|desc] [, field [asc|desc] ...]]
23///     [limit number [offset number]]
24/// )
25/// ```
26///
27/// # Supported Operators
28///
29/// - `=` - Equality comparison
30/// - `>`, `<`, `>=`, `<=` - Numeric comparisons
31/// - `like` - Pattern matching (use `%` as wildcard)
32/// - `in` - Match against multiple values: `field in (val1, val2, ...)` or `field in (iterator)`
33///
34/// # Examples
35///
36/// Basic query with field selection:
37/// ```ignore
38/// use quick_oxibooks_sql::qb_sql;
39/// use quickbooks_types::Customer;
40///
41/// let query = qb_sql!(
42///     select display_name, balance from Customer
43///     where balance >= 1000.0
44///     order by display_name asc
45///     limit 10
46/// );
47/// ```
48///
49/// Using Rust variables in conditions:
50/// ```ignore
51/// let min_balance = 500.0;
52/// let name_pattern = "Acme%";
53///
54/// let query = qb_sql!(
55///     select * from Customer
56///     where balance >= min_balance
57///     and display_name like name_pattern
58/// );
59/// ```
60///
61/// Using the `in` operator with a tuple or iterator:
62/// ```ignore
63/// // With literal values
64/// let query = qb_sql!(
65///     select * from Customer
66///     where id in (1, 2, 3)
67/// );
68///
69/// // With an iterator (single expression)
70/// let ids = vec!["1", "2", "3"];
71/// let query = qb_sql!(
72///     select * from Customer
73///     where id in (ids)
74/// );
75/// ```
76///
77/// Executing a query (requires the `api` feature):
78/// ```ignore
79/// use quick_oxibooks::{Environment, QBContext};
80/// use ureq::Agent;
81///
82/// let client = Agent::new();
83/// let qb = QBContext::new(Environment::SANDBOX, "company_id".into(), "token".into(), &client)?;
84///
85/// let results = query.execute(&qb, &client)?;
86/// ```
87///
88/// # Notes
89///
90/// - Field names are automatically converted from snake_case to CamelCase (e.g., `display_name` → `DisplayName`)
91/// - All field names are validated at compile time against the entity type
92/// - The generated query can be converted to a string with `.query_string()` or by displaying it
93/// - For the `in` operator, use a tuple for literals or a single iterator expression
94#[proc_macro]
95pub fn qb_sql(input: TokenStream) -> TokenStream {
96    let query = syn::parse_macro_input!(input as SqlQuery);
97    let expanded = query.expand();
98    TokenStream::from(expanded)
99}
100
101/// Represents the entire SQL query
102struct SqlQuery {
103    item_type: Type,
104    conditions: Vec<Condition>,
105    order_by: Option<OrderBy>,
106    limit: Option<LimitClause>,
107}
108
109/// Represents a field, possibly nested (e.g., address.city)
110enum Field {
111    Root(Ident),
112    Nested(Ident, Box<Field>),
113}
114
115impl Parse for Field {
116    fn parse(input: ParseStream) -> syn::Result<Self> {
117        let root: Ident = input.parse()?;
118        if !input.peek(Token![.]) {
119            return Ok(Field::Root(root));
120        }
121        input.parse::<Token![.]>()?;
122        let nested = Field::parse(input)?;
123        Ok(Field::Nested(root, Box::new(nested)))
124    }
125}
126
127impl ToString for Field {
128    fn to_string(&self) -> String {
129        match self {
130            Field::Root(ident) => ident.to_string(),
131            Field::Nested(ident, nested) => format!("{}.{}", ident, nested.to_string()),
132        }
133    }
134}
135
136impl quote::ToTokens for Field {
137    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
138        match self {
139            Field::Root(ident) => {
140                ident.to_tokens(tokens);
141            }
142            Field::Nested(ident, nested) => {
143                ident.to_tokens(tokens);
144                tokens.extend(quote! { . });
145                nested.to_tokens(tokens);
146            }
147        }
148    }
149}
150
151/// A single WHERE condition
152struct Condition {
153    field: Field,
154    operator: Operator,
155    values: Vec<syn::Expr>,
156}
157
158/// Operator types
159enum Operator {
160    Equal,
161    Less,
162    Greater,
163    LessEqual,
164    GreaterEqual,
165    In,
166    Like,
167}
168
169/// ORDER BY clause
170struct OrderBy {
171    orders: Vec<OrderField>,
172}
173
174struct OrderField {
175    field: Field,
176    direction: Option<OrderDirection>,
177}
178
179enum OrderDirection {
180    Asc,
181    Desc,
182}
183
184/// LIMIT clause with optional OFFSET
185struct LimitClause {
186    number: LitInt,
187    offset: Option<syn::Expr>,
188}
189
190impl Parse for SqlQuery {
191    fn parse(input: ParseStream) -> syn::Result<Self> {
192        // Parse select * from
193        input.parse::<kw::select>()?;
194        input.parse::<Token![*]>()?;
195        input.parse::<kw::from>()?;
196
197        let item_type: Type = input.parse()?;
198
199        let mut conditions = vec![];
200        if input.peek(Token![where]) {
201            // Parse WHERE
202            input.parse::<Token![where]>()?;
203            // Parse first condition
204            conditions.push(Condition::parse(input)?);
205            // Parse additional AND conditions
206            while input.peek(kw::and) {
207                input.parse::<kw::and>()?;
208                conditions.push(Condition::parse(input)?);
209            }
210        }
211
212        // Parse optional ORDER BY
213        let order_by = if input.peek(kw::order) {
214            Some(OrderBy::parse(input)?)
215        } else {
216            None
217        };
218
219        // Parse optional LIMIT
220        let limit = if input.peek(kw::limit) {
221            Some(LimitClause::parse(input)?)
222        } else {
223            None
224        };
225
226        Ok(SqlQuery {
227            item_type,
228            conditions,
229            order_by,
230            limit,
231        })
232    }
233}
234
235impl Parse for Condition {
236    fn parse(input: ParseStream) -> syn::Result<Self> {
237        let field: Field = input.parse()?;
238        let operator = Operator::parse(input)?;
239
240        let values = if matches!(operator, Operator::In) {
241            // Parse parenthesized list for IN operator
242            let content;
243            syn::parenthesized!(content in input);
244            let exprs = Punctuated::<syn::Expr, Token![,]>::parse_separated_nonempty(&content)?;
245            exprs.into_iter().collect()
246        } else {
247            // Parse single value for other operators
248            vec![input.parse()?]
249        };
250
251        Ok(Condition {
252            field,
253            operator,
254            values,
255        })
256    }
257}
258
259impl Parse for Operator {
260    fn parse(input: ParseStream) -> syn::Result<Self> {
261        let lookahead = input.lookahead1();
262
263        if lookahead.peek(Token![=]) {
264            input.parse::<Token![=]>()?;
265            Ok(Operator::Equal)
266        } else if lookahead.peek(Token![<]) {
267            input.parse::<Token![<]>()?;
268            if input.peek(Token![=]) {
269                input.parse::<Token![=]>()?;
270                Ok(Operator::LessEqual)
271            } else {
272                Ok(Operator::Less)
273            }
274        } else if lookahead.peek(Token![>]) {
275            input.parse::<Token![>]>()?;
276            if input.peek(Token![=]) {
277                input.parse::<Token![=]>()?;
278                Ok(Operator::GreaterEqual)
279            } else {
280                Ok(Operator::Greater)
281            }
282        } else if lookahead.peek(Token![in]) {
283            input.parse::<Token![in]>()?;
284            Ok(Operator::In)
285        } else if lookahead.peek(kw::like) {
286            input.parse::<kw::like>()?;
287            Ok(Operator::Like)
288        } else {
289            Err(lookahead.error())
290        }
291    }
292}
293
294impl Parse for OrderBy {
295    fn parse(input: ParseStream) -> syn::Result<Self> {
296        input.parse::<kw::order>()?;
297        input.parse::<kw::by>()?;
298
299        let orders = Punctuated::<OrderField, Token![,]>::parse_separated_nonempty(input)?;
300
301        Ok(OrderBy {
302            orders: orders.into_iter().collect(),
303        })
304    }
305}
306
307impl Parse for OrderField {
308    fn parse(input: ParseStream) -> syn::Result<Self> {
309        let field: Field = input.parse()?;
310
311        let direction = if input.peek(kw::asc) {
312            input.parse::<kw::asc>()?;
313            Some(OrderDirection::Asc)
314        } else if input.peek(kw::desc) {
315            input.parse::<kw::desc>()?;
316            Some(OrderDirection::Desc)
317        } else {
318            None
319        };
320
321        Ok(OrderField { field, direction })
322    }
323}
324
325impl Parse for LimitClause {
326    fn parse(input: ParseStream) -> syn::Result<Self> {
327        input.parse::<kw::limit>()?;
328        let number: LitInt = input.parse()?;
329
330        let offset = if input.peek(kw::offset) {
331            input.parse::<kw::offset>()?;
332            Some(input.parse()?)
333        } else {
334            None
335        };
336
337        Ok(LimitClause { number, offset })
338    }
339}
340
341impl SqlQuery {
342    fn expand(&self) -> proc_macro2::TokenStream {
343        let item_type = &self.item_type;
344
345        // Collect all fields for type checking
346        let all_fields: Vec<&Field> = {
347            let mut fields = Vec::new();
348
349            fields.extend(self.conditions.iter().map(|c| &c.field));
350
351            if let Some(ref order_by) = self.order_by {
352                fields.extend(order_by.orders.iter().map(|o| &o.field));
353            }
354
355            fields
356        };
357
358        // Generate type checking code
359        let type_check = if !all_fields.is_empty() {
360            quote! {
361                const _: () = {
362                    fn _check_fields(v: #item_type) {
363                        #(let _ = v.#all_fields;)*
364                    }
365                };
366            }
367        } else {
368            quote! {}
369        };
370
371        // Generate condition code
372        let condition_code: Vec<_> = self
373            .conditions
374            .iter()
375            .map(|c| {
376                let field = &c.field;
377                let field_name = to_camel_case(&field.to_string());
378                let operator = c.operator.to_tokens();
379                let values = &c.values;
380
381                // For IN operator with a single expression, treat it as an iterator
382                let values_code = if matches!(c.operator, Operator::In) && values.len() == 1 {
383                    let expr = &values[0];
384                    quote! {
385                      #expr.into_iter().map(|v| v.to_string()).collect::<Vec<String>>()
386                    }
387                } else {
388                    // Multiple values or non-IN operators: call to_string on each
389                    quote! { vec![#(#values.to_string()),*] }
390                };
391
392                quote! {
393                    let clause = WhereClause {
394                        field: stringify!(#field_name),
395                        operator: #operator,
396                        values: #values_code,
397                    };
398                    unsafe {
399                        query = query.condition(clause);
400                    }
401                }
402            })
403            .collect();
404
405        // Generate order by code
406        let order_code = if let Some(ref order_by) = self.order_by {
407            let orders: Vec<_> = order_by
408                .orders
409                .iter()
410                .map(|o| {
411                    let field = &o.field;
412                    let field_name = to_camel_case(&field.to_string());
413                    let direction = match &o.direction {
414                        Some(OrderDirection::Asc) => quote! { Order::Asc },
415                        Some(OrderDirection::Desc) => quote! { Order::Desc },
416                        None => quote! { Order::Asc },
417                    };
418
419                    quote! {
420                        unsafe {
421                            query = query.order(stringify!(#field_name), #direction);
422                        }
423                    }
424                })
425                .collect();
426
427            quote! { #(#orders)* }
428        } else {
429            quote! {}
430        };
431
432        // Generate limit code
433        let limit_code = if let Some(ref limit) = self.limit {
434            let number = &limit.number;
435            let offset_code = if let Some(ref offset) = limit.offset {
436                quote! { Some(#offset) }
437            } else {
438                quote! { None }
439            };
440
441            quote! {
442                query = query.limit(#number, #offset_code);
443            }
444        } else {
445            quote! {}
446        };
447
448        quote! {
449            {
450                #type_check
451
452                let mut query = Query::<#item_type>::new();
453
454                #(#condition_code)*
455                #order_code
456                #limit_code
457
458                query
459            }
460        }
461    }
462}
463
464impl Operator {
465    fn to_tokens(&self) -> proc_macro2::TokenStream {
466        match self {
467            Operator::Equal => quote! { Operator::Equal },
468            Operator::Less => quote! { Operator::Less },
469            Operator::Greater => quote! { Operator::Greater },
470            Operator::LessEqual => quote! { Operator::LessEqual },
471            Operator::GreaterEqual => quote! { Operator::GreaterEqual },
472            Operator::In => quote! { Operator::In },
473            Operator::Like => quote! { Operator::Like },
474        }
475    }
476}
477
478/// Convert snake_case to CamelCase
479fn to_camel_case(s: &str) -> syn::Ident {
480    let camel = s
481        .split('_')
482        .map(|word| {
483            let mut chars = word.chars();
484            match chars.next() {
485                None => String::new(),
486                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
487            }
488        })
489        .collect::<String>();
490
491    syn::Ident::new(&camel, proc_macro2::Span::call_site())
492}
493
494// Custom keywords
495mod kw {
496    syn::custom_keyword!(select);
497    syn::custom_keyword!(from);
498    syn::custom_keyword!(and);
499    syn::custom_keyword!(order);
500    syn::custom_keyword!(by);
501    syn::custom_keyword!(limit);
502    syn::custom_keyword!(offset);
503    syn::custom_keyword!(asc);
504    syn::custom_keyword!(desc);
505    syn::custom_keyword!(like);
506}