Skip to main content

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