tank-macros 0.21.0

Procedural macros for Tank: the Rust data layer. Not intended to be used directly.
Documentation
use crate::decode_column;
use crate::decode_column::ColumnMetadata;
use convert_case::{Case, Casing};
use proc_macro2::Span;
use quote::ToTokens;
use std::convert::identity;
use syn::spanned::Spanned;
use syn::{Error, Expr, ExprLit, ExprPath, ItemStruct, Lit, LitStr, Result, parse::ParseBuffer};
use tank_core::{PrimaryKeyType, matches_path};

pub(crate) struct TableMetadata {
    pub(crate) columns: Vec<ColumnMetadata>,
    pub(crate) name: String,
    pub(crate) item: ItemStruct,
    pub(crate) schema: String,
    pub(crate) primary_key: Vec<usize>,
    pub(crate) unique: Vec<Vec<usize>>,
}

fn decode_set_columns<'a, I: Iterator<Item = &'a ColumnMetadata> + Clone>(
    item: &ItemStruct,
    col: Expr,
    columns: I,
) -> Result<Vec<usize>> {
    Ok(match col {
        Expr::Lit(ExprLit {
            lit: Lit::Str(v), ..
        }) => {
            let v = v.value();
            let Some((i, _)) = columns.enumerate().find(|(_i, c)| c.name == v) else {
                return Err(Error::new(
                    v.span(),
                    format!("Column `{}` does not exist in the table", v),
                ));
            };
            vec![i]
        }
        Expr::Path(ExprPath { path, .. }) => {
            let Some((i, _)) = columns.enumerate().find(|(_i, c)| {
                let c = c.ident.to_string();
                matches_path(&path, &["Self", &c])
                    || matches_path(&path, &[&item.ident.to_string(), &c])
            }) else {
                return Err(Error::new(
                    path.span(),
                    format!(
                        "Field `{}` does not exist in the entity",
                        path.to_token_stream().to_string()
                    ),
                ));
            };
            vec![i]
        }
        Expr::Tuple(tuple) => {
            let elems: Vec<_> = tuple
                .elems
                .iter()
                .map(|v| decode_set_columns(&item, v.clone(), columns.clone()))
                .collect::<Result<_>>()?;
            if elems.iter().any(|v| v.len() != 1) {
                return Err(Error::new(
                    tuple.span(),
                    "Fields list inside tuple must either be a string literal column name or a column reference path",
                ));
            }
            elems.into_iter().flat_map(identity).collect()
        }
        _ => {
            return Err(Error::new(Span::call_site(), "Unexpected column set"));
        }
    })
}

pub fn decode_table(item: ItemStruct) -> TableMetadata {
    let mut columns: Vec<_> = item
        .fields
        .iter()
        .map(|f| decode_column(f))
        .filter(|c| !c.ignored)
        .collect();
    let mut name = item.ident.to_string().to_case(Case::Snake);
    let mut schema = String::new();
    let mut primary_key: Vec<_> = columns
        .iter()
        .enumerate()
        .filter_map(|(i, c)| {
            if c.primary_key != PrimaryKeyType::None {
                Some(i)
            } else {
                None
            }
        })
        .collect();
    let mut unique = vec![];
    if name.starts_with('_') {
        name.remove(0);
    }
    for attr in &item.attrs {
        let meta = &attr.meta;
        if meta.path().is_ident("tank") {
            let Ok(list) = meta.require_list() else {
                panic!("Error while parsing `tank`, use it like: `#[tank(attribute = value, ..)]`",);
            };
            let _ = list.parse_nested_meta(|arg| {
                if arg.path.is_ident("name") {
                    let Ok(value) = arg.value().and_then(ParseBuffer::parse::<LitStr>) else {
                        panic!(
                            "Error while parsing `name`, use it like: `#[tank(name = \"my_table\")]`"
                        );
                    };
                    name = value.value();
                } else if arg.path.is_ident("schema") {
                    let Ok(value) = arg.value().and_then(ParseBuffer::parse::<LitStr>) else {
                        panic!(
                            "Error while parsing `schema`, use it like: `#[tank(schema = \"my_schema\")]`"
                        );
                    };
                    schema = value.value();
                } else if arg.path.is_ident("primary_key") {
                    let cols_ids = arg
                        .value()
                        .and_then(ParseBuffer::parse::<Expr>)
                        .and_then(|v| decode_set_columns(&item, v, columns.iter()));
                    let cols_ids = match cols_ids {
                        Ok(v) => v,
                        Err(e) => {
                            panic!("Error while parsing `primary_key`, use it like: `#[tank(primary_key = (\"k1\", \"k2\", ..))]` ({e})");
                        }
                    };
                    if !primary_key.is_empty() {
                        panic!("Primary key attribute can appear just once on a table");
                    }
                    if let Some(column) = columns.iter().find(|c| c.primary_key != PrimaryKeyType::None) {
                        panic!(
                            "Column `{}` cannot be declared as a primary key while the table also specifies one",
                            column.name
                        )
                    }
                    primary_key = cols_ids
                } else if arg.path.is_ident("unique") {
                    let Ok(value) = arg
                        .value()
                        .and_then(ParseBuffer::parse::<Expr>)
                        .and_then(|v| decode_set_columns(&item, v, columns.iter())) else {
                        panic!("Error while parsing `unique`, use it like: `#[tank(unique = (\"k1\", \"k2\", ..))]`, you can specify more than one");
                    };
                    unique.push(value);
                } else {
                    panic!("Unknown attribute `{}` inside tank macro", arg.path.to_token_stream().to_string());
                }
                Ok(())
            });
        }
    }
    if !primary_key.is_empty() {
        let pk_type = if primary_key.len() == 1 {
            PrimaryKeyType::PrimaryKey
        } else {
            PrimaryKeyType::PartOfPrimaryKey
        };
        for pk in primary_key.iter() {
            columns[*pk].primary_key = pk_type;
        }
    }
    TableMetadata {
        columns,
        name,
        item,
        schema,
        primary_key,
        unique,
    }
}