sabi-rust 0.8.0

A small framework to separate logics and data accesses for Rust application.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
// Copyright (C) 2024-2026 Takayuki Sato. All Rights Reserved.
// This program is free software under MIT License.
// See the file LICENSE in this distribution for more details.

//! This crate provides a small framework for Rust, designed to separate application logic
//! from data access.
//!
//! In this framework, the logic exclusively takes a data access trait as its argument,
//! and all necessary data access is defined by a single data access trait.
//! Conversely, the concrete implementations of data access methods are provided as default methods
//! of `DataAcc` derived traits, allowing for flexible grouping, often by data service.
//!
//! The `DataHub` bridges these two parts.
//! It attaches all `DataAcc` derived traits, and then, using the
//! [override_macro](https://github.com/sttk/override_macro-rust) crate, it overrides
//! the methods of the data access trait used by the logic to point to the implementations
//! found in the `DataAcc` derived traits.
//! This clever use of this macro compensates for Rust's lack of native method overriding,
//! allowing the logic to interact with data through an abstract interface.
//!
//! Furthermore, the `DataHub` provides transaction control for data operations performed
//! within the logic.
//! You can execute logic functions with transaction control using its [`DataHub::txn`] method,
//! or without transaction control using its [`DataHub::run`] method.
//!
//! This framework brings clear separation and robustness to Rust application design.

#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![allow(unused_features)]

mod async_group;
mod data_acc;
mod data_conn;
mod data_hub;
mod data_src;
mod non_null;
mod txn_failure;

use std::collections::HashMap;
use std::sync::Arc;
use std::{any, cell, marker, ptr, thread};

pub use async_group::AsyncGroupError;
pub use data_conn::DataConnError;
pub use data_hub::DataHubError;
pub use data_src::{create_static_data_src_container, setup, setup_with_order, uses, DataSrcError};

#[cfg_attr(docsrs, doc(cfg(feature = "tokio")))]
#[cfg(feature = "tokio")]
pub mod tokio;

/// Represents an entry containing an error, along with its context.
///
/// This structure is used to aggregate errors that occur during parallel
/// or asynchronous operations, providing the index of the operation,
/// a descriptive name, and the error itself.
#[derive(Debug)]
pub struct ErrEntry {
    /// The index of the operation or handler that generated the error.
    pub index: usize,
    /// A descriptive name for the operation or context.
    pub name: Arc<str>,
    /// The actual error that occurred.
    pub err: errs::Err,
}

/// The structure that allows for the concurrent execution of multiple functions
/// using `std::thread` and waits for all of them to complete.
///
/// Functions are added using the `add` method and are then run concurrently in separate threads.
/// The `AsyncGroup` ensures that all tasks finish before proceeding,
/// and can collect any errors that occur.
pub struct AsyncGroup {
    handlers: Vec<(usize, Arc<str>, thread::JoinHandle<errs::Result<()>>)>,
    _index: usize,
    _name: Arc<str>,
}

/// The trait that abstracts a connection per session to an external data service,
/// such as a database, file system, or messaging service.
///
/// Its primary purpose is to enable cohesive transaction operations across multiple
/// external data services within a single transaction context. Implementations of this
/// trait provide the concrete input/output operations for their respective data services.
///
/// Methods declared within this trait are designed to handle transactional logic.
/// The [`AsyncGroup`] parameter in various methods allows for concurrent processing
/// when commit or rollback operations are time-consuming.
#[allow(unused_variables)] // rustdoc
pub trait DataConn {
    /// Attempts to commit the changes made within this data connection's transaction.
    ///
    /// This method should encapsulate the logic required to finalize the transaction
    /// for the specific external data service.
    ///
    /// # Parameters
    ///
    /// * `ag`: A mutable reference to an [`AsyncGroup`] for potentially offloading
    ///   time-consuming commit operations to a separate thread.
    ///
    /// # Returns
    ///
    /// * `errs::Result<()>`: `Ok(())` if the commit is successful, or an [`errs::Err`]
    ///   if the commit fails.
    fn commit(&mut self, ag: &mut AsyncGroup) -> errs::Result<()>;

