raisfast_derive/lib.rs
1//! # raisfast-derive
2//!
3//! Proc-macro crate for the `raisfast` blog/CMS system.
4//!
5//! Provides two categories of macros:
6//!
7//! ## 1. Derive macros
8//!
9//! - **`#[derive(EventMeta)]`** — auto-generates `name()`, `display_name()`, `table()` methods
10//! on event enums, with per-variant `#[event(table = "...", name = "...", dynamic)]` attributes.
11//!
12//! ## 2. Attribute macros
13//!
14//! - **`#[aspect_service(entity = "...", model = Type)]`** — generates a service struct with
15//! before/after hooks that delegate to the aspect engine, and auto-emits domain events.
16//!
17//! ## 3. Bang macros (SQL CRUD helpers)
18//!
19//! All macros use optional `tenant: expr` named section for tenant filtering.
20//! When provided, the SQL includes `AND tenant_id = ?` at runtime.
21//!
22//! | Macro | SQL operation |
23//! |-------|---------------|
24//! | `crud_delete!` | `DELETE FROM ... WHERE WhereExpr` |
25//! | `crud_insert!` | `INSERT INTO ... (...) VALUES (...)` |
26//! | `crud_scalar!` | `SELECT scalar ...` |
27//! | `crud_query!` | `SELECT ...` via `query_as` |
28//! | `crud_find!` | `SELECT cols FROM ... WHERE WhereExpr` → `fetch_optional` |
29//! | `crud_find_one!` | same → `fetch_one` |
30//! | `crud_find_all!` | same → `fetch_all` |
31//! | `crud_list!` | `SELECT cols FROM ...` → `fetch_all` (no WHERE) |
32//! | `crud_update!` | `UPDATE ... SET ... WHERE WhereExpr` |
33//! | `crud_count!` | `SELECT COUNT(*) FROM ... WHERE WhereExpr` |
34//! | `crud_exists!` | `SELECT EXISTS(SELECT 1 ... WHERE WhereExpr)` |
35//! | `crud_query_paged!` | paginated data + COUNT |
36//! | `crud_join_paged!` | paginated JOIN + COUNT |
37//! | `crud_resolve_id!` | `SELECT id FROM table WHERE id = ?` → `Option<i64>` |
38//! | `crud_resolve_ids!` | `SELECT id FROM table WHERE id IN (...)` → `Vec<i64>` (validates all exist) |
39//!
40//! ### Schema validation
41//!
42//! - **`check_schema!("table", "col1", "col2", ...)`** — compile-time validation only;
43//! expands to nothing. Emits a compile error if the table or any column is missing.
44//!
45//! ## Architecture notes
46//!
47//! - `schema.rs` — parses `schema.sqlite.sql` + `tenantable.sqlite.sql` at compile time
48//! into a `Schema` struct. Used for table/column validation and for generating explicit
49//! column lists (replacing `SELECT *`).
50//! - `crud.rs` — all CRUD macro implementations + input parsing structs.
51//! - `event_meta.rs` — `#[derive(EventMeta)]`.
52//! - `aspect_service.rs` — `#[aspect_service]`.
53
54mod aspect_service;
55mod crud;
56mod event_meta;
57mod schema;
58mod where_dsl;
59
60use proc_macro::TokenStream;
61
62/// Derive macro for event enums.
63///
64/// Generates `name()`, `display_name()`, `table()` methods.
65/// Supports per-variant attributes: `#[event(table = "...", name = "...", dynamic)]`.
66#[proc_macro_derive(EventMeta, attributes(event))]
67pub fn derive_event_meta(input: TokenStream) -> TokenStream {
68 event_meta::derive_event_meta(input)
69}
70
71/// `crud_delete!(pool, "table", where: WhereExpr [, tenant: expr])`
72///
73/// Generates `DELETE FROM table WHERE ...` via `sqlx::query()`.
74/// Uses Where DSL for conditions. When `tenant:` is provided, adds `AND tenant_id = ?` filter.
75#[proc_macro]
76pub fn crud_delete(input: TokenStream) -> TokenStream {
77 crud::crud_delete(input)
78}
79
80/// `crud_insert!(pool, "table", ["col1" => val1, "col2" => val2] [, tenant: expr])`
81///
82/// Generates `INSERT INTO table (cols) VALUES (placeholders)` via `sqlx::query!()`.
83/// Values are pre-bound to `let` variables to avoid E0716 temporary lifetime issues.
84#[proc_macro]
85pub fn crud_insert(input: TokenStream) -> TokenStream {
86 crud::crud_insert(input)
87}
88
89/// `crud_scalar!(pool, Type, sql, [vals], method [, tenant: expr])`
90///
91/// Generates `sqlx::query_scalar::<_, Type>(sql)` with binds. Runtime query.
92#[proc_macro]
93pub fn crud_scalar(input: TokenStream) -> TokenStream {
94 crud::crud_scalar(input)
95}
96
97/// `crud_select!(pool, "table", ["col1", "col2"], where: WhereExpr [, tenant: expr])`
98///
99/// Generates `SELECT col1, col2 FROM table WHERE ...` via `sqlx::query_as`.
100#[proc_macro]
101pub fn crud_select(input: TokenStream) -> TokenStream {
102 crud::crud_select(input)
103}
104
105/// `crud_join!(pool, Type, select: [...], from: "...", joins: [...], where: WhereExpr [, tenant: expr, method: fetch_all, order_by: "...", limit: expr, offset: expr])`
106///
107/// Generates a JOIN query with optional Where DSL conditions and tenant filtering.
108#[proc_macro]
109pub fn crud_join(input: TokenStream) -> TokenStream {
110 crud::crud_join(input)
111}
112
113/// `crud_join_paged!(pool, Type, select: [...], from: "...", joins: [...], tenant_alias: "...", tenant: tid, order_by: "...", page: page, page_size: page_size)`
114///
115/// Generates a paginated JOIN query with COUNT. Returns `(Vec<T>, i64)`.
116#[proc_macro]
117pub fn crud_join_paged(input: TokenStream) -> TokenStream {
118 crud::crud_join_paged(input)
119}
120
121/// `crud_count!(pool, "table", where: WhereExpr [, tenant: expr])`
122///
123/// `SELECT COUNT(*) FROM table WHERE ...` → `i64`.
124#[proc_macro]
125pub fn crud_count(input: TokenStream) -> TokenStream {
126 crud::crud_count(input)
127}
128
129/// `crud_query!(pool, Type, sql, [vals], method [, tenant: expr])`
130///
131/// Generates `sqlx::query_as::<_, Type>(sql)` with binds. Runtime query.
132#[proc_macro]
133pub fn crud_query(input: TokenStream) -> TokenStream {
134 crud::crud_query(input)
135}
136
137/// `crud_find!(pool, "table", Type, where: WhereExpr [, tenant: expr, order_by: "expr"])`
138///
139/// `SELECT {all_columns} FROM table WHERE ...` → `fetch_optional`.
140/// Column list is generated from schema (replaces `SELECT *`).
141#[proc_macro]
142pub fn crud_find(input: TokenStream) -> TokenStream {
143 crud::crud_find(input)
144}
145
146/// `crud_find_one!(...)` — same as `crud_find!` but uses `fetch_one`.
147#[proc_macro]
148pub fn crud_find_one(input: TokenStream) -> TokenStream {
149 crud::crud_find_one(input)
150}
151
152/// `crud_find_all!(...)` — same as `crud_find!` but uses `fetch_all`.
153#[proc_macro]
154pub fn crud_find_all(input: TokenStream) -> TokenStream {
155 crud::crud_find_all(input)
156}
157
158/// `crud_list!(pool, "table", Type)`
159///
160/// `SELECT {all_columns} FROM table` → `fetch_all`. No WHERE clause.
161/// Optional: `order_by: "expr"` — appends `ORDER BY expr`.
162/// Optional: `tenant: tid` — adds `WHERE 1=1 AND tenant_id = ?` filter.
163///
164/// ```ignore
165/// crud_list!(pool, "tags" => Tag)
166/// crud_list!(pool, "tags" => Tag, order_by: "name")
167/// crud_list!(pool, "tags" => Tag, order_by: "name", tenant: tenant_id)
168/// ```
169#[proc_macro]
170pub fn crud_list(input: TokenStream) -> TokenStream {
171 crud::crud_list(input)
172}
173
174/// `check_schema!("table", "col1", "col2", ...)`
175///
176/// Compile-time validation only — expands to nothing (empty token stream).
177/// Emits a compile error if the table or any named column is missing from the schema.
178#[proc_macro]
179pub fn check_schema(input: TokenStream) -> TokenStream {
180 crud::check_schema(input)
181}
182
183/// `crud_exists!(pool, "table", where: WhereExpr [, tenant: expr])`
184///
185/// `SELECT EXISTS(SELECT 1 FROM table WHERE ...)` → `bool`.
186/// Uses `sqlx::query_scalar` with compile-time verified SQL.
187#[proc_macro]
188pub fn crud_exists(input: TokenStream) -> TokenStream {
189 crud::crud_exists(input)
190}
191
192/// `crud_upsert!(pool, "table", key: ["conflict_col"], bind: ["col" => val, ...], update: ["col1", "col2"] [, tenant: expr])`
193///
194/// Generates `INSERT INTO table (...) VALUES (...) ON CONFLICT(...) DO UPDATE SET ...`
195/// via `sqlx::query!()` (compile-time verified).
196#[proc_macro]
197pub fn crud_upsert(input: TokenStream) -> TokenStream {
198 crud::crud_upsert(input)
199}
200
201/// `crud_update!(pool, "table", bind: [...], optional: [...], raw: [...], where: WhereExpr [, tenant: tid])`
202///
203/// Generates a runtime `sqlx::query()` UPDATE. Supports `bind:` (always-set),
204/// `optional:` (set only when Some), `raw:` (SQL expressions).
205#[proc_macro]
206pub fn crud_update(input: TokenStream) -> TokenStream {
207 crud::crud_update(input)
208}
209
210/// `crud_query_paged!(pool, Type, data_sql: "...", count_sql: "...", binds: [...], where: ["col" => opt_val, ...], tenant: tid, page: page, page_size: page_size)`
211///
212/// Generates a paginated query pair (data + COUNT) with optional tenant filtering
213/// and optional dynamic WHERE conditions.
214///
215/// Both `data_sql` and `count_sql` are string literals. Use `{tenant}` as a placeholder
216/// where the `AND tenant_id = ?` clause should be inserted when tenant_id is Some.
217///
218/// The `where:` section accepts optional values — `Some(val)` appends `AND col = ?`
219/// and binds, `None` skips. Values must be `Option` types.
220///
221/// Returns `(Vec<T>, i64)` — the data rows and total count.
222#[proc_macro]
223pub fn crud_query_paged(input: TokenStream) -> TokenStream {
224 crud::crud_query_paged(input)
225}
226
227/// `crud_resolve_id!(pool, table, id [, tenant: expr])`
228///
229/// Resolves a SnowflakeId to an `i64` by verifying it exists in the target table.
230/// Returns `sqlx::Result<Option<i64>>` — `Some(id)` if found, `None` if not.
231///
232/// The `table` parameter is a runtime `&str` expression (not a literal).
233/// Performs `is_safe_identifier()` check before querying.
234/// When `tenant:` is provided, adds `AND tenant_id = ?` filter.
235#[proc_macro]
236pub fn crud_resolve_id(input: TokenStream) -> TokenStream {
237 crud::crud_resolve_id(input)
238}
239
240/// `crud_resolve_ids!(pool, table, ids)`
241///
242/// Batch version of `crud_resolve_id!`. Resolves a slice of `i64` IDs by verifying
243/// they ALL exist in the target table via a single `SELECT id FROM table WHERE id IN (...)`.
244///
245/// Returns `Result<Vec<i64>, sqlx::Error>` — the validated ID list on success.
246/// Returns `sqlx::Error::RowNotFound` if any ID is missing.
247/// Returns error on unsafe table name.
248#[proc_macro]
249pub fn crud_resolve_ids(input: TokenStream) -> TokenStream {
250 crud::crud_resolve_ids(input)
251}
252
253/// `#[aspect_service(entity = "posts", model = Post)]`
254///
255/// Attribute macro applied to a service struct. Generates:
256/// - `new(...)` constructor
257/// - `before_create(...)` / `before_update(...)` / `before_delete(...)` — delegate to aspect engine
258/// - `after_created(...)` / `after_updated(...)` / `after_deleted(...)` — emit domain events
259///
260/// The struct must have one field marked with `#[engine]` pointing to the aspect engine.
261#[proc_macro_attribute]
262pub fn aspect_service(attr: TokenStream, item: TokenStream) -> TokenStream {
263 aspect_service::aspect_service(attr, item)
264}