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
<div align="center">
  <a href="https://github.com/sttk/sabi-rust">
    <img src="./images/sabi-rust.png" width="250" height="auto" alt="Sabi"/>
  </a>

  <h2>
  "sabi" - A small framework to separate logic and data access
  </h2>
  <br>

  [![crates.io][cratesio-img]][cratesio-url] [![doc.rs][docrs-img]][docrs-url] [![CI Status][ci-img]][ci-url] [![MIT License][mit-img]][mit-url]
</div>

## Overview

**sabi** was developed with the goal of thoroughly separating business logic from data access. However, it differs from conventional Dependency Injection (DI) frameworks that merely invert dependencies by placing an interface between the two layers—**sabi** draws a clear line beyond that simple approach.

What elevates **sabi** to an advanced framework in particular are the following two key techniques: introducing a data-access interface optimized for each individual piece of logic, and routing input/output from the controller layer directly to the data access layer via `DataSrc`, completely bypassing the logic layer.

### Introducing Data-Access Traits Optimized Per Logic Unit

The former approach thoroughly embodies the Interface Segregation Principle (ISP)—one of the SOLID principles that has often ended up more nominal than real in practice. Each piece of logic (use case) defines, on the logic side, its own dedicated trait (interface) that specifies only the operations it truly needs. Meanwhile, the data access side implements traits based on its responsibility as a data provider. The `DataHub` then mediates and maps between the two, so that the logic side never needs to be aware of the data access side's structure, and the data access side never needs to depend on the structure of individual pieces of logic—each maintains its own independent responsibility.

This design is grounded in the philosophy that "the world does not exist as a single, fixed, objective reality, but rather reveals its meaning and form according to the questions and purposes held by the observing subject." The trait that logic should see should not be dictated by the constraints of the data access side, but should instead be defined based on the logic's own context and needs. The same holds true in reverse for the trait that data access should see. What matters to the logic is not the structure of the database or the ORM, but the "capability required to realize this particular use case." What matters to the data access side, on the other hand, is how to access storage or external services. By having both sides define their traits according to their own respective contexts, and having the `DataHub` bridge them together, true loose coupling is achieved—one where neither side depends on the other's internal structure.

Furthermore, because logic never needs to know any implementation details of data access, it can easily be swapped out for mocks that provide the necessary capabilities during testing, achieving high testability as well.

### A Structure That Routes Controller-Layer I/O Directly to the Data Access Layer, Bypassing the Logic Layer

The latter approach—routing input/output from the controller layer directly to the data access layer, completely bypassing the logic layer—was conceived by decomposing the controller layer's role into two elements: "invoking logic" and "input/output data." In conventional architectures, because these two elements were never separated, a layered structure whose sole responsibility was data flow (the so-called "data bucket brigade") became unavoidable, forcing data to be transformed every time it crossed a layer. However, by routing input/output data directly to the infrastructure layer (the data access division), this redundant data flow—and the layered structure that existed to support it—becomes entirely unnecessary.

As a result, the hierarchical dependency that would normally remain between logic and data access is completely eliminated, and is elevated instead into an equal relationship based on a contract defined by the trait's method signatures. This can be seen as an evolutionary extension of the Dependency Inversion Principle (DIP)—taking the conventional DIP, which merely reverses the direction of dependency, a step further by minimizing and localizing the dependency itself within the boundary of the contract.

### An AI-Friendly, Capability-Oriented Framework

Moreover, this structure is also well-suited to AI-driven automated programming. For AI-driven automated programming to be performed safely and accurately, the following conditions are required:

* **Localized dependencies**: The scope of dependencies an AI must grasp for a single change should be confined to a narrow portion of the system, not the system as a whole.
* **Explicit boundaries of side effects**: The extent to which side effects can propagate should be made explicit in the code itself, rather than relying on implicit call ordering.
* **Localized impact of change**: Adding features or swapping implementations should not ripple out into existing code that doesn't use them.
* **Substitutability through contract**: An implementation should be safely replaceable as long as it satisfies the contract (trait), without needing to know its implementation details.

**sabi** satisfies all of these conditions at the structural level by defining a dedicated data access trait for each piece of logic and making that logic depend only on that narrow trait—so that adding functionality to a particular `DataAcc` never affects existing logic, and swapping implementations can be done safely as long as the trait's contract is satisfied. In this way, **sabi** is not merely a DI framework, but an AI-friendly, capability-oriented framework suited to the modern AI era.

## Install