    /// This method is executed before the transaction commit process for all [`DataConn`] instances
    /// involved in the transaction.
    ///
    /// This method provides a timing to execute unusual commit processes or update operations not
    /// supported by transactions beforehand.
    /// This allows other update operations to be rolled back if the operations in this method
    /// fail.
    ///
    /// # Parameters
    ///
    /// * `ag`: A mutable reference to an [`AsyncGroup`]. This can be used to run the pre-commit
    ///   asynchronously in a separate thread.
    ///
    /// # Returns
    ///
    /// * `errs::Result<()>`: `Ok(())` if pre-commit is successful, or an [`errs::Err`] if it fails.
    fn pre_commit(&mut self, ag: &mut AsyncGroup) -> errs::Result<()> {
        Ok(())
    }

    /// This method is executed after the transaction commit process has successfully completed
    /// for all [`DataConn`] instances involved in the transaction.
    ///
    /// It provides a moment to perform follow-up actions that depend on a successful commit.
    /// For example, after a database commit, a messaging service's [`DataConn`] might use this
    /// method to send a "transaction completed" message.
    ///
    /// # Parameters
    ///
    /// * `ag`: A mutable reference to an [`AsyncGroup`] for potentially offloading
    ///   concurrent post-commit operations.
    ///
    /// # Returns
    ///
    /// * `errs::Result<()>`: `Ok(())` if post-commit tasks succeed, or an [`errs::Err`] if they
    ///   fail.
    fn post_commit(&mut self, ag: &mut AsyncGroup) -> errs::Result<()> {
        Ok(())
    }

    /// Returns whether the transaction on this connection has been successfully committed.
    fn is_committed(&self) -> bool;

    /// Rolls back any changes made within this data connection's transaction.
    ///
    /// This method undoes all operations performed since the beginning of the transaction,
    /// restoring the data service to its state before the transaction began.
    ///
    /// # Parameters
    ///
    /// * `ag`: A mutable reference to an [`AsyncGroup`]. This can be used to run the rollback
    ///   asynchronously in a separate thread.
    ///
    /// # Returns
    ///
    /// * `errs::Result<()>`: `Ok(())` if the rollback is successful, or an [`errs::Err`] if it
    ///   fails.
    fn rollback(&mut self, ag: &mut AsyncGroup) -> errs::Result<()>;

    /// A lifecycle callback invoked when a transaction fails and a rollback is executed.
    ///
    /// This allows the data connection to handle post-failure tasks or custom logic
    /// based on the provided transaction failure reports.
    ///
    /// # Parameters
    ///
    /// * `ag`: A mutable reference to an [`AsyncGroup`] for asynchronous task execution.
    /// * `reports`: A slice of [`TxnFailureReport`] containing failure details for all connections.
    #[cfg_attr(coverage_nightly, coverage(off))]
    fn on_txn_failure(&mut self, ag: &mut AsyncGroup, reports: &[TxnFailureReport]) {}

    /// Closes the connection to the external data service.
    ///
    /// This method should release any resources held by the data connection, ensuring
    /// a graceful shutdown of the connection.
    fn close(&mut self);
}

struct NoopDataConn {}

#[cfg_attr(coverage_nightly, coverage(off))]
impl DataConn for NoopDataConn {
    fn commit(&mut self, _ag: &mut AsyncGroup) -> errs::Result<()> {
        Ok(())
    }
    fn is_committed(&self) -> bool {
        false
    }
    fn rollback(&mut self, _ag: &mut AsyncGroup) -> errs::Result<()> {
        Ok(())
    }
    fn close(&mut self) {}
}

#[repr(C)]
struct DataConnContainer<C = NoopDataConn>
where
    C: DataConn + 'static,
{
    drop_fn: fn(*const DataConnContainer),
    is_fn: fn(any::TypeId) -> bool,
    type_fn: fn() -> &'static str,
    commit_fn: fn(*const DataConnContainer, &mut AsyncGroup) -> errs::Result<()>,
    pre_commit_fn: fn(*const DataConnContainer, &mut AsyncGroup) -> errs::Result<()>,
    post_commit_fn: fn(*const DataConnContainer, &mut AsyncGroup) -> errs::Result<()>,
    is_committed_fn: fn(*const DataConnContainer) -> bool,
    rollback_fn: fn(*const DataConnContainer, &mut AsyncGroup) -> errs::Result<()>,
    on_txn_failure_fn: fn(*const DataConnContainer, &mut AsyncGroup, &[TxnFailureReport]),
    close_fn: fn(*const DataConnContainer),

    name: Arc<str>,
    data_conn: Box<C>,
}

struct DataConnManager {
    vec: Vec<Option<SendSyncNonNull<DataConnContainer>>>,
    index_map: HashMap<Arc<str>, usize>,
    committed: bool,
}

