Skip to content
Merged
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
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))
}
}
})
}
127 changes: 127 additions & 0 deletions crates/bindings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,133 @@ 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,
/// }
///
/// impl Player {
/// fn merge(self, location: Location) -> PlayerAndLocation {
/// PlayerAndLocation {
/// player_id: self.id,
/// level: self.level,
/// x: location.x,
/// y: location.y,
/// }
/// }
/// }
///
/// #[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,
/// }
///
/// #[derive(SpacetimeType)]
/// struct PlayerAndLocation {
/// player_id: u64,
/// level: u32,
/// x: u64,
/// y: u64,
/// }
///
/// // A view that selects at most one row from a table
/// #[view(public)]
/// fn my_player(ctx: &ViewContext) -> Option<Player> {
/// ctx.db.player().identity().find(ctx.sender)
/// }
///
/// // An example of column projection
/// #[view(public)]
/// fn my_player_id(ctx: &ViewContext) -> Option<PlayerId> {
/// ctx.db.player().identity().find(ctx.sender).map(|Player { id, .. }| PlayerId { id })
/// }
///
/// // An example of a parameterized view
/// #[view(public)]
/// fn players_at_level(ctx: &AnonymousViewContext, level: u32) -> Vec<Player> {
/// ctx.db.player().level().filter(level).collect()
/// }
///
/// // An example that is analogous to a semijoin in sql
/// #[view(public)]
/// 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()
/// }
///
/// // An example of a join that combines fields from two different tables
/// #[view(public)]
/// fn players_with_coordinates(ctx: &AnonymousViewContext, x: u64, y: u64) -> Vec<PlayerAndLocation> {
/// ctx
/// .db
/// .location()
/// .coordinates()
/// .filter((x, y))
/// .filter_map(|location| ctx
/// .db
/// .player()
/// .id()
/// .find(location.player_id)
/// .map(|player| player.merge(location))
/// )
/// .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