# Wire a signup service through an IoC container

**For:** Rust developers who want dependency injection without a framework.

You build a small signup program whose service gets its config, its user repository and an optional notifier from a `fibre_ioc` container, then you test it by swapping the real repository for a test double.

Before you start: you need Rust with Cargo. fibre_ioc comes from crates.io as `fibre_ioc = "0"`.

## Set up the project

Start a new binary crate and add `fibre_ioc`.

```sh
cargo new signup
cd signup
cargo add fibre_ioc@0
```

Your `Cargo.toml` should now have this dependency block:

```toml title="Cargo.toml"
[dependencies]
fibre_ioc = "0"
```

The program has four pieces: an `AppConfig` with a couple of settings, a `UserRepo` trait with an in-memory implementation, a `SignupService` that uses both and an optional `Notifier`. Each one goes into the container and the service asks the container for the others.

## Register a config instance

The simplest registration is an object you already have. `add_instance` stores it as a singleton. `resolve!` gets it back as an `Arc`. Replace `src/main.rs` with this:

```rust title="src/main.rs"
use fibre_ioc::{global, resolve};

struct AppConfig {
    app_name: String,
    max_name_len: usize,
}

fn main() {
    global().add_instance(AppConfig {
        app_name: "Tally".to_string(),
        max_name_len: 8,
    });

    let config = resolve!(AppConfig);
    println!("{} accepts names up to {} characters", config.app_name, config.max_name_len);
}
```

`global()` returns the one application-wide `Container`. It is created the first time you touch it. `resolve!(AppConfig)` looks the service up by its type and returns `Arc<AppConfig>`.

```shell-session
$ cargo run -q
Tally accepts names up to 8 characters
```

## Resolve a repository by trait

The service should depend on what a repository does, so define a trait for it. The trait needs `Send + Sync` because the thread-safe container only stores types that are safe to share between threads. Add the trait and an in-memory implementation below `AppConfig`. Also change the `use` line to bring in `Arc` and `Mutex`:

```rust title="src/main.rs"
use fibre_ioc::{global, resolve};
use std::sync::{Arc, Mutex};

trait UserRepo: Send + Sync {
    fn insert(&self, name: &str) -> u64;
    fn count(&self) -> usize;
}

#[derive(Default)]
struct MemoryUserRepo {
    names: Mutex<Vec<String>>,
}

impl UserRepo for MemoryUserRepo {
    fn insert(&self, name: &str) -> u64 {
        let mut names = self.names.lock().unwrap();
        names.push(name.to_string());
        names.len() as u64
    }

    fn count(&self) -> usize {
        self.names.lock().unwrap().len()
    }
}
```

`add_singleton_trait` registers a factory for a trait object. You name the trait with a turbofish and the factory returns an `Arc` of the concrete type. The factory runs once, on the first resolution. Every later resolution gets the same `Arc`. To resolve it, put `trait` in front of the name. Update `main`:

```rust title="src/main.rs"
fn main() {
    global().add_instance(AppConfig {
        app_name: "Tally".to_string(),
        max_name_len: 8,
    });
    global().add_singleton_trait::<dyn UserRepo>(|| Arc::new(MemoryUserRepo::default()));

    let config = resolve!(AppConfig);
    println!("{} accepts names up to {} characters", config.app_name, config.max_name_len);

    let repo = resolve!(trait UserRepo);
    repo.insert("ada");

    let same_repo = resolve!(trait UserRepo);
    println!("users stored: {}", same_repo.count());
}
```

The second `resolve!` returns the same repository the first one wrote to, so the count is 1.

```shell-session
$ cargo run -q
Tally accepts names up to 8 characters
users stored: 1
```

## Build a service that depends on both

`SignupService` holds an `Arc<AppConfig>` and an `Arc<dyn UserRepo>`. It never names `MemoryUserRepo`. Add it below the repository:

