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