Skip to main content

confroid_derive/
lib.rs

1//! Derive macros for [`confroid`](https://docs.rs/confroid).
2
3mod config;
4mod config_value;
5mod options;
6
7use proc_macro::TokenStream;
8use syn::{DeriveInput, parse_macro_input};
9
10/// Derive `confroid::FromEnv` for a named struct.
11///
12/// `Config` maps struct fields to environment variables. Field names are
13/// converted to `SCREAMING_SNAKE_CASE`, and nested segments are joined with
14/// `__`.
15///
16/// # Example
17///
18/// ```ignore
19/// #[derive(confroid::Config)]
20/// #[confroid(prefix = "GUPPY")]
21/// struct Config {
22///     #[confroid(default = 8080)]
23///     port: u16,
24///     database: Database,
25/// }
26///
27/// #[derive(confroid::Config)]
28/// struct Database {
29///     #[confroid(name = "URL")]
30///     connection_string: String,
31/// }
32///
33/// // Reads `GUPPY__PORT` and `GUPPY__DATABASE__URL`.
34/// let config: Config = confroid::from_env()?;
35/// # Ok::<(), confroid::ConfroidError>(())
36/// ```
37///
38/// # Container attributes
39///
40/// Container attributes are placed on the struct below `#[derive(Config)]`:
41///
42/// - **`prefix = "..."`** — prepend a segment to every environment variable
43///   represented by the struct. For example, `#[confroid(prefix = "GUPPY")]`
44///   makes a field named `port` read from `GUPPY__PORT`. Prefixes compose when
45///   a prefixed struct is nested inside another config.
46///
47/// # Field attributes
48///
49/// Field attributes customize naming, fallback values, parsing, and generated
50/// configuration documentation:
51///
52/// - **`name = "..."`** — override the field's environment-variable segment.
53///   On a nested field, the override applies to the entire nested prefix.
54/// - **`default`** — use [`Default::default()`] when the variable is absent.
55/// - **`default = expression`** — use an explicit fallback when the variable is
56///   absent. String literals and compatible expressions are converted with
57///   [`Into::into`]. A present value that fails to parse still returns an error.
58/// - **`example = expression`** — attach an example value for
59///   `confroid::env_example` and `confroid::markdown_table` when the `docs`
60///   feature is enabled. It does not affect parsing.
61/// - **`auto_vec`** — parse a `Vec<T>` from one delimited variable rather than
62///   indexed variables such as `NAMES__0` and `NAMES__1`.
63/// - **`auto_vec_delimiter = "..."`** — set the delimiter used by `auto_vec`;
64///   the default is `,`.
65/// - **`humantime`** — parse a `std::time::Duration` or
66///   `Option<std::time::Duration>` using values such as `30s` or `1h 15m`.
67///   This requires confroid's `humantime` feature.
68///
69/// Attributes can be combined:
70///
71/// ```ignore
72/// #[derive(confroid::Config)]
73/// struct Config {
74///     #[confroid(
75///         name = "ALLOWED_HOSTS",
76///         auto_vec,
77///         auto_vec_delimiter = ";",
78///         default
79///     )]
80///     hosts: Vec<String>,
81/// }
82/// ```
83///
84/// # Error reporting
85///
86/// All fields are evaluated before a failed read returns. Independent failures
87/// are flattened into `confroid::ConfroidError::Multiple` in field declaration
88/// order, allowing all missing and invalid variables to be fixed in one pass.
89/// A single failure is returned as its leaf variant.
90///
91/// Parser failures are retained as boxed error sources rather than converted to
92/// strings. Consequently, custom scalar parse errors must implement
93/// `std::error::Error + Send + Sync + 'static`.
94///
95/// # Supported structs
96///
97/// The derive supports structs with named fields. Each field's type must
98/// implement `confroid::FromEnv`. Optional values, nested configs, vectors, and
99/// hash maps use their `FromEnv` implementations to determine presence and
100/// deserialize their children.
101#[proc_macro_derive(Config, attributes(confroid))]
102pub fn derive_config(input: TokenStream) -> TokenStream {
103    let input = parse_macro_input!(input as DeriveInput);
104    let opts = match options::InputOpts::parse(&input) {
105        Ok(opts) => opts,
106        Err(error) => return error.into_compile_error().into(),
107    };
108    match config::expand(&opts) {
109        Ok(tokens) => tokens.into(),
110        Err(error) => error.into_compile_error().into(),
111    }
112}
113
114/// Derive `confroid::FromEnv` for a scalar configuration value.
115///
116/// `ConfigValue` is the type-definition alternative to
117/// `confroid::from_str_scalar!`. The type must implement
118/// [`FromStr`](core::str::FromStr), and its parser error must implement
119/// [`Error`](std::error::Error), `Send`, `Sync`, and `'static` so it can be
120/// retained as the source of `confroid::ConfroidError::EnvVarInvalid`.
121///
122/// When confroid's `docs` feature is enabled, the type must also implement
123/// [`Display`](std::fmt::Display). This explicit generated bound makes missing
124/// display support point back to the scalar type rather than to a config field
125/// that happens to use `default` or `example`.
126///
127/// # Example
128///
129/// ```ignore
130/// use std::str::FromStr;
131///
132/// #[derive(Debug, confroid::ConfigValue)]
133/// enum Mode {
134///     Standalone,
135///     Cluster,
136/// }
137///
138/// impl FromStr for Mode {
139///     type Err = std::io::Error;
140///
141///     fn from_str(value: &str) -> Result<Self, Self::Err> {
142///         match value {
143///             "standalone" => Ok(Self::Standalone),
144///             "cluster" => Ok(Self::Cluster),
145///             other => Err(std::io::Error::new(
146///                 std::io::ErrorKind::InvalidInput,
147///                 format!("unknown mode `{other}`"),
148///             )),
149///         }
150///     }
151/// }
152/// ```
153#[proc_macro_derive(ConfigValue)]
154pub fn derive_config_value(input: TokenStream) -> TokenStream {
155    let input = parse_macro_input!(input as DeriveInput);
156    config_value::expand(&input).into()
157}