Skip to content

Commit 2d1e1d9

Browse files
domenkozarclaude
andcommitted
feat: implement builder pattern for SecretSpec with deferred conversions
- Add SecretSpecBuilder generated by define_secrets\! macro - Builder methods accept TryInto<Uri> for providers and TryInto<Profile> for profiles - All conversions and error handling deferred to load() and load_profile() methods - Remove old load_as_profile() static method in favor of builder.load_profile() - Store unresolved conversions as closures until load time - Update documentation and examples to use new builder pattern BREAKING CHANGE: SecretSpec::load_as_profile() removed, use SecretSpec::builder().load_profile() instead 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent c609bf6 commit 2d1e1d9

File tree

10 files changed

+560
-98
lines changed

10 files changed

+560
-98
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/src/content/docs/sdk/rust.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ Basic example:
2121
secretspec::define_secrets!("secretspec.toml");
2222

2323
fn main() -> Result<(), Box<dyn std::error::Error>> {
24-
// Load secrets with type-safe struct
25-
let secretspec = SecretSpec::load(
26-
Some(secretspec::Provider::Keyring),
27-
None // Use default profile
28-
)?;
24+
// Load secrets using the builder pattern
25+
let secretspec = SecretSpec::builder()
26+
.with_provider("keyring") // Can use provider name or URI like "dotenv:/path/to/.env"
27+
.with_profile("development") // Can use string or Profile enum
28+
.load()?; // All conversions and errors are handled here
2929

3030
// Access secrets (field names are lowercased)
3131
println!("Database: {}", secretspec.secrets.database_url); // DATABASE_URL → database_url
@@ -46,19 +46,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
4646
}
4747
```
4848

49-
## Loading with Profile Information
49+
## Loading with Profile-Specific Types
5050

51-
The `load_as_profile` method provides profile-specific types for your secrets:
51+
The `load_profile()` method on the builder provides profile-specific types for your secrets:
5252

5353
```rust
5454
secretspec::define_secrets!("secretspec.toml");
5555

