tank-macros 0.21.0

Procedural macros for Tank: the Rust data layer. Not intended to be used directly.
Documentation
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::{
    BinOp, Expr, ExprGroup, ExprLit, ExprMacro, ExprPath, LitStr, Macro, Member, Path, Type,
    TypePath, punctuated::Punctuated, spanned::Spanned, token::Comma,
};
use tank_core::decode_type;

fn unwrap_group(expr: &Expr) -> &Expr {
    match expr {
        Expr::Group(v) => &*v.expr,
        _ => expr,
    }
}

pub fn decode_expression(expr: &Expr) -> TokenStream {
    match expr {
        Expr::Binary(expr_binary) => {
            let lhs = expr_binary.left.as_ref();
            let mut rhs = expr_binary.right.as_ref();
            let op = expr_binary.op;
            let op = match expr_binary.op {
                BinOp::Add(..) => quote! { ::tank::BinaryOpType::Addition },
                BinOp::Sub(..) => quote! { ::tank::BinaryOpType::Subtraction },
                BinOp::Mul(..) => quote! { ::tank::BinaryOpType::Multiplication },
                BinOp::Div(..) => quote! { ::tank::BinaryOpType::Division },
                BinOp::Rem(..) => quote! { ::tank::BinaryOpType::Remainder },
                BinOp::And(..) => quote! { ::tank::BinaryOpType::And },
                BinOp::Or(..) => quote! { ::tank::BinaryOpType::Or },
                BinOp::BitAnd(..) => quote! { ::tank::BinaryOpType::BitwiseAnd },
                BinOp::BitOr(..) => quote! { ::tank::BinaryOpType::BitwiseOr },
                BinOp::Shl(..) => quote! { ::tank::BinaryOpType::ShiftLeft },
                BinOp::Shr(..) => quote! { ::tank::BinaryOpType::ShiftRight },
                BinOp::Eq(..) | BinOp::Ne(..) => {
                    let mut result = match op {
                        BinOp::Eq(..) => quote! { ::tank::BinaryOpType::Equal },
                        BinOp::Ne(..) => quote! { ::tank::BinaryOpType::NotEqual },
                        _ => unreachable!(),
                    };
                    if let Expr::Cast(cast) = unwrap_group(expr_binary.right.as_ref()) {
                        if let Type::Path(TypePath {
                            path: Path { segments, .. },
                            ..
                        }) = cast.ty.as_ref()
                        {
                            if segments.len() == 1 {
                                let identifier = &segments
                                    .last()
                                    .expect("The path has exactly one segment on this branch")
                                    .ident;
                                if identifier == "IN" {
                                    rhs = &cast.expr;
                                    result = match op {
                                        BinOp::Eq(..) => quote! { ::tank::BinaryOpType::In },
                                        BinOp::Ne(..) => quote! { ::tank::BinaryOpType::NotIn },
                                        _ => unreachable!(),
                                    }
                                } else if identifier == "IS" {
                                    rhs = &cast.expr;
                                    result = match op {
                                        BinOp::Eq(..) => quote! { ::tank::BinaryOpType::Is },
                                        BinOp::Ne(..) => quote! { ::tank::BinaryOpType::IsNot },
                                        _ => unreachable!(),
                                    }
                                } else if identifier == "LIKE" {
                                    rhs = &cast.expr;
                                    result = match op {
                                        BinOp::Eq(..) => quote! { ::tank::BinaryOpType::Like },
                                        BinOp::Ne(..) => quote! { ::tank::BinaryOpType::NotLike },
                                        _ => unreachable!(),
                                    }
                                } else if identifier == "REGEXP" {
                                    rhs = &cast.expr;
                                    result = match op {
                                        BinOp::Eq(..) => quote! { ::tank::BinaryOpType::Regexp },
                                        BinOp::Ne(..) => quote! { ::tank::BinaryOpType::NotRegexp },
                                        _ => unreachable!(),
                                    }
                                } else if identifier == "GLOB" {
                                    rhs = &cast.expr;
                                    result = match op {
                                        BinOp::Eq(..) => quote! { ::tank::BinaryOpType::Glob },
                                        BinOp::Ne(..) => quote! { ::tank::BinaryOpType::NotGlob },
                                        _ => unreachable!(),
                                    }
                                }
                            }
                        }
                    } else if let Expr::Path(ExprPath {
                        path: Path { segments, .. },
                        ..
                    }) = expr_binary.right.as_ref()
                    {
                        if segments.iter().map(|v| &v.ident).eq(["NULL"].iter()) {
                            result = match op {
                                BinOp::Eq(..) => quote! { ::tank::BinaryOpType::Is },
                                BinOp::Ne(..) => quote! { ::tank::BinaryOpType::IsNot },
                                _ => unreachable!(),
                            };
                        }
                    }
                    result
                }
                BinOp::Lt(..) => quote! { ::tank::BinaryOpType::Less },
                BinOp::Le(..) => quote! { ::tank::BinaryOpType::LessEqual },
                BinOp::Ge(..) => quote! { ::tank::BinaryOpType::GreaterEqual },
                BinOp::Gt(..) => quote! { ::tank::BinaryOpType::Greater },
                _ => panic!("Unexpected binary operator"),
            };
            let lhs = decode_expression(lhs);
            let rhs = decode_expression(rhs);
            quote! {
                ::tank::BinaryOp {
                    op: #op,
                    lhs: #lhs,
                    rhs: #rhs,
                }
            }
        }
        Expr::Index(v) => {
            let lhs = decode_expression(&v.expr);
            let rhs = decode_expression(&v.index);
            quote! {
                ::tank::BinaryOp {
                    op: ::tank::BinaryOpType::Indexing,
                    lhs: #lhs,
                    rhs: #rhs,
                }
            }
        }
        Expr::Cast(cast) => {
            let lhs = decode_expression(&cast.expr);
            let rhs = match cast.ty.as_ref() {
                Type::Path(TypePath { path, .. }) => decode_expression(&Expr::Path(ExprPath {
                    attrs: Default::default(),
                    qself: None,
                    path: path.clone(),
                })),
                _ => panic!(
                    "Unexpected cast type, cast can only be a type or the special keyworkds: `IS`, `LIKE`, `REGEXP`, `GLOB`"
                ),
            };
            quote! {
                ::tank::BinaryOp {
                    op: ::tank::BinaryOpType::Alias,
                    lhs: #lhs,
                    rhs: #rhs,
                }
            }
        }
        Expr::Unary(v) => {
            let op = match v.op {
                syn::UnOp::Not(..) => quote! { ::tank::UnaryOpType::Not },
                syn::UnOp::Neg(..) => quote! { ::tank::UnaryOpType::Negative },
                _ => panic!("Unsupported operator `{}`", v.op.to_token_stream()),
            };
            let arg = decode_expression(v.expr.as_ref());
            quote! {
                ::tank::UnaryOp {
                    op: #op,
                    arg: #arg,
                }
            }
        }
        Expr::Call(v) => {
            let f = v.func.as_ref();
            let Expr::Path(ExprPath { path, .. }) = f else {
                panic!(
                    "Function `{}` must be a path or some identifier",
                    v.into_token_stream(),
                );
            };
            if path.is_ident("CAST")
                && v.args.len() == 1
                && let Some(Expr::Cast(cast)) = v.args.first()
            {
                let lhs = decode_expression(&cast.expr);
                let rhs = match cast.ty.as_ref() {
                    Type::Path(..) => decode_type(&cast.ty).0.value.into_token_stream(),
                    _ => panic!(
                        "Unexpected cast type, cast can only be a Rust valid type (check tank::Value)"
                    ),
                };
                quote! {
                    ::tank::BinaryOp {
                        op: ::tank::BinaryOpType::Cast,
                        lhs: #lhs,
                        rhs: ::tank::Operand::Type(#rhs),
                    }
                }
            } else {
                let args = v.args.iter().map(|v| decode_expression(v));
                let path = path.into_token_stream().to_string();
                quote! { ::tank::Operand::Call(&#path, &[#(&#args),*]) }
            }
        }
        Expr::Lit(ExprLit { lit: v, .. }) => {
            let v = match v {
                syn::Lit::Bool(v) => quote! { ::tank::Operand::LitBool(#v) },
                syn::Lit::Int(v) => quote! { ::tank::Operand::LitInt(#v as _) },
                syn::Lit::Float(v) => quote! { ::tank::Operand::LitFloat(#v as _) },
                syn::Lit::Str(v) => quote! { ::tank::Operand::LitStr(#v) },
                syn::Lit::Char(v) => quote! { ::tank::Operand::LitStr(#v) },
                _ => panic!(
                    "Unexpected value {:?} in a sql expression",
                    v.into_token_stream()
                ),
            };
            quote! { #v }
        }
        Expr::Macro(ExprMacro {
            mac: Macro { path, tokens, .. },
            ..
        }) => {
            if path
                .segments
                .iter()
                .map(|v| v.ident.to_string())
                .eq(["tank", "evaluated"].into_iter())
            {
                quote! { ::tank::Operand::Variable(::tank::AsValue::as_value(#tokens)) }
            } else if path
                .segments
                .iter()
                .map(|v| v.ident.to_string())
                .eq(["tank", "asterisk"].into_iter())
            {
                quote! { ::tank::Operand::Asterisk }
            } else if path.segments.iter().map(|v| v.ident.to_string()).eq([
                "tank",
                "question_mark",
            ]
            .into_iter())
            {
                quote! { ::tank::Operand::QuestionMark }
            } else if path.segments.iter().map(|v| v.ident.to_string()).eq([
                "tank",
                "current_timestamp_ms",
            ]
            .into_iter())
            {
                quote! { ::tank::Operand::CurrentTimestampMs }
            } else {
                quote! { #path!(#tokens) }
            }
        }
        Expr::MethodCall(_) => todo!("Expr::MethodCall"),
        Expr::Paren(v) => decode_expression(&v.expr),
        Expr::Path(ExprPath { path, .. }) => {
            if path.segments.len() > 1 {
                quote! { #path }
            } else {
                let ident = path
                    .get_ident()
                    .expect("The path is expected to be identifier");
                if ident == "NULL" {
                    quote!(::tank::Operand::Null)
                } else {
                    let v = LitStr::new(&path.to_token_stream().to_string(), path.span());
                    quote!(::tank::Operand::LitIdent(#v))
                }
            }
        }
        Expr::Field(v) => {
            let mut current = expr;
            let mut segments = Vec::new();
            let do_panic = || {
                panic!(
                    "Unexpected field expression {}, it should dot separated names like: namespace.table.column",
                    v.to_token_stream().to_string()
                )
            };
            loop {
                match current {
                    Expr::Field(field) => {
                        match &field.member {
                            Member::Named(ident) => segments.push(ident.to_string()),
                            _ => do_panic(),
                        }
                        current = &field.base;
                    }
                    Expr::Path(path) => {
                        for segment in path.path.segments.iter().rev() {
                            segments.push(segment.ident.to_string());
                        }
                        break;
                    }
                    _ => do_panic(),
                }
            }
            let segments = segments.into_iter().rev().collect::<Vec<_>>();
            quote! { ::tank::Operand::LitField(&[#(#segments),*]) }
        }
        Expr::Array(v) => {
            let v = v
                .elems
                .iter()
                .map(|v| decode_expression(v))
                .collect::<Punctuated<_, Comma>>();
            quote! { ::tank::Operand::LitArray(&[#v]) }
        }
        Expr::Tuple(v) => {
            let v = v
                .elems
                .iter()
                .map(|v| decode_expression(v))
                .collect::<Punctuated<_, Comma>>();
            quote! { ::tank::Operand::LitTuple(&[#v]) }
        }
        Expr::Group(ExprGroup { expr, .. }) => decode_expression(&expr),
        _ => panic!(
            "Unexpected expression `{}`",
            expr.to_token_stream().to_string()
        ),
    }
}