Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
10 changes: 10 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 All @@ -38,6 +39,7 @@ mod sym {
}

symbol!(at);
symbol!(anonymous);
symbol!(auto_inc);
symbol!(btree);
symbol!(client_connected);
Expand Down Expand Up @@ -112,6 +114,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
284 changes: 284 additions & 0 deletions crates/bindings-macro/src/view.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
use proc_macro2::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 {
anonymous: 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(
proc_macro2::Span::call_site(),
"views must be declared as #[view(public)]; public is required",
));
}
let mut public = false;
let mut anonymous = false;
syn::meta::parser(|meta| {
match_meta!(match meta {
sym::public => {
public = true;
}
sym::anonymous => {
anonymous = true;
}
});
Ok(())
})
.parse2(input)?;
if !public {
return Err(syn::Error::new(
proc_macro2::Span::call_site(),
"views must be declared as #[view(public)]; public is required",
));
}
Ok(Self { anonymous })
}
}

fn view_impl_anon(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 all function parameters, except for `self` ones that aren't allowed.
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, "`self` arguments not allowed in views")),
})
.collect::<syn::Result<Vec<_>>>()?;

// Extract all function 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
let ctx_ty = arg_tys.first().ok_or_else(|| {
syn::Error::new_spanned(
original_function.sig.fn_token,
"An anonymous view must have `&AnonymousViewContext` as its first argument",
)
})?;

// Extract the return type
let ret_ty = match &original_function.sig.output {
syn::ReturnType::Default => None,
syn::ReturnType::Type(_, t) => Some(&**t),
}
.ok_or_else(|| {
syn::Error::new_spanned(
original_function.sig.fn_token,
"views must return `Vec<T>` where `T` is a `SpacetimeType`",
)
})?;

// Extract the non-context parameters
let arg_tys = arg_tys.iter().skip(1);

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::register_anonymous_view::<_, #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::AnonymousViewContextArg>::_ITEM;
let _ = <#ret_ty as spacetimedb::rt::ViewReturn>::_ITEM;
#(let _ = <#arg_tys as spacetimedb::rt::ViewArg>::_ITEM;)*
}
};

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

#[automatically_derived]
impl spacetimedb::rt::FnInfo for #func_name {
/// The type of this function
type Invoke = spacetimedb::rt::AnonymousFn;

/// 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))
}
}
})
}

fn view_impl_client(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 all function parameters, except for `self` ones that aren't allowed.
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, "`self` arguments not allowed in views")),
})
.collect::<syn::Result<Vec<_>>>()?;

// Extract all function 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
let ctx_ty = arg_tys.first().ok_or_else(|| {
syn::Error::new_spanned(
original_function.sig.fn_token,
"A view must have `&ViewContext` as its first argument",
)
})?;

// Extract the return type
let ret_ty = match &original_function.sig.output {
syn::ReturnType::Default => None,
syn::ReturnType::Type(_, t) => Some(&**t),
}
.ok_or_else(|| {
syn::Error::new_spanned(
original_function.sig.fn_token,
"views must return `Vec<T>` where `T` is a `SpacetimeType`",
)
})?;

// Extract the non-context parameters
let arg_tys = arg_tys.iter().skip(1);

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::register_view::<_, #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;
#(let _ = <#arg_tys as spacetimedb::rt::ViewArg>::_ITEM;)*
}
};

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

#[automatically_derived]
impl spacetimedb::rt::FnInfo for #func_name {
/// The type of this function
type Invoke = spacetimedb::rt::ViewFn;

/// 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))
}
}
})
}

pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Result<TokenStream> {
if args.anonymous {
view_impl_anon(original_function)
} else {
view_impl_client(original_function)
}
}
51 changes: 51 additions & 0 deletions crates/bindings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,57 @@ 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>` where `T` is a `SpacetimeType`.
///
/// ```no_run
/// # mod demo {
/// use spacetimedb::{view, table, AnonymousViewContext, ViewContext};
///
/// #[table(name = player)]
/// struct Player {
/// #[unique]
/// identity: Identity,
/// #[index(btree)]
/// level: u32,
/// }
///
/// #[view(public)]
/// pub fn player(ctx: &ViewContext) -> Vec<Player> {
/// ctx.db.player().identity().find(ctx.sender).into_iter().collect()
/// }
///
/// #[view(public, anonymous)]
/// pub fn player(ctx: &AnonymousViewContext, level: u32) -> Vec<Player> {
/// ctx.db.player().level().filter(level).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
Loading