Skip to content

Commit d75b549

Browse files
domenkozarclaude
andcommitted
refactor: implement URI-based provider configuration
This commit introduces a major refactoring of the provider system to support flexible URI-based configuration. Providers can now be specified as simple names (e.g., "keyring") or as URIs with configuration (e.g., "1password://vault"). Key changes: - Add ProviderRegistry with centralized provider info and creation - Convert all providers to support URI-based configuration - Add config structs for each provider with from_uri() methods - Support provider URIs like: - 1password://account@vault - 1password+token://service-account-token@vault - dotenv:/path/to/.env - lastpass://folder - Update CLI to show provider examples in selection menu - Add comprehensive tests for URI parsing and provider creation - Remove stateful provider instances from SecretSpec This enables more flexible provider configuration while maintaining backward compatibility with simple provider names. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 567476d commit d75b549

File tree

13 files changed

+598
-124
lines changed

13 files changed

+598
-124
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ secretspec-derive = { path = "./secretspec-derive", optional = true }
3131
secretspec-types = { path = "./secretspec-types" }
3232
serde_json = "1.0"
3333
tempfile = "3.0"
34+
http = "1.0"
3435

3536
[features]
3637
default = ["macros"]

README.md

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@ This separation enables:
4747

4848
## Features
4949

50-
- **Declarative Configuration**: Define your secrets in `secretspec.toml` with descriptions and requirements
51-
- **Multiple Provider Backends**: [Keyring](https://docs.rs/keyring/latest/keyring/) (system credential store), [.env](https://www.dotenv.org/), and environment variable support
52-
- **Type-Safe Rust SDK**: Generate strongly-typed structs from your `secretspec.toml` for compile-time safety
53-
- **Profile Support**: Override secret requirements and defaults per profile (development, production, etc.)
54-
- **Configuration Inheritance**: Extend and override shared configurations using the `extends` feature
50+
- **[Declarative Configuration](https://secretspec.dev/docs/reference/configuration/)**: Define your secrets in `secretspec.toml` with descriptions and requirements
51+
- **[Multiple Provider Backends](https://secretspec.dev/docs/concepts/providers/)**: [Keyring](https://docs.rs/keyring/latest/keyring/) (system credential store), [.env](https://www.dotenv.org/), and environment variable support
52+
- **[Type-Safe Rust SDK](https://secretspec.dev/docs/sdk/rust/)**: Generate strongly-typed structs from your `secretspec.toml` for compile-time safety
53+
- **[Profile Support](https://secretspec.dev/docs/concepts/profiles/)**: Override secret requirements and defaults per profile (development, production, etc.)
54+
- **[Configuration Inheritance](https://secretspec.dev/docs/concepts/inheritance/)**: Extend and override shared configurations using the `extends` feature
5555
- **Discovery**: `secretspec init` to discover secrets from existing `.env` files
5656

5757
## Quick Start
@@ -258,12 +258,14 @@ SecretSpec provider can be configured through three methods (in order of precede
258258
SecretSpec includes five built-in provider backends:
259259

260260
- **keyring** - Secure system credential store integration
261-
- **dotenv** - Local .env file storage
261+
- **dotenv** - Local .env file storage (e.g., `dotenv:/path/to/.env`)
262262
- **env** - Read-only environment variable access
263-
- **lastpass** - LastPass password manager integration
264-
- **1password** - 1Password secrets management
263+
- **lastpass** - LastPass password manager integration (e.g., `lastpass://folder`)
264+
- **1password** - 1Password secrets management (e.g., `1password://vault`, `1password://work@Production`)
265265

266-
*Additional provider backends are welcome!**
266+
Providers can be specified as simple names (`keyring`) or as URIs with configuration (`1password://vault`).
267+
268+
*Additional provider backends are welcome!*
267269

268270
### Keyring Provider (Recommended)
269271

@@ -450,12 +452,13 @@ The macro generates several types based on your `secretspec.toml`:
450452

451453
## Adding a New Provider Backend
452454

453-
To implement a new provider backend in this repository:
455+
To implement a new provider backend:
454456

455457
1. **Create a new backend module** in `src/provider/your_backend.rs`:
456458
```rust
457-
use crate::Result;
458-
use super::Provider;
459+
use crate::{Result, SecretSpecError};
460+
use crate::provider::Provider;
461+
use http::Uri;
459462

460463
pub struct YourBackendProvider {
461464
// Your backend-specific configuration
@@ -467,6 +470,22 @@ To implement a new provider backend in this repository:
467470
// Initialize your configuration
468471
}
469472
}
473+
474+
pub fn from_uri(uri: &Uri) -> Result<Self> {
475+
let scheme = uri.scheme_str()
476+
.ok_or_else(|| SecretSpecError::ProviderOperationFailed("URI must have a scheme".to_string()))?;
477+
478+
if scheme != "your_backend" {
479+
return Err(SecretSpecError::ProviderOperationFailed(
480+
format!("Invalid scheme '{}' for your_backend provider", scheme)
481+
));
482+
}
483+
484+
// Parse any configuration from the URI
485+
// e.g., authority, path, query parameters
486+
487+
Ok(Self::new())
488+
}
470489
}
471490

472491
impl Provider for YourBackendProvider {
@@ -493,22 +512,44 @@ To implement a new provider backend in this repository:
493512
}
494513
```
495514

496-
2. **Register your backend** in `src/provider/mod.rs`:
515+
2. **Add your provider to the registry** in `src/provider/registry.rs`:
516+
```rust
517+
impl ProviderRegistry {
518+
pub fn providers() -> Vec<ProviderInfo> {
519+
vec![
520+
// ... existing providers ...
521+
ProviderInfo {
522+
name: "your_backend",
523+
description: "Your backend description",
524+
examples: vec!["your_backend://example"],
525+
},
526+
]
527+
}
528+
529+
pub fn create_from_string(s: &str) -> Result<Box<dyn Provider>> {
530+
// ... existing code ...
531+
match scheme {
532+
// ... existing providers ...
533+
"your_backend" => Ok(Box::new(YourBackendProvider::from_uri(&uri)?)),
534+
// ...
535+
}
536+
}
537+
}
538+
```
539+
540+
3. **Export your module** in `src/provider/mod.rs`:
497541
```rust
498-
// Add to module exports
499542
pub mod your_backend;
500543
pub use your_backend::YourBackendProvider;
501-
502-
// Add to ProviderRegistry::new()
503-
backends.insert(
504-
"your_backend".to_string(),
505-
Box::new(YourBackendProvider::new()) as Box<dyn Provider>,
506-
);
507544
```
508545

509-
3. **Use your new backend**:
546+
4. **Use your new backend**:
510547
```bash
548+
# Simple usage
511549
$ secretspec set SECRET_NAME --provider your_backend
550+
551+
# With URI configuration
552+
$ secretspec set SECRET_NAME --provider "your_backend://config"
512553
```
513554

514555
## License

src/lib.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -160,15 +160,13 @@ pub fn generate_toml_with_comments(config: &ProjectConfig) -> Result<String> {
160160
}
161161

162162
pub struct SecretSpec {
163-
registry: ProviderRegistry,
164163
config: ProjectConfig,
165164
global_config: Option<GlobalConfig>,
166165
}
167166

168167
impl SecretSpec {
169168
pub fn new(config: ProjectConfig, global_config: Option<GlobalConfig>) -> Self {
170169
Self {
171-
registry: ProviderRegistry::new(),
172170
config,
173171
global_config,
174172
}
@@ -204,9 +202,9 @@ impl SecretSpec {
204202
fn get_provider_backend(
205203
&self,
206204
provider_arg: Option<String>,
207-
) -> Result<(String, &Box<dyn ProviderTrait>)> {
208-
let provider_name = if let Some(name) = provider_arg {
209-
name
205+
) -> Result<(String, Box<dyn ProviderTrait>)> {
206+
let provider_spec = if let Some(spec) = provider_arg {
207+
spec
210208
} else if let Some(global_config) = &self.global_config {
211209
global_config
212210
.projects
@@ -217,10 +215,10 @@ impl SecretSpec {
217215
return Err(SecretSpecError::NoProviderConfigured);
218216
};
219217

220-
let backend = self
221-
.registry
222-
.get(&provider_name)
223-
.ok_or_else(|| SecretSpecError::ProviderNotFound(provider_name.clone()))?;
218+
let backend = ProviderRegistry::create_from_string(&provider_spec)?;
219+
220+
// Extract the provider name for display purposes
221+
let provider_name = backend.name().to_string();
224222

225223
Ok((provider_name, backend))
226224
}

src/main.rs

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -173,16 +173,10 @@ fn main() -> Result<()> {
173173
use inquire::Select;
174174
use secretspec::provider::ProviderRegistry;
175175

176-
// Get providers from registry
177-
let registry = ProviderRegistry::new();
178-
let mut providers = registry.list_providers();
179-
// Sort providers by name for consistent display
180-
providers.sort_by_key(|(name, _)| *name);
181-
182-
// Create provider choices with descriptions
183-
let provider_choices: Vec<String> = providers
184-
.iter()
185-
.map(|(name, provider)| format!("{}: {}", name, provider.description()))
176+
// Get provider choices from the centralized registry
177+
let provider_choices: Vec<String> = ProviderRegistry::providers()
178+
.into_iter()
179+
.map(|info| info.display_with_examples())
186180
.collect();
187181

188182
let selected_choice =

src/provider/dotenv.rs

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,72 @@
11
use super::Provider;
2-
use crate::Result;
2+
use crate::{Result, SecretSpecError};
3+
use http::Uri;
4+
use serde::{Deserialize, Serialize};
35
use std::collections::HashMap;
46
use std::fs;
57
use std::path::PathBuf;
68

9+
#[derive(Debug, Clone, Serialize, Deserialize)]
10+
pub struct DotEnvConfig {
11+
pub path: PathBuf,
12+
}
13+
14+
impl Default for DotEnvConfig {
15+
fn default() -> Self {
16+
Self {
17+
path: PathBuf::from(".env"),
18+
}
19+
}
20+
}
21+
22+
impl DotEnvConfig {
23+
pub fn from_uri(uri: &Uri) -> Result<Self> {
24+
let scheme = uri.scheme_str().ok_or_else(|| {
25+
SecretSpecError::ProviderOperationFailed("URI must have a scheme".to_string())
26+
})?;
27+
28+
if scheme != "dotenv" {
29+
return Err(SecretSpecError::ProviderOperationFailed(format!(
30+
"Invalid scheme '{}' for dotenv provider",
31+
scheme
32+
)));
33+
}
34+
35+
// Extract path from URI, default to .env if not specified
36+
let path = uri.path().trim_start_matches('/');
37+
let path = if path.is_empty() || path == "/" {
38+
".env"
39+
} else {
40+
path
41+
};
42+
43+
Ok(Self {
44+
path: PathBuf::from(path),
45+
})
46+
}
47+
}
48+
749
pub struct DotEnvProvider {
8-
dotenv_path: PathBuf,
50+
config: DotEnvConfig,
951
}
1052

1153
impl DotEnvProvider {
12-
pub fn new(dotenv_path: PathBuf) -> Self {
13-
Self { dotenv_path }
54+
pub fn new(config: DotEnvConfig) -> Self {
55+
Self { config }
56+
}
57+
58+
pub fn from_uri(uri: &Uri) -> Result<Self> {
59+
let config = DotEnvConfig::from_uri(uri)?;
60+
Ok(Self::new(config))
1461
}
1562

1663
fn load_env_vars(&self) -> Result<HashMap<String, String>> {
17-
if !self.dotenv_path.exists() {
64+
if !self.config.path.exists() {
1865
return Ok(HashMap::new());
1966
}
2067

2168
let mut vars = HashMap::new();
22-
let env_vars = dotenvy::from_path_iter(&self.dotenv_path)?;
69+
let env_vars = dotenvy::from_path_iter(&self.config.path)?;
2370
for item in env_vars {
2471
let (key, value) = item?;
2572
vars.insert(key, value);
@@ -32,7 +79,7 @@ impl DotEnvProvider {
3279
for (key, value) in vars {
3380
content.push_str(&format!("{}={}\n", key, value));
3481
}
35-
fs::write(&self.dotenv_path, content)?;
82+
fs::write(&self.config.path, content)?;
3683
Ok(())
3784
}
3885
}

src/provider/env.rs

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,41 @@
11
use super::Provider;
2-
use crate::Result;
2+
use crate::{Result, SecretSpecError};
3+
use http::Uri;
4+
use serde::{Deserialize, Serialize};
35
use std::env;
46

5-
pub struct EnvProvider;
7+
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8+
pub struct EnvConfig {}
9+
10+
impl EnvConfig {
11+
pub fn from_uri(uri: &Uri) -> Result<Self> {
12+
let scheme = uri.scheme_str().ok_or_else(|| {
13+
SecretSpecError::ProviderOperationFailed("URI must have a scheme".to_string())
14+
})?;
15+
16+
if scheme != "env" {
17+
return Err(SecretSpecError::ProviderOperationFailed(format!(
18+
"Invalid scheme '{}' for env provider",
19+
scheme
20+
)));
21+
}
22+
23+
Ok(Self::default())
24+
}
25+
}
26+
27+
pub struct EnvProvider {
28+
_config: EnvConfig,
29+
}
630

731
impl EnvProvider {
8-
pub fn new() -> Self {
9-
Self
32+
pub fn new(config: EnvConfig) -> Self {
33+
Self { _config: config }
34+
}
35+
36+
pub fn from_uri(uri: &Uri) -> Result<Self> {
37+
let config = EnvConfig::from_uri(uri)?;
38+
Ok(Self::new(config))
1039
}
1140
}
1241

0 commit comments

Comments
 (0)