cartel-gen 0.1.2

Procedural macros for cartel
Documentation
use syn::spanned::Spanned;
use syn::{Attribute, Expr, ExprClosure, ExprField, ExprPath, Ident, Member, Pat, Path, Stmt};

pub(super) trait PathExt {
    fn without_last(&self) -> syn::Result<Path>;
}

impl PathExt for Path {
    fn without_last(&self) -> syn::Result<Path> {
        let mut t = self.clone();
        t.segments.pop();
        if let Some(pair) = t.segments.pop() {
            t.segments.push(pair.into_value());
        }
        if t.segments.is_empty() {
            return Err(syn::Error::new(
                self.span(),
                "expected `Table::<method>`; missing Table path before `::<method>`",
            ));
        }
        Ok(t)
    }
}

pub(super) trait ExprExt {
    fn as_column_ref(&self, row_var: &Ident) -> Option<String>;
    fn is_synthetic_true(&self) -> bool;
    fn normalized_closure_body(&self) -> Expr;
    fn as_closure_form(
        &self,
        expected_n: Option<usize>,
        method_name: &str,
    ) -> syn::Result<(Vec<Ident>, Expr)>;
    fn as_closure_n(&self, n: usize, method_name: &str) -> syn::Result<(Vec<Ident>, Expr)>;
    fn as_closure_any_arity(&self, method_name: &str) -> syn::Result<(Vec<Ident>, Expr)>;
    fn as_closure_single(&self, method_name: &str) -> syn::Result<(Ident, Expr)>;
}

impl ExprExt for Expr {
    fn as_column_ref(&self, row_var: &Ident) -> Option<String> {
        let Expr::Field(ExprField { base, member, .. }) = self else {
            return None;
        };
        let Expr::Path(ExprPath { path, .. }) = base.as_ref() else {
            return None;
        };
        if path.segments.len() != 1 {
            return None;
        }
        if path.segments[0].ident != *row_var {
            return None;
        }
        let Member::Named(col) = member else {
            return None;
        };
        Some(col.to_string())
    }

    fn is_synthetic_true(&self) -> bool {
        matches!(
            self,
            Expr::Lit(syn::ExprLit {
                lit: syn::Lit::Bool(b),
                ..
            }) if b.value
        )
    }

    fn normalized_closure_body(&self) -> Expr {
        if let Expr::Block(eb) = self
            && eb.block.stmts.len() == 1
            && let Stmt::Expr(inner, None) = &eb.block.stmts[0]
        {
            return inner.clone();
        }
        self.clone()
    }

    fn as_closure_form(
        &self,
        expected_n: Option<usize>,
        method_name: &str,
    ) -> syn::Result<(Vec<Ident>, Expr)> {
        let Expr::Closure(ExprClosure { inputs, body, .. }) = self else {
            return Err(syn::Error::new(
                self.span(),
                format!("`{method_name}` argument must be a closure"),
            ));
        };
        if let Some(n) = expected_n
            && inputs.len() != n
        {
            return Err(syn::Error::new(
                inputs.span(),
                format!("`{method_name}` closure must take exactly {n} parameter(s)"),
            ));
        }
        let mut idents = Vec::with_capacity(inputs.len());
        for inp in inputs {
            idents.push(inp.closure_ident()?);
        }
        Ok((idents, body.normalized_closure_body()))
    }

    fn as_closure_n(&self, n: usize, method_name: &str) -> syn::Result<(Vec<Ident>, Expr)> {
        self.as_closure_form(Some(n), method_name)
    }

    fn as_closure_any_arity(&self, method_name: &str) -> syn::Result<(Vec<Ident>, Expr)> {
        self.as_closure_form(None, method_name)
    }

    fn as_closure_single(&self, method_name: &str) -> syn::Result<(Ident, Expr)> {
        let (mut idents, body) = self.as_closure_form(Some(1), method_name)?;
        Ok((idents.remove(0), body))
    }
}

