procenv_macro/lib.rs
1//! # `procenv_macro`
2//!
3//! Procedural macro implementation for the `procenv` crate.
4//!
5//! This crate provides the `#[derive(EnvConfig)]` procedural macro that
6//! generates code for loading configuration from environment variables.
7//! It is a proc-macro crate and can only export procedural macros.
8//!
9//! **Note:** Users should depend on the `procenv` crate, not this one directly.
10//! The `procenv` crate re-exports this macro along with runtime types.
11//!
12//! # Module Structure
13//!
14//! - `parse` - Attribute parsing for `#[env(...)]` and `#[env_config(...)]`
15//! - `field` - Field type processing and code generation strategies
16//! - `expand` - Macro expansion orchestration and code generation
17//!
18//! # Generated Code
19//!
20//! The macro generates the following methods on the annotated struct:
21//!
22//! | Method | Description |
23//! |--------|-------------|
24//! | `from_env()` | Load from environment variables |
25//! | `from_env_with_sources()` | Load with source attribution |
26//! | `from_config()` | Load from files + env (requires `file` feature) |
27//! | `from_args()` | Load from CLI + env (requires CLI attributes) |
28//! | `env_example()` | Generate `.env.example` template |
29//!
30//! It also generates a custom `Debug` implementation that masks secret fields.
31
32#![deny(missing_docs)]
33#![warn(clippy::pedantic)]
34#![warn(clippy::nursery)]
35
36use proc_macro::TokenStream;
37use syn::{DeriveInput, parse_macro_input};
38
39// Internal modules - not exposed publicly
40mod expand;
41mod field;
42mod parse;
43
44/// Derive macro for loading configuration from environment variables.
45///
46/// This macro generates methods that read environment variables and construct
47/// the struct, with full error accumulation support. All configuration errors
48/// are collected and reported together, rather than failing on the first error.
49///
50/// # Field Attributes
51///
52/// Each field must have an `#[env(...)]` attribute:
53///
54/// | Attribute | Description |
55/// |-----------|-------------|
56/// | `var = "NAME"` | Environment variable name (required) |
57/// | `default = "value"` | Default value if env var is missing |
58/// | `optional` | Field is `Option<T>`, becomes `None` if missing |
59/// | `secret` | Masks value in Debug output and error messages |
60/// | `no_prefix` | Skip struct-level prefix for this field |
61/// | `flatten` | Embed a nested config struct |
62/// | `format = "json"` | Parse value as JSON/TOML/YAML |
63/// | `arg = "name"` | CLI argument name (enables `from_args()`) |
64/// | `short = 'n'` | CLI short flag (requires `arg`) |
65///
66/// # Struct Attributes
67///
68/// Optional `#[env_config(...)]` attribute on the struct:
69///
70/// | Attribute | Description |
71/// |-----------|-------------|
72/// | `prefix = "APP_"` | Prefix all env var names |
73/// | `dotenv` | Load `.env` file automatically |
74/// | `dotenv = ".env.local"` | Load specific dotenv file |
75/// | `file = "config.toml"` | Load required config file |
76/// | `file_optional = "..."` | Load optional config file |
77/// | `profile_env = "APP_ENV"` | Env var for profile selection |
78/// | `profiles = ["dev", "prod"]` | Valid profile names |
79///
80/// # Profile Attributes
81///
82/// Per-field profile defaults with `#[profile(...)]`:
83///
84/// ```ignore
85/// #[env(var = "DATABASE_URL")]
86/// #[profile(dev = "postgres://localhost/dev", prod = "postgres://prod/app")]
87/// database_url: String,
88/// ```
89///
90/// # Basic Example
91///
92/// ```ignore
93/// use procenv::EnvConfig;
94///
95/// #[derive(EnvConfig)]
96/// struct Config {
97/// #[env(var = "DATABASE_URL")]
98/// db_url: String,
99///
100/// #[env(var = "PORT", default = "8080")]
101/// port: u16,
102///
103/// #[env(var = "API_KEY", secret)]
104/// api_key: String,
105///
106/// #[env(var = "DEBUG_MODE", optional)]
107/// debug: Option<bool>,
108/// }
109///
110/// fn main() -> Result<(), procenv::Error> {
111/// let config = Config::from_env()?;
112/// println!("Server running on port {}", config.port);
113/// Ok(())
114/// }
115/// ```
116///
117/// # Advanced Example
118///
119/// ```ignore
120/// use procenv::EnvConfig;
121/// use serde::Deserialize;
122///
123/// #[derive(EnvConfig, Deserialize)]
124/// #[env_config(
125/// prefix = "APP_",
126/// dotenv,
127/// file_optional = "config.toml",
128/// profile_env = "APP_ENV",
129/// profiles = ["dev", "staging", "prod"]
130/// )]
131/// struct Config {
132/// #[env(var = "DATABASE_URL")]
133/// #[profile(dev = "postgres://localhost/dev")]
134/// db_url: String,
135///
136/// #[env(var = "PORT", default = "8080", arg = "port", short = 'p')]
137/// port: u16,
138///
139/// #[env(flatten)]
140/// database: DatabaseConfig,
141/// }
142///
143/// // Load with source attribution
144/// let (config, sources) = Config::from_env_with_sources()?;
145/// println!("{}", sources);
146///
147/// // Or load from files + env with layering
148/// let config = Config::from_config()?;
149/// ```
150///
151/// # Generated Methods
152///
153/// The macro generates:
154///
155/// - `from_env()` - Load from environment variables
156/// - `from_env_with_sources()` - Load with source attribution
157/// - `from_config()` - Load from files + env (when files configured)
158/// - `from_config_with_sources()` - Layered loading with sources
159/// - `from_args()` - Load from CLI + env (when `arg` attributes present)
160/// - `env_example()` - Generate `.env.example` template
161/// - Custom `Debug` impl with secret masking
162#[proc_macro_derive(EnvConfig, attributes(env, env_config, profile))]
163pub fn derive_env_config(input: TokenStream) -> TokenStream {
164 // Parse the input TokenStream into syn's DeriveInput AST
165 // This gives us structured access to the struct definition
166 let input = parse_macro_input!(input as DeriveInput);
167
168 // Delegate to the Expander which orchestrates code generation
169 // On error, convert to a compile_error!() invocation for better error messages
170 expand::Expander::expand(&input).unwrap_or_else(|err| err.to_compile_error().into())
171}