sabi/lib.rs
1// Copyright (C) 2024-2025 Takayuki Sato. All Rights Reserved.
2// This program is free software under MIT License.
3// See the file LICENSE in this distribution for more details.
4
5//! This crate provides a small framework for Rust, designed to separate application logic
6//! from data access.
7//!
8//! In this framework, the logic exclusively takes a data access trait as its argument,
9//! and all necessary data access is defined by a single data access trait.
10//! Conversely, the concrete implementations of data access methods are provided as default methods
11//! of `DataAcc` derived traits, allowing for flexible grouping, often by data service.
12//!
13//! The `DataHub` bridges these two parts.
14//! It attaches all `DataAcc` derived traits, and then, using the
15//! [override_macro](https://github.com/sttk/override_macro-rust) crate, it overrides
16//! the methods of the data access trait used by the logic to point to the implementations
17//! found in the `DataAcc` derived traits.
18//! This clever use of this macro compensates for Rust's lack of native method overriding,
19//! allowing the logic to interact with data through an abstract interface.
20//!
21//! Furthermore, the `DataHub` provides transaction control for data operations performed
22//! within the logic.
23//! You can execute logic functions with transaction control using the `txn!` macro,
24//! or without transaction control using the `run!` macro.
25//! Furthermore, you can execute asynchronous logic function with transaction control using
26//! `txn_async!` macro or without transaction control using the `run_async!` macro.
27//!
28//! This framework brings clear separation and robustness to Rust application design.
29//!
30//! ## Example
31//!
32//! The following is a sample code using this framework:
33//!
34//! ```rust
35//! use sabi::{AsyncGroup, DataSrc, DataConn, DataAcc, DataHub};
36//! use errs::Err;
37//! use override_macro::{overridable, override_with};
38//!
39//! // (1) Implements DataSrc(s) and DataConn(s).
40//!
41//! struct FooDataSrc { /* ... */ }
42//!
43//! impl DataSrc<FooDataConn> for FooDataSrc {
44//! fn setup(&mut self, ag: &mut AsyncGroup) -> Result<(), Err> { /* ... */ Ok(()) }
45//! fn close(&mut self) { /* ... */ }
46//! fn create_data_conn(&mut self) -> Result<Box<FooDataConn>, Err> {
47//! Ok(Box::new(FooDataConn{ /* ... */ }))
48//! }
49//! }
50//!
51//! struct FooDataConn { /* ... */ }
52//!
53//! impl FooDataConn { /* ... */ }
54//!
55//! impl DataConn for FooDataConn {
56//! fn commit(&mut self, ag: &mut AsyncGroup) -> Result<(), Err> { /* ... */ Ok(()) }
57//! fn rollback(&mut self, ag: &mut AsyncGroup) { /* ... */ }
58//! fn close(&mut self) { /* ... */ }
59//! }
60//!
61//! struct BarDataSrc { /* ... */ }
62//!
63//! impl DataSrc<BarDataConn> for BarDataSrc {
64//! fn setup(&mut self, ag: &mut AsyncGroup) -> Result<(), Err> { /* ... */ Ok(()) }
65//! fn close(&mut self) { /* ... */ }
66//! fn create_data_conn(&mut self) -> Result<Box<BarDataConn>, Err> {
67//! Ok(Box::new(BarDataConn{ /* ... */ }))
68//! }
69//! }
70//!
71//! struct BarDataConn { /* ... */ }
72//!
73//! impl BarDataConn { /* ... */ }
74//!
75//! impl DataConn for BarDataConn {
76//! fn commit(&mut self, ag: &mut AsyncGroup) -> Result<(), Err> { /* ... */ Ok(()) }
77//! fn rollback(&mut self, ag: &mut AsyncGroup) { /* ... */ }
78//! fn close(&mut self) { /* ... */ }
79//! }
80//!
81//! // (2) Implements logic functions and data traits
82//!
83//! #[overridable]
84//! trait MyData {
85//! fn get_text(&mut self) -> Result<String, Err>;
86//! fn set_text(&mut self, text: String) -> Result<(), Err>;
87//! }
88//!
89//! fn my_logic(data: &mut impl MyData) -> Result<(), Err> {
90//! let text = data.get_text()?;
91//! let _ = data.set_text(text)?;
92//! Ok(())
93//! }
94//!
95//! // (3) Implements DataAcc(s)
96//!
97//! #[overridable]
98//! trait GettingDataAcc: DataAcc {
99//! fn get_text(&mut self) -> Result<String, Err> {
100//! let conn = self.get_data_conn::<FooDataConn>("foo")?;
101//! /* ... */
102//! Ok("output text".to_string())
103//! }
104//! }
105//!
106//! #[overridable]
107//! trait SettingDataAcc: DataAcc {
108//! fn set_text(&mut self, text: String) -> Result<(), Err> {
109//! let conn = self.get_data_conn::<BarDataConn>("bar")?;
110//! /* ... */
111//! Ok(())
112//! }
113//! }
114//!
115//! // (4) Consolidate data traits and DataAcc traits to a DataHub.
116//!
117//! impl GettingDataAcc for DataHub {}
118//! impl SettingDataAcc for DataHub {}
119//!
120//! #[override_with(GettingDataAcc, SettingDataAcc)]
121//! impl MyData for DataHub {}
122//!
123//! // (5) Use the logic functions and the DataHub
124//!
125//! fn main() {
126//! // Register global DataSrc.
127//! sabi::uses("foo", FooDataSrc{});
128//! // Set up the sabi framework.
129//! // _auto_shutdown automatically closes and drops global DataSrc at the end of the scope.
130//! // NOTE: Don't write as `let _ = ...` because the return variable is dropped immediately.
131//! let _auto_shutdown = sabi::setup().unwrap();
132//!
133//! // Create a new instance of DataHub.
134//! let mut data = DataHub::new();
135//! // Register session-local DataSrc with DataHub.
136//! data.uses("bar", BarDataSrc{});
137//!
138//! // Execute application logic within a transaction.
139//! // my_logic performs data operations via DataHub.
140//! let _ = sabi::txn!(my_logic, data).unwrap();
141//! }
142//! ```
143//!
144//! If you want to run this framework within an async function/block, you should use `setup_async`
145//! instead of `setup`, `run_async` instead of `run`, and `txn_async` instead of `txn`, as shown
146//! below.
147//!
148//! ```rust,ignore
149//! async fn my_logic(data: &mut impl MyData) -> Result<(), Err> {
150//! let text = data.get_text()?;
151//! let _ = data.set_text(text)?;
152//! Ok(())
153//! }
154//!
155//! #[tokio::main]
156//! async fn main() {
157//! // Register global DataSrc.
158//! sabi::uses("foo", FooDataSrc{}).await;
159//! // Set up the sabi framework.
160//! // _auto_shutdown automatically closes and drops global DataSrc at the end of the scope.
161//! // NOTE: Don't write as `let _ = ...` because the return variable is dropped immediately.
162//! let _auto_shutdown = sabi::setup_async().await.unwrap();
163//!
164//! // Create a new instance of DataHub.
165//! let mut data = DataHub::new();
166//! // Register session-local DataSrc with DataHub.
167//! data.uses("bar", BarDataSrc{});
168//!
169//! // Execute application logic within a transaction.
170//! // my_logic performs data operations via DataHub.
171//! let _ = sabi::txn_async!(my_logic, data).await.unwrap();
172//! }
173//! ```
174
175use std::any;
176
177use errs::Err;
178
179mod async_group;
180pub use async_group::{AsyncGroup, AsyncGroupError};
181
182mod data_acc;
183mod data_conn;
184mod data_hub;
185mod data_src;
186pub use data_acc::DataAcc;
187pub use data_hub::{setup, setup_async, uses, AutoShutdown, DataHub, DataHubError};
188
189/// The trait that abstracts a connection per session to an external data service,
190/// such as a database, file system, or messaging service.
191///
192/// Its primary purpose is to enable cohesive transaction operations across multiple
193/// external data services within a single transaction context. Implementations of this
194/// trait provide the concrete input/output operations for their respective data services.
195///
196/// Methods declared within this trait are designed to handle transactional logic.
197/// The `AsyncGroup` parameter in various methods allows for asynchronous processing
198/// when commit or rollback operations are time-consuming.
199#[allow(unused_variables)] // for rustdoc
200pub trait DataConn {
201 /// Attempts to commit the changes made within this data connection's transaction.
202 ///
203 /// This method should encapsulate the logic required to finalize the transaction
204 /// for the specific external data service.
205 ///
206 /// # Parameters
207 ///
208 /// * `ag`: A mutable reference to an `AsyncGroup` for potentially offloading
209 /// time-consuming commit operations to an asynchronous runtime.
210 ///
211 /// # Returns
212 ///
213 /// * `Result<(), Err>`: `Ok(())` if the commit is successful, or an `Err`
214 /// if the commit fails.
215 fn commit(&mut self, ag: &mut AsyncGroup) -> Result<(), Err>;
216
217 /// This method is executed before the transaction commit process for all `DataConn` instances
218 /// involved in the transaction.
219 ///
220 /// This method provides a timing to execute unusual commit processes or update operations not
221 /// supported by transactions beforehand.
222 /// This allows other update operations to be rolled back if the operations in this method
223 /// fail.
224 fn pre_commit(&mut self, ag: &mut AsyncGroup) -> Result<(), Err> {
225 Ok(())
226 }
227
228 /// This method is executed after the transaction commit process has successfully completed
229 /// for all `DataConn` instances involved in the transaction.
230 ///
231 /// It provides a moment to perform follow-up actions that depend on a successful commit.
232 /// For example, after a database commit, a messaging service's `DataConn` might use this
233 /// method to send a "transaction completed" message.
234 ///
235 /// # Parameters
236 ///
237 /// * `ag`: A mutable reference to an `AsyncGroup` for potentially offloading
238 /// asynchronous post-commit operations.
239 fn post_commit(&mut self, ag: &mut AsyncGroup) {}
240
241 /// Determines whether a "force back" operation is required for this data connection.
242 ///
243 /// A force back is typically executed if one external data service successfully commits
244 /// its changes, but a subsequent external data service within the same transaction fails
245 /// its commit. This method indicates if the committed changes of *this* data service
246 /// need to be undone (forced back).
247 ///
248 /// # Returns
249 ///
250 /// * `bool`: `true` if a force back is needed for this connection, `false` otherwise.
251 fn should_force_back(&self) -> bool {
252 false
253 }
254
255 /// Rolls back any changes made within this data connection's transaction.
256 ///
257 /// This method undoes all operations performed since the beginning of the transaction,
258 /// restoring the data service to its state before the transaction began.
259 ///
260 /// # Parameters
261 ///
262 /// * `ag`: A mutable reference to an `AsyncGroup` for potentially offloading
263 /// time-consuming rollback operations to an asynchronous runtime.
264 fn rollback(&mut self, ag: &mut AsyncGroup);
265
266 /// Executes an operation to revert committed changes.
267 ///
268 /// This method provides an opportunity to undo changes that were successfully committed
269 /// to this external data service, typically when a commit fails for *another* data service
270 /// within the same distributed transaction, necessitating a rollback of already committed
271 /// changes.
272 ///
273 /// # Parameters
274 ///
275 /// * `ag`: A mutable reference to an `AsyncGroup` for potentially offloading
276 /// asynchronous force back operations.
277 fn force_back(&mut self, ag: &mut AsyncGroup) {}
278
279 /// Closes the connection to the external data service.
280 ///
281 /// This method should release any resources held by the data connection, ensuring
282 /// a graceful shutdown of the connection.
283 fn close(&mut self);
284}
285
286struct NoopDataConn {}
287
288impl DataConn for NoopDataConn {
289 fn commit(&mut self, _ag: &mut AsyncGroup) -> Result<(), Err> {
290 Ok(())
291 }
292 fn rollback(&mut self, _ag: &mut AsyncGroup) {}
293 fn close(&mut self) {}
294}
295
296#[repr(C)]
297struct DataConnContainer<C = NoopDataConn>
298where
299 C: DataConn + 'static,
300{
301 drop_fn: fn(*const DataConnContainer),
302 is_fn: fn(any::TypeId) -> bool,
303
304 commit_fn: fn(*const DataConnContainer, &mut AsyncGroup) -> Result<(), Err>,
305 pre_commit_fn: fn(*const DataConnContainer, &mut AsyncGroup) -> Result<(), Err>,
306 post_commit_fn: fn(*const DataConnContainer, &mut AsyncGroup),
307 should_force_back_fn: fn(*const DataConnContainer) -> bool,
308 rollback_fn: fn(*const DataConnContainer, &mut AsyncGroup),
309 force_back_fn: fn(*const DataConnContainer, &mut AsyncGroup),
310 close_fn: fn(*const DataConnContainer),
311
312 prev: *mut DataConnContainer,
313 next: *mut DataConnContainer,
314 name: String,
315 data_conn: Box<C>,
316}
317
318/// The trait that abstracts a data source responsible for managing connections
319/// to external data services, such as databases, file systems, or messaging services.
320///
321/// It receives configuration for connecting to an external data service and then
322/// creates and supplies `DataConn` instance, representing a single session connection.
323#[allow(unused_variables)] // for rustdoc
324pub trait DataSrc<C>
325where
326 C: DataConn + 'static,
327{
328 /// Performs the setup process for the data source.
329 ///
330 /// This method is responsible for establishing global connections, configuring
331 /// connection pools, or performing any necessary initializations required
332 /// before `DataConn` instances can be created.
333 ///
334 /// # Parameters
335 ///
336 /// * `ag`: A mutable reference to an `AsyncGroup`. This is used if the setup
337 /// process is potentially time-consuming and can benefit from asynchronous
338 /// execution.
339 ///
340 /// # Returns
341 ///
342 /// * `Result<(), Err>`: `Ok(())` if the setup is successful, or an `Err`
343 /// if any part of the setup fails.
344 fn setup(&mut self, ag: &mut AsyncGroup) -> Result<(), Err>;
345
346 /// Closes the data source and releases any globally held resources.
347 ///
348 /// This method should perform cleanup operations, such as closing global connections
349 /// or shutting down connection pools, that were established during the `setup` phase.
350 fn close(&mut self);
351
352 /// Creates a new `DataConn` instance which is a connection per session.
353 ///
354 /// Each call to this method should yield a distinct `DataConn` object tailored
355 /// for a single session's operations.
356 ///
357 /// # Returns
358 ///
359 /// * `Result<Box<C>, Err>`: `Ok(Box<C>)` containing the newly created `DataConn`
360 /// if successful, or an `Err` if the connection could not be created.
361 fn create_data_conn(&mut self) -> Result<Box<C>, Err>;
362}
363
364struct NoopDataSrc {}
365
366impl DataSrc<NoopDataConn> for NoopDataSrc {
367 fn setup(&mut self, _ag: &mut AsyncGroup) -> Result<(), Err> {
368 Ok(())
369 }
370 fn close(&mut self) {}
371 fn create_data_conn(&mut self) -> Result<Box<NoopDataConn>, Err> {
372 Ok(Box::new(NoopDataConn {}))
373 }
374}
375
376#[repr(C)]
377struct DataSrcContainer<S = NoopDataSrc, C = NoopDataConn>
378where
379 S: DataSrc<C>,
380 C: DataConn + 'static,
381{
382 drop_fn: fn(*const DataSrcContainer),
383 setup_fn: fn(*const DataSrcContainer, &mut AsyncGroup) -> Result<(), Err>,
384 close_fn: fn(*const DataSrcContainer),
385 create_data_conn_fn: fn(*const DataSrcContainer) -> Result<Box<DataConnContainer<C>>, Err>,
386 is_data_conn_fn: fn(any::TypeId) -> bool,
387
388 prev: *mut DataSrcContainer,
389 next: *mut DataSrcContainer,
390 local: bool,
391 name: String,
392
393 data_src: S,
394}