Skip to main content

confers_macros/
lib.rs

1//! Procedural macros for the confers configuration library.
2//!
3//! This crate provides derive macros for zero-boilerplate configuration
4//! management with security-first design.
5//!
6//! # Features
7//!
8//! - **Security-first**: Path traversal protection, sensitive data handling
9//! - **Multiple sources**: Environment variables, files, defaults
10//! - **Type-safe**: Compile-time validation and strong typing
11//! - **Flexible**: Support for nested configs, migrations, and CLI args
12//! - **High Performance**: Optimized type resolution and code generation
13//!
14//! # Quick Start
15//!
16//! ```rust,ignore
17//! use confers::Config;
18//! use serde::Deserialize;
19//!
20//! #[derive(Config, Deserialize, Debug)]
21//! #[config(env_prefix = "APP_")]
22//! struct AppConfig {
23//!     #[config(default = "localhost")]
24//!     host: String,
25//!
26//!     #[config(default = 8080)]
27//!     port: u16,
28//!
29//!     #[config(sensitive = true)]
30//!     api_key: Option<String>,
31//! }
32//!
33//! // Load configuration
34//! let config = AppConfig::load_sync().expect("config should load");
35//! println!("{:?}", config);
36//! ```
37//!
38//! # Security Features
39//!
40//! ## Sensitive Data Protection
41//!
42//! Sensitive fields are automatically protected:
43//!
44//! ```rust,ignore
45//! #[derive(Config, Deserialize)]
46//! struct Config {
47//!     #[config(sensitive = true)]
48//!     password: String,  // Automatically uses SecretString
49//!
50//!     #[config(encrypt = "xchacha20")]
51//!     secret_key: String,  // Encrypted at rest
52//! }
53//! ```
54//!
55//! ## Path Traversal Protection
56//!
57//! File paths for secrets are validated to prevent directory traversal attacks:
58//!
59//! ```text
60//! // These are blocked:
61//! // APP_KEY_FILE=../../../etc/passwd
62//! // APP_KEY_FILE=/etc/passwd
63//! // APP_KEY_FILE=%2e%2e/etc/passwd
64//! ```
65//!
66//! # Architecture
67//!
68//! The crate is organized into several modules:
69//!
70//! - `parse`: Attribute parsing and validation
71//! - `codegen`: Code generation for different features
72//!   - `security`: Security utilities (path validation, encryption)
73//!   - `defaults`: Default value generation
74//!   - `load`: Configuration loading methods
75//!   - `schema`: JSON Schema generation
76//!   - `clap`: CLI argument generation
77//!
78//! # Derive Macros
79//!
80//! ## `Config`
81//!
82//! Main derive macro for configuration loading.
83//!
84//! ## `ConfigSchema`
85//!
86//! Generate JSON Schema from configuration structs.
87//!
88//! ## `ConfigMigration`
89//!
90//! Support for configuration version migrations.
91//!
92//! ## `ConfigModules`
93//!
94//! Group configuration into logical modules.
95//!
96//! ## `ConfigClap`
97//!
98//! Generate CLI argument parsers using clap.
99
100#![forbid(unsafe_code)]
101
102use darling::FromDeriveInput;
103use proc_macro::TokenStream;
104use proc_macro2::TokenStream as TokenStream2;
105use quote::quote;
106use syn::{parse_macro_input, Data, DeriveInput, Ident, Type};
107
108mod codegen;
109mod parse;
110
111use codegen::{
112    generate_clap_impl, generate_defaults_impl, generate_load_impl, generate_migration_impl,
113    generate_modules_impl, generate_schema_impl, generate_validate_impl,
114};
115use darling::FromField;
116use parse::{FieldAttrs, StructAttrs};
117
118/// Derive macro for configuration loading.
119///
120/// # Example
121///
122/// ```ignore
123/// use confers::Config;
124/// use serde::Deserialize;
125///
126/// #[derive(Config, Deserialize)]
127/// #[config(env_prefix = "APP_")]
128/// struct AppConfig {
129///     #[config(default = "localhost")]
130///     host: String,
131///
132///     #[config(default = 8080)]
133///     port: u16,
134///
135///     #[config(sensitive = true)]
136///     api_key: Option<String>,
137/// }
138///
139/// // Load configuration
140/// let config = AppConfig::load().expect("config should load");
141/// ```
142///
143/// # Struct Attributes
144///
145/// - `env_prefix = "APP_"` - Prefix for environment variables
146/// - `app_name = "myapp"` - Application name for config search
147/// - `validate` - Enable validation with garde
148/// - `watch` - Enable file watching for hot reload
149/// - `version = 1` - Configuration version for migrations
150/// - `profile` - Enable APP_ENV profile overlay
151///
152/// # Field Attributes
153///
154/// - `default = <expr>` - Default value expression
155/// - `description = "..."` - Field description for docs
156/// - `name = "key"` - Override configuration key name
157/// - `name_env = "VAR"` - Override environment variable name
158/// - `sensitive = true` - Mark as sensitive (hidden in logs)
159/// - `encrypt = "xchacha20"` - Enable encryption for this field
160/// - `flatten` - Flatten nested struct into parent namespace
161/// - `skip` - Skip this field during loading
162/// - `interpolate = true` - Enable `${VAR:default}` interpolation
163/// - `dynamic` - Generate DynamicField handle
164/// - `module_group = "group"` - Assign field to a config module group
165#[proc_macro_derive(Config, attributes(config))]
166pub fn config_derive(input: TokenStream) -> TokenStream {
167    let input = parse_macro_input!(input as DeriveInput);
168
169    match impl_config_derive(&input) {
170        Ok(tokens) => tokens.into(),
171        Err(err) => err.to_compile_error().into(),
172    }
173}
174
175/// Derive macro for generating JSON Schema.
176#[proc_macro_derive(ConfigSchema, attributes(config))]
177pub fn config_schema_derive(input: TokenStream) -> TokenStream {
178    let input = parse_macro_input!(input as DeriveInput);
179
180    match impl_config_schema_derive(&input) {
181        Ok(tokens) => tokens.into(),
182        Err(err) => err.to_compile_error().into(),
183    }
184}
185
186/// Derive macro for generating migration support.
187#[proc_macro_derive(ConfigMigration, attributes(config))]
188pub fn config_migration_derive(input: TokenStream) -> TokenStream {
189    let input = parse_macro_input!(input as DeriveInput);
190
191    match impl_config_migration_derive(&input) {
192        Ok(tokens) => tokens.into(),
193        Err(err) => err.to_compile_error().into(),
194    }
195}
196
197/// Derive macro for generating module registry.
198#[proc_macro_derive(ConfigModules, attributes(config))]
199pub fn config_modules_derive(input: TokenStream) -> TokenStream {
200    let input = parse_macro_input!(input as DeriveInput);
201
202    match impl_config_modules_derive(&input) {
203        Ok(tokens) => tokens.into(),
204        Err(err) => err.to_compile_error().into(),
205    }
206}
207
208/// Derive macro for generating CLI arguments.
209#[proc_macro_derive(ConfigClap, attributes(config))]
210pub fn config_clap_derive(input: TokenStream) -> TokenStream {
211    let input = parse_macro_input!(input as DeriveInput);
212
213    match impl_config_clap_derive(&input) {
214        Ok(tokens) => tokens.into(),
215        Err(err) => err.to_compile_error().into(),
216    }
217}
218
219fn impl_config_derive(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
220    // Parse struct-level attributes
221    let struct_attrs = StructAttrs::from_derive_input(input)
222        .map_err(|e| syn::Error::new_spanned(input, e.to_string()))?;
223
224    // Validate struct attributes
225    struct_attrs
226        .validate(input)
227        .map_err(|e| syn::Error::new_spanned(input, e.to_string()))?;
228
229    // Get the struct identifier
230    let struct_ident = &input.ident;
231
232    // Get fields if it's a named struct
233    let fields = match &input.data {
234        Data::Struct(data) => &data.fields,
235        _ => {
236            return Err(syn::Error::new_spanned(
237                input,
238                "Config can only be derived for named structs",
239            ))
240        }
241    };
242
243    // Parse field attributes
244    let field_info: Vec<(&syn::Ident, &syn::Type, FieldAttrs)> = fields
245        .iter()
246        .filter_map(|field| {
247            let ident = field.ident.as_ref()?;
248            let attrs = FieldAttrs::from_field(field).ok()?;
249            Some((ident, &field.ty, attrs))
250        })
251        .collect();
252
253    // Validate field attributes
254    for (ident, _ty, attrs) in &field_info {
255        attrs
256            .validate(
257                fields
258                    .iter()
259                    .find(|f| f.ident.as_ref() == Some(ident))
260                    .expect("field must exist in parsed fields"),
261            )
262            .map_err(|e| syn::Error::new_spanned(ident, e.to_string()))?;
263    }
264
265    // Generate code
266    let defaults_impl = generate_defaults_impl(struct_ident, &field_info);
267    let load_impl = generate_load_impl(struct_ident, &struct_attrs, fields);
268    let validate_impl = generate_validate_impl(&struct_attrs, &field_info);
269    // Generate sensitive_paths() for ConfigProvider::keys() filtering
270    let sensitive_paths = generate_sensitive_paths(struct_ident, &field_info);
271    Ok(quote! {
272        #defaults_impl
273        #load_impl
274        #validate_impl
275        #sensitive_paths
276    })
277}
278
279/// Generate a `sensitive_paths()` method that returns paths of all
280/// fields marked `#[config(sensitive = true)]` or `#[config(encrypt = "...")]`.
281/// Used by ConfigProvider backends to filter `keys()` output.
282fn generate_sensitive_paths(
283    struct_ident: &Ident,
284    fields: &[(&Ident, &Type, FieldAttrs)],
285) -> TokenStream2 {
286    use syn::LitStr;
287
288    let sensitive_paths: Vec<LitStr> = fields
289        .iter()
290        .filter(|(_, _, attrs)| attrs.sensitive || attrs.encrypt.is_some())
291        .map(|(ident, _, attrs)| {
292            let name = attrs.name.clone().unwrap_or_else(|| ident.to_string());
293            LitStr::new(&name, ident.span())
294        })
295        .collect();
296
297    if sensitive_paths.is_empty() {
298        return TokenStream2::new();
299    }
300
301    quote! {
302        impl #struct_ident {
303            fn sensitive_paths() -> &'static [&'static str] {
304                &[#(#sensitive_paths),*]
305            }
306        }
307    }
308}
309
310fn impl_config_schema_derive(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
311    let struct_attrs = StructAttrs::from_derive_input(input)
312        .map_err(|e| syn::Error::new_spanned(input, e.to_string()))?;
313
314    let struct_ident = &input.ident;
315
316    let fields = match &input.data {
317        Data::Struct(data) => &data.fields,
318        _ => {
319            return Err(syn::Error::new_spanned(
320                input,
321                "ConfigSchema can only be derived for named structs",
322            ))
323        }
324    };
325
326    let schema_impl = generate_schema_impl(struct_ident, &struct_attrs, fields);
327
328    Ok(quote! {
329        #schema_impl
330    })
331}
332
333fn impl_config_migration_derive(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
334    let struct_attrs = StructAttrs::from_derive_input(input)
335        .map_err(|e| syn::Error::new_spanned(input, e.to_string()))?;
336
337    let struct_ident = &input.ident;
338
339    let fields = match &input.data {
340        Data::Struct(data) => &data.fields,
341        _ => {
342            return Err(syn::Error::new_spanned(
343                input,
344                "ConfigMigration can only be derived for named structs",
345            ))
346        }
347    };
348
349    let migration_impl = generate_migration_impl(struct_ident, &struct_attrs, fields);
350
351    Ok(quote! {
352        #migration_impl
353    })
354}
355
356fn impl_config_modules_derive(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
357    let struct_attrs = StructAttrs::from_derive_input(input)
358        .map_err(|e| syn::Error::new_spanned(input, e.to_string()))?;
359
360    let struct_ident = &input.ident;
361
362    let fields = match &input.data {
363        Data::Struct(data) => &data.fields,
364        _ => {
365            return Err(syn::Error::new_spanned(
366                input,
367                "ConfigModules can only be derived for named structs",
368            ))
369        }
370    };
371
372    let modules_impl = generate_modules_impl(struct_ident, &struct_attrs, fields);
373
374    Ok(quote! {
375        #modules_impl
376    })
377}
378
379fn impl_config_clap_derive(input: &DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
380    let struct_attrs = StructAttrs::from_derive_input(input)
381        .map_err(|e| syn::Error::new_spanned(input, e.to_string()))?;
382
383    let struct_ident = &input.ident;
384
385    let fields = match &input.data {
386        Data::Struct(data) => &data.fields,
387        _ => {
388            return Err(syn::Error::new_spanned(
389                input,
390                "ConfigClap can only be derived for named structs",
391            ))
392        }
393    };
394
395    let clap_impl = generate_clap_impl(struct_ident, &struct_attrs, fields);
396
397    Ok(quote! {
398        #clap_impl
399    })
400}