secretspec_derive/
lib.rs

1//! # SecretSpec Derive Macros
2//!
3//! This crate provides procedural macros for the SecretSpec library, enabling compile-time
4//! generation of strongly-typed secret structs from `secretspec.toml` configuration files.
5//!
6//! ## Overview
7//!
8//! The macro system reads your `secretspec.toml` at compile time and generates:
9//! - A `SecretSpec` struct with all secrets as fields (union of all profiles)
10//! - A `SecretSpecProfile` enum with profile-specific structs
11//! - A `Profile` enum representing available profiles
12//! - Type-safe loading methods with automatic validation
13//!
14//! ## Key Features
15//!
16//! - **Compile-time validation**: Invalid configurations are caught during compilation
17//! - **Type safety**: Secrets are accessed as struct fields, not strings
18//! - **Profile awareness**: Different types for different profiles (e.g., production vs development)
19//! - **Builder pattern**: Flexible configuration with method chaining
20//! - **Environment integration**: Automatic environment variable handling
21
22use proc_macro::TokenStream;
23use quote::{format_ident, quote};
24use secretspec::{Config, Secret};
25use std::collections::{BTreeMap, HashSet};
26use syn::{LitStr, parse_macro_input};
27
28/// Holds metadata about a field in the generated struct.
29///
30/// This struct contains all the information needed to generate:
31/// - Struct field declarations
32/// - Field assignments from secret maps
33/// - Environment variable setters
34///
35/// # Fields
36///
37/// * `name` - The original secret name (e.g., "DATABASE_URL")
38/// * `field_type` - The Rust type for this field (String or Option<String>)
39/// * `is_optional` - Whether this field is optional across all profiles
40#[derive(Clone)]
41struct FieldInfo {
42    name: String,
43    field_type: proc_macro2::TokenStream,
44    is_optional: bool,
45}
46
47impl FieldInfo {
48    /// Creates a new FieldInfo instance.
49    ///
50    /// # Arguments
51    ///
52    /// * `name` - The secret name as defined in the config
53    /// * `field_type` - The generated Rust type (String or Option<String>)
54    /// * `is_optional` - Whether the field should be optional
55    fn new(name: String, field_type: proc_macro2::TokenStream, is_optional: bool) -> Self {
56        Self {
57            name,
58            field_type,
59            is_optional,
60        }
61    }
62
63    /// Get the field name as a Rust identifier.
64    ///
65    /// Converts the secret name to a valid Rust field name by:
66    /// - Converting to lowercase
67    /// - Preserving underscores
68    ///
69    /// # Example
70    ///
71    /// - "DATABASE_URL" becomes `database_url`
72    /// - "API_KEY" becomes `api_key`
73    fn field_name(&self) -> proc_macro2::Ident {
74        field_name_ident(&self.name)
75    }
76
77    /// Generate the struct field declaration.
78    ///
79    /// Creates a public field declaration for use in the generated struct.
80    ///
81    /// # Returns
82    ///
83    /// A token stream representing `pub field_name: FieldType`
84    ///
85    /// # Example Output
86    ///
87    /// ```ignore
88    /// pub database_url: String
89    /// pub api_key: Option<String>
90    /// ```
91    fn generate_struct_field(&self) -> proc_macro2::TokenStream {
92        let field_name = self.field_name();
93        let field_type = &self.field_type;
94        quote! { pub #field_name: #field_type }
95    }
96
97    /// Generate a field assignment from a secrets map.
98    ///
99    /// Creates code to assign a value from a HashMap<String, String> to this field.
100    /// Handles both required and optional fields appropriately.
101    ///
102    /// # Arguments
103    ///
104    /// * `source` - The token stream representing the source map (e.g., `secrets`)
105    ///
106    /// # Returns
107    ///
108    /// Token stream for the field assignment, with proper error handling for required fields
109    fn generate_assignment(&self, source: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
110        generate_secret_assignment(&self.field_name(), &self.name, source, self.is_optional)
111    }
112
113    /// Generate environment variable setter.
114    ///
115    /// Creates code to set an environment variable from this field's value.
116    /// For optional fields, only sets the variable if a value is present.
117    ///
118    /// # Safety
119    ///
120    /// The generated code uses `unsafe` because `std::env::set_var` is unsafe
121    /// in multi-threaded contexts. Users should ensure thread safety when calling
122    /// the generated `set_as_env_vars` method.
123    ///
124    /// # Returns
125    ///
126    /// Token stream that sets the environment variable when executed
127    fn generate_env_setter(&self) -> proc_macro2::TokenStream {
128        let field_name = self.field_name();
129        let env_name = &self.name;
130
131        if self.is_optional {
132            quote! {
133                if let Some(ref value) = self.#field_name {
134                    unsafe {
135                        std::env::set_var(#env_name, value);
136                    }
137                }
138            }
139        } else {
140            quote! {
141                unsafe {
142                    std::env::set_var(#env_name, &self.#field_name);
143                }
144            }
145        }
146    }
147}
148
149/// Profile variant information for enum generation.
150///
151/// Represents a profile that will become an enum variant in the generated code.
152/// Handles the conversion from profile names to valid Rust enum variants.
153///
154/// # Fields
155///
156/// * `name` - The original profile name (e.g., "production", "development")
157/// * `capitalized` - The capitalized variant name (e.g., "Production", "Development")
158struct ProfileVariant {
159    name: String,
160    capitalized: String,
161}
162
163impl ProfileVariant {
164    /// Creates a new ProfileVariant with automatic capitalization.
165    ///
166    /// # Arguments
167    ///
168    /// * `name` - The profile name from the configuration
169    ///
170    /// # Example
171    ///
172    /// ```ignore
173    /// let variant = ProfileVariant::new("production".to_string());
174    /// // variant.name == "production"
175    /// // variant.capitalized == "Production"
176    /// ```
177    fn new(name: String) -> Self {
178        let capitalized = capitalize_first(&name);
179        Self { name, capitalized }
180    }
181
182    /// Convert the variant to a Rust identifier.
183    ///
184    /// # Returns
185    ///
186    /// A proc_macro2::Ident suitable for use as an enum variant
187    fn as_ident(&self) -> proc_macro2::Ident {
188        format_ident!("{}", self.capitalized)
189    }
190}
191
192/// Generates typed SecretSpec structs from your secretspec.toml file.
193///
194/// # Example
195/// ```ignore
196/// // In your main.rs or lib.rs:
197/// secretspec_derive::declare_secrets!("secretspec.toml");
198///
199/// use secretspec::Provider;
200///
201/// fn main() -> Result<(), Box<dyn std::error::Error>> {
202///     // Load with union types (safe for any profile) using the builder pattern
203///     let secrets = SecretSpec::builder()
204///         .with_provider(Provider::Keyring)
205///         .load()?;
206///     println!("Database URL: {}", secrets.secrets.database_url);
207///
208///     // Load with profile-specific types
209///     let profile_secrets = SecretSpec::builder()
210///         .with_provider(Provider::Keyring)
211///         .with_profile(Profile::Production)
212///         .load_profile()?;
213///     
214///     match profile_secrets.secrets {
215///         SecretSpecProfile::Production { api_key, database_url, .. } => {
216///             println!("Production API key: {}", api_key);
217///         }
218///         _ => unreachable!(),
219///     }
220///
221///     Ok(())
222/// }
223/// ```
224#[proc_macro]
225pub fn declare_secrets(input: TokenStream) -> TokenStream {
226    let path = parse_macro_input!(input as LitStr).value();
227
228    // Get the manifest directory of the crate using the macro
229    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
230    let full_path = std::path::Path::new(&manifest_dir).join(&path);
231
232    let config: Config = match Config::try_from(full_path.as_path()) {
233        Ok(config) => config,
234        Err(e) => {
235            let error = format!("Failed to parse TOML: {}", e);
236            return quote! { compile_error!(#error); }.into();
237        }
238    };
239
240    // Validate the configuration at compile time
241    if let Err(validation_errors) = validate_config_for_codegen(&config) {
242        let error_message = format!(
243            "Invalid secretspec configuration:\n{}",
244            validation_errors.join("\n")
245        );
246        return quote! { compile_error!(#error_message); }.into();
247    }
248
249    // Generate all the code
250    let output = generate_secret_spec_code(config);
251    output.into()
252}
253
254// ===== Core Helper Functions =====
255
256/// Validate configuration for code generation concerns only.
257///
258/// This performs compile-time validation to ensure the configuration can be
259/// converted into valid Rust code. This is different from runtime validation -
260/// we only check things that would prevent generating valid Rust code.
261///
262/// # Validation Checks
263///
264/// - Secret names must produce valid Rust identifiers
265/// - Secret names must not be Rust keywords
266/// - Profile names must produce valid enum variants
267/// - No duplicate field names within a profile (case-insensitive)
268///
269/// # Arguments
270///
271/// * `config` - The parsed project configuration
272///
273/// # Returns
274///
275/// - `Ok(())` if validation passes
276/// - `Err(Vec<String>)` containing all validation errors if any are found
277fn validate_config_for_codegen(config: &Config) -> Result<(), Vec<String>> {
278    let mut errors = Vec::new();
279
280    // Validate secret names produce valid Rust identifiers
281    validate_rust_identifiers(config, &mut errors);
282
283    // Validate profile names produce valid Rust enum variants
284    validate_profile_identifiers(config, &mut errors);
285
286    if errors.is_empty() {
287        Ok(())
288    } else {
289        Err(errors)
290    }
291}
292
293/// Validate all secret names produce valid Rust identifiers.
294///
295/// Checks that each secret name, when converted to a field name:
296/// - Forms a valid Rust identifier (alphanumeric + underscores)
297/// - Doesn't conflict with Rust keywords
298/// - Doesn't create duplicate field names within a profile
299///
300/// # Arguments
301///
302/// * `config` - The project configuration to validate
303/// * `errors` - Mutable vector to collect error messages
304///
305/// # Error Cases
306///
307/// - Secret names with invalid characters (e.g., "my-secret" with hyphen)
308/// - Secret names that are Rust keywords (e.g., "TYPE", "IMPL")
309/// - Multiple secrets producing the same field name (e.g., "API_KEY" and "api_key")
310fn validate_rust_identifiers(config: &Config, errors: &mut Vec<String>) {
311    let rust_keywords = [
312        "as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum",
313        "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move",
314        "mut", "pub", "ref", "return", "self", "Self", "static", "struct", "super", "trait",
315        "true", "type", "unsafe", "use", "where", "while", "abstract", "become", "box", "do",
316        "final", "macro", "override", "priv", "typeof", "unsized", "virtual", "yield", "try",
317    ];
318
319    for (profile_name, profile_config) in &config.profiles {
320        let mut profile_field_names = HashSet::new();
321
322        for secret_name in profile_config.secrets.keys() {
323            let field_name = secret_name.to_lowercase();
324
325            // Check if it produces a valid Rust identifier
326            if !is_valid_rust_identifier(&field_name) {
327                errors.push(format!(
328                    "Secret '{}' in profile '{}' produces invalid Rust field name '{}'",
329                    secret_name, profile_name, field_name
330                ));
331            }
332
333            // Check for Rust keywords
334            if rust_keywords.contains(&field_name.as_str()) {
335                errors.push(format!(
336                    "Secret '{}' in profile '{}' produces Rust keyword '{}' as field name",
337                    secret_name, profile_name, field_name
338                ));
339            }
340
341            // Check for duplicate field names within the same profile
342            if !profile_field_names.insert(field_name.clone()) {
343                errors.push(format!(
344                    "Profile '{}' has multiple secrets that produce the same field name '{}' (names are case-insensitive)",
345                    profile_name, field_name
346                ));
347            }
348        }
349    }
350}
351
352/// Check if a string is a valid Rust identifier.
353///
354/// A valid Rust identifier must:
355/// - Start with a letter or underscore
356/// - Contain only letters, numbers, and underscores
357/// - Not be empty
358///
359/// # Arguments
360///
361/// * `s` - The string to validate
362///
363/// # Returns
364///
365/// `true` if the string is a valid Rust identifier, `false` otherwise
366///
367/// # Examples
368///
369/// ```ignore
370/// assert!(is_valid_rust_identifier("my_var"));
371/// assert!(is_valid_rust_identifier("_private"));
372/// assert!(!is_valid_rust_identifier("123start"));
373/// assert!(!is_valid_rust_identifier("my-var"));
374/// ```
375fn is_valid_rust_identifier(s: &str) -> bool {
376    if s.is_empty() {
377        return false;
378    }
379
380    let mut chars = s.chars();
381    if let Some(first) = chars.next() {
382        // First character must be alphabetic or underscore
383        if !first.is_alphabetic() && first != '_' {
384            return false;
385        }
386        // Remaining characters must be alphanumeric or underscore
387        chars.all(|c| c.is_alphanumeric() || c == '_')
388    } else {
389        false
390    }
391}
392
393/// Validate profile names produce valid Rust enum variants.
394///
395/// Ensures that each profile name, when capitalized, forms a valid Rust enum variant.
396///
397/// # Arguments
398///
399/// * `config` - The project configuration to validate
400/// * `errors` - Mutable vector to collect error messages
401///
402/// # Error Cases
403///
404/// - Profile names that start with numbers (e.g., "1production")
405/// - Profile names with invalid characters (e.g., "prod-env")
406fn validate_profile_identifiers(config: &Config, errors: &mut Vec<String>) {
407    for profile_name in config.profiles.keys() {
408        let variant_name = capitalize_first(profile_name);
409        if !is_valid_rust_identifier(&variant_name) {
410            errors.push(format!(
411                "Profile '{}' produces invalid Rust enum variant '{}'",
412                profile_name, variant_name
413            ));
414        }
415    }
416}
417
418/// Convert a secret name to a field identifier.
419///
420/// Converts environment variable style names to Rust field names by:
421/// - Converting to lowercase
422/// - Preserving underscores
423///
424/// # Arguments
425///
426/// * `name` - The secret name (typically uppercase with underscores)
427///
428/// # Returns
429///
430/// A proc_macro2::Ident suitable for use as a struct field
431///
432/// # Example
433///
434/// ```ignore
435/// let ident = field_name_ident("DATABASE_URL");
436/// // Generates: database_url
437/// ```
438fn field_name_ident(name: &str) -> proc_macro2::Ident {
439    format_ident!("{}", name.to_lowercase())
440}
441
442/// Helper function to check if a secret is optional.
443///
444/// A secret is considered optional only if:
445/// - It has `required = false` in the config
446///
447/// Having a default value does not make a secret optional.
448///
449/// # Arguments
450///
451/// * `secret_config` - The secret's configuration
452///
453/// # Returns
454///
455/// `true` if the secret is optional, `false` if required
456fn is_secret_optional(secret_config: &Secret) -> bool {
457    !secret_config.required
458}
459
460/// Determines if a field should be optional across all profiles.
461///
462/// For the union struct (SecretSpec), a field is optional if it's optional
463/// in ANY profile or missing from ANY profile. This ensures the union type
464/// can safely represent secrets from any profile.
465///
466/// # Arguments
467///
468/// * `secret_name` - The name of the secret to check
469/// * `config` - The project configuration
470///
471/// # Returns
472///
473/// `true` if the field should be Option<String> in the union struct
474///
475/// # Logic
476///
477/// - If the secret is missing from any profile → optional
478/// - If the secret is optional in any profile → optional
479/// - Only if required in ALL profiles → not optional
480fn is_field_optional_across_profiles(secret_name: &str, config: &Config) -> bool {
481    // Check each profile
482    for profile_config in config.profiles.values() {
483        if let Some(secret_config) = profile_config.secrets.get(secret_name) {
484            if is_secret_optional(secret_config) {
485                return true;
486            }
487        } else {
488            // Secret doesn't exist in this profile, so it's optional
489            return true;
490        }
491    }
492    false
493}
494
495/// Generate a unified secret assignment from a HashMap.
496///
497/// Creates the code to assign a value from a secrets map to a struct field,
498/// with appropriate error handling based on whether the field is optional.
499///
500/// # Arguments
501///
502/// * `field_name` - The struct field identifier
503/// * `secret_name` - The key to look up in the map
504/// * `source` - Token stream representing the source map
505/// * `is_optional` - Whether to generate Option<String> or String assignment
506///
507/// # Generated Code
508///
509/// For required fields:
510/// ```ignore
511/// field_name: source.get("SECRET_NAME")
512///     .ok_or_else(|| SecretSpecError::RequiredSecretMissing("SECRET_NAME".to_string()))?
513///     .clone()
514/// ```
515///
516/// For optional fields:
517/// ```ignore
518/// field_name: source.get("SECRET_NAME").cloned()
519/// ```
520fn generate_secret_assignment(
521    field_name: &proc_macro2::Ident,
522    secret_name: &str,
523    source: proc_macro2::TokenStream,
524    is_optional: bool,
525) -> proc_macro2::TokenStream {
526    if is_optional {
527        quote! {
528            #field_name: #source.get(#secret_name).map(|s| s.expose_secret().to_string())
529        }
530    } else {
531        quote! {
532            #field_name: #source.get(#secret_name)
533                .ok_or_else(|| secretspec::SecretSpecError::RequiredSecretMissing(#secret_name.to_string()))?
534                .expose_secret()
535                .to_string()
536        }
537    }
538}
539
540/// Analyzes all profiles to determine field types for the union struct.
541///
542/// This function examines all secrets across all profiles to determine:
543/// - Which secrets exist across profiles
544/// - Whether each secret should be optional in the union type
545/// - The appropriate Rust type for each field
546///
547/// # Arguments
548///
549/// * `config` - The project configuration
550///
551/// # Returns
552///
553/// A BTreeMap (for consistent ordering) mapping secret names to their FieldInfo
554///
555/// # Algorithm
556///
557/// 1. Collect all unique secret names from all profiles
558/// 2. For each secret, determine if it's optional across profiles
559/// 3. Generate appropriate type (String or Option<String>)
560/// 4. Create FieldInfo with all metadata needed for code generation
561fn analyze_field_types(config: &Config) -> BTreeMap<String, FieldInfo> {
562    let mut field_info = BTreeMap::new();
563
564    // Collect all unique secrets across all profiles
565    for profile_config in config.profiles.values() {
566        for secret_name in profile_config.secrets.keys() {
567            field_info.entry(secret_name.clone()).or_insert_with(|| {
568                let is_optional = is_field_optional_across_profiles(secret_name, config);
569                let field_type = if is_optional {
570                    quote! { Option<String> }
571                } else {
572                    quote! { String }
573                };
574                FieldInfo::new(secret_name.clone(), field_type, is_optional)
575            });
576        }
577    }
578
579    field_info
580}
581
582/// Get normalized profile variants for enum generation.
583///
584/// Converts profile names into ProfileVariant structs, handling the special
585/// case of empty profiles (generates a "Default" variant).
586///
587/// # Arguments
588///
589/// * `profiles` - Set of profile names from the configuration
590///
591/// # Returns
592///
593/// A sorted vector of ProfileVariant structs
594///
595/// # Special Cases
596///
597/// - Empty profiles → returns vec![ProfileVariant("default", "Default")]
598/// - Otherwise → sorted list of profile variants
599fn get_profile_variants(profiles: &HashSet<String>) -> Vec<ProfileVariant> {
600    if profiles.is_empty() {
601        vec![ProfileVariant::new("default".to_string())]
602    } else {
603        let mut variants: Vec<_> = profiles
604            .iter()
605            .map(|name| ProfileVariant::new(name.clone()))
606            .collect();
607        variants.sort_by(|a, b| a.name.cmp(&b.name));
608        variants
609    }
610}
611
612// ===== Profile Generation Module =====
613
614/// Module for generating Profile enum and related implementations.
615///
616/// This module handles:
617/// - Profile enum definition
618/// - TryFrom implementations for string conversion
619/// - as_str() method for profile serialization
620mod profile_generation {
621    use super::*;
622
623    /// Generate just the Profile enum.
624    ///
625    /// Creates an enum with variants for each profile in the configuration.
626    ///
627    /// # Arguments
628    ///
629    /// * `variants` - List of profile variants to generate
630    ///
631    /// # Generated Code Example
632    ///
633    /// ```ignore
634    /// #[derive(Debug, Clone, Copy)]
635    /// pub enum Profile {
636    ///     Development,
637    ///     Production,
638    ///     Staging,
639    /// }
640    /// ```
641    pub fn generate_enum(variants: &[ProfileVariant]) -> proc_macro2::TokenStream {
642        let enum_variants = variants.iter().map(|v| {
643            let ident = v.as_ident();
644            quote! { #ident }
645        });
646
647        quote! {
648            #[derive(Debug, Clone, Copy)]
649            pub enum Profile {
650                #(#enum_variants,)*
651            }
652        }
653    }
654
655    /// Generate TryFrom implementations for Profile.
656    ///
657    /// Creates implementations to convert strings to Profile enum variants,
658    /// supporting both &str and String inputs.
659    ///
660    /// # Arguments
661    ///
662    /// * `variants` - List of profile variants
663    ///
664    /// # Generated Code
665    ///
666    /// - `TryFrom<&str>` implementation with match arms for each profile
667    /// - `TryFrom<String>` implementation that delegates to &str
668    /// - Returns `SecretSpecError::InvalidProfile` for unknown profiles
669    pub fn generate_try_from_impls(variants: &[ProfileVariant]) -> proc_macro2::TokenStream {
670        let from_str_arms = variants.iter().map(|v| {
671            let ident = v.as_ident();
672            let str_val = &v.name;
673            quote! { #str_val => Ok(Profile::#ident) }
674        });
675
676        quote! {
677            impl std::convert::TryFrom<&str> for Profile {
678                type Error = secretspec::SecretSpecError;
679
680                fn try_from(value: &str) -> Result<Self, Self::Error> {
681                    match value {
682                        #(#from_str_arms,)*
683                        _ => Err(secretspec::SecretSpecError::InvalidProfile(value.to_string())),
684                    }
685                }
686            }
687
688            impl std::convert::TryFrom<String> for Profile {
689                type Error = secretspec::SecretSpecError;
690
691                fn try_from(value: String) -> Result<Self, Self::Error> {
692                    Profile::try_from(value.as_str())
693                }
694            }
695        }
696    }
697
698    /// Generate as_str implementation for Profile.
699    ///
700    /// Creates a method to convert Profile enum variants back to their string representation.
701    ///
702    /// # Arguments
703    ///
704    /// * `variants` - List of profile variants
705    ///
706    /// # Generated Code Example
707    ///
708    /// ```ignore
709    /// impl Profile {
710    ///     fn as_str(&self) -> &'static str {
711    ///         match self {
712    ///             Profile::Development => "development",
713    ///             Profile::Production => "production",
714    ///         }
715    ///     }
716    /// }
717    /// ```
718    pub fn generate_as_str_impl(variants: &[ProfileVariant]) -> proc_macro2::TokenStream {
719        let to_str_arms = variants.iter().map(|v| {
720            let ident = v.as_ident();
721            let str_val = &v.name;
722            quote! { Profile::#ident => #str_val }
723        });
724
725        quote! {
726            impl Profile {
727                fn as_str(&self) -> &'static str {
728                    match self {
729                        #(#to_str_arms,)*
730                    }
731                }
732            }
733        }
734    }
735
736    /// Generate all profile-related code.
737    ///
738    /// Combines all profile generation functions into a single token stream.
739    ///
740    /// # Arguments
741    ///
742    /// * `variants` - List of profile variants
743    ///
744    /// # Returns
745    ///
746    /// Complete token stream containing:
747    /// - Profile enum definition
748    /// - TryFrom implementations
749    /// - as_str() method
750    pub fn generate_all(variants: &[ProfileVariant]) -> proc_macro2::TokenStream {
751        let enum_def = generate_enum(variants);
752        let try_from_impls = generate_try_from_impls(variants);
753        let as_str_impl = generate_as_str_impl(variants);
754
755        quote! {
756            #enum_def
757            #try_from_impls
758            #as_str_impl
759        }
760    }
761}
762
763// ===== SecretSpec Generation Module =====
764
765/// Module for generating SecretSpec struct and related implementations.
766///
767/// This module handles:
768/// - SecretSpec struct (union of all secrets)
769/// - SecretSpecProfile enum (profile-specific types)
770/// - Loading implementations
771/// - Environment variable integration
772mod secret_spec_generation {
773    use super::*;
774
775    /// Generate the SecretSpec struct.
776    ///
777    /// Creates a struct containing all secrets from all profiles as fields.
778    /// This is the "union" type that can safely hold secrets from any profile.
779    ///
780    /// # Arguments
781    ///
782    /// * `field_info` - Map of all fields with their type information
783    ///
784    /// # Generated Code Example
785    ///
786    /// ```ignore
787    /// #[derive(Debug, serde::Serialize, serde::Deserialize)]
788    /// pub struct SecretSpec {
789    ///     pub database_url: String,
790    ///     pub api_key: Option<String>,
791    ///     pub redis_url: Option<String>,
792    /// }
793    /// ```
794    pub fn generate_struct(field_info: &BTreeMap<String, FieldInfo>) -> proc_macro2::TokenStream {
795        let fields = field_info.values().map(|info| info.generate_struct_field());
796
797        quote! {
798            #[derive(Debug, serde::Serialize, serde::Deserialize)]
799            pub struct SecretSpec {
800                #(#fields,)*
801            }
802        }
803    }
804
805    /// Generate the SecretSpecProfile enum.
806    ///
807    /// Creates an enum where each variant contains only the secrets defined
808    /// for that specific profile. This provides stronger type safety when
809    /// working with profile-specific secrets.
810    ///
811    /// # Arguments
812    ///
813    /// * `profile_variants` - Generated enum variant definitions
814    ///
815    /// # Generated Code Example
816    ///
817    /// ```ignore
818    /// #[derive(Debug, serde::Serialize, serde::Deserialize)]
819    /// pub enum SecretSpecProfile {
820    ///     Development {
821    ///         database_url: String,
822    ///         redis_url: Option<String>,
823    ///     },
824    ///     Production {
825    ///         database_url: String,
826    ///         api_key: String,
827    ///         redis_url: String,
828    ///     },
829    /// }
830    /// ```
831    pub fn generate_profile_enum(
832        profile_variants: &[proc_macro2::TokenStream],
833    ) -> proc_macro2::TokenStream {
834        quote! {
835            #[derive(Debug, serde::Serialize, serde::Deserialize)]
836            pub enum SecretSpecProfile {
837                #(#profile_variants,)*
838            }
839        }
840    }
841
842    /// Generate SecretSpecProfile enum variants.
843    ///
844    /// Creates the individual variants for the SecretSpecProfile enum,
845    /// each containing only the fields defined for that profile.
846    ///
847    /// # Arguments
848    ///
849    /// * `config` - The project configuration
850    /// * `field_info` - Field information (used for empty profile case)
851    /// * `variants` - Profile variants to generate
852    ///
853    /// # Returns
854    ///
855    /// Vector of token streams, each representing one enum variant
856    ///
857    /// # Special Cases
858    ///
859    /// - Empty profiles → generates a Default variant with all fields
860    /// - Each profile → generates variant with profile-specific fields
861    pub fn generate_profile_enum_variants(
862        config: &Config,
863        field_info: &BTreeMap<String, FieldInfo>,
864        variants: &[ProfileVariant],
865    ) -> Vec<proc_macro2::TokenStream> {
866        if config.profiles.is_empty() {
867            // If no profiles, create a Default variant with all fields
868            let fields = field_info.values().map(|info| info.generate_struct_field());
869            vec![quote! {
870                Default {
871                    #(#fields,)*
872                }
873            }]
874        } else {
875            variants
876                .iter()
877                .filter_map(|variant| {
878                    config.profiles.get(&variant.name).map(|profile_config| {
879                        let variant_ident = variant.as_ident();
880                        let fields =
881                            profile_config
882                                .secrets
883                                .iter()
884                                .map(|(secret_name, secret_config)| {
885                                    let field_name = field_name_ident(secret_name);
886                                    let field_type = if is_secret_optional(secret_config) {
887                                        quote! { Option<String> }
888                                    } else {
889                                        quote! { String }
890                                    };
891                                    quote! { #field_name: #field_type }
892                                });
893
894                        quote! {
895                            #variant_ident {
896                                #(#fields,)*
897                            }
898                        }
899                    })
900                })
901                .collect()
902        }
903    }
904
905    /// Generate load_profile match arms.
906    ///
907    /// Creates the match arms for loading profile-specific secrets into
908    /// the appropriate SecretSpecProfile variant.
909    ///
910    /// # Arguments
911    ///
912    /// * `config` - The project configuration
913    /// * `field_info` - Field information (for empty profile case)
914    /// * `variants` - Profile variants to generate arms for
915    ///
916    /// # Returns
917    ///
918    /// Vector of match arms for the profile loading logic
919    ///
920    /// # Generated Code Example
921    ///
922    /// ```ignore
923    /// Profile::Production => Ok(SecretSpecProfile::Production {
924    ///     database_url: secrets.get("DATABASE_URL")
925    ///         .ok_or_else(|| SecretSpecError::RequiredSecretMissing("DATABASE_URL".to_string()))?
926    ///         .clone(),
927    ///     api_key: secrets.get("API_KEY").cloned(),
928    /// })
929    /// ```
930    pub fn generate_load_profile_arms(
931        config: &Config,
932        field_info: &BTreeMap<String, FieldInfo>,
933        variants: &[ProfileVariant],
934    ) -> Vec<proc_macro2::TokenStream> {
935        if config.profiles.is_empty() {
936            // Handle Default profile
937            let assignments = field_info
938                .values()
939                .map(|info| info.generate_assignment(quote! { secrets }));
940
941            vec![quote! {
942                Profile::Default => Ok(SecretSpecProfile::Default {
943                    #(#assignments,)*
944                })
945            }]
946        } else {
947            variants
948                .iter()
949                .filter_map(|variant| {
950                    config.profiles.get(&variant.name).map(|profile_config| {
951                        let variant_ident = variant.as_ident();
952                        let assignments =
953                            profile_config
954                                .secrets
955                                .iter()
956                                .map(|(secret_name, secret_config)| {
957                                    let field_name = field_name_ident(secret_name);
958                                    generate_secret_assignment(
959                                        &field_name,
960                                        secret_name,
961                                        quote! { secrets },
962                                        is_secret_optional(secret_config),
963                                    )
964                                });
965
966                        quote! {
967                            Profile::#variant_ident => Ok(SecretSpecProfile::#variant_ident {
968                                #(#assignments,)*
969                            })
970                        }
971                    })
972                })
973                .collect()
974        }
975    }
976
977    /// Generate the shared load_internal implementation.
978    ///
979    /// Creates a helper function that handles the common loading logic
980    /// for both SecretSpec and SecretSpecProfile loading methods.
981    ///
982    /// # Generated Function
983    ///
984    /// The function:
985    /// 1. Loads the SecretSpec configuration
986    /// 2. Validates it with the given provider and profile
987    /// 3. Returns the validation result containing loaded secrets
988    pub fn generate_load_internal() -> proc_macro2::TokenStream {
989        quote! {
990            fn load_internal(
991                provider_str: Option<String>,
992                profile_str: Option<String>,
993            ) -> Result<secretspec::ValidatedSecrets, secretspec::SecretSpecError> {
994                let mut spec = secretspec::Secrets::load()?;
995                if let Some(provider) = provider_str {
996                    spec.set_provider(provider);
997                }
998                if let Some(profile) = profile_str {
999                    spec.set_profile(profile);
1000                }
1001                match spec.validate()? {
1002                    Ok(valid_secrets) => Ok(valid_secrets),
1003                    Err(validation_errors) => Err(secretspec::SecretSpecError::RequiredSecretMissing(
1004                        validation_errors.missing_required.join(", ")
1005                    ))
1006                }
1007            }
1008        }
1009    }
1010
1011    /// Generate SecretSpec implementation.
1012    ///
1013    /// Creates the impl block for SecretSpec with:
1014    /// - builder() method for creating a builder
1015    /// - load() method for loading with union types
1016    /// - set_as_env_vars() method for environment variable integration
1017    ///
1018    /// # Arguments
1019    ///
1020    /// * `load_assignments` - Field assignments for the load method
1021    /// * `env_setters` - Environment variable setter statements
1022    /// * `_field_info` - Field information (currently unused)
1023    ///
1024    /// # Generated Methods
1025    ///
1026    /// - `builder()` - Creates a new SecretSpecBuilder
1027    /// - `load()` - Loads secrets with optional provider/profile
1028    /// - `set_as_env_vars()` - Sets all secrets as environment variables
1029    pub fn generate_impl(
1030        load_assignments: &[proc_macro2::TokenStream],
1031        env_setters: Vec<proc_macro2::TokenStream>,
1032        _field_info: &BTreeMap<String, FieldInfo>,
1033    ) -> proc_macro2::TokenStream {
1034        quote! {
1035            impl SecretSpec {
1036                /// Create a new builder for loading secrets
1037                pub fn builder() -> SecretSpecBuilder {
1038                    SecretSpecBuilder::new()
1039                }
1040
1041                /// Load secrets with optional provider and/or profile
1042                /// Provider can be any type that implements Into<String> (e.g., &str, String, etc.)
1043                /// If provider is None, uses SECRETSPEC_PROVIDER env var or global config
1044                /// If profile is None, uses SECRETSPEC_PROFILE env var if set
1045                pub fn load<P>(provider: Option<P>, profile: Option<Profile>) -> Result<secretspec::Resolved<Self>, secretspec::SecretSpecError>
1046                where
1047                    P: Into<String>,
1048                {
1049                    // Convert options to strings
1050                    let provider_str = provider.map(Into::into).or_else(|| std::env::var("SECRETSPEC_PROVIDER").ok());
1051
1052                    let profile_str = match profile {
1053                        Some(p) => Some(p.as_str().to_string()),
1054                        None => std::env::var("SECRETSPEC_PROFILE").ok(),
1055                    };
1056
1057                    let validation_result = load_internal(provider_str, profile_str)?;
1058                    let provider_name = validation_result.resolved.provider.clone();
1059                    let profile = validation_result.resolved.profile.clone();
1060                    let secrets = validation_result.resolved.secrets;
1061
1062                    let data = Self {
1063                        #(#load_assignments,)*
1064                    };
1065
1066                    Ok(secretspec::Resolved::new(
1067                        data,
1068                        provider_name,
1069                        profile
1070                    ))
1071                }
1072
1073                pub fn set_as_env_vars(&self) {
1074                    #(#env_setters)*
1075                }
1076            }
1077        }
1078    }
1079}
1080
1081// ===== Builder Generation Module =====
1082
1083/// Module for generating the builder pattern implementation.
1084///
1085/// The builder provides a fluent API for configuring how secrets are loaded,
1086/// with support for:
1087/// - Custom providers (via URIs)
1088/// - Profile selection
1089/// - Type-safe loading (union or profile-specific)
1090mod builder_generation {
1091    use super::*;
1092
1093    /// Generate the builder struct definition.
1094    ///
1095    /// The builder uses boxed closures to defer provider/profile resolution
1096    /// until load time, allowing for flexible configuration.
1097    ///
1098    /// # Generated Struct
1099    ///
1100    /// ```ignore
1101    /// pub struct SecretSpecBuilder {
1102    ///     provider: Option<Box<dyn FnOnce() -> Result<Box<dyn secretspec::Provider>, String>>>,
1103    ///     profile: Option<Box<dyn FnOnce() -> Result<Profile, String>>>,
1104    /// }
1105    /// ```
1106    pub fn generate_struct() -> proc_macro2::TokenStream {
1107        quote! {
1108            pub struct SecretSpecBuilder {
1109                provider: Option<Box<dyn FnOnce() -> Result<Box<dyn secretspec::Provider>, String>>>,
1110                profile: Option<Box<dyn FnOnce() -> Result<Profile, String>>>,
1111            }
1112        }
1113    }
1114
1115    /// Generate builder basic methods.
1116    ///
1117    /// Creates the foundational builder methods:
1118    /// - Default implementation
1119    /// - new() constructor
1120    /// - with_provider() for setting provider
1121    /// - with_profile() for setting profile
1122    ///
1123    /// # Type Flexibility
1124    ///
1125    /// Both with_provider and with_profile accept anything that can be
1126    /// converted to the target type (Uri or Profile), providing flexibility:
1127    ///
1128    /// ```ignore
1129    /// builder.with_provider("keyring://")           // &str
1130    ///        .with_provider(Provider::Keyring)      // Provider enum
1131    ///        .with_profile("production")            // &str
1132    ///        .with_profile(Profile::Production)      // Profile enum
1133    /// ```
1134    pub fn generate_basic_methods() -> proc_macro2::TokenStream {
1135        quote! {
1136            impl Default for SecretSpecBuilder {
1137                fn default() -> Self {
1138                    Self::new()
1139                }
1140            }
1141
1142            impl SecretSpecBuilder {
1143                pub fn new() -> Self {
1144                    Self {
1145                        provider: None,
1146                        profile: None,
1147                    }
1148                }
1149
1150                pub fn with_provider<T>(mut self, provider: T) -> Self
1151                where
1152                    T: TryInto<Box<dyn secretspec::Provider>> + 'static,
1153                    T::Error: std::fmt::Display + 'static,
1154                {
1155                    self.provider = Some(Box::new(move || {
1156                        provider.try_into()
1157                            .map_err(|e| format!("Invalid provider: {}", e))
1158                    }));
1159                    self
1160                }
1161
1162                pub fn with_profile<T>(mut self, profile: T) -> Self
1163                where
1164                    T: TryInto<Profile>,
1165                    T::Error: std::fmt::Display
1166                {
1167                    match profile.try_into() {
1168                        Ok(p) => {
1169                            self.profile = Some(Box::new(move || Ok(p)));
1170                        }
1171                        Err(e) => {
1172                            let error_msg = format!("{}", e);
1173                            self.profile = Some(Box::new(move || Err(error_msg)));
1174                        }
1175                    }
1176                    self
1177                }
1178            }
1179        }
1180    }
1181
1182    /// Generate provider resolution logic.
1183    ///
1184    /// Creates code to resolve a provider from the builder's boxed closure.
1185    ///
1186    /// # Arguments
1187    ///
1188    /// * `provider_expr` - Expression to access the provider option
1189    ///
1190    /// # Generated Logic
1191    ///
1192    /// 1. If provider is set, call the closure to get the Provider instance
1193    /// 2. Convert any errors to SecretSpecError
1194    /// 3. Extract the provider name to pass to the loading system
1195    fn generate_provider_resolution(
1196        provider_expr: proc_macro2::TokenStream,
1197    ) -> proc_macro2::TokenStream {
1198        quote! {
1199            let provider_str = if let Some(provider_fn) = #provider_expr {
1200                let provider_box = provider_fn()
1201                    .map_err(|e| secretspec::SecretSpecError::ProviderOperationFailed(e))?;
1202                // Get the provider name to pass as a string to set_provider
1203                Some(provider_box.name().to_string())
1204            } else {
1205                None
1206            };
1207        }
1208    }
1209
1210    /// Generate profile resolution logic.
1211    ///
1212    /// Creates code to resolve a profile from the builder's boxed closure.
1213    ///
1214    /// # Arguments
1215    ///
1216    /// * `profile_expr` - Expression to access the profile option
1217    ///
1218    /// # Generated Logic
1219    ///
1220    /// 1. If profile is set, call the closure to get the Profile
1221    /// 2. Convert any errors to SecretSpecError
1222    /// 3. Convert Profile to string for the loading system
1223    fn generate_profile_resolution(
1224        profile_expr: proc_macro2::TokenStream,
1225    ) -> proc_macro2::TokenStream {
1226        quote! {
1227            let profile_str = if let Some(profile_fn) = #profile_expr {
1228                let profile = profile_fn()
1229                    .map_err(|e| secretspec::SecretSpecError::InvalidProfile(e))?;
1230                Some(profile.as_str().to_string())
1231            } else {
1232                None
1233            };
1234        }
1235    }
1236
1237    /// Generate load methods for the builder.
1238    ///
1239    /// Creates two loading methods:
1240    /// - `load()` - Returns SecretSpec (union type)
1241    /// - `load_profile()` - Returns SecretSpecProfile (profile-specific type)
1242    ///
1243    /// # Arguments
1244    ///
1245    /// * `load_assignments` - Field assignments for union type
1246    /// * `load_profile_arms` - Match arms for profile-specific loading
1247    /// * `first_profile_variant` - Default profile if none specified
1248    ///
1249    /// # Key Differences
1250    ///
1251    /// - `load()` returns all secrets with optional fields for safety
1252    /// - `load_profile()` returns only profile-specific secrets with exact types
1253    pub fn generate_load_methods(
1254        load_assignments: &[proc_macro2::TokenStream],
1255        load_profile_arms: &[proc_macro2::TokenStream],
1256        first_profile_variant: &proc_macro2::Ident,
1257    ) -> proc_macro2::TokenStream {
1258        let resolve_provider_load = generate_provider_resolution(quote! { self.provider.take() });
1259        let resolve_profile_load = generate_profile_resolution(quote! { self.profile.take() });
1260        let resolve_provider_profile =
1261            generate_provider_resolution(quote! { self.provider.take() });
1262
1263        quote! {
1264            impl SecretSpecBuilder {
1265                pub fn load(mut self) -> Result<secretspec::Resolved<SecretSpec>, secretspec::SecretSpecError> {
1266                    #resolve_provider_load
1267                    #resolve_profile_load
1268
1269                    let validation_result = load_internal(provider_str, profile_str)?;
1270                    let provider_name = validation_result.resolved.provider.clone();
1271                    let profile = validation_result.resolved.profile.clone();
1272                    let secrets = validation_result.resolved.secrets;
1273
1274                    let data = SecretSpec {
1275                        #(#load_assignments,)*
1276                    };
1277
1278                    Ok(secretspec::Resolved::new(
1279                        data,
1280                        provider_name,
1281                        profile
1282                    ))
1283                }
1284
1285                pub fn load_profile(mut self) -> Result<secretspec::Resolved<SecretSpecProfile>, secretspec::SecretSpecError> {
1286                    #resolve_provider_profile
1287
1288                    let (profile_str, selected_profile) = if let Some(profile_fn) = self.profile.take() {
1289                        let profile = profile_fn()
1290                            .map_err(|e| secretspec::SecretSpecError::InvalidProfile(e))?;
1291                        (Some(profile.as_str().to_string()), profile)
1292                    } else {
1293                        // Check env var for profile
1294                        let profile_str = std::env::var("SECRETSPEC_PROFILE").ok();
1295                        let selected_profile = if let Some(ref profile_name) = profile_str {
1296                            Profile::try_from(profile_name.as_str())?
1297                        } else {
1298                            Profile::#first_profile_variant
1299                        };
1300                        (profile_str, selected_profile)
1301                    };
1302
1303                    let validation_result = load_internal(provider_str, profile_str)?;
1304                    let provider_name = validation_result.resolved.provider.clone();
1305                    let profile = validation_result.resolved.profile.clone();
1306                    let secrets = validation_result.resolved.secrets;
1307
1308                    let data_result: LoadResult<SecretSpecProfile> = match selected_profile {
1309                        #(#load_profile_arms,)*
1310                    };
1311                    let data = data_result?;
1312
1313                    Ok(secretspec::Resolved::new(
1314                        data,
1315                        provider_name,
1316                        profile
1317                    ))
1318                }
1319            }
1320        }
1321    }
1322
1323    /// Generate all builder-related code.
1324    ///
1325    /// Combines all builder components into a complete implementation.
1326    ///
1327    /// # Arguments
1328    ///
1329    /// * `load_assignments` - Field assignments for union loading
1330    /// * `load_profile_arms` - Match arms for profile loading
1331    /// * `first_profile_variant` - Default profile variant
1332    ///
1333    /// # Returns
1334    ///
1335    /// Complete token stream containing:
1336    /// - Builder struct definition
1337    /// - Basic builder methods
1338    /// - Loading methods (load and load_profile)
1339    pub fn generate_all(
1340        load_assignments: &[proc_macro2::TokenStream],
1341        load_profile_arms: &[proc_macro2::TokenStream],
1342        first_profile_variant: &proc_macro2::Ident,
1343    ) -> proc_macro2::TokenStream {
1344        let struct_def = generate_struct();
1345        let basic_methods = generate_basic_methods();
1346        let load_methods =
1347            generate_load_methods(load_assignments, load_profile_arms, first_profile_variant);
1348
1349        quote! {
1350            #struct_def
1351            #basic_methods
1352            #load_methods
1353        }
1354    }
1355}
1356
1357/// Main code generation function.
1358///
1359/// Orchestrates the entire code generation process, coordinating all modules
1360/// to produce the complete macro output.
1361///
1362/// # Arguments
1363///
1364/// * `config` - The validated project configuration
1365///
1366/// # Returns
1367///
1368/// Complete token stream containing all generated code
1369///
1370/// # Generation Process
1371///
1372/// 1. Analyze profiles and field types
1373/// 2. Generate Profile enum and implementations
1374/// 3. Generate SecretSpec struct (union type)
1375/// 4. Generate SecretSpecProfile enum (profile-specific types)
1376/// 5. Generate builder pattern implementation
1377/// 6. Combine all components with necessary imports
1378fn generate_secret_spec_code(config: Config) -> proc_macro2::TokenStream {
1379    // Collect all profiles
1380    let all_profiles: HashSet<String> = config.profiles.keys().cloned().collect();
1381    let profile_variants = get_profile_variants(&all_profiles);
1382
1383    // Analyze field types
1384    let field_info = analyze_field_types(&config);
1385
1386    // Generate field assignments for load()
1387    let load_assignments: Vec<_> = field_info
1388        .values()
1389        .map(|info| info.generate_assignment(quote! { secrets }))
1390        .collect();
1391
1392    // Generate env var setters
1393    let env_setters: Vec<_> = field_info
1394        .values()
1395        .map(|info| info.generate_env_setter())
1396        .collect();
1397
1398    // Generate profile components
1399    let profile_code = profile_generation::generate_all(&profile_variants);
1400
1401    // Generate SecretSpec components
1402    let secret_spec_struct = secret_spec_generation::generate_struct(&field_info);
1403    let profile_enum_variants = secret_spec_generation::generate_profile_enum_variants(
1404        &config,
1405        &field_info,
1406        &profile_variants,
1407    );
1408    let secret_spec_profile_enum =
1409        secret_spec_generation::generate_profile_enum(&profile_enum_variants);
1410    let load_profile_arms =
1411        secret_spec_generation::generate_load_profile_arms(&config, &field_info, &profile_variants);
1412    let load_internal = secret_spec_generation::generate_load_internal();
1413    let secret_spec_impl =
1414        secret_spec_generation::generate_impl(&load_assignments, env_setters, &field_info);
1415
1416    // Get first profile variant for defaults
1417    // Get first profile variant for defaults
1418    let first_profile_variant = profile_variants
1419        .first()
1420        .map(|v| v.as_ident())
1421        .unwrap_or_else(|| format_ident!("Default"));
1422
1423    // Generate builder
1424    let builder_code = builder_generation::generate_all(
1425        &load_assignments,
1426        &load_profile_arms,
1427        &first_profile_variant,
1428    );
1429
1430    // Combine all components
1431    quote! {
1432        use ::secrecy::ExposeSecret;
1433
1434        #secret_spec_struct
1435        #secret_spec_profile_enum
1436        #profile_code
1437
1438
1439        // Type alias to help with type inference
1440        type LoadResult<T> = Result<T, secretspec::SecretSpecError>;
1441
1442        #load_internal
1443        #builder_code
1444        #secret_spec_impl
1445    }
1446}
1447
1448/// Capitalize the first character of a string.
1449///
1450/// Used to convert profile names to enum variant names.
1451///
1452/// # Arguments
1453///
1454/// * `s` - The string to capitalize
1455///
1456/// # Returns
1457///
1458/// A new string with the first character capitalized
1459///
1460/// # Examples
1461///
1462/// ```ignore
1463/// assert_eq!(capitalize_first("production"), "Production");
1464/// assert_eq!(capitalize_first("test_env"), "Test_env");
1465/// assert_eq!(capitalize_first(""), "");
1466/// ```
1467fn capitalize_first(s: &str) -> String {
1468    let mut chars = s.chars();
1469    match chars.next() {
1470        None => String::new(),
1471        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1472    }
1473}
1474
1475#[cfg(test)]
1476#[path = "tests.rs"]
1477mod tests;