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
52 changes: 52 additions & 0 deletions crates/bindings-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,3 +299,55 @@ pub fn client_visibility_filter(args: StdTokenStream, item: StdTokenStream) -> S
})
})
}

/// Known setting names and their registration code generators.
const KNOWN_SETTINGS: &[&str] = &["CASE_CONVERSION_POLICY"];

#[proc_macro_attribute]
pub fn settings(args: StdTokenStream, item: StdTokenStream) -> StdTokenStream {
ok_or_compile_error(|| {
if !args.is_empty() {
return Err(syn::Error::new_spanned(
TokenStream::from(args),
"The `settings` attribute does not accept arguments",
));
}

let item: ItemConst = syn::parse(item)?;
let ident = &item.ident;
let ident_str = ident.to_string();

if !KNOWN_SETTINGS.contains(&ident_str.as_str()) {
return Err(syn::Error::new_spanned(
ident,
format!(
"unknown setting `{ident_str}`. Known settings: {}",
KNOWN_SETTINGS.join(", ")
),
));
}

// Use a fixed export name so that two `#[spacetimedb::settings]` consts
// for the same setting produce a linker error (duplicate symbol).
let register_symbol = format!("__preinit__05_setting_{ident_str}");

// Generate the registration call based on the setting name.
let register_call = match ident_str.as_str() {
"CASE_CONVERSION_POLICY" => quote! {
spacetimedb::rt::register_case_conversion_policy(#ident)
},
_ => unreachable!("validated above"),
};

Ok(quote! {
#item

const _: () = {
#[export_name = #register_symbol]
extern "C" fn __register_setting() {
#register_call
}
};
})
})
}
15 changes: 15 additions & 0 deletions crates/bindings-typescript/src/lib/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type AlgebraicTypeVariants,
} from './algebraic_type';
import type {
CaseConversionPolicy,
RawModuleDefV10,
RawModuleDefV10Section,
RawScopedTypeNameV10,
Expand Down Expand Up @@ -193,9 +194,23 @@ export class ModuleContext {
value: module.explicitNames,
}
);
push(
module.caseConversionPolicy && {
tag: 'CaseConversionPolicy',
value: module.caseConversionPolicy,
}
);
return { sections };
}

/**
* Set the case conversion policy for this module.
* Called by the settings mechanism.
*/
setCaseConversionPolicy(policy: CaseConversionPolicy) {
this.#moduleDef.caseConversionPolicy = policy;
}

get typespace() {
return this.#moduleDef.typespace;
}
Expand Down
8 changes: 7 additions & 1 deletion crates/bindings-typescript/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
export * from '../lib/type_builders';
export { schema, type InferSchema, type ModuleExport } from './schema';
export {
schema,
type InferSchema,
type ModuleExport,
type ModuleSettings,
} from './schema';
export { CaseConversionPolicy } from '../lib/autogen/types';
export { table } from '../lib/table';
export { SenderError, SpacetimeHostError, errors } from './errors';
export type { Reducer, ReducerCtx } from '../lib/reducers';
Expand Down
28 changes: 26 additions & 2 deletions crates/bindings-typescript/src/server/schema.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { moduleHooks, type ModuleDefaultExport } from 'spacetime:sys@2.0';
import { Lifecycle } from '../lib/autogen/types';
import { CaseConversionPolicy, Lifecycle } from '../lib/autogen/types';
import {
type ParamsAsObject,
type ParamsObj,
Expand Down Expand Up @@ -525,10 +525,34 @@ export type InferSchema<SchemaDef extends Schema<any>> =
* });
* ```
*/
/**
* Module-level settings that can be passed to `schema()`.
*/
export interface ModuleSettings {
/**
* The case conversion policy for this module.
* Defaults to `SnakeCase` if not specified.
*
* @example
* ```ts
* export default schema({
* player,
* }, { CASE_CONVERSION_POLICY: CaseConversionPolicy.None });
* ```
*/
CASE_CONVERSION_POLICY?: CaseConversionPolicy;
}

export function schema<const H extends Record<string, UntypedTableSchema>>(
tables: H
tables: H,
moduleSettings?: ModuleSettings
): Schema<TablesToSchema<H>> {
const ctx = new SchemaInner<TablesToSchema<H>>(ctx => {
// Apply module settings.
if (moduleSettings?.CASE_CONVERSION_POLICY != null) {
ctx.setCaseConversionPolicy(moduleSettings.CASE_CONVERSION_POLICY);
}

const tableSchemas: Record<string, UntypedTableDef> = {};
for (const [accName, table] of Object.entries(tables)) {
const tableDef = table.tableDef(ctx, accName);
Expand Down
25 changes: 25 additions & 0 deletions crates/bindings/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ pub use sats::SpacetimeType;
pub use spacetimedb_bindings_macro::__TableHelper;
pub use spacetimedb_bindings_sys as sys;
pub use spacetimedb_lib;
pub use spacetimedb_lib::db::raw_def::v10::CaseConversionPolicy;
pub use spacetimedb_lib::de::{Deserialize, DeserializeOwned};
pub use spacetimedb_lib::sats;
pub use spacetimedb_lib::ser::Serialize;
Expand Down Expand Up @@ -99,6 +100,30 @@ pub use spacetimedb_bindings_macro::duration;
#[doc(inline, hidden)] // TODO: RLS filters are currently unimplemented, and are not enforced.
pub use spacetimedb_bindings_macro::client_visibility_filter;

/// Declare a module-level setting.
///
/// Apply this attribute to a `const` item whose name is a known setting:
///
/// ```ignore
/// use spacetimedb::CaseConversionPolicy;
///
/// #[spacetimedb::settings]
/// const CASE_CONVERSION_POLICY: CaseConversionPolicy = CaseConversionPolicy::SnakeCase;
/// ```
///
/// # Known Settings
///
/// | Const Name | Type | Default | Description |
/// |---|---|---|---|
/// | `CASE_CONVERSION_POLICY` | [`CaseConversionPolicy`] | `SnakeCase` | How identifiers are converted to canonical names |
///
/// # Errors
///
/// - Unknown setting name: compile error listing known settings
/// - Duplicate setting: linker error (duplicate symbol)
#[doc(inline)]
pub use spacetimedb_bindings_macro::settings;