```rust title="src/main.rs"
struct SignupService {
    config: Arc<AppConfig>,
    repo: Arc<dyn UserRepo>,
}

impl SignupService {
    fn sign_up(&self, name: &str) -> Result<u64, String> {
        if name.len() > self.config.max_name_len {
            return Err(format!(
                "{name:?} is longer than {} characters",
                self.config.max_name_len
            ));
        }
        Ok(self.repo.insert(name))
    }
}
```

The service is registered with `add_singleton`. Its factory resolves its own dependencies with `resolve!`, which reads the global container. Put all the registrations in one function so `main` stays short:

```rust title="src/main.rs"
fn register_services() {
    global().add_instance(AppConfig {
        app_name: "Tally".to_string(),
        max_name_len: 8,
    });
    global().add_singleton_trait::<dyn UserRepo>(|| Arc::new(MemoryUserRepo::default()));
    global().add_singleton(|| SignupService {
        config: resolve!(AppConfig),
        repo: resolve!(trait UserRepo),
    });
}

fn main() {
    register_services();

    let signup = resolve!(SignupService);
    println!("{} signups", signup.config.app_name);
    for name in ["ada", "grace", "margaret hamilton"] {
        match signup.sign_up(name) {
            Ok(id) => println!("signed up {name} as user {id}"),
            Err(e) => println!("rejected: {e}"),
        }
    }

    let repo = resolve!(trait UserRepo);
    println!("users stored: {}", repo.count());
}
```

When `main` resolves `SignupService`, the container runs its factory, which resolves the config and the repository. The repository `main` resolves at the end is the same singleton the service wrote to.

```shell-session
$ cargo run -q
Tally signups
signed up ada as user 1
signed up grace as user 2
rejected: "margaret hamilton" is longer than 8 characters
users stored: 2
```

If you resolve a service nobody registered with `resolve!` or `resolve_from!`, the program panics with a message naming the missing service. That is what you want for dependencies the program cannot run without.

## Add an optional notifier

Some dependencies are nice to have. Add a `Notifier` trait with a console implementation:

```rust title="src/main.rs"
trait Notifier: Send + Sync {
    fn notify(&self, message: &str);
}

struct ConsoleNotifier;

impl Notifier for ConsoleNotifier {
    fn notify(&self, message: &str) {
        println!("[notify] {message}");
    }
}
```

Give the service an `Option<Arc<dyn Notifier>>` and call it when it is there:

```rust title="src/main.rs"
struct SignupService {
    config: Arc<AppConfig>,
    repo: Arc<dyn UserRepo>,
    notifier: Option<Arc<dyn Notifier>>,
}

impl SignupService {
    fn sign_up(&self, name: &str) -> Result<u64, String> {
        if name.len() > self.config.max_name_len {
            return Err(format!(
                "{name:?} is longer than {} characters",
                self.config.max_name_len
            ));
        }
        let id = self.repo.insert(name);
        if let Some(notifier) = &self.notifier {
            notifier.notify(&format!("welcome to {}, {name}", self.config.app_name));
        }
        Ok(id)
    }
}
```

In the factory, `maybe_resolve!` returns `None` for a missing service instead of panicking. Add it to the `use` line and to `register_services`:

```rust title="src/main.rs"
use fibre_ioc::{global, maybe_resolve, resolve};

fn register_services() {
    global().add_instance(AppConfig {
        app_name: "Tally".to_string(),
        max_name_len: 8,
    });
    global().add_singleton_trait::<dyn UserRepo>(|| Arc::new(MemoryUserRepo::default()));
    global().add_singleton(|| SignupService {
        config: resolve!(AppConfig),
        repo: resolve!(trait UserRepo),
        notifier: maybe_resolve!(trait Notifier),
    });
}
```

Now register the notifier in `main` only when the program is started with `--notify`. Registration is dynamic, so it can come after `register_services`: the service's factory has not run yet and sees whatever is registered when `SignupService` is first resolved.

