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///     .try_as_utf8_for("SECRET_NAME")?.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///     .try_as_utf8_for("SECRET_NAME")?)
536/// ```
537///
538/// For optional fields:
539/// ```ignore
540/// field_name: source.get("SECRET_NAME").map(|s| s.try_as_utf8_for("SECRET_NAME").map(str::to_owned)).transpose()?
541/// field_name: source.get("SECRET_NAME").map(|s| s.try_as_utf8_for("SECRET_NAME").map(std::path::PathBuf::from)).transpose()?
542/// ```
543///
544/// A value that is not valid UTF-8 fails with `SecretSpecError::SecretNotText`
545/// naming the secret.
546fn generate_secret_assignment(
547    field_name: &proc_macro2::Ident,
548    secret_name: &str,
549    source: proc_macro2::TokenStream,
550    is_optional: bool,
551    as_path: bool,
552) -> proc_macro2::TokenStream {
553    match (is_optional, as_path) {
554        (true, true) => {
555            // Optional PathBuf
556            quote! {
557                #field_name: #source.get(#secret_name)
558                    .map(|s| s.try_as_utf8_for(#secret_name).map(std::path::PathBuf::from))
559                    .transpose()?
560            }
561        }
562        (true, false) => {
563            // Optional String
564            quote! {
565                #field_name: #source.get(#secret_name)
566                    .map(|s| s.try_as_utf8_for(#secret_name).map(str::to_owned))
567                    .transpose()?
568            }
569        }
570        (false, true) => {
571            // Required PathBuf
572            quote! {
573                #field_name: std::path::PathBuf::from(
574                    #source.get(#secret_name)
575                        .ok_or_else(|| secretspec::SecretSpecError::RequiredSecretMissing(#secret_name.to_string()))?
576                        .try_as_utf8_for(#secret_name)?
577                )
578            }
579        }
580        (false, false) => {
581            // Required String
582            quote! {
583                #field_name: #source.get(#secret_name)
584                    .ok_or_else(|| secretspec::SecretSpecError::RequiredSecretMissing(#secret_name.to_string()))?
585                    .try_as_utf8_for(#secret_name)?
586                    .to_string()
587            }
588        }
589    }
590}
591
592/// Build the union struct's fields from the shared IR.
593///
594/// The IR already determined the union field set and each field's
595/// optionality/as_path; this just maps them to `FieldInfo`, keyed and ordered
596/// by name (the IR union is pre-sorted).
597fn union_field_info(ir: &CodegenIr) -> BTreeMap<String, FieldInfo> {
598    ir.union
599        .iter()
600        .map(|field| (field.name.clone(), FieldInfo::from_ir(field)))
601        .collect()
602}
603
604/// Profile variants for enum generation, taken from the shared IR.
605///
606/// The IR's profile list is already sorted and already substitutes a single
607/// `default` profile when the manifest declares none, so this is a direct map.
608fn profile_variants_from_ir(ir: &CodegenIr) -> Vec<ProfileVariant> {
609    ir.profiles
610        .iter()
611        .map(|name| ProfileVariant::new(name.clone()))
612        .collect()
613}
614
615// ===== Profile Generation Module =====
616
617/// Module for generating Profile enum and related implementations.
618///
619/// This module handles:
620/// - Profile enum definition
621/// - TryFrom implementations for string conversion
622/// - as_str() method for profile serialization
623mod profile_generation {
624    use super::*;
625
626    /// Generate just the Profile enum.
627    ///
628    /// Creates an enum with variants for each profile in the configuration.
629    ///
630    /// # Arguments
631    ///
632    /// * `variants` - List of profile variants to generate
633    ///
634    /// # Generated Code Example
635    ///
636    /// ```ignore
637    /// #[derive(Debug, Clone, Copy)]
638    /// pub enum Profile {
639    ///     Development,
640    ///     Production,
641    ///     Staging,
642    /// }
643    /// ```
644    pub fn generate_enum(variants: &[ProfileVariant]) -> proc_macro2::TokenStream {
645        let enum_variants = variants.iter().map(|v| {
646            let ident = v.as_ident();
647            quote! { #ident }
648        });
649
650        quote! {
651            #[derive(Debug, Clone, Copy)]
652            pub enum Profile {
653                #(#enum_variants,)*
654            }
655        }
656    }
657
658    /// Generate TryFrom implementations for Profile.
659    ///
660    /// Creates implementations to convert strings to Profile enum variants,
661    /// supporting both &str and String inputs.
662    ///
663    /// # Arguments
664    ///
665    /// * `variants` - List of profile variants
666    ///
667    /// # Generated Code
668    ///
669    /// - `TryFrom<&str>` implementation with match arms for each profile
670    /// - `TryFrom<String>` implementation that delegates to &str
671    /// - Returns `SecretSpecError::InvalidProfile` for unknown profiles
672    pub fn generate_try_from_impls(variants: &[ProfileVariant]) -> proc_macro2::TokenStream {
673        let from_str_arms = variants.iter().map(|v| {
674            let ident = v.as_ident();
675            let str_val = &v.name;
676            quote! { #str_val => Ok(Profile::#ident) }
677        });
678
679        quote! {
680            impl std::convert::TryFrom<&str> for Profile {
681                type Error = secretspec::SecretSpecError;
682
683                fn try_from(value: &str) -> Result<Self, Self::Error> {
684                    match value {
685                        #(#from_str_arms,)*
686                        _ => Err(secretspec::SecretSpecError::InvalidProfile(value.to_string())),
687                    }
688                }
689            }
690
691            impl std::convert::TryFrom<String> for Profile {
692                type Error = secretspec::SecretSpecError;
693
694                fn try_from(value: String) -> Result<Self, Self::Error> {
695                    Profile::try_from(value.as_str())
696                }
697            }
698        }
699    }
700
701    /// Generate as_str implementation for Profile.
702    ///
703    /// Creates a method to convert Profile enum variants back to their string representation.
704    ///
705    /// # Arguments
706    ///
707    /// * `variants` - List of profile variants
708    ///
709    /// # Generated Code Example
710    ///
711    /// ```ignore
712    /// impl Profile {
713    ///     fn as_str(&self) -> &'static str {
714    ///         match self {
715    ///             Profile::Development => "development",
716    ///             Profile::Production => "production",
717    ///         }
718    ///     }
719    /// }
720    /// ```
721    pub fn generate_as_str_impl(variants: &[ProfileVariant]) -> proc_macro2::TokenStream {
722        let to_str_arms = variants.iter().map(|v| {
723            let ident = v.as_ident();
724            let str_val = &v.name;
725            quote! { Profile::#ident => #str_val }
726        });
727
728        quote! {
729            impl Profile {
730                fn as_str(&self) -> &'static str {
731                    match self {
732                        #(#to_str_arms,)*
733                    }
734                }
735            }
736        }
737    }
738
739    /// Generate all profile-related code.
740    ///
741    /// Combines all profile generation functions into a single token stream.
742    ///
743    /// # Arguments
744    ///
745    /// * `variants` - List of profile variants
746    ///
747    /// # Returns
748    ///
749    /// Complete token stream containing:
750    /// - Profile enum definition
751    /// - TryFrom implementations
752    /// - as_str() method
753    pub fn generate_all(variants: &[ProfileVariant]) -> proc_macro2::TokenStream {
754        let enum_def = generate_enum(variants);
755        let try_from_impls = generate_try_from_impls(variants);
756        let as_str_impl = generate_as_str_impl(variants);
757
758        quote! {
759            #enum_def
760            #try_from_impls
761            #as_str_impl
762        }
763    }
764}
765
766// ===== SecretSpec Generation Module =====
767
768/// Module for generating SecretSpec struct and related implementations.
769///
770/// This module handles:
771/// - SecretSpec struct (union of all secrets)
772/// - SecretSpecProfile enum (profile-specific types)
773/// - Loading implementations
774/// - Environment variable integration
775mod secret_spec_generation {
776    use super::*;
777
778    /// Generate the SecretSpec struct.
779    ///
780    /// Creates a struct containing all secrets from all profiles as fields.
781    /// This is the "union" type that can safely hold secrets from any profile.
782    ///
783    /// # Arguments
784    ///
785    /// * `field_info` - Map of all fields with their type information
786    ///
787    /// # Generated Code Example
788    ///
789    /// ```ignore
790    /// #[derive(Debug, ::secretspec::__private::serde::Serialize, ::secretspec::__private::serde::Deserialize)]
791    /// #[serde(crate = "::secretspec::__private::serde")]
792    /// pub struct SecretSpec {
793    ///     pub database_url: String,
794    ///     pub api_key: Option<String>,
795    ///     pub redis_url: Option<String>,
796    /// }
797    /// ```
798    pub fn generate_struct(field_info: &BTreeMap<String, FieldInfo>) -> proc_macro2::TokenStream {
799        let fields = field_info.values().map(|info| info.generate_struct_field());
800
801        quote! {
802            #[derive(Debug, ::secretspec::__private::serde::Serialize, ::secretspec::__private::serde::Deserialize)]
803            #[serde(crate = "::secretspec::__private::serde")]
804            pub struct SecretSpec {
805                #(#fields,)*
806            }
807        }
808    }
809
810    /// Generate the SecretSpecProfile enum.
811    ///
812    /// Creates an enum where each variant contains only the secrets defined
813    /// for that specific profile. This provides stronger type safety when
814    /// working with profile-specific secrets.
815    ///
816    /// # Arguments
817    ///
818    /// * `profile_variants` - Generated enum variant definitions
819    ///
820    /// # Generated Code Example
821    ///
822    /// ```ignore
823    /// #[derive(Debug, ::secretspec::__private::serde::Serialize, ::secretspec::__private::serde::Deserialize)]
824    /// #[serde(crate = "::secretspec::__private::serde")]
825    /// pub enum SecretSpecProfile {
826    ///     Development {
827    ///         database_url: String,
828    ///         redis_url: Option<String>,
829    ///     },
830    ///     Production {
831    ///         database_url: String,
832    ///         api_key: String,
833    ///         redis_url: String,
834    ///     },
835    /// }
836    /// ```
837    pub fn generate_profile_enum(
838        profile_variants: &[proc_macro2::TokenStream],
839    ) -> proc_macro2::TokenStream {
840        quote! {
841            #[derive(Debug, ::secretspec::__private::serde::Serialize, ::secretspec::__private::serde::Deserialize)]
842            #[serde(crate = "::secretspec::__private::serde")]
843            pub enum SecretSpecProfile {
844                #(#profile_variants,)*
845            }
846        }
847    }
848
849    /// Generate SecretSpecProfile enum variants.
850    ///
851    /// Creates the individual variants for the SecretSpecProfile enum,
852    /// each containing only the fields defined for that profile.
853    ///
854    /// # Arguments
855    ///
856    /// * `config` - The project configuration
857    /// * `field_info` - Field information (used for empty profile case)
858    /// * `variants` - Profile variants to generate
859    ///
860    /// # Returns
861    ///
862    /// Vector of token streams, each representing one enum variant
863    ///
864    /// # Special Cases
865    ///
866    /// - Empty profiles → generates a Default variant with all fields
867    /// - Each profile → generates variant with profile-specific fields
868    pub fn generate_profile_enum_variants(ir: &CodegenIr) -> Vec<proc_macro2::TokenStream> {
869        // The IR's per-profile field sets already handle the empty-profiles case
870        // (a single `default` profile carrying the union), so there is no special
871        // branch here: one variant per IR profile, with that profile's exact
872        // (raw, non-merged) fields.
873        ir.profile_fields
874            .iter()
875            .map(|profile| {
876                let variant_ident = ProfileVariant::new(profile.name.clone()).as_ident();
877                let fields = profile.fields.iter().map(|field| {
878                    let field_name = field_name_ident(&field.name);
879                    let field_type = ir_field_type(field);
880                    quote! { #field_name: #field_type }
881                });
882                quote! {
883                    #variant_ident {
884                        #(#fields,)*
885                    }
886                }
887            })
888            .collect()
889    }
890
891    /// Generate load_profile match arms.
892    ///
893    /// Creates the match arms for loading profile-specific secrets into
894    /// the appropriate SecretSpecProfile variant.
895    ///
896    /// # Arguments
897    ///
898    /// * `config` - The project configuration
899    /// * `field_info` - Field information (for empty profile case)
900    /// * `variants` - Profile variants to generate arms for
901    ///
902    /// # Returns
903    ///
904    /// Vector of match arms for the profile loading logic
905    ///
906    /// # Generated Code Example
907    ///
908    /// ```ignore
909    /// Profile::Production => Ok(SecretSpecProfile::Production {
910    ///     database_url: secrets.get("DATABASE_URL")
911    ///         .ok_or_else(|| SecretSpecError::RequiredSecretMissing("DATABASE_URL".to_string()))?
912    ///         .clone(),
913    ///     api_key: secrets.get("API_KEY").cloned(),
914    /// })
915    /// ```
916    pub fn generate_load_profile_arms(ir: &CodegenIr) -> Vec<proc_macro2::TokenStream> {
917        // One arm per IR profile, assigning that profile's exact fields. The
918        // empty-profiles case is already a single `default` profile in the IR.
919        ir.profile_fields
920            .iter()
921            .map(|profile| {
922                let variant_ident = ProfileVariant::new(profile.name.clone()).as_ident();
923                let assignments = profile.fields.iter().map(|field| {
924                    generate_secret_assignment(
925                        &field_name_ident(&field.name),
926                        &field.name,
927                        quote! { secrets },
928                        field.optional,
929                        field.as_path,
930                    )
931                });
932                quote! {
933                    Profile::#variant_ident => Ok(SecretSpecProfile::#variant_ident {
934                        #(#assignments,)*
935                    })
936                }
937            })
938            .collect()
939    }
940
941    /// Generate the shared load_internal implementation.
942    ///
943    /// Creates a helper function that handles the common loading logic
944    /// for both SecretSpec and SecretSpecProfile loading methods.
945    ///
946    /// # Generated Function
947    ///
948    /// The function:
949    /// 1. Loads the SecretSpec configuration
950    /// 2. Validates it with the given provider and profile
951    /// 3. Converts the values and returns them with their temporary-file owners
952    pub fn generate_load_internal() -> proc_macro2::TokenStream {
953        quote! {
954            fn load_internal<T>(
955                provider_str: Option<String>,
956                profile_str: Option<String>,
957                reason: Option<String>,
958                caller: Option<secretspec::CallerContext>,
959                prompt_missing: bool,
960                convert: impl FnOnce(&std::collections::HashMap<String, secretspec::SecretBytes>) -> Result<T, secretspec::SecretSpecError>,
961            ) -> Result<secretspec::Resolved<T>, secretspec::SecretSpecError> {
962                let mut spec = secretspec::Secrets::load()?;
963                // A typed loader expects the full generated struct shape, so an
964                // ambient `SECRETSPEC_SCOPE` must not silently narrow it below that
965                // shape (which would surface as a spurious `RequiredSecretMissing`).
966                // The untyped CLI/SDK paths keep honoring the env scope.
967                spec.set_ignore_ambient_scope(true);
968                if let Some(provider) = provider_str {
969                    spec.set_provider(provider);
970                }
971                if let Some(profile) = profile_str {
972                    spec.set_profile(profile);
973                }
974                // Apply an explicit builder reason on top of any SECRETSPEC_REASON
975                // already resolved by `Secrets::load`. Required to satisfy the
976                // `require_reason` policy (default "agents") from typed SDK code,
977                // which otherwise has no way to supply a reason. A blank reason is
978                // ignored by `with_reason`, leaving the env-resolved value intact.
979                if let Some(reason) = reason {
980                    spec = spec.with_reason(reason);
981                }
982                if let Some(caller) = caller {
983                    spec = spec.with_caller(caller);
984                }
985                spec.load_typed(prompt_missing, convert)
986            }
987        }
988    }
989
990    /// Generate SecretSpec implementation.
991    ///
992    /// Creates the impl block for SecretSpec with:
993    /// - builder() method for creating a builder
994    /// - load() method for loading with union types
995    /// - set_as_env_vars() method for environment variable integration
996    ///
997    /// # Arguments
998    ///
999    /// * `load_assignments` - Field assignments for the load method
1000    /// * `env_setters` - Environment variable setter statements
1001    /// * `_field_info` - Field information (currently unused)
1002    ///
1003    /// # Generated Methods
1004    ///
1005    /// - `builder()` - Creates a new SecretSpecBuilder
1006    /// - `load()` - Loads secrets with optional provider/profile
1007    /// - `set_as_env_vars()` - Sets all secrets as environment variables
1008    pub fn generate_impl(
1009        load_assignments: &[proc_macro2::TokenStream],
1010        env_setters: Vec<proc_macro2::TokenStream>,
1011        _field_info: &BTreeMap<String, FieldInfo>,
1012    ) -> proc_macro2::TokenStream {
1013        quote! {
1014            impl SecretSpec {
1015                /// Create a new builder for loading secrets
1016                pub fn builder() -> SecretSpecBuilder {
1017                    SecretSpecBuilder::new()
1018                }
1019
1020                /// Load secrets with optional provider and/or profile
1021                /// Provider can be any type that implements Into<String> (e.g., &str, String, etc.)
1022                /// If provider is None, uses SECRETSPEC_PROVIDER env var or global config
1023                /// If profile is None, uses SECRETSPEC_PROFILE env var if set
1024                pub fn load<P>(provider: Option<P>, profile: Option<Profile>) -> Result<secretspec::Resolved<Self>, secretspec::SecretSpecError>
1025                where
1026                    P: Into<String>,
1027                {
1028                    // Convert options to strings
1029                    let provider_str = provider.map(Into::into).or_else(|| std::env::var("SECRETSPEC_PROVIDER").ok());
1030
1031                    let profile_str = match profile {
1032                        Some(p) => Some(p.as_str().to_string()),
1033                        None => std::env::var("SECRETSPEC_PROFILE").ok(),
1034                    };
1035
1036                    // The static `load` has no reason parameter; a reason is supplied
1037                    // via the SECRETSPEC_REASON env var (honored by `Secrets::load`)
1038                    // or through `SecretSpec::builder().with_reason(...)`.
1039                    load_internal(provider_str, profile_str, None, None, false, |secrets| {
1040                        Ok(Self {
1041                            #(#load_assignments,)*
1042                        })
1043                    })
1044                }
1045
1046                pub fn set_as_env_vars(&self) {
1047                    #(#env_setters)*
1048                }
1049            }
1050        }
1051    }
1052}
1053
1054// ===== Builder Generation Module =====
1055
1056/// Module for generating the builder pattern implementation.
1057///
1058/// The builder provides a fluent API for configuring how secrets are loaded,
1059/// with support for:
1060/// - Custom providers (via URIs)
1061/// - Profile selection
1062/// - Type-safe loading (union or profile-specific)
1063mod builder_generation {
1064    use super::*;
1065
1066    /// Generate the builder struct definition.
1067    ///
1068    /// The builder uses boxed closures to defer provider/profile resolution
1069    /// until load time, allowing for flexible configuration.
1070    ///
1071    /// # Generated Struct
1072    ///
1073    /// ```ignore
1074    /// pub struct SecretSpecBuilder {
1075    ///     provider: Option<Box<dyn FnOnce() -> Result<Box<dyn secretspec::Provider>, String>>>,
1076    ///     profile: Option<Box<dyn FnOnce() -> Result<Profile, String>>>,
1077    ///     reason: Option<String>,
1078    ///     caller: Option<secretspec::CallerContext>,
1079    ///     prompt_missing: bool,
1080    /// }
1081    /// ```
1082    pub fn generate_struct() -> proc_macro2::TokenStream {
1083        quote! {
1084            pub struct SecretSpecBuilder {
1085                provider: Option<Box<dyn FnOnce() -> Result<Box<dyn secretspec::Provider>, String>>>,
1086                profile: Option<Box<dyn FnOnce() -> Result<Profile, String>>>,
1087                reason: Option<String>,
1088                caller: Option<secretspec::CallerContext>,
1089                prompt_missing: bool,
1090            }
1091        }
1092    }
1093
1094    /// Generate builder basic methods.
1095    ///
1096    /// Creates the foundational builder methods:
1097    /// - Default implementation
1098    /// - new() constructor
1099    /// - with_provider() for setting provider
1100    /// - with_profile() for setting profile
1101    ///
1102    /// # Type Flexibility
1103    ///
1104    /// Both with_provider and with_profile accept anything that can be
1105    /// converted to the target type (Uri or Profile), providing flexibility:
1106    ///
1107    /// ```ignore
1108    /// builder.with_provider("keyring://")           // &str
1109    ///        .with_provider(Provider::Keyring)      // Provider enum
1110    ///        .with_profile("production")            // &str
1111    ///        .with_profile(Profile::Production)      // Profile enum
1112    /// ```
1113    pub fn generate_basic_methods() -> proc_macro2::TokenStream {
1114        quote! {
1115            impl Default for SecretSpecBuilder {
1116                fn default() -> Self {
1117                    Self::new()
1118                }
1119            }
1120
1121            impl SecretSpecBuilder {
1122                pub fn new() -> Self {
1123                    Self {
1124                        provider: None,
1125                        profile: None,
1126                        reason: None,
1127                        caller: None,
1128                        prompt_missing: false,
1129                    }
1130                }
1131
1132                /// Set a human-readable reason for this session's secret access.
1133                ///
1134                /// Required to satisfy the project's `require_reason` policy
1135                /// (`[project].require_reason` in secretspec.toml, default `"agents"`)
1136                /// when loading from agent contexts, and recorded in the audit log.
1137                /// Mirrors the CLI `--reason` flag and `Secrets::with_reason`. A blank
1138                /// reason is ignored, falling back to the `SECRETSPEC_REASON` env var.
1139                pub fn with_reason<T>(mut self, reason: T) -> Self
1140                where
1141                    T: Into<String>,
1142                {
1143                    self.reason = Some(reason.into());
1144                    self
1145                }
1146
1147                /// Set structured context for the software integration invoking
1148                /// SecretSpec (0.20+). This metadata is audited but never satisfies
1149                /// the `require_reason` policy.
1150                pub fn with_caller(mut self, caller: secretspec::CallerContext) -> Self {
1151                    self.caller = Some(caller);
1152                    self
1153                }
1154
1155                /// Prompt for and store any missing required secrets interactively
1156                /// instead of failing with `RequiredSecretMissing`, mirroring
1157                /// `Secrets::ensure_secrets`. Only prompts when stdin is a real
1158                /// terminal; otherwise falls back to the same error as when this
1159                /// is left unset (the default).
1160                pub fn prompt_missing(mut self, prompt_missing: bool) -> Self {
1161                    self.prompt_missing = prompt_missing;
1162                    self
1163                }
1164
1165                pub fn with_provider<T>(mut self, provider: T) -> Self
1166                where
1167                    T: TryInto<Box<dyn secretspec::Provider>> + 'static,
1168                    T::Error: std::fmt::Display + 'static,
1169                {
1170                    self.provider = Some(Box::new(move || {
1171                        provider.try_into()
1172                            .map_err(|e| format!("Invalid provider: {}", e))
1173                    }));
1174                    self
1175                }
1176
1177                pub fn with_profile<T>(mut self, profile: T) -> Self
1178                where
1179                    T: TryInto<Profile>,
1180                    T::Error: std::fmt::Display
1181                {
1182                    match profile.try_into() {
1183                        Ok(p) => {
1184                            self.profile = Some(Box::new(move || Ok(p)));
1185                        }
1186                        Err(e) => {
1187                            let error_msg = format!("{}", e);
1188                            self.profile = Some(Box::new(move || Err(error_msg)));
1189                        }
1190                    }
1191                    self
1192                }
1193            }
1194        }
1195    }
1196
1197    /// Generate provider resolution logic.
1198    ///
1199    /// Creates code to resolve a provider from the builder's boxed closure.
1200    ///
1201    /// # Arguments
1202    ///
1203    /// * `provider_expr` - Expression to access the provider option
1204    ///
1205    /// # Generated Logic
1206    ///
1207    /// 1. If provider is set, call the closure to get the Provider instance
1208    /// 2. Convert any errors to SecretSpecError
1209    /// 3. Extract the provider name to pass to the loading system
1210    fn generate_provider_resolution(
1211        provider_expr: proc_macro2::TokenStream,
1212    ) -> proc_macro2::TokenStream {
1213        quote! {
1214            let provider_str = if let Some(provider_fn) = #provider_expr {
1215                let provider_box = provider_fn()
1216                    .map_err(|e| secretspec::SecretSpecError::ProviderOperationFailed(e))?;
1217                // Get the full URI to pass as a string to set_provider (preserves vault info)
1218                Some(provider_box.uri())
1219            } else {
1220                None
1221            };
1222        }
1223    }
1224
1225    /// Generate profile resolution logic.
1226    ///
1227    /// Creates code to resolve a profile from the builder's boxed closure.
1228    ///
1229    /// # Arguments
1230    ///
1231    /// * `profile_expr` - Expression to access the profile option
1232    ///
1233    /// # Generated Logic
1234    ///
1235    /// 1. If profile is set, call the closure to get the Profile
1236    /// 2. Convert any errors to SecretSpecError
1237    /// 3. Convert Profile to string for the loading system
1238    fn generate_profile_resolution(
1239        profile_expr: proc_macro2::TokenStream,
1240    ) -> proc_macro2::TokenStream {
1241        quote! {
1242            let profile_str = if let Some(profile_fn) = #profile_expr {
1243                let profile = profile_fn()
1244                    .map_err(|e| secretspec::SecretSpecError::InvalidProfile(e))?;
1245                Some(profile.as_str().to_string())
1246            } else {
1247                None
1248            };
1249        }
1250    }
1251
1252    /// Generate load methods for the builder.
1253    ///
1254    /// Creates two loading methods:
1255    /// - `load()` - Returns SecretSpec (union type)
1256    /// - `load_profile()` - Returns SecretSpecProfile (profile-specific type)
1257    ///
1258    /// # Arguments
1259    ///
1260    /// * `load_assignments` - Field assignments for union type
1261    /// * `load_profile_arms` - Match arms for profile-specific loading
1262    /// * `first_profile_variant` - Default profile if none specified
1263    ///
1264    /// # Key Differences
1265    ///
1266    /// - `load()` returns all secrets with optional fields for safety
1267    /// - `load_profile()` returns only profile-specific secrets with exact types
1268    pub fn generate_load_methods(
1269        load_assignments: &[proc_macro2::TokenStream],
1270        load_profile_arms: &[proc_macro2::TokenStream],
1271        first_profile_variant: &proc_macro2::Ident,
1272    ) -> proc_macro2::TokenStream {
1273        let resolve_provider_load = generate_provider_resolution(quote! { self.provider.take() });
1274        let resolve_profile_load = generate_profile_resolution(quote! { self.profile.take() });
1275        let resolve_provider_profile =
1276            generate_provider_resolution(quote! { self.provider.take() });
1277
1278        quote! {
1279            impl SecretSpecBuilder {
1280                pub fn load(mut self) -> Result<secretspec::Resolved<SecretSpec>, secretspec::SecretSpecError> {
1281                    #resolve_provider_load
1282                    #resolve_profile_load
1283                    let reason_str = self.reason.take();
1284                    let caller = self.caller.take();
1285
1286                    load_internal(
1287                        provider_str,
1288                        profile_str,
1289                        reason_str,
1290                        caller,
1291                        self.prompt_missing,
1292                        |secrets| Ok(SecretSpec {
1293                            #(#load_assignments,)*
1294                        }),
1295                    )
1296                }
1297
1298                pub fn load_profile(mut self) -> Result<secretspec::Resolved<SecretSpecProfile>, secretspec::SecretSpecError> {
1299                    #resolve_provider_profile
1300                    let reason_str = self.reason.take();
1301                    let caller = self.caller.take();
1302
1303                    let (profile_str, selected_profile) = if let Some(profile_fn) = self.profile.take() {
1304                        let profile = profile_fn()
1305                            .map_err(|e| secretspec::SecretSpecError::InvalidProfile(e))?;
1306                        (Some(profile.as_str().to_string()), profile)
1307                    } else {
1308                        // Check env var for profile. A blank value is treated as
1309                        // unset (matching `secretspec::Secrets`) and a padded
1310                        // value is trimmed, so a stray empty var or a `$(cat
1311                        // file)` trailing newline neither errors here nor selects
1312                        // a nonexistent profile.
1313                        let profile_str = std::env::var("SECRETSPEC_PROFILE")
1314                            .ok()
1315                            .map(|s| s.trim().to_string())
1316                            .filter(|s| !s.is_empty());
1317                        let selected_profile = if let Some(ref profile_name) = profile_str {
1318                            Profile::try_from(profile_name.as_str())?
1319                        } else {
1320                            Profile::#first_profile_variant
1321                        };
1322                        (profile_str, selected_profile)
1323                    };
1324
1325                    load_internal(
1326                        provider_str,
1327                        profile_str,
1328                        reason_str,
1329                        caller,
1330                        self.prompt_missing,
1331                        |secrets| match selected_profile {
1332                            #(#load_profile_arms,)*
1333                        },
1334                    )
1335                }
1336            }
1337        }
1338    }
1339
1340    /// Generate all builder-related code.
1341    ///
1342    /// Combines all builder components into a complete implementation.
1343    ///
1344    /// # Arguments
1345    ///
1346    /// * `load_assignments` - Field assignments for union loading
1347    /// * `load_profile_arms` - Match arms for profile loading
1348    /// * `first_profile_variant` - Default profile variant
1349    ///
1350    /// # Returns
1351    ///
1352    /// Complete token stream containing:
1353    /// - Builder struct definition
1354    /// - Basic builder methods
1355    /// - Loading methods (load and load_profile)
1356    pub fn generate_all(
1357        load_assignments: &[proc_macro2::TokenStream],
1358        load_profile_arms: &[proc_macro2::TokenStream],
1359        first_profile_variant: &proc_macro2::Ident,
1360    ) -> proc_macro2::TokenStream {
1361        let struct_def = generate_struct();
1362        let basic_methods = generate_basic_methods();
1363        let load_methods =
1364            generate_load_methods(load_assignments, load_profile_arms, first_profile_variant);
1365
1366        quote! {
1367            #struct_def
1368            #basic_methods
1369            #load_methods
1370        }
1371    }
1372}
1373
1374/// Main code generation function.
1375///
1376/// Orchestrates the entire code generation process, coordinating all modules
1377/// to produce the complete macro output.
1378///
1379/// # Arguments
1380///
1381/// * `config` - The validated project configuration
1382///
1383/// # Returns
1384///
1385/// Complete token stream containing all generated code
1386///
1387/// # Generation Process
1388///
1389/// 1. Analyze profiles and field types
1390/// 2. Generate Profile enum and implementations
1391/// 3. Generate SecretSpec struct (union type)
1392/// 4. Generate SecretSpecProfile enum (profile-specific types)
1393/// 5. Generate builder pattern implementation
1394/// 6. Combine all components with necessary imports
1395fn generate_secret_spec_code(ir: CodegenIr) -> proc_macro2::TokenStream {
1396    let profile_variants = profile_variants_from_ir(&ir);
1397
1398    // Union struct fields.
1399    let field_info = union_field_info(&ir);
1400
1401    // Generate field assignments for load()
1402    let load_assignments: Vec<_> = field_info
1403        .values()
1404        .map(|info| info.generate_assignment(quote! { secrets }))
1405        .collect();
1406
1407    // Generate env var setters
1408    let env_setters: Vec<_> = field_info
1409        .values()
1410        .map(|info| info.generate_env_setter())
1411        .collect();
1412
1413    // Generate profile components
1414    let profile_code = profile_generation::generate_all(&profile_variants);
1415
1416    // Generate SecretSpec components
1417    let secret_spec_struct = secret_spec_generation::generate_struct(&field_info);
1418    let profile_enum_variants = secret_spec_generation::generate_profile_enum_variants(&ir);
1419    let secret_spec_profile_enum =
1420        secret_spec_generation::generate_profile_enum(&profile_enum_variants);
1421    let load_profile_arms = secret_spec_generation::generate_load_profile_arms(&ir);
1422    let load_internal = secret_spec_generation::generate_load_internal();
1423    let secret_spec_impl =
1424        secret_spec_generation::generate_impl(&load_assignments, env_setters, &field_info);
1425
1426    // Get first profile variant for defaults
1427    // Get first profile variant for defaults
1428    let first_profile_variant = profile_variants
1429        .first()
1430        .map(|v| v.as_ident())
1431        .unwrap_or_else(|| format_ident!("Default"));
1432
1433    // Generate builder
1434    let builder_code = builder_generation::generate_all(
1435        &load_assignments,
1436        &load_profile_arms,
1437        &first_profile_variant,
1438    );
1439
1440    // Combine all components
1441    quote! {
1442        #secret_spec_struct
1443        #secret_spec_profile_enum
1444        #profile_code
1445
1446
1447        #load_internal
1448        #builder_code
1449        #secret_spec_impl
1450    }
1451}
1452
1453#[cfg(test)]
1454#[path = "tests.rs"]
1455mod derive_tests;