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 && secret_config.as_path == Some(true)
550 {
551 return true;
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 reason: Option<String>,
1093 ) -> Result<secretspec::ValidatedSecrets, secretspec::SecretSpecError> {
1094 let mut spec = secretspec::Secrets::load()?;
1095 if let Some(provider) = provider_str {
1096 spec.set_provider(provider);
1097 }
1098 if let Some(profile) = profile_str {
1099 spec.set_profile(profile);
1100 }
1101 // Apply an explicit builder reason on top of any SECRETSPEC_REASON
1102 // already resolved by `Secrets::load`. Required to satisfy the
1103 // `require_reason` policy (default "agents") from typed SDK code,
1104 // which otherwise has no way to supply a reason. A blank reason is
1105 // ignored by `with_reason`, leaving the env-resolved value intact.
1106 if let Some(reason) = reason {
1107 spec = spec.with_reason(reason);
1108 }
1109 match spec.validate()? {
1110 Ok(valid_secrets) => Ok(valid_secrets),
1111 Err(validation_errors) => Err(secretspec::SecretSpecError::RequiredSecretMissing(
1112 validation_errors.missing_required.join(", ")
1113 ))
1114 }
1115 }
1116 }
1117 }
1118
1119 /// Generate SecretSpec implementation.
1120 ///
1121 /// Creates the impl block for SecretSpec with:
1122 /// - builder() method for creating a builder
1123 /// - load() method for loading with union types
1124 /// - set_as_env_vars() method for environment variable integration
1125 ///
1126 /// # Arguments
1127 ///
1128 /// * `load_assignments` - Field assignments for the load method
1129 /// * `env_setters` - Environment variable setter statements
1130 /// * `_field_info` - Field information (currently unused)
1131 ///
1132 /// # Generated Methods
1133 ///
1134 /// - `builder()` - Creates a new SecretSpecBuilder
1135 /// - `load()` - Loads secrets with optional provider/profile
1136 /// - `set_as_env_vars()` - Sets all secrets as environment variables
1137 pub fn generate_impl(
1138 load_assignments: &[proc_macro2::TokenStream],
1139 env_setters: Vec<proc_macro2::TokenStream>,
1140 _field_info: &BTreeMap<String, FieldInfo>,
1141 ) -> proc_macro2::TokenStream {
1142 quote! {
1143 impl SecretSpec {
1144 /// Create a new builder for loading secrets
1145 pub fn builder() -> SecretSpecBuilder {
1146 SecretSpecBuilder::new()
1147 }
1148
1149 /// Load secrets with optional provider and/or profile
1150 /// Provider can be any type that implements Into<String> (e.g., &str, String, etc.)
1151 /// If provider is None, uses SECRETSPEC_PROVIDER env var or global config
1152 /// If profile is None, uses SECRETSPEC_PROFILE env var if set
1153 pub fn load<P>(provider: Option<P>, profile: Option<Profile>) -> Result<secretspec::Resolved<Self>, secretspec::SecretSpecError>
1154 where
1155 P: Into<String>,
1156 {
1157 // Convert options to strings
1158 let provider_str = provider.map(Into::into).or_else(|| std::env::var("SECRETSPEC_PROVIDER").ok());
1159
1160 let profile_str = match profile {
1161 Some(p) => Some(p.as_str().to_string()),
1162 None => std::env::var("SECRETSPEC_PROFILE").ok(),
1163 };
1164
1165 // The static `load` has no reason parameter; a reason is supplied
1166 // via the SECRETSPEC_REASON env var (honored by `Secrets::load`)
1167 // or through `SecretSpec::builder().with_reason(...)`.
1168 let validation_result = load_internal(provider_str, profile_str, None)?;
1169 let provider_name = validation_result.resolved.provider.clone();
1170 let profile = validation_result.resolved.profile.clone();
1171 let secrets = validation_result.resolved.secrets;
1172
1173 let data = Self {
1174 #(#load_assignments,)*
1175 };
1176
1177 Ok(secretspec::Resolved::new(
1178 data,
1179 provider_name,
1180 profile
1181 ))
1182 }
1183
1184 pub fn set_as_env_vars(&self) {
1185 #(#env_setters)*
1186 }
1187 }
1188 }
1189 }
1190}
1191
1192// ===== Builder Generation Module =====
1193
1194/// Module for generating the builder pattern implementation.
1195///
1196/// The builder provides a fluent API for configuring how secrets are loaded,
1197/// with support for:
1198/// - Custom providers (via URIs)
1199/// - Profile selection
1200/// - Type-safe loading (union or profile-specific)
1201mod builder_generation {
1202 use super::*;
1203
1204 /// Generate the builder struct definition.
1205 ///
1206 /// The builder uses boxed closures to defer provider/profile resolution
1207 /// until load time, allowing for flexible configuration.
1208 ///
1209 /// # Generated Struct
1210 ///
1211 /// ```ignore
1212 /// pub struct SecretSpecBuilder {
1213 /// provider: Option<Box<dyn FnOnce() -> Result<Box<dyn secretspec::Provider>, String>>>,
1214 /// profile: Option<Box<dyn FnOnce() -> Result<Profile, String>>>,
1215 /// reason: Option<String>,
1216 /// }
1217 /// ```
1218 pub fn generate_struct() -> proc_macro2::TokenStream {
1219 quote! {
1220 pub struct SecretSpecBuilder {
1221 provider: Option<Box<dyn FnOnce() -> Result<Box<dyn secretspec::Provider>, String>>>,
1222 profile: Option<Box<dyn FnOnce() -> Result<Profile, String>>>,
1223 reason: Option<String>,
1224 }
1225 }
1226 }
1227
1228 /// Generate builder basic methods.
1229 ///
1230 /// Creates the foundational builder methods:
1231 /// - Default implementation
1232 /// - new() constructor
1233 /// - with_provider() for setting provider
1234 /// - with_profile() for setting profile
1235 ///
1236 /// # Type Flexibility
1237 ///
1238 /// Both with_provider and with_profile accept anything that can be
1239 /// converted to the target type (Uri or Profile), providing flexibility:
1240 ///
1241 /// ```ignore
1242 /// builder.with_provider("keyring://") // &str
1243 /// .with_provider(Provider::Keyring) // Provider enum
1244 /// .with_profile("production") // &str
1245 /// .with_profile(Profile::Production) // Profile enum
1246 /// ```
1247 pub fn generate_basic_methods() -> proc_macro2::TokenStream {
1248 quote! {
1249 impl Default for SecretSpecBuilder {
1250 fn default() -> Self {
1251 Self::new()
1252 }
1253 }
1254
1255 impl SecretSpecBuilder {
1256 pub fn new() -> Self {
1257 Self {
1258 provider: None,
1259 profile: None,
1260 reason: None,
1261 }
1262 }
1263
1264 /// Set a human-readable reason for this session's secret access.
1265 ///
1266 /// Required to satisfy the project's `require_reason` policy
1267 /// (`[project].require_reason` in secretspec.toml, default `"agents"`)
1268 /// when loading from agent contexts, and recorded in the audit log.
1269 /// Mirrors the CLI `--reason` flag and `Secrets::with_reason`. A blank
1270 /// reason is ignored, falling back to the `SECRETSPEC_REASON` env var.
1271 pub fn with_reason<T>(mut self, reason: T) -> Self
1272 where
1273 T: Into<String>,
1274 {
1275 self.reason = Some(reason.into());
1276 self
1277 }
1278
1279 pub fn with_provider<T>(mut self, provider: T) -> Self
1280 where
1281 T: TryInto<Box<dyn secretspec::Provider>> + 'static,
1282 T::Error: std::fmt::Display + 'static,
1283 {
1284 self.provider = Some(Box::new(move || {
1285 provider.try_into()
1286 .map_err(|e| format!("Invalid provider: {}", e))
1287 }));
1288 self
1289 }
1290
1291 pub fn with_profile<T>(mut self, profile: T) -> Self
1292 where
1293 T: TryInto<Profile>,
1294 T::Error: std::fmt::Display
1295 {
1296 match profile.try_into() {
1297 Ok(p) => {
1298 self.profile = Some(Box::new(move || Ok(p)));
1299 }
1300 Err(e) => {
1301 let error_msg = format!("{}", e);
1302 self.profile = Some(Box::new(move || Err(error_msg)));
1303 }
1304 }
1305 self
1306 }
1307 }
1308 }
1309 }
1310
1311 /// Generate provider resolution logic.
1312 ///
1313 /// Creates code to resolve a provider from the builder's boxed closure.
1314 ///
1315 /// # Arguments
1316 ///
1317 /// * `provider_expr` - Expression to access the provider option
1318 ///
1319 /// # Generated Logic
1320 ///
1321 /// 1. If provider is set, call the closure to get the Provider instance
1322 /// 2. Convert any errors to SecretSpecError
1323 /// 3. Extract the provider name to pass to the loading system
1324 fn generate_provider_resolution(
1325 provider_expr: proc_macro2::TokenStream,
1326 ) -> proc_macro2::TokenStream {
1327 quote! {
1328 let provider_str = if let Some(provider_fn) = #provider_expr {
1329 let provider_box = provider_fn()
1330 .map_err(|e| secretspec::SecretSpecError::ProviderOperationFailed(e))?;
1331 // Get the full URI to pass as a string to set_provider (preserves vault info)
1332 Some(provider_box.uri())
1333 } else {
1334 None
1335 };
1336 }
1337 }
1338
1339 /// Generate profile resolution logic.
1340 ///
1341 /// Creates code to resolve a profile from the builder's boxed closure.
1342 ///
1343 /// # Arguments
1344 ///
1345 /// * `profile_expr` - Expression to access the profile option
1346 ///
1347 /// # Generated Logic
1348 ///
1349 /// 1. If profile is set, call the closure to get the Profile
1350 /// 2. Convert any errors to SecretSpecError
1351 /// 3. Convert Profile to string for the loading system
1352 fn generate_profile_resolution(
1353 profile_expr: proc_macro2::TokenStream,
1354 ) -> proc_macro2::TokenStream {
1355 quote! {
1356 let profile_str = if let Some(profile_fn) = #profile_expr {
1357 let profile = profile_fn()
1358 .map_err(|e| secretspec::SecretSpecError::InvalidProfile(e))?;
1359 Some(profile.as_str().to_string())
1360 } else {
1361 None
1362 };
1363 }
1364 }
1365
1366 /// Generate load methods for the builder.
1367 ///
1368 /// Creates two loading methods:
1369 /// - `load()` - Returns SecretSpec (union type)
1370 /// - `load_profile()` - Returns SecretSpecProfile (profile-specific type)
1371 ///
1372 /// # Arguments
1373 ///
1374 /// * `load_assignments` - Field assignments for union type
1375 /// * `load_profile_arms` - Match arms for profile-specific loading
1376 /// * `first_profile_variant` - Default profile if none specified
1377 ///
1378 /// # Key Differences
1379 ///
1380 /// - `load()` returns all secrets with optional fields for safety
1381 /// - `load_profile()` returns only profile-specific secrets with exact types
1382 pub fn generate_load_methods(
1383 load_assignments: &[proc_macro2::TokenStream],
1384 load_profile_arms: &[proc_macro2::TokenStream],
1385 first_profile_variant: &proc_macro2::Ident,
1386 ) -> proc_macro2::TokenStream {
1387 let resolve_provider_load = generate_provider_resolution(quote! { self.provider.take() });
1388 let resolve_profile_load = generate_profile_resolution(quote! { self.profile.take() });
1389 let resolve_provider_profile =
1390 generate_provider_resolution(quote! { self.provider.take() });
1391
1392 quote! {
1393 impl SecretSpecBuilder {
1394 pub fn load(mut self) -> Result<secretspec::Resolved<SecretSpec>, secretspec::SecretSpecError> {
1395 #resolve_provider_load
1396 #resolve_profile_load
1397 let reason_str = self.reason.take();
1398
1399 let validation_result = load_internal(provider_str, profile_str, reason_str)?;
1400 let provider_name = validation_result.resolved.provider.clone();
1401 let profile = validation_result.resolved.profile.clone();
1402 let secrets = validation_result.resolved.secrets;
1403
1404 let data = SecretSpec {
1405 #(#load_assignments,)*
1406 };
1407
1408 Ok(secretspec::Resolved::new(
1409 data,
1410 provider_name,
1411 profile
1412 ))
1413 }
1414
1415 pub fn load_profile(mut self) -> Result<secretspec::Resolved<SecretSpecProfile>, secretspec::SecretSpecError> {
1416 #resolve_provider_profile
1417 let reason_str = self.reason.take();
1418
1419 let (profile_str, selected_profile) = if let Some(profile_fn) = self.profile.take() {
1420 let profile = profile_fn()
1421 .map_err(|e| secretspec::SecretSpecError::InvalidProfile(e))?;
1422 (Some(profile.as_str().to_string()), profile)
1423 } else {
1424 // Check env var for profile
1425 let profile_str = std::env::var("SECRETSPEC_PROFILE").ok();
1426 let selected_profile = if let Some(ref profile_name) = profile_str {
1427 Profile::try_from(profile_name.as_str())?
1428 } else {
1429 Profile::#first_profile_variant
1430 };
1431 (profile_str, selected_profile)
1432 };
1433
1434 let validation_result = load_internal(provider_str, profile_str, reason_str)?;
1435 let provider_name = validation_result.resolved.provider.clone();
1436 let profile = validation_result.resolved.profile.clone();
1437 let secrets = validation_result.resolved.secrets;
1438
1439 let data_result: LoadResult<SecretSpecProfile> = match selected_profile {
1440 #(#load_profile_arms,)*
1441 };
1442 let data = data_result?;
1443
1444 Ok(secretspec::Resolved::new(
1445 data,
1446 provider_name,
1447 profile
1448 ))
1449 }
1450 }
1451 }
1452 }
1453
1454 /// Generate all builder-related code.
1455 ///
1456 /// Combines all builder components into a complete implementation.
1457 ///
1458 /// # Arguments
1459 ///
1460 /// * `load_assignments` - Field assignments for union loading
1461 /// * `load_profile_arms` - Match arms for profile loading
1462 /// * `first_profile_variant` - Default profile variant
1463 ///
1464 /// # Returns
1465 ///
1466 /// Complete token stream containing:
1467 /// - Builder struct definition
1468 /// - Basic builder methods
1469 /// - Loading methods (load and load_profile)
1470 pub fn generate_all(
1471 load_assignments: &[proc_macro2::TokenStream],
1472 load_profile_arms: &[proc_macro2::TokenStream],
1473 first_profile_variant: &proc_macro2::Ident,
1474 ) -> proc_macro2::TokenStream {
1475 let struct_def = generate_struct();
1476 let basic_methods = generate_basic_methods();
1477 let load_methods =
1478 generate_load_methods(load_assignments, load_profile_arms, first_profile_variant);
1479
1480 quote! {
1481 #struct_def
1482 #basic_methods
1483 #load_methods
1484 }
1485 }
1486}
1487
1488/// Main code generation function.
1489///
1490/// Orchestrates the entire code generation process, coordinating all modules
1491/// to produce the complete macro output.
1492///
1493/// # Arguments
1494///
1495/// * `config` - The validated project configuration
1496///
1497/// # Returns
1498///
1499/// Complete token stream containing all generated code
1500///
1501/// # Generation Process
1502///
1503/// 1. Analyze profiles and field types
1504/// 2. Generate Profile enum and implementations
1505/// 3. Generate SecretSpec struct (union type)
1506/// 4. Generate SecretSpecProfile enum (profile-specific types)
1507/// 5. Generate builder pattern implementation
1508/// 6. Combine all components with necessary imports
1509fn generate_secret_spec_code(config: Config) -> proc_macro2::TokenStream {
1510 // Collect all profiles
1511 let all_profiles: HashSet<String> = config.profiles.keys().cloned().collect();
1512 let profile_variants = get_profile_variants(&all_profiles);
1513
1514 // Analyze field types
1515 let field_info = analyze_field_types(&config);
1516
1517 // Generate field assignments for load()
1518 let load_assignments: Vec<_> = field_info
1519 .values()
1520 .map(|info| info.generate_assignment(quote! { secrets }))
1521 .collect();
1522
1523 // Generate env var setters
1524 let env_setters: Vec<_> = field_info
1525 .values()
1526 .map(|info| info.generate_env_setter())
1527 .collect();
1528
1529 // Generate profile components
1530 let profile_code = profile_generation::generate_all(&profile_variants);
1531
1532 // Generate SecretSpec components
1533 let secret_spec_struct = secret_spec_generation::generate_struct(&field_info);
1534 let profile_enum_variants = secret_spec_generation::generate_profile_enum_variants(
1535 &config,
1536 &field_info,
1537 &profile_variants,
1538 );
1539 let secret_spec_profile_enum =
1540 secret_spec_generation::generate_profile_enum(&profile_enum_variants);
1541 let load_profile_arms =
1542 secret_spec_generation::generate_load_profile_arms(&config, &field_info, &profile_variants);
1543 let load_internal = secret_spec_generation::generate_load_internal();
1544 let secret_spec_impl =
1545 secret_spec_generation::generate_impl(&load_assignments, env_setters, &field_info);
1546
1547 // Get first profile variant for defaults
1548 // Get first profile variant for defaults
1549 let first_profile_variant = profile_variants
1550 .first()
1551 .map(|v| v.as_ident())
1552 .unwrap_or_else(|| format_ident!("Default"));
1553
1554 // Generate builder
1555 let builder_code = builder_generation::generate_all(
1556 &load_assignments,
1557 &load_profile_arms,
1558 &first_profile_variant,
1559 );
1560
1561 // Combine all components
1562 quote! {
1563 use ::secrecy::ExposeSecret;
1564
1565 #secret_spec_struct
1566 #secret_spec_profile_enum
1567 #profile_code
1568
1569
1570 // Type alias to help with type inference
1571 type LoadResult<T> = Result<T, secretspec::SecretSpecError>;
1572
1573 #load_internal
1574 #builder_code
1575 #secret_spec_impl
1576 }
1577}
1578
1579/// Capitalize the first character of a string.
1580///
1581/// Used to convert profile names to enum variant names.
1582///
1583/// # Arguments
1584///
1585/// * `s` - The string to capitalize
1586///
1587/// # Returns
1588///
1589/// A new string with the first character capitalized
1590///
1591/// # Examples
1592///
1593/// ```ignore
1594/// assert_eq!(capitalize_first("production"), "Production");
1595/// assert_eq!(capitalize_first("test_env"), "Test_env");
1596/// assert_eq!(capitalize_first(""), "");
1597/// ```
1598fn capitalize_first(s: &str) -> String {
1599 let mut chars = s.chars();
1600 match chars.next() {
1601 None => String::new(),
1602 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1603 }
1604}
1605
1606#[cfg(test)]
1607#[path = "tests.rs"]
1608mod tests;