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
284 changes: 102 additions & 182 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ members = [
"modules/sdk-test-view",
"modules/sdk-test-case-conversion",
"modules/sdk-test-view-pk",
"modules/sdk-test-procedural-view-pk",
"modules/sdk-test-event-table",
"sdks/rust/tests/test-client",
"sdks/rust/tests/test-counter",
Expand All @@ -58,6 +59,7 @@ members = [
"sdks/rust/tests/view-client",
"sdks/rust/tests/case-conversion-client",
"sdks/rust/tests/view-pk-client",
"sdks/rust/tests/procedural-view-pk-client",
"sdks/rust/tests/event-table-client",
"tools/ci",
"tools/upgrade-version",
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 85 additions & 0 deletions crates/bindings-macro/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,49 @@ use crate::util::{check_duplicate_msg, match_meta};
pub(crate) struct ViewArgs {
name: Option<LitStr>,
accessor: Ident,
primary_key: Option<ViewPrimaryKeyArg>,
#[allow(unused)]
public: bool,
}

/// Argument accepted by `#[view(primary_key = ...)]`.
///
/// Both identifier and string literal syntax is supported.
enum ViewPrimaryKeyArg {
Ident(Ident),
Literal(LitStr),
}

impl ViewPrimaryKeyArg {
fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
if input.peek(LitStr) {
input.parse().map(Self::Literal)
} else {
input.parse().map(Self::Ident)
}
}

fn name(&self) -> String {
match self {
Self::Ident(ident) => ident.unraw().to_string(),
Self::Literal(lit) => lit.value(),
}
}

fn ident(&self) -> Option<&Ident> {
match self {
Self::Ident(ident) => Some(ident),
Self::Literal(_) => None,
}
}
}

impl ViewArgs {
/// Parse `#[view(accessor = ..., public)]` where both `name` and `public` are required.
pub(crate) fn parse(input: TokenStream, func_ident: &Ident) -> syn::Result<Self> {
let mut name = None;
let mut accessor = None;
let mut primary_key = None;
let mut public = None;
syn::meta::parser(|meta| {
match_meta!(match meta {
Expand All @@ -36,6 +70,10 @@ impl ViewArgs {
check_duplicate_msg(&accessor, &meta, "`accessor` already specified")?;
accessor = Some(meta.value()?.parse()?);
}
sym::primary_key => {
check_duplicate_msg(&primary_key, &meta, "`primary_key` already specified")?;
primary_key = Some(ViewPrimaryKeyArg::parse(meta.value()?)?);
}
});
Ok(())
})
Expand All @@ -51,6 +89,7 @@ impl ViewArgs {
.ok_or_else(|| syn::Error::new(Span::call_site(), "views must be `public`, e.g. `#[view(public)]`"))?;
Ok(Self {
name,
primary_key,
public: true,
accessor,
})
Expand All @@ -74,6 +113,29 @@ fn extract_impl_query_inner(ty: &syn::Type) -> Option<&syn::Type> {
None
}

/// If `ty` is a supported view return type, returns the row type `T`.
fn extract_view_return_row_type(ty: &syn::Type) -> Option<&syn::Type> {
if let Some(inner) = extract_impl_query_inner(ty) {
return Some(inner);
}

let syn::Type::Path(path) = ty else {
return None;
};

let seg = path.path.segments.last()?;
if !matches!(seg.ident.to_string().as_str(), "Vec" | "Option" | "RawQuery") {
return None;
}
let syn::PathArguments::AngleBracketed(args) = &seg.arguments else {
return None;
};
let Some(syn::GenericArgument::Type(inner)) = args.args.first() else {
return None;
};
Some(inner)
}

pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Result<TokenStream> {
let vis = &original_function.vis;
let func_name = &original_function.sig.ident;
Expand Down Expand Up @@ -221,6 +283,25 @@ pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Resu
};

let eff_ret_ty = &effective_ret_ty;
let primary_key_column_name = args.primary_key.as_ref().map(ViewPrimaryKeyArg::name);
let primary_key_field_check = args
.primary_key
.as_ref()
.and_then(ViewPrimaryKeyArg::ident)
.zip(extract_view_return_row_type(ret_ty))
.map(|(primary_key, row_ty)| {
quote! {
const _: () = {
fn _assert_view_primary_key_column #lt_params (__row: &#row_ty) #lt_where_clause {
fn _assert_view_primary_key_column_type<T: spacetimedb::ViewPrimaryKeyColumn>(_: &T) {}
_assert_view_primary_key_column_type(&__row.#primary_key);
}
};
}
});
let primary_key_column_const = primary_key_column_name
.as_ref()
.map(|primary_key| quote! { const VIEW_PRIMARY_KEY_COLUMNS: &'static [&'static str] = &[#primary_key]; });

Ok(quote! {
#emitted_fn
Expand All @@ -243,6 +324,8 @@ pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Resu
}
};

#primary_key_field_check

impl #func_name {
fn invoke(__ctx: #ctx_ty, __args: &[u8]) -> Vec<u8> {
spacetimedb::rt::ViewDispatcher::<#ctx_ty>::invoke::<_, _, _>(#func_name, __ctx, __args)
Expand All @@ -266,6 +349,8 @@ pub(crate) fn view_impl(args: ViewArgs, original_function: &ItemFn) -> syn::Resu
/// The pointer for invoking this function
const INVOKE: Self::Invoke = #func_name::invoke;

#primary_key_column_const

/// The return type of this function
fn return_type(
ts: &mut impl spacetimedb::sats::typespace::TypespaceBuilder
Expand Down
9 changes: 9 additions & 0 deletions crates/bindings-typescript/src/lib/autogen/types.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions crates/bindings-typescript/src/lib/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export class ModuleContext {
schedules: [],
procedures: [],
views: [],
viewPrimaryKeys: [],
lifeCycleReducers: [],
caseConversionPolicy: { tag: 'SnakeCase' },
explicitNames: {
Expand All @@ -220,6 +221,12 @@ export class ModuleContext {
push(module.reducers && { tag: 'Reducers', value: module.reducers });
push(module.procedures && { tag: 'Procedures', value: module.procedures });
push(module.views && { tag: 'Views', value: module.views });
push(
module.viewPrimaryKeys && {
tag: 'ViewPrimaryKeys',
value: module.viewPrimaryKeys,
}
);
push(module.schedules && { tag: 'Schedules', value: module.schedules });
push(
module.lifeCycleReducers && {
Expand Down
17 changes: 15 additions & 2 deletions crates/bindings-typescript/src/server/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
type ViewFn,
type ViewOpts,
type ViewReturnTypeBuilder,
type ValidateViewPrimaryKey,
type Views,
} from './views';
import type { UntypedTableDef } from '../lib/table';
Expand Down Expand Up @@ -347,7 +348,11 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
view<Ret extends ViewReturnTypeBuilder, F extends ViewFn<S, {}, Ret>>(
opts: ViewOpts,
ret: Ret,
fn: F
fn: F,
// Compile-time-only guard: this rest parameter is `[]` for valid return
// builders, but becomes a required error tuple when a returned row builder
// marks more than one column with `.primaryKey()`.
..._: ValidateViewPrimaryKey<Ret>
): ViewExport<F> {
return makeViewExport<S, {}, Ret, F>(this.#ctx, opts, {}, ret, fn);
}
Expand Down Expand Up @@ -380,7 +385,15 @@ export class Schema<S extends UntypedSchemaDef> implements ModuleDefaultExport {
anonymousView<
Ret extends ViewReturnTypeBuilder,
F extends AnonymousViewFn<S, {}, Ret>,
>(opts: ViewOpts, ret: Ret, fn: F): ViewExport<F> {
>(
opts: ViewOpts,
ret: Ret,
fn: F,
// Compile-time-only guard: this rest parameter is `[]` for valid return
// builders, but becomes a required error tuple when a returned row builder
// marks more than one column with `.primaryKey()`.
..._: ValidateViewPrimaryKey<Ret>
): ViewExport<F> {
return makeAnonViewExport<S, {}, Ret, F>(this.#ctx, opts, {}, ret, fn);
}

Expand Down
22 changes: 22 additions & 0 deletions crates/bindings-typescript/src/server/view.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,33 @@ const spacetime = schema({

const arrayRetValue = t.array(person.rowType);
const optionalPerson = t.option(person.rowType);
const multiplePrimaryKeyRows = t.array(
t.row('MultiplePrimaryKeyRows', {
id: t.u32().primaryKey(),
name: t.string().primaryKey(),
})
);

spacetime.anonymousView({ name: 'v1', public: true }, arrayRetValue, ctx => {
return ctx.from.person.build();
});

// @ts-expect-error views can have at most one primaryKey column on the returned row type.
spacetime.anonymousView(
{ name: 'multiplePrimaryRows', public: true },
multiplePrimaryKeyRows,
() => []
);

// @ts-expect-error the same multiple-primary-key check also applies to query-builder views.
spacetime.anonymousView(
{ name: 'multiplePrimaryRowsQuery', public: true },
multiplePrimaryKeyRows,
ctx => {
return ctx.from.person;
}
);

spacetime.anonymousView(
{ name: 'optionalPerson', public: true },
optionalPerson,
Expand Down
Loading
Loading