diff --git a/README.md b/README.md index 7847658..73c0c84 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,108 @@ The sabi data access library for Redis in Rust. +`sabi_redis` is a Rust crate that provides a streamlined way to access various Redis configurations +within the sabi framework. It includes `DataSrc` and `DataConn` derived classes designed to make +your development process more efficient + +`RedisDataSrc` and `RedisDataConn` are designed for a standalone Redis server and provide +synchronous connections for processing Redis commands. + +Unlike relational databases, Redis does not support data rollbacks. This can lead to data +inconsistency if a transaction involving both Redis and another database fails mid-process. +To address this, `sabi_redis` offers three unique features to help developers manage Redis updates +and revert changes when necessary: *"force back"*, *"pre-commit"*, and *"post-commit"*. + +## Installation + +In Cargo.toml, write this crate as a dependency: + +```toml +[dependencies] +sabi_redis = "0.0.0" +``` + +## Usage + +### For Standalone Server And Synchronous Commands + +Here is an example of how to use `RedisDataSrc` and `RedisDataConn` to connect to Redis and +execute a simple command. + +```rust +use errs; +use override_macro::{overridable, override_with}; +use redis::TypedCommands; +use sabi; +use sabi_redis::{RedisDataSrc, RedisDataConn}; + +fn main() -> Result<(), errs::Err> { + // Register a `RedisDataSrc` instance to connect to a Redis server with the key "redis". + sabi::uses("redis", RedisDataSrc::new("redis://127.0.0.1:6379/0")); + + // In this setup process, the registered `RedisDataSrc` instance connects to a Redis server. + let _auto_shutdown = sabi::setup()?; + + my_app() +} + +fn my_app() -> Result<(), errs::Err> { + let mut data = sabi::DataHub::new(); + sabi::txn!(my_logic, data) +} + +fn my_logic(data: &mut impl MyData) -> Result<(), errs::Err> { + let greeting = data.get_greeting()?; + data.say_greeting(&greeting) +} + +#[overridable] +trait MyData { + fn get_greeting(&mut self) -> Result; + fn say_greeting(&mut self, greeting: &str) -> Result<(), errs::Err>; +} + +#[overridable] +trait GettingDataAcc: sabi::DataAcc { + fn get_greeting(&mut self) -> Result { + Ok("Hello!".to_string()) + } +} + +#[overridable] +trait RedisSayingDataAcc: sabi::DataAcc { + fn say_greeting(&mut self, greeting: &str) -> Result<(), errs::Err> { + // Retrieve a `RedisDataConn` instance by the key "redis". + let data_conn = self.get_data_conn::("redis")?; + + // Get a Redis connection to execute Redis synchronous commands. + let mut redis_conn = data_conn.get_connection()?; + + if let Err(e) = redis_conn.set("greeting", greeting) { + return Err(errs::Err::with_source("fail to set greeting", e)); + } + + // Register a force back process to revert updates to Redis when an error occurs. + data_conn.add_force_back(|redis_conn| { + let result = redis_conn.del("greeting"); + if let Err(e) = result { + return Err(errs::Err::with_source("fail to force back", e)); + } + Ok(()) + }); + + Ok(()) + } +} + +impl GettingDataAcc for sabi::DataHub {} +impl RedisSayingDataAcc for sabi::DataHub {} + +#[override_with(GettingDataAcc, RedisSayingDataAcc)] +impl MyData for sabi::DataHub {} +``` + + ## Supported Rust versions This crate supports Rust 1.85.1 or later. diff --git a/src/standalone.rs b/src/standalone.rs index 40e058c..1424c4b 100644 --- a/src/standalone.rs +++ b/src/standalone.rs @@ -43,6 +43,81 @@ pub enum RedisDataSrcError { /// The `add_force_back` method allows registering functions to manually revert changes /// if an error occurs during a multi-step process or a transaction involving /// other external data sources. +/// +/// ## Example +/// +/// ```rust +/// use errs; +/// use override_macro::{overridable, override_with}; +/// use redis::TypedCommands; +/// use sabi; +/// use sabi_redis::{RedisDataSrc, RedisDataConn}; +/// +/// fn main() -> Result<(), errs::Err> { +/// // Register a `RedisDataSrc` instance to connect to a Redis server with the key "redis". +/// sabi::uses("redis", RedisDataSrc::new("redis://127.0.0.1:6379/0")); +/// +/// // In this setup process, the registered `RedisDataSrc` instance connects to a Redis server. +/// let _auto_shutdown = sabi::setup()?; +/// +/// my_app() +/// } +/// +/// fn my_app() -> Result<(), errs::Err> { +/// let mut data = sabi::DataHub::new(); +/// sabi::txn!(my_logic, data) +/// } +/// +/// fn my_logic(data: &mut impl MyData) -> Result<(), errs::Err> { +/// let greeting = data.get_greeting()?; +/// data.say_greeting(&greeting) +/// } +/// +/// #[overridable] +/// trait MyData { +/// fn get_greeting(&mut self) -> Result; +/// fn say_greeting(&mut self, greeting: &str) -> Result<(), errs::Err>; +/// } +/// +/// #[overridable] +/// trait GettingDataAcc: sabi::DataAcc { +/// fn get_greeting(&mut self) -> Result { +/// Ok("Hello!".to_string()) +/// } +/// } +/// +/// #[overridable] +/// trait RedisSayingDataAcc: sabi::DataAcc { +/// fn say_greeting(&mut self, greeting: &str) -> Result<(), errs::Err> { +/// // Retrieve a `RedisDataConn` instance by the key "redis". +/// let data_conn = self.get_data_conn::("redis")?; +/// +/// // Get a Redis connection to execute Redis synchronous commands. +/// let mut redis_conn = data_conn.get_connection()?; +/// +/// if let Err(e) = redis_conn.set("greeting", greeting) { +/// return Err(errs::Err::with_source("fail to set greeting", e)); +/// } +/// +/// // Register a force back process to revert updates to Redis when an error occurs. +/// data_conn.add_force_back(|redis_conn| { +/// let result = redis_conn.del("greeting"); +/// if let Err(e) = result { +/// return Err(errs::Err::with_source("fail to force back", e)); +/// } +/// Ok(()) +/// }); +/// +/// Ok(()) +/// } +/// } +/// +/// impl GettingDataAcc for sabi::DataHub {} +/// impl RedisSayingDataAcc for sabi::DataHub {} +/// +/// #[override_with(GettingDataAcc, RedisSayingDataAcc)] +/// impl MyData for sabi::DataHub {} +/// ``` pub struct RedisDataConn { pool: r2d2::Pool, pre_commit_vec: Vec Result<(), Err>>>, @@ -75,7 +150,7 @@ impl RedisDataConn { /// finished, and right before their commit processes are made, pub fn add_pre_commit(&mut self, f: F) where - F: FnMut(&mut redis::Connection) -> Result<(), Err> + 'static + F: FnMut(&mut redis::Connection) -> Result<(), Err> + 'static, { self.pre_commit_vec.push(Box::new(f)); } @@ -85,7 +160,7 @@ impl RedisDataConn { /// The provided function will be called as post-transaction processes. pub fn add_post_commit(&mut self, f: F) where - F: FnMut(&mut redis::Connection) -> Result<(), Err> + 'static + F: FnMut(&mut redis::Connection) -> Result<(), Err> + 'static, { self.post_commit_vec.push(Box::new(f)); } @@ -96,7 +171,7 @@ impl RedisDataConn { /// due to a failure in a subsequent operation. pub fn add_force_back(&mut self, f: F) where - F: FnMut(&mut redis::Connection) -> Result<(), Err> + 'static + F: FnMut(&mut redis::Connection) -> Result<(), Err> + 'static, { self.force_back_vec.push(Box::new(f)); } @@ -117,7 +192,10 @@ impl DataConn for RedisDataConn { } Ok(()) } - Err(e) => Err(Err::with_source(RedisDataSrcError::FailToGetConnectionFromPool, e)), + Err(e) => Err(Err::with_source( + RedisDataSrcError::FailToGetConnectionFromPool, + e, + )), } } diff --git a/tests/standalone_sync_test.rs b/tests/standalone_sync_test.rs new file mode 100644 index 0000000..9220941 --- /dev/null +++ b/tests/standalone_sync_test.rs @@ -0,0 +1,68 @@ +#[cfg(test)] +mod integration_tests_of_sabi_redis { + use errs; + use override_macro::{overridable, override_with}; + use redis::TypedCommands; + use sabi; + use sabi_redis::{RedisDataConn, RedisDataSrc}; + + #[test] + fn test() -> Result<(), errs::Err> { + sabi::uses("redis", RedisDataSrc::new("redis://127.0.0.1:6379/10")); + + let _auto_shutdown = sabi::setup()?; + + my_app() + } + + fn my_app() -> Result<(), errs::Err> { + let mut data = sabi::DataHub::new(); + sabi::txn!(my_logic, data) + } + + fn my_logic(data: &mut impl MyData) -> Result<(), errs::Err> { + let greeting = data.get_greeting()?; + data.say_greeting(&greeting) + } + + #[overridable] + trait MyData { + fn get_greeting(&mut self) -> Result; + fn say_greeting(&mut self, greeting: &str) -> Result<(), errs::Err>; + } + + #[overridable] + trait GettingDataAcc: sabi::DataAcc { + fn get_greeting(&mut self) -> Result { + Ok("Hello!".to_string()) + } + } + + #[overridable] + trait RedisSayingDataAcc: sabi::DataAcc { + fn say_greeting(&mut self, greeting: &str) -> Result<(), errs::Err> { + let data_conn = self.get_data_conn::("redis")?; + let mut redis_conn = data_conn.get_connection()?; + + if let Err(e) = redis_conn.set("greeting", greeting) { + return Err(errs::Err::with_source("fail to set greeting", e)); + } + + data_conn.add_force_back(|redis_conn| { + let result = redis_conn.del("greeting"); + if let Err(e) = result { + return Err(errs::Err::with_source("fail to force back", e)); + } + Ok(()) + }); + + Ok(()) + } + } + + impl GettingDataAcc for sabi::DataHub {} + impl RedisSayingDataAcc for sabi::DataHub {} + + #[override_with(GettingDataAcc, RedisSayingDataAcc)] + impl MyData for sabi::DataHub {} +}