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}