confroid-derive 0.0.4

Derive macro for confroid.
Documentation
//! Derive macros for [`confroid`](https://docs.rs/confroid).

mod config;
mod config_value;
mod options;

use proc_macro::TokenStream;
use syn::{DeriveInput, parse_macro_input};

/// Derive `confroid::FromEnv` for a named struct.
///
/// `Config` maps struct fields to environment variables. Field names are
/// converted to `SCREAMING_SNAKE_CASE`, and nested segments are joined with
/// `__`.
///
/// # Example
///
/// ```ignore
/// #[derive(confroid::Config)]
/// #[confroid(prefix = "GUPPY")]
/// struct Config {
///     #[confroid(default = 8080)]
///     port: u16,
///     database: Database,
/// }
///
/// #[derive(confroid::Config)]
/// struct Database {
///     #[confroid(name = "URL")]
///     connection_string: String,
/// }
///
/// // Reads `GUPPY__PORT` and `GUPPY__DATABASE__URL`.
/// let config: Config = confroid::from_env()?;
/// # Ok::<(), confroid::ConfroidError>(())
/// ```
///
/// # Container attributes
///
/// Container attributes are placed on the struct below `#[derive(Config)]`:
///
/// - **`prefix = "..."`** — prepend a segment to every environment variable
///   represented by the struct. For example, `#[confroid(prefix = "GUPPY")]`
///   makes a field named `port` read from `GUPPY__PORT`. Prefixes compose when
///   a prefixed struct is nested inside another config.
///
/// # Field attributes
///
/// Field attributes customize naming, fallback values, parsing, and generated
/// configuration documentation:
///
/// - **`name = "..."`** — override the field's environment-variable segment.
///   On a nested field, the override applies to the entire nested prefix.
/// - **`default`** — use [`Default::default()`] when the variable is absent.
/// - **`default = expression`** — use an explicit fallback when the variable is
///   absent. String literals and compatible expressions are converted with
///   [`Into::into`]. A present value that fails to parse still returns an error.
/// - **`example = expression`** — attach an example value for
///   `confroid::env_example` and `confroid::markdown_table` when the `docs`
///   feature is enabled. It does not affect parsing.
/// - **`auto_vec`** — parse a `Vec<T>` from one delimited variable rather than
///   indexed variables such as `NAMES__0` and `NAMES__1`.
/// - **`auto_vec_delimiter = "..."`** — set the delimiter used by `auto_vec`;
///   the default is `,`.
/// - **`humantime`** — parse a `std::time::Duration` or
///   `Option<std::time::Duration>` using values such as `30s` or `1h 15m`.
///   This requires confroid's `humantime` feature.
///
/// Attributes can be combined:
///
/// ```ignore
/// #[derive(confroid::Config)]
/// struct Config {
///     #[confroid(
///         name = "ALLOWED_HOSTS",
///         auto_vec,
///         auto_vec_delimiter = ";",
///         default
///     )]
///     hosts: Vec<String>,
/// }
/// ```
///
/// # Error reporting
///
/// All fields are evaluated before a failed read returns. Independent failures
/// are flattened into `confroid::ConfroidError::Multiple` in field declaration
/// order, allowing all missing and invalid variables to be fixed in one pass.
/// A single failure is returned as its leaf variant.
///
/// Parser failures are retained as boxed error sources rather than converted to
/// strings. Consequently, custom scalar parse errors must implement
/// `std::error::Error + Send + Sync + 'static`.
///
/// # Supported structs
///
/// The derive supports structs with named fields. Each field's type must
/// implement `confroid::FromEnv`. Optional values, nested configs, vectors, and
/// hash maps use their `FromEnv` implementations to determine presence and
/// deserialize their children.
#[proc_macro_derive(Config, attributes(confroid))]
pub fn derive_config(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let opts = match options::InputOpts::parse(&input) {
        Ok(opts) => opts,
        Err(error) => return error.into_compile_error().into(),
    };
    match config::expand(&opts) {
        Ok(tokens) => tokens.into(),
        Err(error) => error.into_compile_error().into(),
    }
}

/// Derive `confroid::FromEnv` for a scalar configuration value.
///
/// `ConfigValue` is the type-definition alternative to
/// `confroid::from_str_scalar!`. The type must implement
/// [`FromStr`](core::str::FromStr), and its parser error must implement
/// [`Error`](std::error::Error), `Send`, `Sync`, and `'static` so it can be
/// retained as the source of `confroid::ConfroidError::EnvVarInvalid`.
///
/// When confroid's `docs` feature is enabled, the type must also implement
/// [`Display`](std::fmt::Display). This explicit generated bound makes missing
/// display support point back to the scalar type rather than to a config field
/// that happens to use `default` or `example`.
///
/// # Example
///
/// ```ignore
/// use std::str::FromStr;
///
/// #[derive(Debug, confroid::ConfigValue)]
/// enum Mode {
///     Standalone,
///     Cluster,
/// }
///
/// impl FromStr for Mode {
///     type Err = std::io::Error;
///
///     fn from_str(value: &str) -> Result<Self, Self::Err> {
///         match value {
///             "standalone" => Ok(Self::Standalone),
///             "cluster" => Ok(Self::Cluster),
///             other => Err(std::io::Error::new(
///                 std::io::ErrorKind::InvalidInput,
///                 format!("unknown mode `{other}`"),
///             )),
///         }
///     }
/// }
/// ```
#[proc_macro_derive(ConfigValue)]
pub fn derive_config_value(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    config_value::expand(&input).into()
}