Skip to main content

rango_macro/
lib.rs

1//! Procedural macros for Rango: the `Model` derive that turns a struct into a stored row, plus the
2//! `form`, `input`, `template`, and `main` attribute macros that wire serde, Askama, and tokio.
3
4#![warn(missing_docs)]
5
6use proc_macro::TokenStream;
7use quote::quote;
8use syn::{Data, DeriveInput, Expr, ExprLit, Fields, ItemFn, LitStr};
9
10fn with_serde(mut input: DeriveInput, default: bool) -> TokenStream {
11    let derives = if default {
12        quote!(rango::prelude::Deserialize, Default)
13    } else {
14        quote!(rango::prelude::Deserialize)
15    };
16    input.attrs.push(syn::parse_quote!(#[derive(#derives)]));
17    input
18        .attrs
19        .push(syn::parse_quote!(#[serde(crate = "rango::serde")]));
20    quote!(#input).into()
21}
22
23/// Derives `Deserialize` and `Default` for a form struct, using `rango::serde`.
24///
25/// Usage: `#[rango::form]` on a struct.
26#[proc_macro_attribute]
27pub fn form(_attr: TokenStream, item: TokenStream) -> TokenStream {
28    let input = syn::parse_macro_input!(item as DeriveInput);
29    with_serde(input, true)
30}
31
32/// Derives only `Deserialize` for an input struct, using `rango::serde`.
33///
34/// Usage: `#[rango::input]` on a struct.
35#[proc_macro_attribute]
36pub fn input(_attr: TokenStream, item: TokenStream) -> TokenStream {
37    let input = syn::parse_macro_input!(item as DeriveInput);
38    with_serde(input, false)
39}
40
41/// Runs an `async fn main` on a multi-threaded tokio runtime with all features enabled.
42///
43/// Usage: `#[rango::main]` on `async fn main() -> ExitCode`.
44#[proc_macro_attribute]
45pub fn main(_attr: TokenStream, item: TokenStream) -> TokenStream {
46    let func = syn::parse_macro_input!(item as ItemFn);
47    if func.sig.asyncness.is_none() {
48        return syn::Error::new_spanned(&func.sig, "expected `async fn main`")
49            .to_compile_error()
50            .into();
51    }
52    let output = &func.sig.output;
53    let body = &func.block;
54    let attrs = &func.attrs;
55    let vis = &func.vis;
56    quote! {
57        #(#attrs)*
58        #vis fn main() #output {
59            rango::tokio::runtime::Builder::new_multi_thread()
60                .enable_all()
61                .build()
62                .expect("failed to build tokio runtime")
63                .block_on(async #body)
64        }
65    }
66    .into()
67}
68
69/// Derives `Template` via Askama, forwarding the attribute arguments unchanged.
70///
71/// Usage: `#[rango::template(path = "index.html")]`.
72#[proc_macro_attribute]
73pub fn template(attr: TokenStream, item: TokenStream) -> TokenStream {
74    let attr: proc_macro2::TokenStream = attr.into();
75    let mut input = syn::parse_macro_input!(item as DeriveInput);
76    input
77        .attrs
78        .push(syn::parse_quote!(#[derive(rango::prelude::Template)]));
79    input
80        .attrs
81        .push(syn::parse_quote!(#[template(#attr, askama = rango::askama)]));
82    quote!(#input).into()
83}
84
85/// Implements `Model` for a struct, mapping each field to a column.
86///
87/// Type-level `#[model(table = "messages", actions = "duplicate")]` sets the table name
88/// (defaults to the lowercased type name plus `s`) and lists row-action functions.
89/// Field attributes: `#[key]`, `#[references("table.column")]`, `#[via("through.mine.theirs")]`,
90/// `#[default(value)]`.
91#[proc_macro_derive(Model, attributes(model, key, references, via, default))]
92pub fn derive_model(input: TokenStream) -> TokenStream {
93    let input = syn::parse_macro_input!(input as DeriveInput);
94    match expand(&input) {
95        Ok(ts) => ts.into(),
96        Err(e) => e.to_compile_error().into(),
97    }
98}
99
100fn expand(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
101    let name = &input.ident;
102    let (table, deeds) = parse_model(input)?;
103
104    let fields = match &input.data {
105        Data::Struct(data) => match &data.fields {
106            Fields::Named(fields) => &fields.named,
107            _ => return Err(syn::Error::new_spanned(input, "only named fields")),
108        },
109        _ => return Err(syn::Error::new_spanned(input, "only structs")),
110    };
111
112    let mut field_defs = Vec::new();
113    let mut write_parts = Vec::new();
114    let mut read_parts = Vec::new();
115    let mut key_ident = None;
116
117    for field in fields.into_iter() {
118        let ident = field.ident.as_ref().unwrap();
119        let attrs = &field.attrs;
120        let ty = &field.ty;
121
122        let is_key = attrs.iter().any(|a| a.path().is_ident("key"));
123        let references = parse_references(attrs)?;
124        let references = references.as_deref();
125        let via = parse_via(attrs)?;
126        let default = parse_default(attrs)?;
127
128        let (field_type, is_optional) = unpack_option(ty);
129        let (cell_type, is_many) = unpack_vec(field_type);
130        if is_many && is_optional {
131            return Err(syn::Error::new_spanned(
132                field,
133                "optional lists need plain Vec",
134            ));
135        }
136        if is_many && is_key {
137            return Err(syn::Error::new_spanned(field, "key needs a single value"));
138        }
139        let name = ident.to_string();
140        let is_id = is_key && name == "id" && !is_many;
141
142        let field_def = if is_id {
143            quote! { rango::Field::id() }
144        } else if is_key {
145            let mut def = quote! { rango::Field::key::<#cell_type>(#name) };
146            if let Some(ref_path) = references {
147                def = quote! { #def.references(#ref_path) };
148            }
149            def
150        } else if is_many {
151            let mut def = quote! { rango::Field::many(#name) };
152            if let Some((through, mine, theirs)) = via {
153                def = quote! {
154                    #def.link(rango::Link::Via(
155                        rango::Table(#through),
156                        rango::Name(#mine),
157                        rango::Name(#theirs)
158                    ))
159                };
160            }
161            def
162        } else {
163            let mut def = quote! { rango::Field::cell::<#cell_type>(#name) };
164            if is_optional {
165                def = quote! { #def.optional() };
166            }
167            if let Some(ref_path) = references {
168                def = quote! { #def.references(#ref_path) };
169            }
170            if let Some(default_val) = default {
171                def = quote! { #def.default_value(#default_val) };
172            }
173            def
174        };
175
176        field_defs.push(field_def);
177
178        if is_many {
179            read_parts.push(quote! { #ident: Vec::new() });
180            continue;
181        }
182        if is_key {
183            key_ident = Some(ident);
184        }
185        if !is_id {
186            write_parts.push(quote! { rango::Storable::put(&self.#ident, w); });
187        }
188        read_parts.push(quote! { #ident: rango::Storable::take(r)? });
189    }
190
191    let write_id = match &key_ident {
192        Some(ident) => quote! { rango::Storable::put(&self.#ident, w); },
193        None => quote! { w.nothing(); },
194    };
195    let read_id = match &key_ident {
196        Some(ident) => quote! {
197            self.#ident = rango::Storable::take(r)?;
198            Ok(())
199        },
200        None => quote! { Ok(()) },
201    };
202    let actions = if deeds.is_empty() {
203        quote! {}
204    } else {
205        quote! {
206            fn actions() -> Vec<rango::Action> {
207                vec![#(#deeds()),*, rango::Action::wipe()]
208            }
209        }
210    };
211
212    Ok(quote! {
213        impl rango::Model for #name {
214            fn table() -> rango::Table {
215                rango::Table(#table)
216            }
217
218            fn fields() -> Vec<rango::Field> {
219                vec![#(#field_defs),*]
220            }
221
222            fn write(&self, w: &mut dyn rango::Writer) {
223                #(#write_parts)*
224            }
225
226            fn read(r: &mut dyn rango::Reader) -> Result<Self, rango::StoreError> {
227                Ok(Self {
228                    #(#read_parts),*
229                })
230            }
231
232            fn write_id(&self, w: &mut dyn rango::Writer) {
233                #write_id
234            }
235
236            fn read_id(&mut self, r: &mut dyn rango::Reader) -> Result<(), rango::StoreError> {
237                #read_id
238            }
239
240            #actions
241        }
242    })
243}
244
245fn parse_model(input: &DeriveInput) -> syn::Result<(String, Vec<syn::Path>)> {
246    let mut table = None;
247    let mut deeds = Vec::new();
248    for attr in &input.attrs {
249        if !attr.path().is_ident("model") {
250            continue;
251        }
252        attr.parse_nested_meta(|meta| {
253            if meta.path.is_ident("table") {
254                let value = meta.value()?;
255                let lit: LitStr = value.parse()?;
256                table = Some(lit.value());
257                Ok(())
258            } else if meta.path.is_ident("actions") {
259                let value = meta.value()?;
260                let lit: LitStr = value.parse()?;
261                for raw in lit.value().split(',') {
262                    let raw = raw.trim();
263                    if raw.is_empty() {
264                        continue;
265                    }
266                    deeds.push(
267                        syn::parse_str::<syn::Path>(raw)
268                            .map_err(|_| meta.error("expected action paths"))?,
269                    );
270                }
271                Ok(())
272            } else {
273                Err(meta.error("expected `table = \"...\"` or `actions = \"...\"`"))
274            }
275        })?;
276    }
277    let name = input.ident.to_string().to_lowercase();
278    Ok((table.unwrap_or(format!("{name}s")), deeds))
279}
280
281fn parse_references(attrs: &[syn::Attribute]) -> syn::Result<Option<String>> {
282    for attr in attrs {
283        if !attr.path().is_ident("references") {
284            continue;
285        }
286        let lit: LitStr = attr.parse_args()?;
287        return Ok(Some(lit.value()));
288    }
289    Ok(None)
290}
291
292fn parse_via(attrs: &[syn::Attribute]) -> syn::Result<Option<(String, String, String)>> {
293    for attr in attrs {
294        if !attr.path().is_ident("via") {
295            continue;
296        }
297        let lit: LitStr = attr.parse_args()?;
298        let text = lit.value();
299        let parts: Vec<&str> = text.split('.').collect();
300        if let [through, mine, theirs] = parts.as_slice() {
301            return Ok(Some((
302                through.to_string(),
303                mine.to_string(),
304                theirs.to_string(),
305            )));
306        }
307        return Err(syn::Error::new_spanned(
308            attr,
309            "expected `#[via(\"through.mine.theirs\")]`",
310        ));
311    }
312    Ok(None)
313}
314
315fn parse_default(attrs: &[syn::Attribute]) -> syn::Result<Option<proc_macro2::TokenStream>> {
316    for attr in attrs {
317        if !attr.path().is_ident("default") {
318            continue;
319        }
320        let expr: Expr = attr.parse_args()?;
321        if let Expr::Lit(ExprLit {
322            lit: syn::Lit::Str(s),
323            ..
324        }) = expr
325        {
326            let val = s.value();
327            return Ok(Some(quote! { String::from(#val) }));
328        }
329        return Ok(Some(quote! { #expr }));
330    }
331    Ok(None)
332}
333
334fn unpack_option(ty: &syn::Type) -> (&syn::Type, bool) {
335    if let syn::Type::Path(tp) = ty
336        && let Some(segment) = tp.path.segments.last()
337        && segment.ident == "Option"
338        && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
339        && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
340    {
341        return (inner, true);
342    }
343    (ty, false)
344}
345
346fn unpack_vec(ty: &syn::Type) -> (&syn::Type, bool) {
347    if let syn::Type::Path(tp) = ty
348        && let Some(segment) = tp.path.segments.last()
349        && segment.ident == "Vec"
350        && let syn::PathArguments::AngleBracketed(args) = &segment.arguments
351        && let Some(syn::GenericArgument::Type(inner)) = args.args.first()
352    {
353        return (inner, true);
354    }
355    (ty, false)
356}