psibase_macros_lib/
service_tables_macro.rs

1mod tables;
2
3use darling::{ast::NestedMeta, Error, FromMeta};
4use proc_macro2::TokenStream;
5use proc_macro_error::{abort, emit_error};
6use quote::quote;
7use std::{collections::HashMap, str::FromStr};
8use syn::{Ident, Item, ItemMod, Type};
9use tables::{is_table_attr, process_service_tables, process_table_schema_root};
10
11#[derive(Debug, FromMeta)]
12#[darling(default)]
13pub struct Options {
14    pub psibase_mod: String,
15}
16
17impl Default for Options {
18    fn default() -> Self {
19        Options {
20            psibase_mod: "psibase".into(),
21        }
22    }
23}
24
25pub fn service_tables_macro_impl(attr: TokenStream, item: TokenStream) -> TokenStream {
26    let attr_args = match NestedMeta::parse_meta_list(attr) {
27        Ok(v) => v,
28        Err(e) => {
29            return TokenStream::from(Error::from(e).write_errors());
30        }
31    };
32    let options: Options = match Options::from_list(&attr_args) {
33        Ok(val) => val,
34        Err(err) => {
35            return err.write_errors().into();
36        }
37    };
38    let psibase_mod = proc_macro2::TokenStream::from_str(&options.psibase_mod).unwrap();
39
40    let item = syn::parse2::<syn::Item>(item).unwrap();
41    match item {
42        Item::Mod(mut impl_mod) => {
43            process_mod(&psibase_mod, &mut impl_mod);
44            quote! { #impl_mod }.into()
45        }
46        _ => {
47            abort!(
48                item,
49                "service_tables attribute may only be used on a module"
50            )
51        }
52    }
53}
54
55pub(crate) fn process_mod(psibase_mod: &proc_macro2::TokenStream, impl_mod: &mut ItemMod) {
56    if let Some((_, items)) = &mut impl_mod.content {
57        let mut table_structs: HashMap<Ident, Vec<usize>> = HashMap::new();
58        let mut table_names = Vec::new();
59
60        // collect all tables
61        for (item_index, item) in items.iter_mut().enumerate() {
62            if let Item::Struct(s) = item {
63                if s.attrs.iter().any(is_table_attr) {
64                    table_structs.insert(s.ident.clone(), vec![item_index]);
65                }
66            }
67        }
68
69        // do second table loop in case the code has `impl` for a relevant table above the struct definition
70        for (item_index, item) in items.iter().enumerate() {
71            if let Item::Impl(i) = item {
72                if let Type::Path(type_path) = &*i.self_ty {
73                    if let Some(tpps) = type_path.path.segments.first() {
74                        table_structs.entry(tpps.ident.clone()).and_modify(|refs| {
75                            refs.push(item_index);
76                        });
77                    }
78                }
79            }
80        }
81
82        // Transform table attributes into expanded code
83        let mut processed_tables = Vec::new();
84        for (tb_name, items_idxs) in table_structs.iter() {
85            let table_idx =
86                process_service_tables(psibase_mod, tb_name, items, items_idxs, &mut table_names);
87            if table_idx.is_ok() {
88                processed_tables.push((tb_name, table_idx.unwrap()));
89            }
90        }
91
92        process_table_schema_root(psibase_mod, items, table_names);
93
94        // Validate table indexes
95        processed_tables.sort_by_key(|t| t.1);
96        for (expected_idx, (table_struct, tb_index)) in processed_tables.iter().enumerate() {
97            if *tb_index as usize != expected_idx {
98                emit_error!(
99                    table_struct,
100                    format!("Missing expected table index {}; tables may not have gaps and may not be removed or reordered.", expected_idx)
101                );
102            }
103        }
104    } else {
105        emit_error!(
106            impl_mod,
107            "#[psibase::service_tables] module must have inline contents"
108        )
109    }
110} // process_mod