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