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