Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions crates/bindings-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod reducer;
mod sats;
mod table;
mod util;
mod view;

use proc_macro::TokenStream as StdTokenStream;
use proc_macro2::TokenStream;
Expand Down Expand Up @@ -112,6 +113,14 @@ pub fn reducer(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
})
}

#[proc_macro_attribute]
pub fn view(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
cvt_attr::<ItemFn>(args, item, quote!(), |args, original_function| {
let args = view::ViewArgs::parse(args)?;
view::view_impl(args, original_function)
})
}

/// It turns out to be shockingly difficult to construct an [`Attribute`].
/// That type is not [`Parse`], instead having two distinct methods
/// for parsing "inner" vs "outer" attributes.
Expand Down
5 changes: 3 additions & 2 deletions crates/bindings-macro/src/reducer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,11 +139,12 @@ pub(crate) fn reducer_impl(args: ReducerArgs, original_function: &ItemFn) -> syn
}
}
#[automatically_derived]
impl spacetimedb::rt::ReducerInfo for #func_name {
impl spacetimedb::rt::FnInfo for #func_name {
type Invoke = spacetimedb::rt::ReducerFn;
const NAME: &'static str = #reducer_name;
#(const LIFECYCLE: Option<spacetimedb::rt::LifecycleReducer> = Some(#lifecycle);)*
const ARG_NAMES: &'static [Option<&'static str>] = &[#(#opt_arg_names),*];
const INVOKE: spacetimedb::rt::ReducerFn = #func_name::invoke;
const INVOKE: Self::Invoke = #func_name::invoke;
}
})
}
2 changes: 1 addition & 1 deletion crates/bindings-macro/src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -821,7 +821,7 @@ pub(crate) fn table_impl(mut args: TableArgs, item: &syn::DeriveInput) -> syn::R
let reducer = &sched.reducer;
let scheduled_at_id = scheduled_at_column.index;
let desc = quote!(spacetimedb::table::ScheduleDesc {
reducer_name: <#reducer as spacetimedb::rt::ReducerInfo>::NAME,
reducer_name: <#reducer as spacetimedb::rt::FnInfo>::NAME,
scheduled_at_column: #scheduled_at_id,
});

Expand Down
178 changes: 178 additions & 0 deletions crates/bindings-macro/src/view.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::parse::Parser;
use syn::{FnArg, ItemFn};

use crate::sym;
use crate::util::{ident_to_litstr, match_meta};

pub(crate) struct ViewArgs {
#[allow(unused)]
public: bool,
}

impl ViewArgs {
/// Parse `#[view(public)]` where `public` is required.
pub(crate) fn parse(input: TokenStream) -> syn::Result<Self> {
if input.is_empty() {
return Err(syn::Error::new(
Span::call_site(),
"views must be declared as `#[view(public)]`; `public` is required",
));
}
let mut public = false;
syn::meta::parser(|meta| {
match_meta!(match meta {
sym::public => {
if public {
return Err(syn::Error::new(
Span::call_site(),
"duplicate attribute argument: `public`",
));
}
public = true;
}
});
Ok(())
})
.parse2(input)?;
if !public {
return Err(syn::Error::new(
Span::call_site(),
"views must be declared as `#[view(public)]`; `public` is required",
));
}
Ok(Self { public })
}
}

