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 if:
445/// - It has `required = false` in the config, OR
446/// - It has a default value specified
447///
448/// # Arguments
449///
450/// * `secret_config` - The secret's configuration
451///
452/// # Returns
453///
454/// `true` if the secret is optional, `false` if required
455fn is_secret_optional(secret_config: &Secret) -> bool {
456    !secret_config.required || secret_config.default.is_some()
457}
458
459/// Determines if a field should be optional across all profiles.
460///
461/// For the union struct (SecretSpec), a field is optional if it's optional
462/// in ANY profile or missing from ANY profile. This ensures the union type
463/// can safely represent secrets from any profile.
464///
465/// # Arguments
466///
467/// * `secret_name` - The name of the secret to check
468/// * `config` - The project configuration
469///
470/// # Returns
471///
472/// `true` if the field should be Option<String> in the union struct
473///
474/// # Logic
475///
476/// - If the secret is missing from any profile → optional
477/// - If the secret is optional in any profile → optional
478/// - Only if required in ALL profiles → not optional
479fn is_field_optional_across_profiles(secret_name: &str, config: &Config) -> bool {
480    // Check each profile
481    for profile_config in config.profiles.values() {
482        if let Some(secret_config) = profile_config.secrets.get(secret_name) {
483            if is_secret_optional(secret_config) {
484                return true;
485            }
486        } else {
487            // Secret doesn't exist in this profile, so it's optional
488            return true;
489        }
490    }
491    false
492}
493
494/// Generate a unified secret assignment from a HashMap.
495///
496/// Creates the code to assign a value from a secrets map to a struct field,
497/// with appropriate error handling based on whether the field is optional.
498///
499/// # Arguments
500///
501/// * `field_name` - The struct field identifier
502/// * `secret_name` - The key to look up in the map
503/// * `source` - Token stream representing the source map
504/// * `is_optional` - Whether to generate Option<String> or String assignment
505///
506/// # Generated Code
507///
508/// For required fields:
509/// ```ignore
510/// field_name: source.get("SECRET_NAME")
511///     .ok_or_else(|| SecretSpecError::RequiredSecretMissing("SECRET_NAME".to_string()))?
512///     .clone()
513/// ```
514///
515/// For optional fields:
516/// ```ignore
517/// field_name: source.get("SECRET_NAME").cloned()
518/// ```
519fn generate_secret_assignment(
520    field_name: &proc_macro2::Ident,
521    secret_name: &str,
522    source: proc_macro2::TokenStream,
523    is_optional: bool,
524) -> proc_macro2::TokenStream {
525    if is_optional {
526        quote! {
527            #field_name: #source.get(#secret_name).cloned()
528        }
529    } else {
530        quote! {
531            #field_name: #source.get(#secret_name)
532                .ok_or_else(|| secretspec::SecretSpecError::RequiredSecretMissing(#secret_name.to_string()))?
533                .clone()
534        }
535    }
536}
537
538/// Analyzes all profiles to determine field types for the union struct.
539///
540/// This function examines all secrets across all profiles to determine:
541/// - Which secrets exist across profiles
542/// - Whether each secret should be optional in the union type
543/// - The appropriate Rust type for each field
544///
545/// # Arguments
546///
547/// * `config` - The project configuration
548///
549/// # Returns
550///
551/// A BTreeMap (for consistent ordering) mapping secret names to their FieldInfo
552///
553/// # Algorithm
554///
555/// 1. Collect all unique secret names from all profiles
556/// 2. For each secret, determine if it's optional across profiles
557/// 3. Generate appropriate type (String or Option<String>)
558/// 4. Create FieldInfo with all metadata needed for code generation
559fn analyze_field_types(config: &Config) -> BTreeMap<String, FieldInfo> {
560    let mut field_info = BTreeMap::new();
561
562    // Collect all unique secrets across all profiles
563    for profile_config in config.profiles.values() {
564        for secret_name in profile_config.secrets.keys() {
565            field_info.entry(secret_name.clone()).or_insert_with(|| {
566                let is_optional = is_field_optional_across_profiles(secret_name, config);
567                let field_type = if is_optional {
568                    quote! { Option<String> }
569                } else {
570                    quote! { String }
571                };
572                FieldInfo::new(secret_name.clone(), field_type, is_optional)
573            });
574        }
575    }
576
577    field_info
578}
579
580/// Get normalized profile variants for enum generation.
581///
582/// Converts profile names into ProfileVariant structs, handling the special
583/// case of empty profiles (generates a "Default" variant).
584///
585/// # Arguments
586///
587/// * `profiles` - Set of profile names from the configuration
588///
589/// # Returns
590///
591/// A sorted vector of ProfileVariant structs
592///
593/// # Special Cases
594///
595/// - Empty profiles → returns vec![ProfileVariant("default", "Default")]
596/// - Otherwise → sorted list of profile variants
597fn get_profile_variants(profiles: &HashSet<String>) -> Vec<ProfileVariant> {
598    if profiles.is_empty() {
599        vec![ProfileVariant::new("default".to_string())]
600    } else {
601        let mut variants: Vec<_> = profiles
602            .iter()
603            .map(|name| ProfileVariant::new(name.clone()))
604            .collect();
605        variants.sort_by(|a, b| a.name.cmp(&b.name));
606        variants
607    }
608}
609
610// ===== Profile Generation Module =====
611
612/// Module for generating Profile enum and related implementations.
613///
614/// This module handles:
615/// - Profile enum definition
616/// - TryFrom implementations for string conversion
617/// - as_str() method for profile serialization
618mod profile_generation {
619    use super::*;
620
621    /// Generate just the Profile enum.
622    ///
623    /// Creates an enum with variants for each profile in the configuration.
624    ///
625    /// # Arguments
626    ///
627    /// * `variants` - List of profile variants to generate
628    ///
629    /// # Generated Code Example
630    ///
631    /// ```ignore
632    /// #[derive(Debug, Clone, Copy)]
633    /// pub enum Profile {
634    ///     Development,
635    ///     Production,
636    ///     Staging,
637    /// }
638    /// ```
639    pub fn generate_enum(variants: &[ProfileVariant]) -> proc_macro2::TokenStream {
640        let enum_variants = variants.iter().map(|v| {
641            let ident = v.as_ident();
642            quote! { #ident }
643        });
644
645        quote! {
646            #[derive(Debug, Clone, Copy)]
647            pub enum Profile {
648                #(#enum_variants,)*
649            }
650        }
651    }
652
653    /// Generate TryFrom implementations for Profile.
654    ///
655    /// Creates implementations to convert strings to Profile enum variants,
656    /// supporting both &str and String inputs.
657    ///
658    /// # Arguments
659    ///
660    /// * `variants` - List of profile variants
661    ///
662    /// # Generated Code
663    ///
664    /// - `TryFrom<&str>` implementation with match arms for each profile
665    /// - `TryFrom<String>` implementation that delegates to &str
666    /// - Returns `SecretSpecError::InvalidProfile` for unknown profiles
667    pub fn generate_try_from_impls(variants: &[ProfileVariant]) -> proc_macro2::TokenStream {
668        let from_str_arms = variants.iter().map(|v| {
669            let ident = v.as_ident();
670            let str_val = &v.name;
671            quote! { #str_val => Ok(Profile::#ident) }
672        });
673
674        quote! {
675            impl std::convert::TryFrom<&str> for Profile {
676                type Error = secretspec::SecretSpecError;
677
678                fn try_from(value: &str) -> Result<Self, Self::Error> {
679                    match value {
680                        #(#from_str_arms,)*
681                        _ => Err(secretspec::SecretSpecError::InvalidProfile(value.to_string())),
682                    }
683                }
684            }
685
686            impl std::convert::TryFrom<String> for Profile {
687                type Error = secretspec::SecretSpecError;
688
689                fn try_from(value: String) -> Result<Self, Self::Error> {
690                    Profile::try_from(value.as_str())
691                }
692            }
693        }
694    }
695
696    /// Generate as_str implementation for Profile.
697    ///
698    /// Creates a method to convert Profile enum variants back to their string representation.
699    ///
700    /// # Arguments
701    ///
702    /// * `variants` - List of profile variants
703    ///
704    /// # Generated Code Example
705    ///
706    /// ```ignore
707    /// impl Profile {
708    ///     fn as_str(&self) -> &'static str {
709    ///         match self {
710    ///             Profile::Development => "development",
711    ///             Profile::Production => "production",
712    ///         }
713    ///     }
714    /// }
715    /// ```
716    pub fn generate_as_str_impl(variants: &[ProfileVariant]) -> proc_macro2::TokenStream {
717        let to_str_arms = variants.iter().map(|v| {
718            let ident = v.as_ident();
719            let str_val = &v.name;
720            quote! { Profile::#ident => #str_val }
721        });
722
723        quote! {
724            impl Profile {
725                fn as_str(&self) -> &'static str {
726                    match self {
727                        #(#to_str_arms,)*
728                    }
729                }
730            }
731        }
732    }
733
734    /// Generate all profile-related code.
735    ///
736    /// Combines all profile generation functions into a single token stream.
737    ///
738    /// # Arguments
739    ///
740    /// * `variants` - List of profile variants
741    ///
742    /// # Returns
743    ///
744    /// Complete token stream containing:
745    /// - Profile enum definition
746    /// - TryFrom implementations
747    /// - as_str() method
748    pub fn generate_all(variants: &[ProfileVariant]) -> proc_macro2::TokenStream {
749        let enum_def = generate_enum(variants);
750        let try_from_impls = generate_try_from_impls(variants);
751        let as_str_impl = generate_as_str_impl(variants);
752
753        quote! {
754            #enum_def
755            #try_from_impls
756            #as_str_impl
757        }
758    }
759}
760
761// ===== SecretSpec Generation Module =====
762
763/// Module for generating SecretSpec struct and related implementations.
764///
765/// This module handles:
766/// - SecretSpec struct (union of all secrets)
767/// - SecretSpecProfile enum (profile-specific types)
768/// - Loading implementations
769/// - Environment variable integration
770mod secret_spec_generation {
771    use super::*;
772
773    /// Generate the SecretSpec struct.
774    ///
775    /// Creates a struct containing all secrets from all profiles as fields.
776    /// This is the "union" type that can safely hold secrets from any profile.
777    ///
778    /// # Arguments
779    ///
780    /// * `field_info` - Map of all fields with their type information
781    ///
782    /// # Generated Code Example
783    ///
784    /// ```ignore
785    /// #[derive(Debug, serde::Serialize, serde::Deserialize)]
786    /// pub struct SecretSpec {
787    ///     pub database_url: String,
788    ///     pub api_key: Option<String>,
789    ///     pub redis_url: Option<String>,
790    /// }
791    /// ```
792    pub fn generate_struct(field_info: &BTreeMap<String, FieldInfo>) -> proc_macro2::TokenStream {
793        let fields = field_info.values().map(|info| info.generate_struct_field());
794
795        quote! {
796            #[derive(Debug, serde::Serialize, serde::Deserialize)]
797            pub struct SecretSpec {
798                #(#fields,)*
799            }
800        }
801    }
802
803    /// Generate the SecretSpecProfile enum.
804    ///
805    /// Creates an enum where each variant contains only the secrets defined
806    /// for that specific profile. This provides stronger type safety when
807    /// working with profile-specific secrets.
808    ///
809    /// # Arguments
810    ///
811    /// * `profile_variants` - Generated enum variant definitions
812    ///
813    /// # Generated Code Example
814    ///
815    /// ```ignore
816    /// #[derive(Debug, serde::Serialize, serde::Deserialize)]
817    /// pub enum SecretSpecProfile {
818    ///     Development {
819    ///         database_url: String,
820    ///         redis_url: Option<String>,
821    ///     },
822    ///     Production {
823    ///         database_url: String,
824    ///         api_key: String,
825    ///         redis_url: String,
826    ///     },
827    /// }
828    /// ```
829    pub fn generate_profile_enum(
830        profile_variants: &[proc_macro2::TokenStream],
831    ) -> proc_macro2::TokenStream {
832        quote! {
833            #[derive(Debug, serde::Serialize, serde::Deserialize)]
834            pub enum SecretSpecProfile {
835                #(#profile_variants,)*
836            }
837        }
838    }
839
840    /// Generate SecretSpecProfile enum variants.
841    ///
842    /// Creates the individual variants for the SecretSpecProfile enum,
843    /// each containing only the fields defined for that profile.
844    ///
845    /// # Arguments
846    ///
847    /// * `config` - The project configuration
848    /// * `field_info` - Field information (used for empty profile case)
849    /// * `variants` - Profile variants to generate
850    ///
851    /// # Returns
852    ///
853    /// Vector of token streams, each representing one enum variant
854    ///
855    /// # Special Cases
856    ///
857    /// - Empty profiles → generates a Default variant with all fields
858    /// - Each profile → generates variant with profile-specific fields
859    pub fn generate_profile_enum_variants(
860        config: &Config,
861        field_info: &BTreeMap<String, FieldInfo>,
862        variants: &[ProfileVariant],
863    ) -> Vec<proc_macro2::TokenStream> {
864        if config.profiles.is_empty() {
865            // If no profiles, create a Default variant with all fields
866            let fields = field_info.values().map(|info| info.generate_struct_field());
867            vec![quote! {
868                Default {
869                    #(#fields,)*
870                }
871            }]
872        } else {
873            variants
874                .iter()
875                .filter_map(|variant| {
876                    config.profiles.get(&variant.name).map(|profile_config| {
877                        let variant_ident = variant.as_ident();
878                        let fields =
879                            profile_config
880                                .secrets
881                                .iter()
882                                .map(|(secret_name, secret_config)| {
883                                    let field_name = field_name_ident(secret_name);
884                                    let field_type = if is_secret_optional(secret_config) {
885                                        quote! { Option<String> }
886                                    } else {
887                                        quote! { String }
888                                    };
889                                    quote! { #field_name: #field_type }
890                                });
891
892                        quote! {
893                            #variant_ident {
894                                #(#fields,)*
895                            }
896                        }
897                    })
898                })
899                .collect()
900        }
901    }
902
903    /// Generate load_profile match arms.
904    ///
905    /// Creates the match arms for loading profile-specific secrets into
906    /// the appropriate SecretSpecProfile variant.
907    ///
908    /// # Arguments
909    ///
910    /// * `config` - The project configuration
911    /// * `field_info` - Field information (for empty profile case)
912    /// * `variants` - Profile variants to generate arms for
913    ///
914    /// # Returns
915    ///
916    /// Vector of match arms for the profile loading logic
917    ///
918    /// # Generated Code Example
919    ///
920    /// ```ignore
921    /// Profile::Production => Ok(SecretSpecProfile::Production {
922    ///     database_url: secrets.get("DATABASE_URL")
923    ///         .ok_or_else(|| SecretSpecError::RequiredSecretMissing("DATABASE_URL".to_string()))?
924    ///         .clone(),
925    ///     api_key: secrets.get("API_KEY").cloned(),
926    /// })
927    /// ```
928    pub fn generate_load_profile_arms(
929        config: &Config,
930        field_info: &BTreeMap<String, FieldInfo>,
931        variants: &[ProfileVariant],
932    ) -> Vec<proc_macro2::TokenStream> {
933        if config.profiles.is_empty() {
934            // Handle Default profile
935            let assignments = field_info
936                .values()
937                .map(|info| info.generate_assignment(quote! { secrets }));
938
939            vec![quote! {
940                Profile::Default => Ok(SecretSpecProfile::Default {
941                    #(#assignments,)*
942                })
943            }]
944        } else {
945            variants
946                .iter()
947                .filter_map(|variant| {
948                    config.profiles.get(&variant.name).map(|profile_config| {
949                        let variant_ident = variant.as_ident();
950                        let assignments =
951                            profile_config
952                                .secrets
953                                .iter()
954                                .map(|(secret_name, secret_config)| {
955                                    let field_name = field_name_ident(secret_name);
956                                    generate_secret_assignment(
957                                        &field_name,
958                                        secret_name,
959                                        quote! { secrets },
960                                        is_secret_optional(secret_config),
961                                    )
962                                });
963
964                        quote! {
965                            Profile::#variant_ident => Ok(SecretSpecProfile::#variant_ident {
966                                #(#assignments,)*
967                            })
968                        }
969                    })
970                })
971                .collect()
972        }
973    }
974
975    /// Generate the shared load_internal implementation.
976    ///
977    /// Creates a helper function that handles the common loading logic
978    /// for both SecretSpec and SecretSpecProfile loading methods.
979    ///
980    /// # Generated Function
981    ///
982    /// The function:
983    /// 1. Loads the SecretSpec configuration
984    /// 2. Validates it with the given provider and profile
985    /// 3. Returns the validation result containing loaded secrets
986    pub fn generate_load_internal() -> proc_macro2::TokenStream {
987        quote! {
988            fn load_internal(
989                provider_str: Option<String>,
990                profile_str: Option<String>,
991            ) -> Result<secretspec::ValidatedSecrets, secretspec::SecretSpecError> {
992                let spec = secretspec::Secrets::load()?;
993                spec.validate(provider_str, profile_str)
994            }
995        }
996    }
997
998    /// Generate SecretSpec implementation.
999    ///
1000    /// Creates the impl block for SecretSpec with:
1001    /// - builder() method for creating a builder
1002    /// - load() method for loading with union types
1003    /// - set_as_env_vars() method for environment variable integration
1004    ///
1005    /// # Arguments
1006    ///
1007    /// * `load_assignments` - Field assignments for the load method
1008    /// * `env_setters` - Environment variable setter statements
1009    /// * `_field_info` - Field information (currently unused)
1010    ///
1011    /// # Generated Methods
1012    ///
1013    /// - `builder()` - Creates a new SecretSpecBuilder
1014    /// - `load()` - Loads secrets with optional provider/profile
1015    /// - `set_as_env_vars()` - Sets all secrets as environment variables
1016    pub fn generate_impl(
1017        load_assignments: &[proc_macro2::TokenStream],
1018        env_setters: Vec<proc_macro2::TokenStream>,
1019        _field_info: &BTreeMap<String, FieldInfo>,
1020    ) -> proc_macro2::TokenStream {
1021        quote! {
1022            impl SecretSpec {
1023                /// Create a new builder for loading secrets
1024                pub fn builder() -> SecretSpecBuilder {
1025                    SecretSpecBuilder::new()
1026                }
1027
1028                /// Load secrets with optional provider and/or profile
1029                /// Provider can be any type that implements Into<String> (e.g., &str, String, etc.)
1030                /// If provider is None, uses SECRETSPEC_PROVIDER env var or global config
1031                /// If profile is None, uses SECRETSPEC_PROFILE env var if set
1032                pub fn load<P>(provider: Option<P>, profile: Option<Profile>) -> Result<secretspec::Resolved<Self>, secretspec::SecretSpecError>
1033                where
1034                    P: Into<String>,
1035                {
1036                    // Convert options to strings
1037                    let provider_str = provider.map(Into::into).or_else(|| std::env::var("SECRETSPEC_PROVIDER").ok());
1038
1039                    let profile_str = match profile {
1040                        Some(p) => Some(p.as_str().to_string()),
1041                        None => std::env::var("SECRETSPEC_PROFILE").ok(),
1042                    };
1043
1044                    let validation_result = load_internal(provider_str, profile_str)?;
1045                    let provider_name = validation_result.provider_name();
1046                    let profile = validation_result.profile;
1047                    let secrets = validation_result.secrets;
1048
1049                    let data = Self {
1050                        #(#load_assignments,)*
1051                    };
1052
1053                    Ok(secretspec::Resolved::new(
1054                        data,
1055                        provider_name,
1056                        profile
1057                    ))
1058                }
1059
1060                pub fn set_as_env_vars(&self) {
1061                    #(#env_setters)*
1062                }
1063            }
1064        }
1065    }
1066}
1067
1068// ===== Builder Generation Module =====
1069
1070/// Module for generating the builder pattern implementation.
1071///
1072/// The builder provides a fluent API for configuring how secrets are loaded,
1073/// with support for:
1074/// - Custom providers (via URIs)
1075/// - Profile selection
1076/// - Type-safe loading (union or profile-specific)
1077mod builder_generation {
1078    use super::*;
1079
1080    /// Generate the builder struct definition.
1081    ///
1082    /// The builder uses boxed closures to defer provider/profile resolution
1083    /// until load time, allowing for flexible configuration.
1084    ///
1085    /// # Generated Struct
1086    ///
1087    /// ```ignore
1088    /// pub struct SecretSpecBuilder {
1089    ///     provider: Option<Box<dyn FnOnce() -> Result<url::Url, String>>>,
1090    ///     profile: Option<Box<dyn FnOnce() -> Result<Profile, String>>>,
1091    /// }
1092    /// ```
1093    pub fn generate_struct() -> proc_macro2::TokenStream {
1094        quote! {
1095            pub struct SecretSpecBuilder {
1096                provider: Option<Box<dyn FnOnce() -> Result<url::Url, String>>>,
1097                profile: Option<Box<dyn FnOnce() -> Result<Profile, String>>>,
1098            }
1099        }
1100    }
1101
1102    /// Generate builder basic methods.
1103    ///
1104    /// Creates the foundational builder methods:
1105    /// - Default implementation
1106    /// - new() constructor
1107    /// - with_provider() for setting provider
1108    /// - with_profile() for setting profile
1109    ///
1110    /// # Type Flexibility
1111    ///
1112    /// Both with_provider and with_profile accept anything that can be
1113    /// converted to the target type (Uri or Profile), providing flexibility:
1114    ///
1115    /// ```ignore
1116    /// builder.with_provider("keyring://")           // &str
1117    ///        .with_provider(Provider::Keyring)      // Provider enum
1118    ///        .with_profile("production")            // &str
1119    ///        .with_profile(Profile::Production)      // Profile enum
1120    /// ```
1121    pub fn generate_basic_methods() -> proc_macro2::TokenStream {
1122        quote! {
1123            impl Default for SecretSpecBuilder {
1124                fn default() -> Self {
1125                    Self::new()
1126                }
1127            }
1128
1129            impl SecretSpecBuilder {
1130                pub fn new() -> Self {
1131                    Self {
1132                        provider: None,
1133                        profile: None,
1134                    }
1135                }
1136
1137                pub fn with_provider<T>(mut self, provider: T) -> Self
1138                where
1139                    T: TryInto<url::Url> + 'static,
1140                    T::Error: std::fmt::Display + 'static,
1141                {
1142                    self.provider = Some(Box::new(move || {
1143                        provider.try_into()
1144                            .map_err(|e| format!("Invalid provider URI: {}", e))
1145                    }));
1146                    self
1147                }
1148
1149                pub fn with_profile<T>(mut self, profile: T) -> Self
1150                where
1151                    T: TryInto<Profile> + 'static,
1152                    T::Error: std::fmt::Display + 'static
1153                {
1154                    self.profile = Some(Box::new(move || {
1155                        profile.try_into()
1156                            .map_err(|e| format!("{}", e))
1157                    }));
1158                    self
1159                }
1160            }
1161        }
1162    }
1163
1164    /// Generate provider resolution logic.
1165    ///
1166    /// Creates code to resolve a provider from the builder's boxed closure.
1167    ///
1168    /// # Arguments
1169    ///
1170    /// * `provider_expr` - Expression to access the provider option
1171    ///
1172    /// # Generated Logic
1173    ///
1174    /// 1. If provider is set, call the closure to get the URI
1175    /// 2. Convert any errors to SecretSpecError
1176    /// 3. Convert URI to string for the loading system
1177    fn generate_provider_resolution(
1178        provider_expr: proc_macro2::TokenStream,
1179    ) -> proc_macro2::TokenStream {
1180        quote! {
1181            let provider_str = if let Some(provider_fn) = #provider_expr {
1182                let uri = provider_fn()
1183                    .map_err(|e| secretspec::SecretSpecError::ProviderOperationFailed(e))?;
1184                Some(uri.to_string())
1185            } else {
1186                None
1187            };
1188        }
1189    }
1190
1191    /// Generate profile resolution logic.
1192    ///
1193    /// Creates code to resolve a profile from the builder's boxed closure.
1194    ///
1195    /// # Arguments
1196    ///
1197    /// * `profile_expr` - Expression to access the profile option
1198    ///
1199    /// # Generated Logic
1200    ///
1201    /// 1. If profile is set, call the closure to get the Profile
1202    /// 2. Convert any errors to SecretSpecError
1203    /// 3. Convert Profile to string for the loading system
1204    fn generate_profile_resolution(
1205        profile_expr: proc_macro2::TokenStream,
1206    ) -> proc_macro2::TokenStream {
1207        quote! {
1208            let profile_str = if let Some(profile_fn) = #profile_expr {
1209                let profile = profile_fn()
1210                    .map_err(|e| secretspec::SecretSpecError::InvalidProfile(e))?;
1211                Some(profile.as_str().to_string())
1212            } else {
1213                None
1214            };
1215        }
1216    }
1217
1218    /// Generate load methods for the builder.
1219    ///
1220    /// Creates two loading methods:
1221    /// - `load()` - Returns SecretSpec (union type)
1222    /// - `load_profile()` - Returns SecretSpecProfile (profile-specific type)
1223    ///
1224    /// # Arguments
1225    ///
1226    /// * `load_assignments` - Field assignments for union type
1227    /// * `load_profile_arms` - Match arms for profile-specific loading
1228    /// * `first_profile_variant` - Default profile if none specified
1229    ///
1230    /// # Key Differences
1231    ///
1232    /// - `load()` returns all secrets with optional fields for safety
1233    /// - `load_profile()` returns only profile-specific secrets with exact types
1234    pub fn generate_load_methods(
1235        load_assignments: &[proc_macro2::TokenStream],
1236        load_profile_arms: &[proc_macro2::TokenStream],
1237        first_profile_variant: &proc_macro2::Ident,
1238    ) -> proc_macro2::TokenStream {
1239        let resolve_provider_load = generate_provider_resolution(quote! { self.provider.take() });
1240        let resolve_profile_load = generate_profile_resolution(quote! { self.profile.take() });
1241        let resolve_provider_profile =
1242            generate_provider_resolution(quote! { self.provider.take() });
1243
1244        quote! {
1245            impl SecretSpecBuilder {
1246                pub fn load(mut self) -> Result<secretspec::Resolved<SecretSpec>, secretspec::SecretSpecError> {
1247                    #resolve_provider_load
1248                    #resolve_profile_load
1249
1250                    let validation_result = load_internal(provider_str, profile_str)?;
1251                    let provider_name = validation_result.provider_name();
1252                    let profile = validation_result.profile;
1253                    let secrets = validation_result.secrets;
1254
1255                    let data = SecretSpec {
1256                        #(#load_assignments,)*
1257                    };
1258
1259                    Ok(secretspec::Resolved::new(
1260                        data,
1261                        provider_name,
1262                        profile
1263                    ))
1264                }
1265
1266                pub fn load_profile(mut self) -> Result<secretspec::Resolved<SecretSpecProfile>, secretspec::SecretSpecError> {
1267                    #resolve_provider_profile
1268
1269                    let (profile_str, selected_profile) = if let Some(profile_fn) = self.profile.take() {
1270                        let profile = profile_fn()
1271                            .map_err(|e| secretspec::SecretSpecError::InvalidProfile(e))?;
1272                        (Some(profile.as_str().to_string()), profile)
1273                    } else {
1274                        // Check env var for profile
1275                        let profile_str = std::env::var("SECRETSPEC_PROFILE").ok();
1276                        let selected_profile = if let Some(ref profile_name) = profile_str {
1277                            Profile::try_from(profile_name.as_str())?
1278                        } else {
1279                            Profile::#first_profile_variant
1280                        };
1281                        (profile_str, selected_profile)
1282                    };
1283
1284                    let validation_result = load_internal(provider_str, profile_str)?;
1285                    let provider_name = validation_result.provider_name();
1286                    let profile = validation_result.profile;
1287                    let secrets = validation_result.secrets;
1288
1289                    let data_result: LoadResult<SecretSpecProfile> = match selected_profile {
1290                        #(#load_profile_arms,)*
1291                    };
1292                    let data = data_result?;
1293
1294                    Ok(secretspec::Resolved::new(
1295                        data,
1296                        provider_name,
1297                        profile
1298                    ))
1299                }
1300            }
1301        }
1302    }
1303
1304    /// Generate all builder-related code.
1305    ///
1306    /// Combines all builder components into a complete implementation.
1307    ///
1308    /// # Arguments
1309    ///
1310    /// * `load_assignments` - Field assignments for union loading
1311    /// * `load_profile_arms` - Match arms for profile loading
1312    /// * `first_profile_variant` - Default profile variant
1313    ///
1314    /// # Returns
1315    ///
1316    /// Complete token stream containing:
1317    /// - Builder struct definition
1318    /// - Basic builder methods
1319    /// - Loading methods (load and load_profile)
1320    pub fn generate_all(
1321        load_assignments: &[proc_macro2::TokenStream],
1322        load_profile_arms: &[proc_macro2::TokenStream],
1323        first_profile_variant: &proc_macro2::Ident,
1324    ) -> proc_macro2::TokenStream {
1325        let struct_def = generate_struct();
1326        let basic_methods = generate_basic_methods();
1327        let load_methods =
1328            generate_load_methods(load_assignments, load_profile_arms, first_profile_variant);
1329
1330        quote! {
1331            #struct_def
1332            #basic_methods
1333            #load_methods
1334        }
1335    }
1336}
1337
1338/// Main code generation function.
1339///
1340/// Orchestrates the entire code generation process, coordinating all modules
1341/// to produce the complete macro output.
1342///
1343/// # Arguments
1344///
1345/// * `config` - The validated project configuration
1346///
1347/// # Returns
1348///
1349/// Complete token stream containing all generated code
1350///
1351/// # Generation Process
1352///
1353/// 1. Analyze profiles and field types
1354/// 2. Generate Profile enum and implementations
1355/// 3. Generate SecretSpec struct (union type)
1356/// 4. Generate SecretSpecProfile enum (profile-specific types)
1357/// 5. Generate builder pattern implementation
1358/// 6. Combine all components with necessary imports
1359fn generate_secret_spec_code(config: Config) -> proc_macro2::TokenStream {
1360    // Collect all profiles
1361    let all_profiles: HashSet<String> = config.profiles.keys().cloned().collect();
1362    let profile_variants = get_profile_variants(&all_profiles);
1363
1364    // Analyze field types
1365    let field_info = analyze_field_types(&config);
1366
1367    // Generate field assignments for load()
1368    let load_assignments: Vec<_> = field_info
1369        .values()
1370        .map(|info| info.generate_assignment(quote! { secrets }))
1371        .collect();
1372
1373    // Generate env var setters
1374    let env_setters: Vec<_> = field_info
1375        .values()
1376        .map(|info| info.generate_env_setter())
1377        .collect();
1378
1379    // Generate profile components
1380    let profile_code = profile_generation::generate_all(&profile_variants);
1381
1382    // Generate SecretSpec components
1383    let secret_spec_struct = secret_spec_generation::generate_struct(&field_info);
1384    let profile_enum_variants = secret_spec_generation::generate_profile_enum_variants(
1385        &config,
1386        &field_info,
1387        &profile_variants,
1388    );
1389    let secret_spec_profile_enum =
1390        secret_spec_generation::generate_profile_enum(&profile_enum_variants);
1391    let load_profile_arms =
1392        secret_spec_generation::generate_load_profile_arms(&config, &field_info, &profile_variants);
1393    let load_internal = secret_spec_generation::generate_load_internal();
1394    let secret_spec_impl =
1395        secret_spec_generation::generate_impl(&load_assignments, env_setters, &field_info);
1396
1397    // Get first profile variant for defaults
1398    // Get first profile variant for defaults
1399    let first_profile_variant = profile_variants
1400        .first()
1401        .map(|v| v.as_ident())
1402        .unwrap_or_else(|| format_ident!("Default"));
1403
1404    // Generate builder
1405    let builder_code = builder_generation::generate_all(
1406        &load_assignments,
1407        &load_profile_arms,
1408        &first_profile_variant,
1409    );
1410
1411    // Combine all components
1412    quote! {
1413        #secret_spec_struct
1414        #secret_spec_profile_enum
1415        #profile_code
1416
1417
1418        // Type alias to help with type inference
1419        type LoadResult<T> = Result<T, secretspec::SecretSpecError>;
1420
1421        #load_internal
1422        #builder_code
1423        #secret_spec_impl
1424    }
1425}
1426
1427/// Capitalize the first character of a string.
1428///
1429/// Used to convert profile names to enum variant names.
1430///
1431/// # Arguments
1432///
1433/// * `s` - The string to capitalize
1434///
1435/// # Returns
1436///
1437/// A new string with the first character capitalized
1438///
1439/// # Examples
1440///
1441/// ```ignore
1442/// assert_eq!(capitalize_first("production"), "Production");
1443/// assert_eq!(capitalize_first("test_env"), "Test_env");
1444/// assert_eq!(capitalize_first(""), "");
1445/// ```
1446fn capitalize_first(s: &str) -> String {
1447    let mut chars = s.chars();
1448    match chars.next() {
1449        None => String::new(),
1450        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1451    }
1452}
1453
1454#[cfg(test)]
1455#[path = "tests.rs"]
1456mod tests;