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;