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    fields: FieldSelection,
104    item_type: Type,
105    conditions: Vec<Condition>,
106    order_by: Option<OrderBy>,
107    limit: Option<LimitClause>,
108}
109
110/// Field selection (SELECT * or SELECT field1, field2, ...)
111enum FieldSelection {
112    All,
113    Specific(Vec<Ident>),
114}
115
116/// A single WHERE condition
117struct Condition {
118    field: Ident,
119    operator: Operator,
120    values: Vec<syn::Expr>,
121}
122
123/// Operator types
124enum Operator {
125    Equal,
126    Less,
127    Greater,
128    LessEqual,
129    GreaterEqual,
130    In,
131    Like,
132}
133
134/// ORDER BY clause
135struct OrderBy {
136    orders: Vec<OrderField>,
137}
138
139struct OrderField {
140    field: Ident,
141    direction: Option<OrderDirection>,
142}
143
144enum OrderDirection {
145    Asc,
146    Desc,
147}
148
149/// LIMIT clause with optional OFFSET
150struct LimitClause {
151    number: LitInt,
152    offset: Option<syn::Expr>,
153}
154
155impl Parse for SqlQuery {
156    fn parse(input: ParseStream) -> syn::Result<Self> {
157        // Parse SELECT
158        input.parse::<kw::select>()?;
159
160        // Parse field selection
161        let fields = if input.peek(Token![*]) {
162            input.parse::<Token![*]>()?;
163            FieldSelection::All
164        } else {
165            let field_list = Punctuated::<Ident, Token![,]>::parse_separated_nonempty(input)?;
166            FieldSelection::Specific(field_list.into_iter().collect())
167        };
168
169        // Parse FROM
170        input.parse::<kw::from>()?;
171        let item_type: Type = input.parse()?;
172
173        let mut conditions = vec![];
174
175        if input.peek(Token![where]) {
176            // Parse WHERE
177            input.parse::<Token![where]>()?;
178            // Parse first condition
179            conditions.push(Condition::parse(input)?);
180            // Parse additional AND conditions
181            while input.peek(kw::and) {
182                input.parse::<kw::and>()?;
183                conditions.push(Condition::parse(input)?);
184            }
185        }
186
187        // Parse optional ORDER BY
188        let order_by = if input.peek(kw::order) {
189            Some(OrderBy::parse(input)?)
190        } else {
191            None
192        };
193
194        // Parse optional LIMIT
195        let limit = if input.peek(kw::limit) {
196            Some(LimitClause::parse(input)?)
197        } else {
198            None
199        };
200
201        Ok(SqlQuery {
202            fields,
203            item_type,
204            conditions,
205            order_by,
206            limit,
207        })
208    }
209}
210
211impl Parse for Condition {
212    fn parse(input: ParseStream) -> syn::Result<Self> {
213        let field: Ident = input.parse()?;
214        let operator = Operator::parse(input)?;
215
216        let values = if matches!(operator, Operator::In) {
217            // Parse parenthesized list for IN operator
218            let content;
219            syn::parenthesized!(content in input);
220            let exprs = Punctuated::<syn::Expr, Token![,]>::parse_separated_nonempty(&content)?;
221            exprs.into_iter().collect()
222        } else {
223            // Parse single value for other operators
224            vec![input.parse()?]
225        };
226
227        Ok(Condition {
228            field,
229            operator,
230            values,
231        })
232    }
233}
234
235impl Parse for Operator {
236    fn parse(input: ParseStream) -> syn::Result<Self> {
237        let lookahead = input.lookahead1();
238
239        if lookahead.peek(Token![=]) {
240            input.parse::<Token![=]>()?;
241            Ok(Operator::Equal)
242        } else if lookahead.peek(Token![<]) {
243            input.parse::<Token![<]>()?;
244            if input.peek(Token![=]) {
245                input.parse::<Token![=]>()?;
246                Ok(Operator::LessEqual)
247            } else {
248                Ok(Operator::Less)
249            }
250        } else if lookahead.peek(Token![>]) {
251            input.parse::<Token![>]>()?;
252            if input.peek(Token![=]) {
253                input.parse::<Token![=]>()?;
254                Ok(Operator::GreaterEqual)
255            } else {
256                Ok(Operator::Greater)
257            }
258        } else if lookahead.peek(Token![in]) {
259            input.parse::<Token![in]>()?;
260            Ok(Operator::In)
261        } else if lookahead.peek(kw::like) {
262            input.parse::<kw::like>()?;
263            Ok(Operator::Like)
264        } else {
265            Err(lookahead.error())
266        }
267    }
268}
269
270impl Parse for OrderBy {
271    fn parse(input: ParseStream) -> syn::Result<Self> {
272        input.parse::<kw::order>()?;
273        input.parse::<kw::by>()?;
274
275        let orders = Punctuated::<OrderField, Token![,]>::parse_separated_nonempty(input)?;
276
277        Ok(OrderBy {
278            orders: orders.into_iter().collect(),
279        })
280    }
281}
282
283impl Parse for OrderField {
284    fn parse(input: ParseStream) -> syn::Result<Self> {
285        let field: Ident = input.parse()?;
286
287        let direction = if input.peek(kw::asc) {
288            input.parse::<kw::asc>()?;
289            Some(OrderDirection::Asc)
290        } else if input.peek(kw::desc) {
291            input.parse::<kw::desc>()?;
292            Some(OrderDirection::Desc)
293        } else {
294            None
295        };
296
297        Ok(OrderField { field, direction })
298    }
299}
300
301impl Parse for LimitClause {
302    fn parse(input: ParseStream) -> syn::Result<Self> {
303        input.parse::<kw::limit>()?;
304        let number: LitInt = input.parse()?;
305
306        let offset = if input.peek(kw::offset) {
307            input.parse::<kw::offset>()?;
308            Some(input.parse()?)
309        } else {
310            None
311        };
312
313        Ok(LimitClause { number, offset })
314    }
315}
316
317impl SqlQuery {
318    fn expand(&self) -> proc_macro2::TokenStream {
319        let item_type = &self.item_type;
320
321        // Collect all fields for type checking
322        let all_fields: Vec<&Ident> = {
323            let mut fields = Vec::new();
324
325            if let FieldSelection::Specific(ref select_fields) = self.fields {
326                fields.extend(select_fields.iter());
327            }
328
329            fields.extend(self.conditions.iter().map(|c| &c.field));
330
331            if let Some(ref order_by) = self.order_by {
332                fields.extend(order_by.orders.iter().map(|o| &o.field));
333            }
334
335            fields
336        };
337
338        // Generate type checking code
339        let type_check = if !all_fields.is_empty() {
340            quote! {
341                const _: () = {
342                    fn _check_fields(v: #item_type) {
343                        #(let _ = v.#all_fields;)*
344                    }
345                };
346            }
347        } else {
348            quote! {}
349        };
350
351        // Generate field selection code
352        let field_code = match &self.fields {
353            FieldSelection::All => quote! {},
354            FieldSelection::Specific(fields) => {
355                let field_names: Vec<_> = fields
356                    .iter()
357                    .map(|f| {
358                        let name = to_camel_case(&f.to_string());
359                        quote! { stringify!(#name) }
360                    })
361                    .collect();
362
363                quote! {
364                    #(
365                        unsafe {
366                            query = query.field(#field_names);
367                        }
368                    )*
369                }
370            }
371        };
372
373        // Generate condition code
374        let condition_code: Vec<_> = self
375            .conditions
376            .iter()
377            .map(|c| {
378                let field = &c.field;
379                let field_name = to_camel_case(&field.to_string());
380                let operator = c.operator.to_tokens();
381                let values = &c.values;
382
383                // For IN operator with a single expression, treat it as an iterator
384                let values_code = if matches!(c.operator, Operator::In) && values.len() == 1 {
385                    let expr = &values[0];
386                    quote! {
387                      #expr.into_iter().map(|v| v.to_string()).collect::<Vec<String>>()
388                    }
389                } else {
390                    // Multiple values or non-IN operators: call to_string on each
391                    quote! { vec![#(#values.to_string()),*] }
392                };
393
394                quote! {
395                    let clause = WhereClause {
396                        field: stringify!(#field_name),
397                        operator: #operator,
398                        values: #values_code,
399                    };
400                    unsafe {
401                        query = query.condition(clause);
402                    }
403                }
404            })
405            .collect();
406
407        // Generate order by code
408        let order_code = if let Some(ref order_by) = self.order_by {
409            let orders: Vec<_> = order_by
410                .orders
411                .iter()
412                .map(|o| {
413                    let field = &o.field;
414                    let field_name = to_camel_case(&field.to_string());
415                    let direction = match &o.direction {
416                        Some(OrderDirection::Asc) => quote! { Order::Asc },
417                        Some(OrderDirection::Desc) => quote! { Order::Desc },
418                        None => quote! { Order::Asc },
419                    };
420
421                    quote! {
422                        unsafe {
423                            query = query.order(stringify!(#field_name), #direction);
424                        }
425                    }
426                })
427                .collect();
428
429            quote! { #(#orders)* }
430        } else {
431            quote! {}
432        };
433
434        // Generate limit code
435        let limit_code = if let Some(ref limit) = self.limit {
436            let number = &limit.number;
437            let offset_code = if let Some(ref offset) = limit.offset {
438                quote! { Some(#offset) }
439            } else {
440                quote! { None }
441            };
442
443            quote! {
444                query = query.limit(#number, #offset_code);
445            }
446        } else {
447            quote! {}
448        };
449
450        quote! {
451            {
452                #type_check
453
454                let mut query = Query::<#item_type>::new();
455
456                #field_code
457                #(#condition_code)*
458                #order_code
459                #limit_code
460
461                query
462            }
463        }
464    }
465}
466
467impl Operator {
468    fn to_tokens(&self) -> proc_macro2::TokenStream {
469        match self {
470            Operator::Equal => quote! { Operator::Equal },
471            Operator::Less => quote! { Operator::Less },
472            Operator::Greater => quote! { Operator::Greater },
473            Operator::LessEqual => quote! { Operator::LessEqual },
474            Operator::GreaterEqual => quote! { Operator::GreaterEqual },
475            Operator::In => quote! { Operator::In },
476            Operator::Like => quote! { Operator::Like },
477        }
478    }
479}
480
481/// Convert snake_case to CamelCase
482fn to_camel_case(s: &str) -> syn::Ident {
483    let camel = s
484        .split('_')
485        .map(|word| {
486            let mut chars = word.chars();
487            match chars.next() {
488                None => String::new(),
489                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
490            }
491        })
492        .collect::<String>();
493
494    syn::Ident::new(&camel, proc_macro2::Span::call_site())
495}
496
497// Custom keywords
498mod kw {
499    syn::custom_keyword!(select);
500    syn::custom_keyword!(from);
501    syn::custom_keyword!(and);
502    syn::custom_keyword!(order);
503    syn::custom_keyword!(by);
504    syn::custom_keyword!(limit);
505    syn::custom_keyword!(offset);
506    syn::custom_keyword!(asc);
507    syn::custom_keyword!(desc);
508    syn::custom_keyword!(like);
509}