service-builder

Crates.ioservice-builder
lib.rsservice-builder
version0.3.0
created_at2024-12-17 14:10:51.544253+00
updated_at2025-08-16 19:36:37.060243+00
descriptionA lightweight, type-safe service construction library for Rust that provides compile-time dependency injection through builder pattern
homepage
repositoryhttps://github.com/krcpa/service-builder
max_upload_size
id1486322
size42,557
(krcpa)

documentation

README

service-builder

Crates.io Documentation License: MIT

A lightweight, type-safe service construction library for Rust that leverages the builder pattern to provide a more idiomatic alternative to traditional dependency injection.

Features

  • 🔒 Type-safe dependency injection at compile time
  • 🚀 Zero runtime overhead - everything is checked at compile time
  • 🛠️ Automatic builder implementation via proc-macros
  • 📦 Field-level getters and setters with attribute control
  • Zero-cost abstractions - no runtime reflection or dynamic dispatch
  • 🔍 Comprehensive error handling with descriptive messages
  • 🎯 Default values support - Optional fields with custom or default values
  • 🔧 Flexible build strategies - Strict or permissive build methods

Why Builder Pattern in Rust?

1. Ownership and Borrowing

Traditional dependency injection frameworks often struggle with Rust's ownership system. The builder pattern works naturally with Rust's ownership rules:

// ❌ Traditional DI approach - fights with ownership
container.register::<UserService>(UserService::new);
let service = container.resolve::<UserService>().unwrap(); // Runtime checks

// ✅ Builder pattern - works with ownership
let service = UserService::builder()
    .repository(repo)
    .cache(cache)
    .build()?; // Compile-time checks

2. Compile-Time Guarantees

Rust's type system can catch dependency issues at compile time with the builder pattern:

#[builder]
struct UserService {
    repository: Arc<dyn Repository>,
    cache: Arc<dyn Cache>,
}

// Won't compile if you forget a dependency
let service = UserService::builder()
    .repository(repo)
    // Forgot .cache()
    .build(); // Compile error!

3. Clear Dependency Flow

Dependencies are explicit and visible in the code:

let auth_service = AuthService::builder()
    .user_repository(user_repo)
    .token_service(token_service)
    .build()?;

let post_service = PostService::builder()
    .post_repository(post_repo)
    .auth_service(auth_service) // Clear dependency chain
    .build()?;

Quick Start

Add this to your Cargo.toml:

[dependencies]
service-builder = "0.3.0"

Basic Usage with Builder Pattern

use service_builder::prelude::*;
use std::sync::Arc;

#[builder]
struct UserService {
    repository: Arc<dyn UserRepository>,
    cache: Arc<dyn Cache>,
}

let user_service = UserService::builder()
    .repository(user_repo)
    .cache(cache)
    .build()?;

Using Getters and Setters

You can add getter and setter methods to your fields using attributes:

#[builder]
struct Config {
    #[builder(getter)]  // Generates get_api_key()
    api_key: String,
    
    #[builder(setter)]  // Generates set_timeout()
    timeout: Duration,
    
    #[builder(getter, setter)]  // Generates both
    max_retries: u32,
}

let mut config = Config::builder()
    .api_key("secret".to_string())
    .timeout(Duration::from_secs(30))
    .max_retries(3)
    .build()?;

// Use generated getter
assert_eq!(config.get_api_key(), &"secret".to_string());

// Use generated setter
config.set_max_retries(5);

Default Values and Optional Fields

service-builder now supports configuration patterns with default values:

use std::time::Duration;

#[builder]
struct DatabaseConfig {
    // Required field - no default
    connection_string: String,
    
    // Optional with custom default
    #[builder(default = "Duration::from_secs(30)")]
    timeout: Duration,
    
    // Optional with Default trait
    #[builder(default)]
    ssl_enabled: bool,
    
    // Optional field - defaults to None
    #[builder(optional)]
    max_connections: Option<usize>,
}

// Use build_with_defaults() for configs
let config = DatabaseConfig::builder()
    .connection_string("postgres://localhost/mydb".to_string())
    .build_with_defaults()?;  // Uses defaults for missing fields

// Or override specific defaults
let config = DatabaseConfig::builder()
    .connection_string("postgres://localhost/mydb".to_string())
    .timeout(Duration::from_secs(60))
    .ssl_enabled(true)
    .build_with_defaults()?;

// Strict build() still requires all non-default fields
let config = DatabaseConfig::builder()
    .connection_string("postgres://localhost/mydb".to_string())
    .build()?;  // Works because other fields have defaults

Composing Services

#[builder]
struct AppServices {
    #[builder(getter)]  // Access services via getters
    user_service: Arc<UserService>,
    post_service: Arc<PostService>,
}

let app_services = AppServices::builder()
    .user_service(Arc::new(user_service))
    .post_service(Arc::new(post_service))
    .build()?;

// Access services using generated getters
let user_service = app_services.get_user_service();

Attribute Reference

Field Attributes

  • #[builder(getter)] - Generates a getter method get_field_name() -> &FieldType
  • #[builder(setter)] - Generates a setter method set_field_name(value: FieldType)
  • #[builder(getter, setter)] - Generates both getter and setter methods
  • #[builder(default)] - Field uses Default::default() if not provided
  • #[builder(default = "expression")] - Field uses custom default expression
  • #[builder(optional)] - For Option<T> fields, defaults to None

Build Methods

  • build() - Strict build, returns error if required fields are missing
  • build_with_defaults() - Permissive build, uses defaults where available

Builder Pattern vs Traditional DI

Advantages of Builder Pattern

  1. Type Safety: All dependencies are checked at compile time
  2. Zero Runtime Cost: No reflection or dynamic dispatch overhead
  3. Ownership Control: Works naturally with Rust's ownership system
  4. Explicit Dependencies: Dependencies are clearly visible in the code
  5. Flexible Access: Optional getter/setter generation for fine-grained control

Disadvantages of Traditional DI

  1. Runtime Overhead: Container resolution and type checking at runtime
  2. Safety Issues: Potential runtime panics from missing dependencies
  3. Ownership Complexity: DI frameworks often struggle with Rust's ownership rules
  4. Hidden Dependencies: Dependencies are often hidden in container configuration
  5. Runtime Failures: Many dependency issues only surface at runtime

Contributing

We welcome contributions! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Commit count: 20

cargo fmt