In Cargo.toml, write this crate as a dependency:

```toml
[dependencies]
sabi-rust = "0.8.0" # For synchronous APIs
```

For asynchronous APIs with `tokio` runtime, enable the `tokio` feature:

```toml
[dependencies]
sabi-rust = { version = "0.8.0", features = ["tokio"] }
tokio = { version = "1", features = ["full"] } # Required for tokio runtime
```

## Usage (Synchronous)

### 1. Implementing a logic function and a data access trait

First, define a function that represents your application logic, along with its dedicated data access trait.
This trait is independent of specific data source implementations, improving testability.
The `#[overridable]` macro is used to allow this trait implementation to be overridden later.

```rust
use override_macro::overridable;

#[overridable]
pub trait MyData {
    fn get_text(&mut self) -> errs::Result<String>;
    fn set_text(&mut self, text: String) -> errs::Result<()>;
}

pub fn my_logic(data: &mut impl MyData) -> errs::Result<()> {
    let text = data.get_text()?;
    let _ = data.set_text(text)?;
    Ok(())
}
```

### 2. Implementing DataAcc derived traits

The `DataAcc` trait provides a simple mechanism to retrieve `DataConn` objects.
However, it's the derived traits (like `GettingDataAcc` and `SettingDataAcc` in this example) that define the application-specific methods for accessing data.
These methods then use `DataAcc::get_data_conn` to obtain the appropriate `DataConn` and perform the actual data operations.
The `#[overridable]` macro is also used here to allow these methods to be integrated with `DataHub`.

```rust
use sabi::DataAcc;
use override_macro::overridable;
use sabi_redis::RedisDataConn;
use sabi_stdio::StdioDataConn;  // This is a conceptual, non-existent DataConn.

#[overridable]
pub trait GettingDataAcc: DataAcc {
    fn get_text(&mut self) -> errs::Result<String> {
        Ok("output text".to_string())
    }
}

#[overridable]
pub trait SettingDataAcc: DataAcc {
    fn set_text(&mut self, text: String) -> errs::Result<()> {
        let redis_data_conn = self.get_data_conn::<RedisDataConn>("redis")?;
        let redis_conn = redis_data_conn.get_connection();
        redis_conn
            .set("key", text)
            .map_err(|e| errs::Err::with_source("fail to set text to key", e))?;

        redis_data_conn.add_rollback(|redis_conn| {
            redis_conn
                .del("key")
                .map_err(|e| errs::Err::with_source("fail to delete key for rollback", e))
        });

        let stdio_data_conn = self.get_data_conn::<StdioDataConn>("stdio")?;
        stdio_data_conn.add_post_commit(|_stdin, stdout, _stderr| {
            stdout.println(text)
        });

        Ok(())
    }
}
```

### 3. Integrating data traits and DataAcc derived traits into DataHub

The `DataHub` is the central component that manages all `DataSrc` and `DataConn`, providing access to them for your application logic.
It implements `Send`.
By implementing the data traits (`MyData`) from step 1. and the `DataAcc` traits from step 2. on `DataHub`, you integrate them.
The `#[override_with]` macro indicates that the methods of the `MyData` trait will be provided by the corresponding methods of the `DataAcc` derived traits.

```rust
use sabi::DataHub;
use override_macro::override_with;
use crate::logic_layer::MyData;
use crate::data_access_layer::{GettingDataAcc, SettingDataAcc};

impl GettingDataAcc for DataHub {}
impl SettingDataAcc for DataHub {}

#[override_with(GettingDataAcc, SettingDataAcc)]
impl MyData for DataHub {}
```

### 4. Using logic functions and DataHub

Inside your `main` function, register a global `DataSrc` and setup the `sabi` framework.
Then, create an instance of `DataHub` and register the necessary local `DataSrc` using the `uses` method.
Finally, use the `run` method or `txn` method of `DataHub` to execute your defined application logic function (`my_logic`) without or within a transaction.

