spacetimedb_bindings_macro/
lib.rs

1//! Defines procedural macros like `#[spacetimedb::table]`,
2//! simplifying writing SpacetimeDB modules in Rust.
3
4// DO NOT WRITE (public) DOCS IN THIS MODULE.
5// Docs should be written in the `spacetimedb` crate (i.e. `bindings/`) at reexport sites
6// using `#[doc(inline)]`.
7// We do this so that links to library traits, structs, etc can resolve correctly.
8//
9// (private documentation for the macro authors is totally fine here and you SHOULD write that!)
10
11mod procedure;
12mod reducer;
13mod sats;
14mod table;
15mod util;
16mod view;
17
18use proc_macro::TokenStream as StdTokenStream;
19use proc_macro2::TokenStream;
20use quote::quote;
21use std::time::Duration;
22use syn::{parse::ParseStream, Attribute};
23use syn::{ItemConst, ItemFn};
24use util::{cvt_attr, ok_or_compile_error};
25
26mod sym {
27    /// A symbol known at compile-time against
28    /// which identifiers and paths may be matched.
29    pub struct Symbol(&'static str);
30
31    macro_rules! symbol {
32        ($ident:ident) => {
33            symbol!($ident, $ident);
34        };
35        ($const:ident, $ident:ident) => {
36            #[allow(non_upper_case_globals)]
37            #[doc = concat!("Matches `", stringify!($ident), "`.")]
38            pub const $const: Symbol = Symbol(stringify!($ident));
39        };
40    }
41
42    symbol!(at);
43    symbol!(auto_inc);
44    symbol!(btree);
45    symbol!(client_connected);
46    symbol!(client_disconnected);
47    symbol!(column);
48    symbol!(columns);
49    symbol!(crate_, crate);
50    symbol!(direct);
51    symbol!(index);
52    symbol!(init);
53    symbol!(name);
54    symbol!(primary_key);
55    symbol!(private);
56    symbol!(public);
57    symbol!(repr);
58    symbol!(sats);
59    symbol!(scheduled);
60    symbol!(unique);
61    symbol!(update);
62    symbol!(default);
63
64    symbol!(u8);
65    symbol!(i8);
66    symbol!(u16);
67    symbol!(i16);
68    symbol!(u32);
69    symbol!(i32);
70    symbol!(u64);
71    symbol!(i64);
72    symbol!(u128);
73    symbol!(i128);
74    symbol!(f32);
75    symbol!(f64);
76
77    impl PartialEq<Symbol> for syn::Ident {
78        fn eq(&self, sym: &Symbol) -> bool {
79            self == sym.0
80        }
81    }
82    impl PartialEq<Symbol> for &syn::Ident {
83        fn eq(&self, sym: &Symbol) -> bool {
84            *self == sym.0
85        }
86    }
87    impl PartialEq<Symbol> for syn::Path {
88        fn eq(&self, sym: &Symbol) -> bool {
89            self.is_ident(sym)
90        }
91    }
92    impl PartialEq<Symbol> for &syn::Path {
93        fn eq(&self, sym: &Symbol) -> bool {
94            self.is_ident(sym)
95        }
96    }
97    impl std::fmt::Display for Symbol {
98        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99            f.write_str(self.0)
100        }
101    }
102    impl std::borrow::Borrow<str> for Symbol {
103        fn borrow(&self) -> &str {
104            self.0
105        }
106    }
107}
108
109#[proc_macro_attribute]
110pub fn procedure(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
111    cvt_attr::<ItemFn>(args, item, quote!(), |args, original_function| {
112        let args = procedure::ProcedureArgs::parse(args)?;
113        procedure::procedure_impl(args, original_function)
114    })
115}
116
117#[proc_macro_attribute]
118pub fn reducer(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
119    cvt_attr::<ItemFn>(args, item, quote!(), |args, original_function| {
120        let args = reducer::ReducerArgs::parse(args)?;
121        reducer::reducer_impl(args, original_function)
122    })
123}
124
125#[proc_macro_attribute]
126pub fn view(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
127    cvt_attr::<ItemFn>(args, item, quote!(), |args, original_function| {
128        let args = view::ViewArgs::parse(args, &original_function.sig.ident)?;
129        view::view_impl(args, original_function)
130    })
131}
132
133/// It turns out to be shockingly difficult to construct an [`Attribute`].
134/// That type is not [`Parse`], instead having two distinct methods
135/// for parsing "inner" vs "outer" attributes.
136///
137/// We need this [`Attribute`] in [`table`] so that we can "pushnew" it
138/// onto the end of a list of attributes. See comments within [`table`].
139fn derive_table_helper_attr() -> Attribute {
140    let source = quote!(#[derive(spacetimedb::__TableHelper)]);
141
142    syn::parse::Parser::parse2(Attribute::parse_outer, source)
143        .unwrap()
144        .into_iter()
145        .next()
146        .unwrap()
147}
148
149#[proc_macro_attribute]
150pub fn table(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
151    // put this on the struct so we don't get unknown attribute errors
152    let derive_table_helper: syn::Attribute = derive_table_helper_attr();
153
154    ok_or_compile_error(|| {
155        let item = TokenStream::from(item);
156        let mut derive_input: syn::DeriveInput = syn::parse2(item.clone())?;
157
158        // Add `derive(__TableHelper)` only if it's not already in the attributes of the `derive_input.`
159        // If multiple `#[table]` attributes are applied to the same `struct` item,
160        // this will ensure that we don't emit multiple conflicting implementations
161        // for traits like `SpacetimeType`, `Serialize` and `Deserialize`.
162        //
163        // We need to push at the end, rather than the beginning,
164        // because rustc expands attribute macros (including derives) top-to-bottom,
165        // and we need *all* `#[table]` attributes *before* the `derive(__TableHelper)`.
166        // This way, the first `table` will insert a `derive(__TableHelper)`,
167        // and all subsequent `#[table]`s on the same `struct` will see it,
168        // and not add another.
169        //
170        // Note, thank goodness, that `syn`'s `PartialEq` impls (provided with the `extra-traits` feature)
171        // skip any [`Span`]s contained in the items,
172        // thereby comparing for syntactic rather than structural equality. This shouldn't matter,
173        // since we expect that the `derive_table_helper` will always have the same [`Span`]s,
174        // but it's nice to know.
175        if !derive_input.attrs.contains(&derive_table_helper) {
176            derive_input.attrs.push(derive_table_helper);
177        }
178
179        let args = table::TableArgs::parse(args.into(), &derive_input.ident)?;
180        let generated = table::table_impl(args, &derive_input)?;
181        Ok(TokenStream::from_iter([quote!(#derive_input), generated]))
182    })
183}
184
185/// Special alias for `derive(SpacetimeType)`, aka [`schema_type`], for use by [`table`].
186///
187/// Provides helper attributes for `#[spacetimedb::table]`, so that we don't get unknown attribute errors.
188#[doc(hidden)]
189#[proc_macro_derive(__TableHelper, attributes(sats, unique, auto_inc, primary_key, index, default))]
190pub fn table_helper(input: StdTokenStream) -> StdTokenStream {
191    schema_type(input)
192}
193
194#[proc_macro]
195pub fn duration(input: StdTokenStream) -> StdTokenStream {
196    let dur = syn::parse_macro_input!(input with parse_duration);
197    let (secs, nanos) = (dur.as_secs(), dur.subsec_nanos());
198    quote!({
199        const DUR: ::core::time::Duration = ::core::time::Duration::new(#secs, #nanos);
200        DUR
201    })
202    .into()
203}
204
205fn parse_duration(input: ParseStream) -> syn::Result<Duration> {
206    let lookahead = input.lookahead1();
207    let (s, span) = if lookahead.peek(syn::LitStr) {
208        let s = input.parse::<syn::LitStr>()?;
209        (s.value(), s.span())
210    } else if lookahead.peek(syn::LitInt) {
211        let i = input.parse::<syn::LitInt>()?;
212        (i.to_string(), i.span())
213    } else {
214        return Err(lookahead.error());
215    };
216    humantime::parse_duration(&s).map_err(|e| syn::Error::new(span, format_args!("can't parse as duration: {e}")))
217}
218
219/// A helper for the common bits of the derive macros.
220fn sats_derive(
221    input: StdTokenStream,
222    assume_in_module: bool,
223    logic: impl FnOnce(&sats::SatsType) -> TokenStream,
224) -> StdTokenStream {
225    let input = syn::parse_macro_input!(input as syn::DeriveInput);
226    let crate_fallback = if assume_in_module {
227        quote!(spacetimedb::spacetimedb_lib)
228    } else {
229        quote!(spacetimedb_lib)
230    };
231    sats::sats_type_from_derive(&input, crate_fallback)
232        .map(|ty| logic(&ty))
233        .unwrap_or_else(syn::Error::into_compile_error)
234        .into()
235}
236
237#[proc_macro_derive(Deserialize, attributes(sats))]
238pub fn deserialize(input: StdTokenStream) -> StdTokenStream {
239    sats_derive(input, false, sats::derive_deserialize)
240}
241
242#[proc_macro_derive(Serialize, attributes(sats))]
243pub fn serialize(input: StdTokenStream) -> StdTokenStream {
244    sats_derive(input, false, sats::derive_serialize)
245}
246
247#[proc_macro_derive(SpacetimeType, attributes(sats))]
248pub fn schema_type(input: StdTokenStream) -> StdTokenStream {
249    sats_derive(input, true, |ty| {
250        let ident = ty.ident;
251        let name = &ty.name;
252
253        let krate = &ty.krate;
254        TokenStream::from_iter([
255            sats::derive_satstype(ty),
256            sats::derive_deserialize(ty),
257            sats::derive_serialize(ty),
258            // unfortunately, generic types don't work in modules at the moment.
259            quote!(#krate::__make_register_reftype!(#ident, #name);),
260        ])
261    })
262}
263
264#[proc_macro_attribute]
265pub fn client_visibility_filter(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
266    ok_or_compile_error(|| {
267        if !args.is_empty() {
268            return Err(syn::Error::new_spanned(
269                TokenStream::from(args),
270                "The `client_visibility_filter` attribute does not accept arguments",
271            ));
272        }
273
274        let item: ItemConst = syn::parse(item)?;
275        let rls_ident = item.ident.clone();
276        let register_rls_symbol = format!("__preinit__20_register_row_level_security_{rls_ident}");
277
278        Ok(quote! {
279            #item
280
281            const _: () = {
282                #[export_name = #register_rls_symbol]
283                extern "C" fn __register_client_visibility_filter() {
284                    spacetimedb::rt::register_row_level_security(#rls_ident.sql_text())
285                }
286            };
287        })
288    })
289}