1 unstable release
0.1.0 | Apr 22, 2024 |
---|
#735 in Procedural macros
15KB
175 lines
Callback function for Rust
callback_fn is a library that adds functions before, after and around the target function.
Features
- Custom Functions: Users can specify custom functions to be executed before, after, and around the target function.
- Seamless Integration: Integrates seamlessly into existing codebases.
- Error Handling: Handles errors that occur within the callback functions
Uses
- Callback Functions: Add functions before and after the target function.
- Useful for tasks such as logging, authentication, and other cross-cutting concerns.
- Design-by-Contracts: Add pre-conditions and post-conditions to the target function.
- Specific conditions can be applied, such as only when testing, using the features flag.
Installation
Add callback_fn to your Cargo.toml
.
[dependencies]
callback_fn = "0.1.0"
Examples
For callback
After user created, user cache will be created.
use callback_fn::after_callback;
#[allow(dead_code)]
#[derive(Clone, Debug)]
struct User {
name: String,
}
struct UserRepository {}
impl UserRepository {
async fn save(&self, _user: User) -> Result<(), String> {
tokio::time::sleep(Duration::from_micros(1)).await;
Ok(())
}
}
struct UserCache {}
impl UserCache {
async fn save(&self, _user: User) -> Result<(), String> {
tokio::time::sleep(Duration::from_micros(1)).await;
Ok(())
}
}
struct UserUseCase {
user_repository: UserRepository,
user_cache: UserCache,
}
impl UserUseCase {
#[after_callback(let _ = self.create_user_cache(ret.clone()?).await?)]
async fn create_user(&self, name: String) -> Result<User, String> {
let user = User { name };
self.user_repository.save(user.clone()).await?;
Ok(user)
}
async fn create_user_cache(&self, user: User) -> Result<User, String> {
self.user_cache.save(user.clone()).await?;
Ok(user)
}
}
For logging
Add logging around the target function.
use callback_fn::around_callback;
#[around_callback(my_logger())]
fn hello(str: &str) {
println!("Hello {}", str);
}
fn my_logger() {
println!("{}", chrono::Local::now());
}
// hello will print:
//
// 2024-04-01T00:00:000.000000+09:00
// Hello world
// 2024-04-01T00:00:000.000100+09:00
#[test]
fn test_hello() {
hello("world");
}
For Authentication
Add authentication before UseCase function.
use callback_fn::before_callback;
use strum_macros::Display;
#[before_callback(has_permission(current_user, Permission::ReadPost).map_err(UseCaseError::from)?)]
fn get_post_by_id(current_user: &User, id: usize) -> Result<Post, UseCaseError> {
Ok(Post {
id,
title: "Dummy Title".to_string(),
body: "Dummy Body".to_string(),
})
}
#[before_callback(has_permission(current_user, Permission::CreatePost).map_err(UseCaseError::from)?)]
fn create_post(current_user: &User, title: String, body: String) -> Result<Post, UseCaseError> {
Ok(Post { id: 1, title, body })
}
#[derive(Debug)]
struct User {
permissions: Vec<Permission>,
}
#[derive(Debug, Display, PartialEq)]
pub enum Permission {
ReadPost,
CreatePost,
}
fn has_permission(user: &User, permission: Permission) -> Result<(), PermissionError> {
if user.permissions.contains(&permission) {
Ok(())
} else {
Err(PermissionError::PermissionDenied(permission))
}
}
#[derive(Debug, PartialEq)]
struct Post {
id: usize,
title: String,
body: String,
}
#[derive(thiserror::Error, Debug)]
pub enum PermissionError {
#[error("User don't have {0} permission.")]
PermissionDenied(Permission),
}
#[derive(thiserror::Error, Debug)]
pub enum UseCaseError {
#[error("PermissionError: {0}")]
PermissionError(#[from] PermissionError),
}
For Design-by-contract
After adding to the cart or cleaning, ensure if the total_price is correct.
use callback_fn::around_callback;
struct Cart {
total_price: usize,
items: Vec<Item>,
}
struct Item {
price: usize,
}
impl Cart {
fn new() -> Self {
Self {
total_price: 0,
items: vec![],
}
}
// Ensure total_price is correct around add_item.
// Error handling is available in runtime when conditions are not ensure.
#[around_callback(self.ensure_total_price()?)]
fn add_item(&mut self, item: Item) -> Result<(), String> {
self.items.push(item);
self.update_total_price();
Ok(())
}
fn update_total_price(&mut self) {
self.total_price = self.items.iter().map(|item| item.price).sum()
}
fn ensure_total_price(&self) -> Result<(), String> {
if self.total_price == self.items.iter().map(|item| item.price).sum() {
Ok(())
} else {
Err("Total price is not correct".to_string())
}
}
}
Use only in specific features
If you want to use callback_fn only in specific features, you can use cfg_attr
.
use callback_fn::after_callback;
#[cfg_attr(test, after_callback(bar()))]
fn foo() {
println!("foo");
}
fn bar() {
println!("bar");
}
If you run cargo run --features test
, foo function will be like below.
fn foo() {
#[allow(unused_mut)]
let mut ret = {
{
::std::io::_print(format_args!("foo\n"));
};
};
bar();
ret
}
fn bar() {
{
::std::io::_print(format_args!("bar\n"));
};
}
If you run cargo run
, foo function will be like below.
fn foo() {
{
::std::io::_print(format_args!("foo\n"));
};
}
fn bar() {
{
::std::io::_print(format_args!("bar\n"));
};
}
Dependencies
~245–690KB
~16K SLoC