/// The trait that abstracts a data source responsible for managing connections
/// to external data services, such as databases, file systems, or messaging services.
///
/// It receives configuration for connecting to an external data service and then
/// creates and supplies [`DataConn`] instance, representing a single session connection.
#[allow(unused_variables)] // for rustdoc
pub trait DataSrc<C>
where
    C: DataConn + 'static,
{
    /// Performs the setup process for the data source.
    ///
    /// This method is responsible for establishing global connections, configuring
    /// connection pools, or performing any necessary initializations required
    /// before [`DataConn`] instances can be created.
    ///
    /// # Parameters
    ///
    /// * `ag`: A mutable reference to an [`AsyncGroup`]. This is used if the setup
    ///   process is potentially time-consuming and can benefit from concurrent
    ///   execution in a separate thread.
    ///
    /// # Returns
    ///
    /// * `errs::Result<()>`: `Ok(())` if the setup is successful, or an [`errs::Err`]
    ///   if any part of the setup fails.
    fn setup(&mut self, ag: &mut AsyncGroup) -> errs::Result<()>;

    /// Closes the data source and releases any globally held resources.
    ///
    /// This method should perform cleanup operations, such as closing global connections
    /// or shutting down connection pools, that were established during the setup process.
    fn close(&mut self);

    /// Creates a new [`DataConn`] instance which is a connection per session.
    ///
    /// Each call to this method should yield a distinct [`DataConn`] object tailored
    /// for a single session's operations.
    ///
    /// # Returns
    ///
    /// * `errs::Result<Box<C>>`: `Ok(Box<C>)` containing the newly created [`DataConn`]
    ///   if successful, or an [`errs::Err`] if the connection could not be created.
    fn create_data_conn(&mut self) -> errs::Result<Box<C>>;
}

struct NoopDataSrc {}

#[cfg_attr(coverage_nightly, coverage(off))]
impl DataSrc<NoopDataConn> for NoopDataSrc {
    fn setup(&mut self, _ag: &mut AsyncGroup) -> errs::Result<()> {
        Ok(())
    }
    fn close(&mut self) {}
    fn create_data_conn(&mut self) -> errs::Result<Box<NoopDataConn>> {
        Ok(Box::new(NoopDataConn {}))
    }
}

#[repr(C)]
struct DataSrcContainer<S = NoopDataSrc, C = NoopDataConn>
where
    S: DataSrc<C>,
    C: DataConn + 'static,
{
    drop_fn: fn(*const DataSrcContainer),
    setup_fn: fn(*const DataSrcContainer, &mut AsyncGroup) -> errs::Result<()>,
    close_fn: fn(*const DataSrcContainer),
    create_data_conn_fn: fn(*const DataSrcContainer) -> errs::Result<Box<DataConnContainer<C>>>,
    is_data_conn_fn: fn(any::TypeId) -> bool,

    local: bool,
    name: Arc<str>,
    data_src: S,
}

struct DataSrcManager {
    vec_unready: Vec<SendSyncNonNull<DataSrcContainer>>,
    vec_ready: Vec<SendSyncNonNull<DataSrcContainer>>,
    local: bool,
}

/// A utility struct that ensures to close and drop global data sources when it goes out of scope.
///
/// This struct implements the `Drop` trait, and its `drop` method handles the closing and
/// dropping of registered global data sources.
/// Therefore, this ensures that these operations are automatically executed at the end of
/// the scope.
///
/// **NOTE:** Do not receive an instance of this struct into an anonymous variable
/// (`let _ = ...`), because an anonymous variable is dropped immediately at that point.
pub struct AutoShutdown {}

/// The struct that acts as a central hub for data input/output operations, integrating
/// multiple *Data* traits (which are passed to business logic functions as their arguments) with
/// [`DataAcc`] traits (which implement default data I/O methods for external services).
///
/// It facilitates data access by providing [`DataConn`] objects, created from
/// both global data sources (registered via the global [`uses!`] macro) and
/// session-local data sources (registered via [`DataHub::uses`] method).
///
/// The [`DataHub`] is capable of performing aggregated transactional operations
/// on all [`DataConn`] objects created from its registered [`DataSrc`] instances.
pub struct DataHub {
    local_data_src_manager: DataSrcManager,
    data_src_map: HashMap<Arc<str>, (bool, usize)>,
    data_conn_manager: DataConnManager,
    fixed: bool,
}

