Skip to main content

toolu_orm_macros/
lib.rs

1mod column_enum;
2mod expand;
3mod from_row;
4mod from_row_expand;
5mod fts5;
6mod parse;
7mod paths;
8mod relational;
9mod view;
10
11use proc_macro::TokenStream;
12use syn::parse::Parser;
13use syn::{parse_macro_input, Expr, ItemStruct, Lit, Meta};
14
15#[proc_macro_attribute]
16pub fn table(attr: TokenStream, item: TokenStream) -> TokenStream {
17  let mut item_struct = parse_macro_input!(item as ItemStruct);
18
19  let (table_name, strict) = match parse_table_attrs(attr) {
20    Ok(attrs) => attrs,
21    Err(e) => return e.to_compile_error().into(),
22  };
23
24  // Parse and strip #[index] / #[unique_index] attrs from the struct
25  let indexes = match parse::parse_index_attrs(&mut item_struct) {
26    Ok(idxs) => idxs,
27    Err(e) => return e.to_compile_error().into(),
28  };
29
30  // Parse and strip #[view] attrs from the struct
31  let views = match parse_view_attrs(&mut item_struct) {
32    Ok(v) => v,
33    Err(e) => return e.to_compile_error().into(),
34  };
35
36  let columns = match parse::parse_struct(&item_struct) {
37    Ok(cols) => cols,
38    Err(e) => return e.to_compile_error().into(),
39  };
40
41  let input = parse::TableInput {
42    table_name,
43    strict,
44    struct_name: item_struct.ident.clone(),
45    columns,
46    indexes,
47  };
48
49  let expanded = expand::expand(&input);
50  let columns_mod = expand::expand_columns_module(&input);
51  let builder_methods = expand::expand_builder_methods(&input);
52  let vis = &item_struct.vis;
53  let view_structs: Vec<proc_macro2::TokenStream> = views
54    .iter()
55    .map(|v| view::generate_view_struct(v, &input.columns, vis))
56    .collect();
57  let clean_struct = parse::strip_column_attrs(item_struct);
58
59  let output = quote::quote! {
60      #clean_struct
61      #expanded
62      #builder_methods
63      #columns_mod
64      #(#view_structs)*
65  };
66
67  output.into()
68}
69
70/// Declares a `CREATE VIRTUAL TABLE … USING fts5(…)` table.
71///
72/// ```ignore
73/// #[fts5_table(name = "memory_fts", tokenize = "porter unicode61")]
74/// pub struct MemoryFts {
75///   #[column(unindexed)]
76///   pub memory_id: Text,
77///   pub body: Text,
78/// }
79/// ```
80///
81/// Accepts `name` (required), `tokenize`, `prefix`, `content`,
82/// `content_rowid`, `columnsize` and `detail`. Indexes are rejected: SQLite
83/// cannot index a virtual table.
84#[proc_macro_attribute]
85pub fn fts5_table(attr: TokenStream, item: TokenStream) -> TokenStream {
86  match expand_fts5_table(attr, item) {
87    Ok(tokens) => tokens,
88    Err(e) => e.to_compile_error().into(),
89  }
90}
91
92fn expand_fts5_table(attr: TokenStream, item: TokenStream) -> syn::Result<TokenStream> {
93  let item_struct = syn::parse::<ItemStruct>(item)?;
94  let parser = syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated;
95  let metas: Vec<Meta> = parser.parse(attr)?.into_iter().collect();
96  let attrs = fts5::parse_attrs(&metas)?;
97
98  for struct_attr in &item_struct.attrs {
99    if struct_attr.path().is_ident("index") || struct_attr.path().is_ident("unique_index") {
100      return Err(syn::Error::new_spanned(
101        struct_attr,
102        "virtual tables cannot declare indexes; remove it from #[fts5_table]",
103      ));
104    }
105  }
106
107  let columns = parse::parse_struct(&item_struct)?;
108  if columns.is_empty() {
109    return Err(syn::Error::new_spanned(
110      &item_struct,
111      "an fts5 table needs at least one column",
112    ));
113  }
114
115  fts5::check_columns(&columns)?;
116  let table_name = attrs.table_name()?;
117  let input = parse::TableInput {
118    table_name,
119    strict: false,
120    struct_name: item_struct.ident.clone(),
121    columns,
122    indexes: Vec::new(),
123  };
124
125  let schema_impl = fts5::expand(&attrs, &input);
126  let columns_mod = expand::expand_columns_module(&input);
127  let builder_methods = expand::expand_builder_methods(&input);
128  let clean_struct = parse::strip_column_attrs(item_struct);
129
130  Ok(
131    quote::quote! {
132      #clean_struct
133      #schema_impl
134      #builder_methods
135      #columns_mod
136    }
137    .into(),
138  )
139}
140
141fn parse_view_attrs(item: &mut ItemStruct) -> syn::Result<Vec<view::ViewInput>> {
142  let mut views = Vec::new();
143  let mut remaining_attrs = Vec::new();
144  for attr in &item.attrs {
145    if attr.path().is_ident("view") {
146      views.push(view::parse_view_attr(attr)?);
147    } else {
148      remaining_attrs.push(attr.clone());
149    }
150  }
151  item.attrs = remaining_attrs;
152  Ok(views)
153}
154
155/// Parses `#[table(name = "table_name")]` or `#[table(name = "table_name", strict = true)]`.
156/// Returns (table_name, strict). Strict defaults to false (standard SQLite compatibility).
157fn parse_table_attrs(attr: TokenStream) -> syn::Result<(String, bool)> {
158  let parser = syn::punctuated::Punctuated::<Meta, syn::Token![,]>::parse_terminated;
159  let metas = parser.parse(attr)?;
160
161  let mut table_name = None;
162  let mut strict = false; // default: non-strict for SQLite/libsql compatibility
163
164  for meta in &metas {
165    let Meta::NameValue(nv) = meta else {
166      return Err(syn::Error::new_spanned(
167        meta,
168        "expected name = value pairs in #[table(...)]",
169      ));
170    };
171    if nv.path.is_ident("name") {
172      let Expr::Lit(expr_lit) = &nv.value else {
173        return Err(syn::Error::new_spanned(
174          &nv.value,
175          "expected a string literal",
176        ));
177      };
178      let Lit::Str(s) = &expr_lit.lit else {
179        return Err(syn::Error::new_spanned(
180          &expr_lit.lit,
181          "expected a string literal",
182        ));
183      };
184      table_name = Some(s.value());
185    } else if nv.path.is_ident("strict") {
186      let Expr::Lit(expr_lit) = &nv.value else {
187        return Err(syn::Error::new_spanned(
188          &nv.value,
189          "expected a bool literal",
190        ));
191      };
192      let Lit::Bool(b) = &expr_lit.lit else {
193        return Err(syn::Error::new_spanned(
194          &expr_lit.lit,
195          "expected true or false",
196        ));
197      };
198      strict = b.value();
199    } else {
200      return Err(syn::Error::new_spanned(
201        &nv.path,
202        "unknown attribute, expected `name` or `strict`",
203      ));
204    }
205  }
206
207  let Some(name) = table_name else {
208    return Err(syn::Error::new(
209      proc_macro2::Span::call_site(),
210      "missing `name` in #[table(name = \"...\")]",
211    ));
212  };
213
214  Ok((name, strict))
215}
216
217#[proc_macro_derive(ColumnEnum)]
218pub fn derive_column_enum(input: TokenStream) -> TokenStream {
219  let input = parse_macro_input!(input as syn::DeriveInput);
220  match column_enum::expand_column_enum(&input) {
221    Ok(tokens) => tokens.into(),
222    Err(e) => e.to_compile_error().into(),
223  }
224}
225
226#[proc_macro_derive(FromRow, attributes(from_row))]
227pub fn derive_from_row(input: TokenStream) -> TokenStream {
228  let input = parse_macro_input!(input as syn::DeriveInput);
229  match from_row::expand_from_row(&input) {
230    Ok(tokens) => tokens.into(),
231    Err(e) => e.to_compile_error().into(),
232  }
233}
234
235#[proc_macro_derive(Relational, attributes(relational, has_many, belongs_to, many_to_many))]
236pub fn derive_relational(input: TokenStream) -> TokenStream {
237  let input = parse_macro_input!(input as syn::DeriveInput);
238  match relational::parse_relational(&input) {
239    Ok(parsed) => expand::expand_relational(&parsed).into(),
240    Err(e) => e.to_compile_error().into(),
241  }
242}