/// Declares a table with a particular row type.
///
/// This attribute is applied to a struct type with named fields.
Expand Down
20 changes: 19 additions & 1 deletion crates/bindings/src/rt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use crate::query_builder::{FromWhere, HasCols, LeftSemiJoin, RawQuery, RightSemi
use crate::table::IndexAlgo;
use crate::{sys, AnonymousViewContext, IterBuf, ReducerContext, ReducerResult, SpacetimeType, Table, ViewContext};
use spacetimedb_lib::bsatn::EncodeError;
use spacetimedb_lib::db::raw_def::v10::{ExplicitNames as RawExplicitNames, RawModuleDefV10Builder};
use spacetimedb_lib::db::raw_def::v10::{
CaseConversionPolicy, ExplicitNames as RawExplicitNames, RawModuleDefV10Builder,
};
pub use spacetimedb_lib::db::raw_def::v9::Lifecycle as LifecycleReducer;
use spacetimedb_lib::db::raw_def::v9::{RawIndexAlgorithm, TableType, ViewResultHeader};
use spacetimedb_lib::de::{self, Deserialize, DeserializeOwned, Error as _, SeqProductAccess};
Expand Down Expand Up @@ -856,6 +858,22 @@ pub fn register_row_level_security(sql: &'static str) {
})
}

/// Set the case conversion policy for this module.
///
/// This is called by the `#[spacetimedb::settings]` attribute macro.
/// Do not call directly; use the attribute instead:
///
/// ```ignore
/// #[spacetimedb::settings]
/// const CASE_CONVERSION_POLICY: CaseConversionPolicy = CaseConversionPolicy::SnakeCase;
/// ```
#[doc(hidden)]
pub fn register_case_conversion_policy(policy: CaseConversionPolicy) {
register_describer(move |module| {
module.inner.set_case_conversion_policy(policy);
})
}

/// A builder for a module.
#[derive(Default)]
pub struct ModuleBuilder {
Expand Down
16 changes: 16 additions & 0 deletions crates/lib/src/db/raw_def/v10.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1054,6 +1054,22 @@ impl RawModuleDefV10Builder {
self.explicit_names_mut().merge(names);
}

/// Set the case conversion policy for this module.
///
/// By default, SpacetimeDB applies `SnakeCase` conversion to table names,
/// column names, reducer names, etc. Use `CaseConversionPolicy::None` to
/// disable all case conversion (useful for modules with existing data that
/// was stored under the original naming convention).
pub fn set_case_conversion_policy(&mut self, policy: CaseConversionPolicy) {
// Remove any existing policy section.
self.module
.sections
.retain(|s| !matches!(s, RawModuleDefV10Section::CaseConversionPolicy(_)));
self.module
.sections
.push(RawModuleDefV10Section::CaseConversionPolicy(policy));
}

/// Finish building, consuming the builder and returning the module.
/// The module should be validated before use.
pub fn finish(self) -> RawModuleDefV10 {
Expand Down
Loading