/// This trait provides a mechanism to retrieve a mutable reference to a [`DataConn`] object
/// by name, creating it if necessary.
///
/// It is typically implemented as a derived trait with default methods (using
/// the `override_macro` crate) on [`DataHub`], allowing application logic to
/// interact with data services through an abstract interface.
pub trait DataAcc {
    /// Retrieves a mutable reference to a [`DataConn`] object by name, creating it if necessary.
    ///
    /// This is the core method used by [`DataAcc`] implementations to obtain connections
    /// to external data services. It first checks if a [`DataConn`] with the given name
    /// already exists in the current session. If not, it attempts to find a
    /// corresponding [`DataSrc`] and create a new [`DataConn`] from it.
    ///
    /// # Type Parameters
    ///
    /// * `C`: The concrete type of [`DataConn`] expected.
    ///
    /// # Parameters
    ///
    /// * `name`: The name of the data source/connection to retrieve.
    ///
    /// # Returns
    ///
    /// * `errs::Result<&mut C>`: A mutable reference to the [`DataConn`] instance if successful,
    ///   or an [`errs::Err`] if the data source is not found, or if the retrieved/created
    ///   [`DataConn`] cannot be cast to the specified type `C`.
    fn get_data_conn<C: DataConn + 'static>(&mut self, name: &str) -> errs::Result<&mut C>;
}

#[doc(hidden)]
pub struct StaticDataSrcContainer {
    ssnnptr: SendSyncNonNull<DataSrcContainer>,
}

#[doc(hidden)]
pub struct StaticDataSrcRegistration {
    factory: fn() -> StaticDataSrcContainer,
}

struct SendSyncNonNull<T: Send + Sync> {
    non_null_ptr: ptr::NonNull<T>,
    _phantom: marker::PhantomData<cell::Cell<T>>,
}

/// Represents the cause of a transaction failure for a specific data connection.
#[derive(Debug)]
pub enum TxnFailureCause {
    /// No failure occurred, and the transaction was successfully committed.
    NoneByCommitted,
    /// No failure occurred, but the transaction was not committed (e.g., because
    /// another connection in the transaction failed before this one could commit).
    NoneByUncommitted,
    /// The logic execution or pre-commit phase of the data connection failed.
    LogicFailure(errs::Err),
    /// The commit phase of the data connection failed.
    CommitFailure(errs::Err),
    /// The post-commit phase of the data connection failed.
    PostCommitFailure(errs::Err),
}

/// Represents the rollback status of a data connection in a failed transaction.
#[derive(Debug)]
pub enum TxnFailureRollback {
    /// The rollback was executed and succeeded.
    NoneByRolledBack,
    /// Rollback was not executed or not applicable (e.g., because the connection
    /// was already committed).
    NoneByNotRolledBack,
    /// The rollback was executed but failed.
    RollbackFailure(errs::Err),
}

/// Represents the suggested recovery action for a data connection after a transaction failure.
#[derive(Debug, PartialEq)]
pub enum TxnFailureRecovery {
    /// No recovery action is required.
    NoActionRequired,
    /// The transaction was successfully rolled back.
    /// The transaction can be rerun a logic and committed again.
    RerunLogicAndCommit,
    /// The transaction failed to run a logic or pre commit.
    /// After resolving the cause, the transaction can be rerun a logic and commit again.
    ResolveCauseThenRerunLogicAndCommit,
    /// The transaction failed to run post commit.
    /// After resolving the cause, the transaction can be rerun post commit again.
    ResolveCauseThenRerunPostCommit,
    /// The rollback failed and it may be in an inconsistent state.
    /// Resolve the cause and inconsistent state.
    ResolveCauseAndInconsistency,
    /// It is in impossile case under normal conditions.
    /// Investigation of the cause is required.
    InvestigateBecauseImpossible,
    /// The transaction was successfully committed.
    /// It is required to rollback manually.
    ManualRollbackRequired,
}

/// A report detailing the transaction failure cause and rollback status
/// for a specific data connection.
#[derive(Debug)]
pub struct TxnFailureReport {
    /// The name of the data connection.
    pub data_conn_name: Arc<str>,
    /// The type name of the data connection.
    pub data_conn_type: &'static str,
    /// The cause of the transaction failure.
    pub cause: TxnFailureCause,
    /// The rollback status of the data connection.
    pub rollback: TxnFailureRollback,
}