singleton_macro

Crates.iosingleton_macro
lib.rssingleton_macro
version0.1.0
created_at2025-06-06 19:47:35.70855+00
updated_at2025-06-06 19:47:35.70855+00
descriptionSpring Framework-inspired dependency injection and singleton pattern macros for Rust backend services
homepagehttps://github.com/imaustinpark/single_macro.git
repositoryhttps://github.com/imaustinpark/single_macro.git
max_upload_size
id1703385
size90,333
Austin Park (imaustinpark)

documentation

https://docs.rs/singleton_macro

README

Singleton Macro

Crates.io Documentation License: MIT OR Apache-2.0

Spring Framework-inspired dependency injection and singleton pattern macros for Rust backend services.

Features

  • 🚀 Zero-boilerplate singleton pattern with automatic dependency injection
  • 🔄 Spring-like DI: Arc<T> fields are automatically injected via ServiceLocator
  • 📦 Service & Repository macros for clean architecture
  • 🛡️ Thread-safe using Arc and OnceCell
  • 🎯 MongoDB & Redis integration helpers
  • Compile-time dependency resolution with inventory crate

Quick Start

Add to your Cargo.toml:

[dependencies]
singleton_macro = "0.1.0"
once_cell = "1.0"
inventory = "0.3"
async-trait = "0.1"

# Optional: for MongoDB & Redis integration
mongodb = "3.2"
redis = "0.31"

Usage

Basic Service

use singleton_macro::service;
use std::sync::Arc;

#[service]
struct UserService {
    user_repo: Arc<UserRepository>,  // Auto-injected
    email_service: Arc<EmailService>, // Auto-injected
    retry_count: u32,                // Default::default()
}

impl UserService {
    async fn create_user(&self, email: &str) -> Result<User, Error> {
        let user = self.user_repo.find_by_email(email).await?;
        self.email_service.send_welcome(&user).await?;
        Ok(user)
    }
}

// Usage
let service = UserService::instance();

Repository with MongoDB & Redis

use singleton_macro::repository;
use std::sync::Arc;

#[repository(collection = "users")]
struct UserRepository {
    db: Arc<Database>,        // Auto-injected
    redis: Arc<RedisClient>,  // Auto-injected  
}

impl UserRepository {
    async fn find_by_email(&self, email: &str) -> Result<Option<User>, Error> {
        // Check cache first
        let cache_key = self.cache_key(&format!("email:{}", email));
        if let Ok(Some(cached)) = self.redis.get(&cache_key).await {
            return Ok(Some(cached));
        }
        
        // Query database
        let user = self.collection::<User>()
            .find_one(doc! { "email": email })
            .await?;
            
        // Cache result
        if let Some(ref user) = user {
            self.redis.set_with_expiry(&cache_key, user, 300).await?;
        }
        
        Ok(user)
    }
}

Service Registry Setup

use crate::core::registry::ServiceLocator;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Initialize core services
    let db = Arc::new(Database::connect("mongodb://localhost").await?);
    let redis = Arc::new(RedisClient::connect("redis://localhost").await?);
    
    // Register with ServiceLocator
    ServiceLocator::set(db);
    ServiceLocator::set(redis);
    
    // Initialize all services (discovers via inventory)
    ServiceLocator::initialize_all().await?;
    
    // Use your services
    let user_service = UserService::instance();
    let user = user_service.create_user("test@example.com").await?;
    
    Ok(())
}

Generated Code

The #[service] macro generates:

impl UserService {
    pub fn instance() -> Arc<Self> { /* singleton logic */ }
    fn new() -> Self { /* dependency injection */ }
}

impl Service for UserService {
    fn name(&self) -> &str { "userservice" }
    async fn init(&self) -> Result<(), Box<dyn Error>> { /* */ }
}

The #[repository] macro additionally generates:

impl UserRepository {
    pub fn collection<T>(&self) -> mongodb::Collection<T> { /* */ }
    pub fn cache_key(&self, id: &str) -> String { /* */ }
    pub async fn invalidate_cache(&self, id: &str) -> Result<(), Error> { /* */ }
    // ... more caching helpers
}

Dependency Injection Rules

Field Type Field Name Injection
Arc<T> any ServiceLocator::get::<T>()
any db, database ServiceLocator::get::<Database>()
any redis, cache ServiceLocator::get::<RedisClient>()
other any Default::default()

Registry System

Services and repositories are automatically registered:

  • #[service]{name}
  • #[repository]{name}
#[service(name = "auth")]        // Registered as "auth"
#[repository(name = "user")]     // Registered as "user"

Architecture Example

// 1. Repository Layer
#[repository(collection = "users")]
struct UserRepository {
    db: Arc<Database>,
    redis: Arc<RedisClient>,
}

// 2. Service Layer  
#[service]
struct UserService {
    user_repo: Arc<UserRepository>,    // Injected
}

// 3. Application Layer
#[service]
struct UserController {
    user_service: Arc<UserService>,    // Injected
    auth_service: Arc<AuthService>,    // Injected
}

Best Practices

✅ Do

  • Use clear layered architecture (Repository → Service → Controller)
  • Register core dependencies (Database, Redis) before using services
  • Keep services focused on single responsibilities

❌ Avoid

  • Circular dependencies (A → B → A will panic at runtime)
  • Direct repository access from controllers (use services)
  • Mixing business logic in repositories

Error Handling

Common issues and solutions:

// ❌ Circular dependency
#[service] struct A { b: Arc<B> }
#[service] struct B { a: Arc<A> }  // Runtime panic!

// ✅ Proper layering
#[service] struct A { repo: Arc<Repository> }
#[service] struct B { repo: Arc<Repository>, a: Arc<A> }

Author

Janghoon Park ceo@dataengine.co.kr

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

This project is licensed under either of

at your option.


Copyright (c) 2025 Janghoon Park ceo@dataengine.co.kr

Commit count: 0

cargo fmt