```rust
use sabi::{uses, setup, DataHub};
use sabi_redis::RedisDataSrc;
use sabi_stdio::StdioDataSrc;  // This is a conceptual, non-existent DataConn.
use crate::logic_layer::my_logic;
use std::process::ExitCode;

// Register global DataSrc using the `sabi::uses!` macro.
// This makes `RedisDataSrc` available throughout the application.
uses!("redis", RedisDataSrc::new("redis://127.0.0.1:6379/0"));

fn main() -> errs::Result<()> {
    // Register global DataSrc using the `sabi::uses` function.
    // This makes `RedisDataSrc` available throughout the application.
    //uses("baz", RedisDataSrc{})?;

    // Set up the sabi framework
    // _auto_shutdown automatically closes and drops global DataSrc at the end of the scope.
    // NOTE: Don't write as `let _ = ...` because the return variable is dropped immediately.
    let _auto_shutdown = setup()?;

    run()?;
}

fn run() -> errs::Result<()> {
    let mut data = DataHub::new();

    // Register session-local DataSrc with DataHub using the `uses` method.
    // This makes `StdioDataSrc` available only within this `DataHub` instance's session.
    data.uses("stdio", StdioDataSrc::new());

    // Execute application logic without a transactional control.
    data.run(my_logic)?;

    // If you need to execute logic within a transaction, use the `txn` method instead of `run`.
    //data.txn(my_logic)?;
}
```

## Usage (Asynchronous)

When the `tokio` feature is enabled, `sabi-rust` provides asynchronous counterparts for its core components and methods, allowing you to build non-blocking data access layers with the Tokio runtime.

The asynchronous APIs are available under the `sabi::tokio` module.

### 1. Implementing logic functions and data traits (Asynchronous)

Your application logic functions and their associated traits will now be async.
The `#[overridable]` macro still functions the same way to allow trait implementations to be overridden.

Note that if the logic function is executed within a spawned task (e.g., via `tokio::spawn`), the data access trait argument must also implement `Send`.
This is because the future returned by the logic function captures the argument and must itself be `Send`.

```rust
use override_macro::overridable;

#[overridable]
pub trait MyAsyncData {
    async fn get_text_async(&mut self) -> Result<String, Err>;
    async fn set_text_async(&mut self, text: String) -> Result<(), Err>;
    async fn set_flag_async(&mut self, flag: bool) -> Result<(), Err>;
}

pub async fn my_async_logic(data: &mut (impl MyAsyncData + Send)) -> Result<(), Err> {
    let text = data.get_text_async().await?;
    let _ = data.set_text_async(text).await?;
    let _ = data.set_flag_async(true).await?;
    Ok(())
}
```

### 2. Implementing DataAcc derived traits (Asynchronous)

The `DataAcc` trait and its derived traits will now also use async methods and rely on `DataAcc::get_data_conn_async` to retrieve asynchronous data connections.

```rust
use sabi::tokio::DataAcc;
use override_macro::overridable;
use sabi_redis::RedisDataConnAsync;
use sabi_stdio::StdioDataConnAsync;  // This is a conceptual, non-existent DataConn.

#[overridable]
pub trait GettingDataAccAsync: DataAcc {
    async fn get_text_async(&mut self) -> errs::Result<String> {
        Ok("output text".to_string())
    }
}

#[overridable]
pub trait SettingDataAccAsync: DataAcc {
    async fn set_text_async(&mut self, text: String) -> errs::Result<()> {
        let redis_data_conn = self.get_data_conn_async::<RedisDataConnAsync>("redis").await?;
        let redis_conn = redis_data_conn.get_connection();
        redis_conn
            .set("key", text)
            .await
            .map_err(|e| errs::Err::with_source("fail to set text to key", e))?;

        redis_data_conn.add_rollback_async(|redis_conn| {
            redis_conn
                .del("key")
                .await
                .map_err(|e| errs::Err::with_source("fail to delete key for rollback", e))
        }).await;

        let stdio_data_conn = self.get_data_conn_async::<StdioDataConnAsync>("stdio").await?;
        stdio_data_conn.add_post_commit_async(|_stdin, stdout, _stderr| {
            stdout.println(text)
        }).await;

        Ok(())
    }
}
```

### 3. Integrating data traits and DataAcc derived traits into DataHub (Asynchronous)

The `sabi::tokio::DataHub` serves the same central role, but operates asynchronously.
It also implements `Send`.
The integration with traits using `#[override_with]` remains conceptually similar.

```rust
use sabi::tokio::DataHub;
use override_macro::override_with;

use crate::logic_layer::MyAsyncData;
use crate::data_access_layer::{GettingAsyncDataAcc, SettingAsyncDataAcc};

impl GettingAsyncDataAcc for DataHub {}
impl SettingAsyncDataAcc for DataHub {}

#[override_with(GettingAsyncDataAcc, SettingAsyncDataAcc)]
impl MyAsyncData for DataHub {}
```

