wasm_dbms_macros/lib.rs
1#![crate_name = "wasm_dbms_macros"]
2#![crate_type = "lib"]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4#![deny(clippy::print_stdout)]
5#![deny(clippy::print_stderr)]
6
7//! Runtime-agnostic procedural macros for the wasm-dbms DBMS engine.
8//!
9//! This crate provides procedural macros to automatically implement traits
10//! required by the `wasm-dbms` engine.
11//!
12//! ## Provided Derive Macros
13//!
14//! - `Encode`: Automatically implements the `Encode` trait for structs.
15//! - `Table`: Automatically implements the `TableSchema` trait and associated types.
16//! - `DatabaseSchema`: Generates `DatabaseSchema<M>` trait dispatch and `register_tables`.
17//! - `CustomDataType`: Bridge user-defined types into the `Value` system.
18
19#![doc(html_playground_url = "https://play.rust-lang.org")]
20#![doc(
21 html_favicon_url = "https://raw.githubusercontent.com/veeso/wasm-dbms/main/assets/images/cargo/logo-128.png"
22)]
23#![doc(
24 html_logo_url = "https://raw.githubusercontent.com/veeso/wasm-dbms/main/assets/images/cargo/logo-512.png"
25)]
26
27use proc_macro::TokenStream;
28use syn::{DeriveInput, parse_macro_input};
29
30mod custom_data_type;
31mod database_schema;
32mod encode;
33mod table;
34mod utils;
35
36/// Automatically implements the `Encode` trait for a struct.
37///
38/// This derive macro generates two methods required by the `Encode` trait:
39///
40/// - `fn data_size() -> DataSize`
41/// Computes the static size of the encoded type.
42/// If all fields implement `Encode::data_size()` returning
43/// `DataSize::Fixed(n)`, then the type is also considered fixed-size.
44/// Otherwise, the type is `DataSize::Dynamic`.
45///
46/// - `fn size(&self) -> MSize`
47/// Computes the runtime-encoding size of the value by summing the
48/// sizes of all fields.
49///
50/// # What the macro generates
51///
52/// Given a struct like:
53///
54/// ```rust,ignore
55/// #[derive(Encode)]
56/// struct User {
57/// id: Uint32,
58/// name: Text,
59/// }
60/// ```
61///
62/// The macro expands into:
63///
64/// ```rust,ignore
65/// impl Encode for User {
66/// const DATA_SIZE: DataSize = DataSize::Dynamic; // or DataSize::Fixed(n) if applicable
67///
68/// fn size(&self) -> MSize {
69/// self.id.size() + self.name.size()
70/// }
71///
72/// fn encode(&'_ self) -> std::borrow::Cow<'_, [u8]> {
73/// let mut encoded = Vec::with_capacity(self.size() as usize);
74/// encoded.extend_from_slice(&self.id.encode());
75/// encoded.extend_from_slice(&self.name.encode());
76/// std::borrow::Cow::Owned(encoded)
77/// }
78///
79/// fn decode(data: std::borrow::Cow<[u8]>) -> ::wasm_dbms_api::prelude::MemoryResult<Self> {
80/// let mut offset = 0;
81/// let id = Uint32::decode(std::borrow::Borrowed(&data[offset..]))?;
82/// offset += id.size() as usize;
83/// let name = Text::decode(std::borrow::Borrowed(&data[offset..]))?;
84/// offset += name.size() as usize;
85/// Ok(Self { id, name })
86/// }
87/// }
88/// ```
89/// # Requirements
90///
91/// - Each field type must implement `Encode`.
92/// - Only works on `struct`s; enums and unions are not supported.
93/// - All field identifiers must be valid Rust identifiers (no tuple structs).
94///
95/// # Notes
96///
97/// - It is intended for internal use within the `wasm-dbms` DBMS memory
98/// system.
99///
100/// # Errors
101///
102/// The macro will fail to expand if:
103///
104/// - The struct has unnamed fields (tuple struct)
105/// - A field type does not implement `Encode`
106/// - The macro is applied to a non-struct item.
107///
108/// # Example
109///
110/// ```rust,ignore
111/// #[derive(Encode, Debug, PartialEq, Eq)]
112/// struct Position {
113/// x: Int32,
114/// y: Int32,
115/// }
116///
117/// let pos = Position { x: 10.into(), y: 20.into() };
118/// assert_eq!(Position::data_size(), DataSize::Fixed(8));
119/// assert_eq!(pos.size(), 8);
120/// let encoded = pos.encode();
121/// let decoded = Position::decode(encoded).unwrap();
122/// assert_eq!(pos, decoded);
123/// ```
124#[proc_macro_derive(Encode)]
125pub fn derive_encode(input: TokenStream) -> TokenStream {
126 let input = parse_macro_input!(input as DeriveInput);
127 self::encode::encode(input, None)
128 .expect("Failed to derive `Encode`")
129 .into()
130}
131
132/// Given a struct representing a database table, automatically implements
133/// the `TableSchema` trait with all the necessary types to work with the wasm-dbms engine.
134/// So given this struct:
135///
136/// ```rust,ignore
137/// #[derive(Table, Encode)]
138/// #[table = "posts"]
139/// struct Post {
140/// #[primary_key]
141/// id: Uint32,
142/// title: Text,
143/// content: Text,
144/// #[foreign_key(entity = "User", table = "users", column = "id")]
145/// author_id: Uint32,
146/// }
147/// ```
148///
149/// What we expect as output is:
150///
151/// - To implement the `TableSchema` trait for the struct as follows:
152///
153/// ```rust,ignore
154/// impl TableSchema for Post {
155/// type Insert = PostInsertRequest;
156/// type Record = PostRecord;
157/// type Update = PostUpdateRequest;
158/// type ForeignFetcher = PostForeignFetcher;
159///
160/// fn columns() -> &'static [ColumnDef] {
161/// &[
162/// ColumnDef {
163/// name: "id",
164/// data_type: DataTypeKind::Uint32,
165/// auto_increment: false,
166/// nullable: false,
167/// primary_key: true,
168/// unique: true,
169/// foreign_key: None,
170/// },
171/// ColumnDef {
172/// name: "title",
173/// data_type: DataTypeKind::Text,
174/// auto_increment: false,
175/// nullable: false,
176/// primary_key: false,
177/// unique: false,
178/// foreign_key: None,
179/// },
180/// ColumnDef {
181/// name: "content",
182/// data_type: DataTypeKind::Text,
183/// auto_increment: false,
184/// nullable: false,
185/// primary_key: false,
186/// unique: false,
187/// foreign_key: None,
188/// },
189/// ColumnDef {
190/// name: "user_id",
191/// data_type: DataTypeKind::Uint32,
192/// auto_increment: false,
193/// nullable: false,
194/// primary_key: false,
195/// unique: false,
196/// foreign_key: Some(ForeignKeyDef {
197/// local_column: "user_id",
198/// foreign_table: "users",
199/// foreign_column: "id",
200/// }),
201/// },
202/// ]
203/// }
204///
205/// fn table_name() -> &'static str {
206/// "posts"
207/// }
208///
209/// fn primary_key() -> &'static str {
210/// "id"
211/// }
212///
213/// fn to_values(self) -> Vec<(ColumnDef, Value)> {
214/// vec![
215/// (Self::columns()[0], Value::Uint32(self.id)),
216/// (Self::columns()[1], Value::Text(self.title)),
217/// (Self::columns()[2], Value::Text(self.content)),
218/// (Self::columns()[3], Value::Uint32(self.user_id)),
219/// ]
220/// }
221/// }
222/// ```
223///
224/// - Implement the associated `Record` type
225/// - Implement the associated `InsertRecord` type
226/// - Implement the associated `UpdateRecord` type
227/// - If has foreign keys, implement the associated `ForeignFetcher`
228///
229/// So for each struct deriving `Table`, we will generate the following type. Given `${StructName}`, we will generate:
230///
231/// - `${StructName}Record` - implementing `TableRecord`
232/// - `${StructName}InsertRequest` - implementing `InsertRecord`
233/// - `${StructName}UpdateRequest` - implementing `UpdateRecord`
234/// - `${StructName}ForeignFetcher` (only if foreign keys are present)
235///
236/// Also, we will implement the `TableSchema` trait for the struct itself and derive `Encode` for `${StructName}`.
237///
238/// ## Attributes
239///
240/// The `Table` derive macro supports the following attributes:
241///
242/// - `#[alignment = N]`: (optional) Specifies the alignment for the table records. Use only if you know what you are doing.
243/// - `#[autoincrement]`: Marks a field as auto-incrementing. The macro will generate code to automatically fill in values for this field during inserts. Auto-increment fields must be non-nullable and cannot be marked as `#[unique]`.
244/// - `#[candid]`: Marks the table as compatible with Candid serialization.
245/// - `#[custom_type = "TypeName"]`: Specifies a custom data type for the field.
246/// - `#[default = <expr>]`: Field-level default value used by the migration planner when adding a non-nullable column. The expression must convert into the column's `Value` variant via `From`/`Into` (e.g. `#[default = 0]` on a `Uint32` column).
247/// - `#[foreign_key(entity = "EntityName", table = "table_name", column = "column_name")]`: Defines a foreign key relationship.
248/// - `#[index]`: Marks a field to be indexed for faster queries.
249/// - `#[migrate]`: Struct-level attribute that suppresses the macro's default `impl Migrate for T {}` so the user can provide a hand-written impl with custom `default_value` / `transform_column` overrides.
250/// - `#[primary_key]`: Marks a field as the primary key of the table.
251/// - `#[renamed_from("old1", "old2", ...)]`: Field-level list of previous column names. The migration planner uses these to detect rename ops when matching a stored column against the compiled column.
252/// - `#[sanitizer(SanitizerType)]`: Specifies a sanitize for the field.
253/// - `#[table = "table_name"]`: Specifies the name of the table in the database.
254/// - `#[unique]`: Marks a field to have a unique constraint.
255/// - `#[validate(ValidatorType)]`: Specifies a validator for the field.
256///
257#[proc_macro_derive(
258 Table,
259 attributes(
260 alignment,
261 autoincrement,
262 candid,
263 custom_type,
264 default,
265 foreign_key,
266 index,
267 migrate,
268 primary_key,
269 renamed_from,
270 sanitizer,
271 table,
272 unique,
273 validate
274 )
275)]
276pub fn derive_table(input: TokenStream) -> TokenStream {
277 let input = parse_macro_input!(input as DeriveInput);
278 self::table::table(input)
279 .expect("failed to derive `Table`")
280 .into()
281}
282
283/// Derives the [`CustomDataType`] trait and an `impl From<T> for Value` conversion
284/// for a user-defined enum or struct.
285///
286/// The type must also derive [`Encode`] (for binary serialization) and implement
287/// [`Display`](std::fmt::Display) (for the cached display string in [`CustomValue`]).
288///
289/// # Required attribute
290///
291/// - `#[type_tag = "..."]`: A unique string identifier for this custom data type.
292///
293/// # What the macro generates
294///
295/// Given a type like:
296///
297/// ```rust,ignore
298/// #[derive(Encode, CustomDataType)]
299/// #[type_tag = "status"]
300/// enum Status { Active, Inactive }
301/// ```
302///
303/// The macro expands into:
304///
305/// ```rust,ignore
306/// impl CustomDataType for Status {
307/// const TYPE_TAG: &'static str = "status";
308/// }
309///
310/// impl From<Status> for Value {
311/// fn from(val: Status) -> Value {
312/// Value::Custom(CustomValue {
313/// type_tag: "status".to_string(),
314/// encoded: Encode::encode(&val).into_owned(),
315/// display: val.to_string(),
316/// })
317/// }
318/// }
319/// ```
320///
321/// # Note
322///
323/// The user must also provide `Display`, `Default`, and `DataType` implementations
324/// for the type. This macro only bridges the custom type to the `Value` system.
325#[proc_macro_derive(CustomDataType, attributes(type_tag))]
326pub fn derive_custom_data_type(input: TokenStream) -> TokenStream {
327 let input = parse_macro_input!(input as DeriveInput);
328 custom_data_type::custom_data_type(&input)
329 .unwrap_or_else(|e| e.to_compile_error())
330 .into()
331}
332
333/// Generates a [`DatabaseSchema`] implementation that dispatches generic
334/// DBMS operations to the correct concrete table types.
335///
336/// Given a struct annotated with `#[tables(User = "users", Post = "posts")]`,
337/// this macro produces:
338///
339/// - `impl<M: MemoryProvider> DatabaseSchema<M>` with match-arm dispatch
340/// for `select`, `insert`, `delete`, `update`, `validate_insert`,
341/// `validate_update`, and `referenced_tables`.
342/// - An inherent `register_tables` method that registers all tables in a
343/// [`DbmsContext`].
344///
345/// # Example
346///
347/// ```rust,ignore
348/// #[derive(DatabaseSchema)]
349/// #[tables(User = "users", Post = "posts")]
350/// pub struct MySchema;
351///
352/// // Register tables during initialization:
353/// MySchema::register_tables(&ctx)?;
354/// ```
355///
356/// # Requirements
357///
358/// - Each type in the `#[tables(...)]` attribute must implement
359/// [`TableSchema`].
360/// - The generated types (`UserInsertRequest`, `UserUpdateRequest`,
361/// `UserRecord`, etc.) must be in scope.
362#[proc_macro_derive(DatabaseSchema, attributes(tables))]
363pub fn derive_database_schema(input: TokenStream) -> TokenStream {
364 let input = parse_macro_input!(input as DeriveInput);
365 self::database_schema::database_schema(input)
366 .expect("failed to derive `DatabaseSchema`")
367 .into()
368}