pub(super) trait FnParamsExt {
    fn resolve(&self, expr: &Expr) -> syn::Result<Ident>;
    fn resolve_borrowed(&self, expr: &Expr) -> syn::Result<Ident>;
    fn index_of(&self, target: &Ident) -> syn::Result<usize>;
}

impl FnParamsExt for [Ident] {
    fn index_of(&self, target: &Ident) -> syn::Result<usize> {
        self.iter()
            .position(|p| p == target)
            .ok_or_else(|| syn::Error::new(target.span(), "captured ident not in fn params"))
    }

    fn resolve(&self, expr: &Expr) -> syn::Result<Ident> {
        let Expr::Path(ExprPath { path, .. }) = expr else {
            return Err(syn::Error::new(
                expr.span(),
                "#[query] v0 captured value must be a single fn parameter ident",
            ));
        };
        if path.segments.len() != 1 {
            return Err(syn::Error::new(
                path.span(),
                "captured value must be a single ident",
            ));
        }
        let id = &path.segments[0].ident;
        if !self.iter().any(|p| p == id) {
            return Err(syn::Error::new(
                id.span(),
                format!(
                    "`{id}` is not a function parameter — only fn params can be captured in v0"
                ),
            ));
        }
        Ok(id.clone())
    }

    fn resolve_borrowed(&self, expr: &Expr) -> syn::Result<Ident> {
        let inner = match expr {
            Expr::Reference(r) => r.expr.as_ref(),
            other => other,
        };
        self.resolve(inner)
    }
}

pub(super) trait CaptureSet {
    fn intern(&mut self, cap: Ident) -> usize;
}

impl CaptureSet for Vec<Ident> {
    fn intern(&mut self, cap: Ident) -> usize {
        if let Some(i) = self.iter().position(|c| c == &cap) {
            return i;
        }
        self.push(cap);
        self.len() - 1
    }
}

pub(super) trait PatExt {
    fn closure_ident(&self) -> syn::Result<Ident>;
}

impl PatExt for Pat {
    fn closure_ident(&self) -> syn::Result<Ident> {
        match self {
            Pat::Ident(pi) => Ok(pi.ident.clone()),
            Pat::Type(syn::PatType { pat, .. }) => match pat.as_ref() {
                Pat::Ident(pi) => Ok(pi.ident.clone()),
                other => Err(syn::Error::new(
                    other.span(),
                    "closure param must be a plain identifier",
                )),
            },
            other => Err(syn::Error::new(
                other.span(),
                "closure param must be a plain identifier",
            )),
        }
    }
}

pub(super) trait AttrSliceExt {
    fn has_pk(&self) -> bool;
    fn table_name(&self, struct_ident: &Ident) -> syn::Result<String>;
}

impl AttrSliceExt for [Attribute] {
    fn has_pk(&self) -> bool {
        self.iter().any(|a| a.path().is_ident("pk"))
    }

    fn table_name(&self, struct_ident: &Ident) -> syn::Result<String> {
        for a in self {
            if a.path().is_ident("table_name") {
                let s: syn::LitStr = a.parse_args()?;
                return Ok(s.value());
            }
        }
        let mut out = String::new();
        let s = struct_ident.to_string();
        for (i, ch) in s.chars().enumerate() {
            if ch.is_uppercase() && i > 0 {
                out.push('_');
            }
            out.extend(ch.to_lowercase());
        }
        if !out.ends_with('s') {
            out.push('s');
        }
        Ok(out)
    }
}

pub(super) trait SqlIdent {
    fn quote_if_needed(&self) -> String;
}

impl SqlIdent for str {
    fn quote_if_needed(&self) -> String {
        if self.contains('.') {
            return self
                .split('.')
                .map(|p| p.quote_if_needed())
                .collect::<Vec<_>>()
                .join(".");
        }
        let needs_quote = self.is_empty()
            || self
                .chars()
                .next()
                .map(|c| c.is_ascii_digit())
                .unwrap_or(true)
            || self
                .chars()
                .any(|c| !(c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_'));
        if needs_quote {
            format!("\"{}\"", self.replace('"', "\"\""))
        } else {
            self.to_string()
        }
    }
}