### 4. Using logic functions and DataHub (Asynchronous)

Use the `#[tokio::main]` macro to run your main asynchronous function.
Register global `DataSrc` using `sabi::tokio::uses!`.
Set up the framework with `sabi::tokio::setup_async`.
Execute your logic with `data.txn_async` or `data.run_async`.

```rust
use sabi::tokio::{uses, uses_async, setup_async, DataHub, logic};
use tokio; // Ensure tokio is in scope for #[tokio::main]
use sabi_redis::RedisDataSrcAsync;
use sabi_stdio::StdioDataSrcAsync;
use crate::logic_layer::my_async_logic;

// Register global DataSrc using the `sabi::tokio::uses!` macro.
uses!("redis", RedisDataSrcAsync::new("redis://127.0.0.1:6379/0"));

#[tokio::main]
async fn main() -> errs::Result<()> {
    // Register global DataSrc using the `sabi::tokio::uses_async` function.
    //uses_async("redis", RedisDataSrcAsync::new("redis://127.0.0.1:6379/0")).await?;

    // If there is no risk of conflict with other Tokio tasks, you can use the
    // `sabi::tokio::uses` function, which does not wait for the lock to be released.
    //uses("redis", RedisDataSrcAsync::new("redis://127.0.0.1:6379/0"))?;

    // Set up the sabi framework for async operations
    let _auto_shutdown = setup_async().await.unwrap();

    run_async().await?;
}
    
async fn run_async() -> errs::Result<()> {
    tokio::spawn(async move {
        // Create a new instance of DataHub.
        // Since DataHub is Send, it can be moved into another task.
        let mut data = DataHub::new();

        // Register session-local DataSrc with DataHub using the `uses` method.
        // This makes `StdioDataSrc` available only within this `DataHub` instance's session.
        // If this `DataHub` is moved between threads, `ds` must also implement `Send`.
        data.uses("stdio", StdioDataSrc::new());

        // Execute application logic without an asynchronous transaction
        // The `logic!` macro helps convert an async function into the required closure type.
        // The resulting future is `Send`.
        data.run_async(logic!(my_async_logic)).await?;

        // If you need to execute logic within a transaction, use `txn_async` method instead of
        // `run_async`.
        //data.txn_async(logic!(my_async_logic)).await?;
    })
    .await
}
```

## Related Links

### Data Sources
- [sabi_redis (Rust)](https://github.com/sttk/sabi_redis-rust) ... The DataSrc implementation for Redis

### Implementations in other languages
- [sabi (Golang)](https://github.com/sttk/sabi) ... The sabi implementation in Go
- [sabi (Java)](https://github.com/sttk/sabi-java) ... The sabi implementation in Java


## Supported Rust versions

This crate supports Rust 1.87.0 or later.

```bash
% ./build.sh msrv
  [Meta]   cargo-msrv 0.19.3

Compatibility Check #1: Rust 1.77.2
  [FAIL]   Is incompatible

Compatibility Check #2: Rust 1.88.0
  [OK]     Is compatible

Compatibility Check #3: Rust 1.82.0
  [FAIL]   Is incompatible

Compatibility Check #4: Rust 1.85.1
  [FAIL]   Is incompatible

Compatibility Check #5: Rust 1.86.0
  [FAIL]   Is incompatible

Compatibility Check #6: Rust 1.87.0
  [OK]     Is compatible

Result:
   Considered (min … max):   Rust 1.56.1 … Rust 1.98.0
   Search method:            bisect
   MSRV:                     1.87.0
   Target:                   x86_64-apple-darwin
```

## License

Copyright (C) 2024-2026 Takayuki Sato

This program is free software under MIT License.<br>
See the file LICENSE in this distribution for more details.

[repo-url]: https://github.com/sttk/sabi-rust
[cratesio-img]: https://img.shields.io/badge/crates.io-ver.0.8.0-fc8d62?logo=rust
[cratesio-url]: https://crates.io/crates/sabi-rust
[docrs-img]: https://img.shields.io/badge/doc.rs-sabi_rust-66c2a5?logo=docs.rs
[docrs-url]: https://docs.rs/sabi-rust
[ci-img]: https://github.com/sttk/sabi-rust/actions/workflows/rust.yml/badge.svg?branch=main
[ci-url]: https://github.com/sttk/sabi-rust/actions?query=branch%3Amain
[mit-img]: https://img.shields.io/badge/license-MIT-green.svg
[mit-url]: https://opensource.org/licenses/MIT