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