pub(crate) fn view_impl(_args: ViewArgs, original_function: &ItemFn) -> syn::Result<TokenStream> {
let func_name = &original_function.sig.ident;
let view_name = ident_to_litstr(func_name);
let vis = &original_function.vis;

for param in &original_function.sig.generics.params {
let err = |msg| syn::Error::new_spanned(param, msg);
match param {
syn::GenericParam::Lifetime(_) => {}
syn::GenericParam::Type(_) => return Err(err("type parameters are not allowed on views")),
syn::GenericParam::Const(_) => return Err(err("const parameters are not allowed on views")),
}
}

// Extract parameters
let typed_args = original_function
.sig
.inputs
.iter()
.map(|arg| match arg {
FnArg::Typed(arg) => Ok(arg),
FnArg::Receiver(_) => Err(syn::Error::new_spanned(
arg,
"The `self` parameter is not allowed in views",
)),
})
.collect::<syn::Result<Vec<_>>>()?;

// Extract parameter names
let opt_arg_names = typed_args.iter().map(|arg| {
if let syn::Pat::Ident(i) = &*arg.pat {
let name = i.ident.to_string();
quote!(Some(#name))
} else {
quote!(None)
}
});

let arg_tys = typed_args.iter().map(|arg| arg.ty.as_ref()).collect::<Vec<_>>();

// Extract the context type and the rest of the parameter types
let [ctx_ty, arg_tys @ ..] = &arg_tys[..] else {
return Err(syn::Error::new_spanned(
&original_function.sig,
"Views must always have a context parameter: `&ViewContext` or `&AnonymousViewContext`",
));
};

// Extract the context type
let ctx_ty = match ctx_ty {
syn::Type::Reference(ctx_ty) => ctx_ty.elem.as_ref(),
_ => {
return Err(syn::Error::new_spanned(
ctx_ty,
"The first parameter of a view must be a context parameter: `&ViewContext` or `&AnonymousViewContext`; passed by reference",
));
}
};

// Views must return a result
let ret_ty = match &original_function.sig.output {
syn::ReturnType::Type(_, t) => t.as_ref(),
syn::ReturnType::Default => {
return Err(syn::Error::new_spanned(
&original_function.sig,
"views must return `Vec<T>` or `Option<T>` where `T` is a `SpacetimeType`",
));
}
};

let register_describer_symbol = format!("__preinit__20_register_describer_{}", view_name.value());

let lt_params = &original_function.sig.generics;
let lt_where_clause = &lt_params.where_clause;

let generated_describe_function = quote! {
#[export_name = #register_describer_symbol]
pub extern "C" fn __register_describer() {
spacetimedb::rt::ViewRegistrar::<#ctx_ty>::register::<_, #func_name, _, _>(#func_name)
}
};

Ok(quote! {
const _: () = { #generated_describe_function };

#[allow(non_camel_case_types)]
#vis struct #func_name { _never: ::core::convert::Infallible }

const _: () = {
fn _assert_args #lt_params () #lt_where_clause {
let _ = <#ctx_ty as spacetimedb::rt::ViewContextArg>::_ITEM;
let _ = <#ret_ty as spacetimedb::rt::ViewReturn>::_ITEM;
}
};

const _: () = {
fn _assert_args #lt_params () #lt_where_clause {
#(let _ = <#arg_tys as spacetimedb::rt::ViewArg>::_ITEM;)*
}
};

impl #func_name {
fn invoke(__ctx: #ctx_ty, __args: &[u8]) -> Vec<u8> {
spacetimedb::rt::ViewDispatcher::<#ctx_ty>::invoke::<_, _, _>(#func_name, __ctx, __args)
}
}

#[automatically_derived]
impl spacetimedb::rt::FnInfo for #func_name {
/// The type of this function
type Invoke = <spacetimedb::rt::ViewKind<#ctx_ty> as spacetimedb::rt::ViewKindTrait>::InvokeFn;

/// The name of this function
const NAME: &'static str = #view_name;

/// The parameter names of this function
const ARG_NAMES: &'static [Option<&'static str>] = &[#(#opt_arg_names),*];

/// The pointer for invoking this function
const INVOKE: Self::Invoke = #func_name::invoke;

/// The return type of this function
fn return_type(
ts: &mut impl spacetimedb::sats::typespace::TypespaceBuilder
) -> Option<spacetimedb::sats::AlgebraicType> {
Some(<#ret_ty as spacetimedb::SpacetimeType>::make_type(ts))
}
}
})
}
86 changes: 86 additions & 0 deletions crates/bindings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,92 @@ pub use spacetimedb_bindings_macro::table;
#[doc(inline)]
pub use spacetimedb_bindings_macro::reducer;

/// Marks a function as a spacetimedb view.
///
/// A view is a function with read-only access to the database.
///
/// The first argument of a view is always a [`&ViewContext`] or [`&AnonymousViewContext`].
/// The former can only read from the database whereas latter can also access info about the caller.
///
/// After this, a view can take any number of arguments just like reducers.
/// These arguments must implement the [`SpacetimeType`], [`Serialize`], and [`Deserialize`] traits.
/// All of these traits can be derived at once by marking a type with `#[derive(SpacetimeType)]`.
///
/// Views return `Vec<T>` or `Option<T>` where `T` is a `SpacetimeType`.
///
/// ```no_run
/// # mod demo {
/// use spacetimedb::{view, table, AnonymousViewContext, SpacetimeType, ViewContext};
/// use spacetimedb_lib::Identity;
///
/// #[table(name = player)]
/// struct Player {
/// #[auto_inc]
/// #[primary_key]
/// id: u64,
///
/// #[unique]
/// identity: Identity,
///
/// #[index(btree)]
/// level: u32,
/// }
///
/// #[derive(SpacetimeType)]
/// struct PlayerId {
/// id: u64,
/// }
///
/// #[table(name = location, index(name = coordinates, btree(columns = [x, y])))]
/// struct Location {
/// #[unique]
/// player_id: u64,
/// x: u64,
/// y: u64,
/// }
///
/// #[view(public)]
/// pub fn my_player(ctx: &ViewContext) -> Option<Player> {
/// ctx.db.player().identity().find(ctx.sender)
/// }
///
/// #[view(public)]
/// pub fn my_player_id(ctx: &ViewContext) -> Option<PlayerId> {
/// ctx.db.player().identity().find(ctx.sender).map(|Player { id, .. }| PlayerId { id })
/// }
///
/// #[view(public)]
/// pub fn players_at_level(ctx: &AnonymousViewContext, level: u32) -> Vec<Player> {
/// ctx.db.player().level().filter(level).collect()
/// }
///
/// #[view(public)]
/// pub fn players_at_coordinates(ctx: &AnonymousViewContext, x: u64, y: u64) -> Vec<Player> {
/// ctx
/// .db
/// .location()
/// .coordinates()
/// .filter((x, y))
/// .filter_map(|location| ctx.db.player().id().find(location.player_id))
/// .collect()
/// }
/// # }
/// ```
///
/// Just like reducers, views are limited in their ability to interact with the outside world.
/// They have no access to any network or filesystem interfaces.
/// Calling methods from [`std::io`], [`std::net`], or [`std::fs`] will result in runtime errors.
///
/// Views are callable by reducers and other views simply by passing their `ViewContext`..
/// This is a regular function call.
/// The callee will run within the caller's transaction.
///
///
/// [`&ViewContext`]: `ViewContext`
/// [`&AnonymousViewContext`]: `AnonymousViewContext`
#[doc(inline)]
pub use spacetimedb_bindings_macro::view;

/// One of two possible types that can be passed as the first argument to a `#[view]`.
/// The other is [`ViewContext`].
/// Use this type if the view does not depend on the caller's identity.
Expand Down
Loading