5656
fn main() -> Result<(), Box<dyn std::error::Error>> {
5757
// Load secrets with profile-specific types
58-
let secretspec = SecretSpec::load_as_profile(
59-
Some(secretspec::Provider::Keyring),
60-
Some(Profile::Production)
61-
)?;
58+
let secretspec = SecretSpec::builder()
59+
.with_provider("keyring")
60+
.with_profile(Profile::Production)
61+
.load_profile()?;
6262

6363
// Access profile and provider information
6464
println!("Loaded profile: {}", secretspec.profile);

examples/derive/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ version = "0.1.0"
44
edition = "2021"
55

66
[dependencies]
7-
secretspec = { path = "../..", features = ["macros"] }
7+
secretspec = { path = "../..", features = ["macros"] }
8+
http = "1.0"

examples/derive/src/main.rs

Lines changed: 85 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
88
// Create a .env file for testing
99
std::fs::write(
1010
".env",
11-
"DATABASE_URL=postgres://localhost/testdb\nAPI_KEY=test-key-123\n",
11+
"DATABASE_URL=postgres://localhost/testdb\nAPI_KEY=test-key-123\nREDIS_URL=redis://localhost:6379\n",
1212
)?;
1313

14-
// Example 1: Load with union types (safe for any profile)
15-
println!("1. Loading secrets with union types:");
16-
match SecretSpec::load(Some(Provider::Dotenv), None) {
14+
// Example 1: Load with builder pattern
15+
println!("1. Loading secrets with builder pattern:");
16+
match SecretSpec::builder().with_provider("dotenv").load() {
1717
Ok(result) => {
1818
println!(
1919
" ✓ Loaded successfully using provider: {:?}, profile: {}",
@@ -40,51 +40,72 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
4040
}
4141
}
4242

43-
// Example 2: Load development profile with exact types
44-
println!("\n2. Loading development profile:");
45-
match SecretSpec::load_as_profile(Some(Provider::Dotenv), Some(Profile::Development)) {
43+
// Example 2: Load with specific profile
44+
println!("\n2. Loading with specific profile:");
45+
match SecretSpec::builder()
46+
.with_provider("dotenv")
47+
.with_profile(Profile::Development)
48+
.load()
49+
{
4650
Ok(result) => {
4751
println!(
4852
" ✓ Loaded using provider: {:?}, profile: {}",
4953
result.provider, result.profile
5054
);
51-
match result.secrets {
52-
SecretSpecProfile::Development {
53-
database_url,
54-
api_key,
55-
redis_url,
56-
log_level,
57-
} => {
58-
println!(" ✓ Got development profile");
59-
// In development profile, both database_url and api_key have defaults
60-
if let Some(url) = database_url {
61-
println!(" - Database URL: {}", url);
62-
}
63-
if let Some(key) = api_key {
64-
println!(" - API Key: {}", key);
65-
}
66-
if let Some(url) = redis_url {
67-
println!(" - Redis URL: {}", url);
68-
}
69-
if let Some(level) = log_level {
70-
println!(" - Log Level: {}", level);
71-
}
72-
}
73-
SecretSpecProfile::Default { .. } => {
74-
println!(" ✗ Got default profile instead of development");
75-
}
76-
SecretSpecProfile::Production { .. } => {
77-
println!(" ✗ Got production profile instead of development");
78-
}
55+
let secrets = &result.secrets;
56+
if let Some(database_url) = &secrets.database_url {
57+
println!(" - Database URL: {}", database_url);
58+
}
59+
if let Some(api_key) = &secrets.api_key {
60+
println!(" - API Key: {} (found)", api_key);
61+
} else {
62+
println!(" - API Key: None");
63+
}
64+
if let Some(redis_url) = &secrets.redis_url {
65+
println!(" - Redis URL: {}", redis_url);
66+
}
67+
if let Some(log_level) = &secrets.log_level {
68+
println!(" - Log Level: {}", log_level);
7969
}
8070
}
8171
Err(e) => {
8272
println!(" ✗ Failed to load development profile: {}", e);
8373
}
8474
}
8575

86-
println!("\n3. Setting secrets as environment variables:");
87-
if let Ok(result) = SecretSpec::load(Some(Provider::Dotenv), None) {
76+
// Example 3: Using string profile
77+
println!("\n3. Loading with string profile:");
78+
match SecretSpec::builder()
79+
.with_provider("dotenv")
80+
.with_profile("production")
81+
.load()
82+
{
83+
Ok(result) => {
84+
println!(" ✓ Loaded with string profile successfully");
85+
println!(
86+
" - Provider: {:?}, Profile: {}",
87+
result.provider, result.profile
88+
);
89+
}
90+
Err(e) => {
91+
println!(" ✗ Failed to load with string profile: {}", e);
92+
}
93+
}
94+
95+
// Example 4: Using provider URIs
96+
println!("\n4. Loading with provider URI:");
97+
match SecretSpec::builder().with_provider("dotenv:.env").load() {
98+
Ok(result) => {
99+
println!(" ✓ Loaded with URI successfully");
100+
println!(" - Provider: {:?}", result.provider);
101+
}
102+
Err(e) => {
103+
println!(" ✗ Failed to load with URI: {}", e);
104+
}
105+
}
106+
107+
println!("\n5. Setting secrets as environment variables:");
108+
if let Ok(result) = SecretSpec::builder().with_provider("dotenv").load() {
88109
result.secrets.set_as_env_vars();
89110
println!(" ✓ Set all secrets as environment variables");
90111

@@ -96,6 +117,33 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
96117
println!(" - API_KEY env: {:?}", std::env::var("API_KEY").ok());
97118
}
98119

120+
// Example 6: Loading profile-specific types
121+
println!("\n6. Loading profile-specific types:");
122+
match SecretSpec::builder()
123+
.with_provider("dotenv")
124+
.with_profile("production")
125+
.load_profile()
126+
{
127+
Ok(result) => {
128+
println!(" ✓ Loaded profile-specific types");
129+
match result.secrets {
130+
SecretSpecProfile::Production {
131+
database_url,
132+
api_key,
133+
..
134+
} => {
135+
println!(" - Production secrets are strongly typed");
136+
println!(" - Database URL: {}", database_url); // String, not Option<String>
137+
println!(" - API Key: {}", api_key); // String, not Option<String>
138+
}
139+
_ => println!(" - Got different profile"),
140+
}
141+
}
142+
Err(e) => {
143+
println!(" ✗ Failed to load profile: {}", e);
144+
}
145+
}
146+
99147
// Clean up
100148
std::fs::remove_file(".env").ok();
101149

secretspec-derive/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ secretspec-types = { path = "../secretspec-types" }
1717
[dev-dependencies]
1818
trybuild = "1.0"
1919
secretspec = { path = ".." }
20-
insta = "1.34"
20+
insta = "1.34"
21+
http = "1.0"

0 commit comments

Comments
 (0)