```rust title="src/main.rs"
fn main() {
    register_services();
    if std::env::args().any(|arg| arg == "--notify") {
        global().add_singleton_trait::<dyn Notifier>(|| Arc::new(ConsoleNotifier));
    }

    let signup = resolve!(SignupService);
    println!("{} signups", signup.config.app_name);
    for name in ["ada", "grace", "margaret hamilton"] {
        match signup.sign_up(name) {
            Ok(id) => println!("signed up {name} as user {id}"),
            Err(e) => println!("rejected: {e}"),
        }
    }

    let repo = resolve!(trait UserRepo);
    println!("users stored: {}", repo.count());
}
```

Run it without the flag and the output is the same as before. Run it with the flag and the notifier speaks up:

```shell-session
$ cargo run -q -- --notify
Tally signups
[notify] welcome to Tally, ada
signed up ada as user 1
[notify] welcome to Tally, grace
signed up grace as user 2
rejected: "margaret hamilton" is longer than 8 characters
users stored: 2
```

## Swap in a test double

Tests should not share the global container, because every test would see the others' registrations. Each test creates its own `Container`, registers what the test needs on it and resolves from it with `resolve_from!`. When the container goes out of scope at the end of the test, everything in it is dropped.

Here the test registers a config and a `RecordingRepo` for `dyn UserRepo`, then builds the `SignupService` from what that container holds. The test keeps its own `Arc` to the double so it can check what was written. Add this at the bottom of `src/main.rs`:

```rust title="src/main.rs"
#[cfg(test)]
mod tests {
    use super::*;
    use fibre_ioc::{Container, resolve_from};

    #[derive(Default)]
    struct RecordingRepo {
        inserted: Mutex<Vec<String>>,
    }

    impl UserRepo for RecordingRepo {
        fn insert(&self, name: &str) -> u64 {
            self.inserted.lock().unwrap().push(name.to_string());
            42
        }

        fn count(&self) -> usize {
            self.inserted.lock().unwrap().len()
        }
    }

    fn signup_with(repo: Arc<RecordingRepo>) -> SignupService {
        let container = Container::new();
        container.add_instance(AppConfig {
            app_name: "Test".to_string(),
            max_name_len: 8,
        });
        container.add_singleton_trait::<dyn UserRepo>(move || repo.clone());

        SignupService {
            config: resolve_from!(&container, AppConfig),
            repo: resolve_from!(&container, trait UserRepo),
            notifier: None,
        }
    }

    #[test]
    fn stores_valid_names() {
        let repo = Arc::new(RecordingRepo::default());
        let signup = signup_with(Arc::clone(&repo));

        assert_eq!(signup.sign_up("ada"), Ok(42));
        assert_eq!(*repo.inserted.lock().unwrap(), vec!["ada"]);
    }

    #[test]
    fn rejects_long_names_before_the_repo() {
        let repo = Arc::new(RecordingRepo::default());
        let signup = signup_with(Arc::clone(&repo));

        assert!(signup.sign_up("margaret hamilton").is_err());
        assert_eq!(repo.count(), 0);
    }
}
```

The id 42 in the first test can only come from `RecordingRepo`, so a passing test also shows the double is the one in use.

```shell-session
$ cargo test -q
running 2 tests
..
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
```

## Where to go next

This tutorial used instances, singletons and trait singletons. The container also has transient factories that build a fresh value on every resolution (`add_transient`), named registrations for several providers of the same type (`add_singleton_with_name` and `resolve!(AppConfig, "test_config")`) and a `LocalContainer` behind the `local` feature for types that are not `Send` or `Sync`. It also panics with a clear message when two services depend on each other in a cycle.

The usage guide on GitHub covers each of these with examples: [https://github.com/excsn/fibre/blob/main/ioc/README.USAGE.md](https://github.com/excsn/fibre/blob/main/ioc/README.USAGE.md)

The full API is on docs.rs: [https://docs.rs/fibre_ioc](https://docs.rs/fibre_ioc)
