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}