diode-base 0.3.0

Basic services for the diode library
Documentation
# diode-base

Application building blocks for the [`diode`](https://crates.io/crates/diode)
dependency-injection framework: layered configuration, long-running daemons, a
CLI entry point, structured tracing and metrics, and dynamic configuration.

```toml
[dependencies]
diode = "0.2"
diode-base = "0.3"
```

## What it provides

- **Configuration** - `Config` loads and merges layered JSON. Declare a typed
  section with `#[config_section("name")]` and read it with `config.get`.
- **Daemons** - the `Daemon` trait, `AddDaemonExt` / `AddDaemonServiceExt` to
  register background tasks, and `RunDaemonsExt::run_daemons` to run them
  concurrently with cooperative, token-based shutdown.
- **CLI** - the `Command` trait, `AddCommandExt`, and `RunMainExt::run_main`,
  which parses arguments, loads config, sets up tracing/metrics, builds the app,
  and dispatches a subcommand. Built-in `server` runs every daemon; `config`
  prints the resolved configuration.
- **Observability** - `Tracing` and `Metrics` wire up `tracing` and OpenTelemetry
  (OTLP) exporters from the `tracing` / `metrics` config sections.
- **Dynamic configuration** - watch config sources and react to changes at
  runtime (for example to change the tracing level live).
- **Testing** - the `testing` module ships integration-test helpers such as
  `FreePort`.

## Example

A worker service driven by configuration, wired as a daemon and started through
the CLI entry point:

```rust
use std::process::ExitCode;
use std::sync::Arc;
use std::time::Duration;

use diode::{App, AppContext, Service, StdError};
use diode_base::{
    config_section, AddDaemonServiceExt, CancellationToken, Config, Daemon, RunMainExt,
};
use serde::{Deserialize, Serialize};

// A typed configuration section, read from the `worker` key of the config file.
#[derive(Serialize, Deserialize)]
#[config_section("worker")]
struct WorkerConfig {
    interval_secs: u64,
}

// A background worker: a `Service` (built from the container, reading its
// config) that is also a `Daemon` (runs until shutdown).
struct Worker {
    interval: Duration,
}

impl Service for Worker {
    type Handle = Arc<Self>;

    async fn build(ctx: &AppContext) -> Result<Self::Handle, StdError> {
        let config: WorkerConfig = ctx
            .get_component_ref::<Config>()
            .ok_or("config component missing")?
            .get("worker")?;
        Ok(Arc::new(Self {
            interval: Duration::from_secs(config.interval_secs),
        }))
    }
}

impl Daemon for Worker {
    async fn run(&self, _app: &App, shutdown: CancellationToken) -> Result<(), StdError> {
        loop {
            tokio::select! {
                _ = shutdown.cancelled() => break,
                _ = tokio::time::sleep(self.interval) => tracing::info!("tick"),
            }
        }
        Ok(())
    }
}

#[tokio::main]
async fn main() -> ExitCode {
    let mut builder = App::builder();
    builder.add_daemon_service::<Worker>();
    // `run_main` parses CLI args, loads the config file, sets up tracing and
    // metrics, then runs the selected command (the built-in `server` command
    // runs every registered daemon until Ctrl-C).
    builder.run_main().await
}
```

## Features

- `macros` (default) - the `#[config_section(..)]` attribute macro.

## License

Licensed under either of MIT or Apache-2.0 at your option.