# workspace_tools
[](https://crates.io/crates/workspace_tools)
[](https://docs.rs/workspace_tools)
[](https://opensource.org/licenses/MIT)
[](#-testing)
**Stop fighting with file paths in Rust. `workspace_tools` provides foolproof, workspace-relative path resolution that works everywhere: in your tests, binaries, and examples, regardless of the execution context.**
It's the missing piece of the Rust development workflow that lets you focus on building, not on debugging broken paths.
## šÆ The Problem: Brittle File Paths
Every Rust developer has faced this. Your code works on your machine, but breaks in CI or when run from a different directory.
```rust
// ā Brittle: This breaks if you run `cargo test` or execute the binary from a subdirectory.
let config = std::fs::read_to_string( "../../config/app.toml" )?;
// ā Inconsistent: This relies on the current working directory, which is unpredictable.
let data = Path::new( "./data/cache.db" );
```
## ā
The Solution: A Reliable Workspace Anchor
`workspace_tools` gives you a stable anchor to your project's root, making all file operations simple and predictable.
```rust
use workspace_tools::workspace;
// ā
Reliable: This works from anywhere.
let ws = workspace()?; // Automatically finds your project root!
let config = std::fs::read_to_string( ws.join( "config/app.toml" ) )?;
let data = ws.data_dir().join( "cache.db" ); // Use standard, predictable directories.
```
---
## š Quick Start in 60 Seconds
Get up and running with a complete, working example in less than a minute.
**1. Add the Dependency**
In your project's root directory, run:
```bash
cargo add workspace_tools
```
**2. Use it in Your Code**
`workspace_tools` automatically finds your project root by looking for the `Cargo.toml` file that contains your `[workspace]` definition. **No configuration is required.**
<details>
<summary><strong>Click to see a complete `main.rs` example</strong></summary>
```rust
use workspace_tools::workspace;
use std::fs;
use std::path::Path;
fn main() -> Result< (), Box< dyn std::error::Error > >
{
// 1. Get the workspace instance. It just works!
let ws = workspace()?;
println!( "ā
Workspace Root Found: {}", ws.root().display() );
// 2. Create a path to a config file in the standard `/config` directory.
let config_path = ws.config_dir().join( "app.toml" );
println!( "āļø Attempting to read config from: {}", config_path.display() );
// 3. Let's create a dummy config file to read.
// In a real project, this file would already exist.
setup_dummy_config( &config_path )?;
// 4. Now, reliably read the file. This works from anywhere!
let config_content = fs::read_to_string( &config_path )?;
println!( "\nš Successfully read config file! Content:\n---" );
println!( "{}", config_content.trim() );
println!( "---" );
Ok( () )
}
// Helper function to create a dummy config file for the example.
fn setup_dummy_config( path : &Path ) -> Result< (), std::io::Error >
{
if let Some( parent ) = path.parent()
{
fs::create_dir_all( parent )?;
}
fs::write( path, "[server]\nhost = \"127.0.0.1\"\nport = 8080\n" )?;
Ok( () )
}
```
</details>
**3. Run Your Application**
Run your code from different directories to see `workspace_tools` in action:
```bash
# Run from the project root (this will work)
cargo run
# Run from a subdirectory (this will also work!)
cd src
cargo run
```
You have now eliminated brittle, context-dependent file paths from your project!
---
## š A Standard for Project Structure
`workspace_tools` helps standardize your projects, making them instantly familiar to you, your team, and your tools.
```
your-project/
āāā .cargo/
āāā secret/ # (Optional) Securely manage secrets
āāā .workspace/ # Internal workspace metadata
āāā Cargo.toml # Your workspace root
āāā config/ # ( ws.config_dir() ) Application configuration
āāā data/ # ( ws.data_dir() ) Databases, caches, user data
āāā docs/ # ( ws.docs_dir() ) Project documentation
āāā logs/ # ( ws.logs_dir() ) Runtime log files
āāā src/
āāā tests/ # ( ws.tests_dir() ) Integration tests & fixtures
```
---
## š§ Optional Features
Enable additional functionality as needed in your `Cargo.toml`:
**Serde Integration** (`serde`) - *enabled by default*
Load `.toml`, `.json`, and `.yaml` files directly into structs.
```rust
#[ derive( Deserialize ) ]
struct AppConfig { name: String, port: u16 }
let config: AppConfig = workspace()?.load_config( "app" )?; // Supports .toml, .json, .yaml
```
**Resource Discovery** (`glob`)
Find files with glob patterns like `src/**/*.rs`.
```rust
let rust_files = workspace()?.find_resources( "src/**/*.rs" )?;
```
**Secret Management** (`secrets`)
Load secrets from `secret/` directory with environment fallbacks. Supports both `KEY=VALUE` format and shell `export KEY=VALUE` statements.
```rust
let api_key = workspace()?.load_secret_key( "API_KEY", "-secrets.sh" )?;
```
**Memory-Safe Secret Handling** (`secure`)
Advanced secret management with memory-safe `SecretString` types and automatic injection.
```rust
use secrecy::ExposeSecret;
// Memory-safe secret loading
let secrets = workspace()?.load_secrets_secure( "-secrets.sh" )?;
let api_key = secrets.get( "API_KEY" ).unwrap();
println!( "API Key: {}", api_key.expose_secret() );
// Template-based secret injection into configuration files
let config = workspace()?.load_config_with_secret_injection( "config.toml", "-secrets.sh" )?;
// Secret strength validation
workspace()?.validate_secret( "weak123" )?; // Returns error for weak secrets
```
**Config Validation** (`validation`)
Schema-based validation for configuration files.
```rust
let config: AppConfig = workspace()?.load_config_with_validation( "app" )?;
```
---
## š Advanced Security Features
### Type-Safe Secret Injection
The `SecretInjectable` trait allows automatic injection of secrets into configuration types with compile-time safety:
```rust
use workspace_tools::{ workspace, SecretInjectable };
#[derive(Debug)]
struct AppConfig
{
database_url: String,
api_key: String,
}
impl SecretInjectable for AppConfig
{
fn inject_secret(&mut self, key: &str, value: String) -> workspace_tools::Result<()>
{
match key {
"DATABASE_URL" => self.database_url = value,
"API_KEY" => self.api_key = value,
_ => return Err(workspace_tools::WorkspaceError::SecretInjectionError(
format!("unknown secret key: {}", key)
)),
}
Ok(())
}
fn validate_secrets(&self) -> workspace_tools::Result<()>
{
if self.api_key.is_empty() {
return Err(workspace_tools::WorkspaceError::SecretValidationError(
"api_key cannot be empty".to_string()
));
}
Ok(())
}
}
let ws = workspace()?;
let mut config = AppConfig { database_url: String::new(), api_key: String::new() };
config = ws.load_config_with_secrets(config, "-secrets.sh")?; // Automatically validates
```
### Security Best Practices
- **Memory Safety**: All secrets wrapped in `SecretString` types that prevent accidental exposure
- **Debug Protection**: Secrets are automatically redacted from debug output
- **Explicit Access**: Secrets require explicit `expose_secret()` calls for access
- **Validation**: Built-in secret strength validation rejects weak passwords
- **Zeroization**: Secrets are automatically cleared from memory when dropped
---
## š ļø Built for the Real World
`workspace_tools` is designed for production use, with features that support robust testing and flexible deployment.
### Testing with Confidence
Create clean, isolated environments for your tests.
```rust
// In tests/my_test.rs
#![ cfg( feature = "integration" ) ]
use workspace_tools::testing::create_test_workspace_with_structure;
use std::fs;
#[ test ]
fn my_feature_test()
{
// Creates a temporary, isolated workspace that is automatically cleaned up.
let ( _temp_dir, ws ) = create_test_workspace_with_structure();
// Write test-specific files without polluting your project.
let config_path = ws.config_dir().join( "test_config.toml" );
fs::write( &config_path, "[settings]\nenabled = true" ).unwrap();
// ... your test logic here ...
}
```
### Flexible Deployment
Because `workspace_tools` can be configured via `WORKSPACE_PATH`, it adapts effortlessly to any environment.
**Dockerfile:**
```dockerfile
# Your build stages...
# Final stage
FROM debian:bookworm-slim
WORKDIR /app
ENV WORKSPACE_PATH=/app # Set the workspace root inside the container.
COPY --from=builder /app/target/release/my-app .
COPY config/ ./config/
COPY assets/ ./assets/
CMD ["./my-app"] # Your app now runs with the correct workspace context.
```
### Resilient by Design
`workspace_tools` has a smart fallback strategy to find your workspace root, ensuring it always finds a sensible path.
```mermaid
graph TD
A[Start] --> B{Cargo Workspace?};
B -->|Yes| C[Use Cargo Root];
B -->|No| D{WORKSPACE_PATH Env Var?};
D -->|Yes| E[Use Env Var Path];
D -->|No| F{.git folder nearby?};
F -->|Yes| G[Use Git Root];
F -->|No| H[Use Current Directory];
C --> Z[Success];
E --> Z[Success];
G --> Z[Success];
H --> Z[Success];
```
---
## š API Reference
### Core Methods
```rust
// Workspace creation and path operations
let ws = workspace()?; // Auto-detect workspace root
let ws = Workspace::new( "/path/to/root" ); // Explicit path
let path = ws.join( "relative/path" ); // Join paths safely
let root = ws.root(); // Get workspace root
// Standard directories
let config = ws.config_dir(); // ./config/
let data = ws.data_dir(); // ./data/
let logs = ws.logs_dir(); // ./logs/
let docs = ws.docs_dir(); // ./docs/
```
### Path Normalization
`workspace_tools` automatically normalizes all workspace root paths to ensure consistent behavior regardless of how the workspace is created:
```rust
// all workspace creation methods normalize paths
let ws = workspace()?; // normalized automatically
let ws = Workspace::new( PathBuf::from( "/path/to/workspace/." ) ); // trailing "/." removed
let ws = Workspace::from_cargo_workspace()?; // normalized automatically
// path normalization guarantees:
// - absolute paths (relative paths are resolved against current directory)
// - no trailing "/." components
// - no "/./" components in the middle of paths
// - symlinks are preserved (not resolved to canonical paths)
// - empty WORKSPACE_PATH values are rejected with clear error
// examples of normalized paths:
// "/tmp/project/." ā "/tmp/project"
// "/tmp/./project" ā "/tmp/project"
// "./project" ā "/absolute/cwd/project"
// "/tmp/foo/../project" ā "/tmp/project"
```
This normalization ensures that:
- Path comparisons work correctly
- Joined paths remain clean (no accumulated dot components)
- Error messages show absolute paths for better debugging
- Behavior is consistent across different operating systems
### Configuration Loading
```rust
// Load configuration files (supports .toml, .json, .yaml)
let config: MyConfig = ws.load_config( "app" )?;
let config: MyConfig = ws.load_config_from( "config/app.toml" )?;
// Layered configuration (loads multiple files and merges)
let config: MyConfig = ws.load_config_layered( &[ "base", "dev" ] )?;
// Configuration with validation
let config: MyConfig = ws.load_config_with_validation( "app" )?;
```
### Secret Management
```rust
// Basic secret loading
let secrets = ws.load_secrets_from_file( "-secrets.sh" )?;
let api_key = ws.load_secret_key( "API_KEY", "-secrets.sh" )?;
// Memory-safe secret handling (requires 'secure' feature)
let secrets = ws.load_secrets_secure( "-secrets.sh" )?;
let api_key = ws.load_secret_key_secure( "API_KEY", "-secrets.sh" )?;
let token = ws.env_secret( "GITHUB_TOKEN" );
// Secret validation and injection
ws.validate_secret( "password123" )?; // Validates strength
let config_text = ws.load_config_with_secret_injection( "app.toml", "-secrets.sh" )?;
let config: MyConfig = ws.load_config_with_secrets( my_config, "-secrets.sh" )?;
```
### Resource Discovery
```rust
// Find files with glob patterns (requires 'glob' feature)
let rust_files = ws.find_resources( "src/**/*.rs" )?;
let configs = ws.find_resources( "config/**/*.{toml,json,yaml}" )?;
// Find configuration files with priority ordering
let config_path = ws.find_config( "app" )?; // Looks for app.toml, app.json, app.yaml
```
---
## šļø Internal Architecture
`workspace_tools` follows strict design principles to ensure maintainability and code quality:
### DRY Compliance Through Helper Functions
All configuration and validation operations are built on a foundation of reusable internal helpers that eliminate code duplication:
**Format Detection and Parsing (serde feature)**
- `detect_format()` - detect file format from extension (toml/json/yaml)
- `read_file_to_string()` - read file with consistent error wrapping
- `parse_content()` - parse configuration based on detected format
- `serialize_content()` - serialize configuration to target format
**Validation Helpers (validation feature)**
- `parse_to_json()` - convert any format (toml/json/yaml) to JSON for validation
- `validate_against_schema()` - validate JSON against JSON Schema with detailed errors
These helpers provide:
- **Single source of truth**: Format handling logic exists in exactly one place
- **Consistent error messages**: All file operations produce uniform error context
- **Easy extensibility**: Adding new formats requires updating only 2-3 functions
- **Reduced complexity**: Public API functions reduced from 25-40 lines to 4-17 lines each
### Type-Safe Secure Conversion Pattern
The secure feature uses a trait-based pattern for converting plain types to memory-protected types:
```rust
trait AsSecure
{
type Secure;
fn into_secure( self ) -> Self::Secure;
}
impl AsSecure for String
{
type Secure = SecretString;
fn into_secure( self ) -> Self::Secure { SecretString::new( self ) }
}
impl AsSecure for HashMap< String, String >
{
type Secure = HashMap< String, SecretString >;
fn into_secure( self ) -> Self::Secure { /* convert all values */ }
}
```
All `_secure()` methods follow the identical pattern:
```rust
pub fn load_secret_key_secure( &self, key: &str, file: &str ) -> Result< SecretString >
{
self.load_secret_key( key, file ).map( AsSecure::into_secure )
}
```
Benefits:
- **Zero duplication**: All 5 secure wrappers share identical implementation pattern
- **Type safety**: Compiler enforces correct conversions
- **Clear intent**: `.map(AsSecure::into_secure)` explicitly shows conversion
- **Extensible**: New secure types only require implementing the trait
### Code Quality Metrics
After comprehensive refactoring (2025-10-04):
- **~127 lines of duplicated code eliminated** across 4 refactoring phases
- **13 functions simplified** with 60% average complexity reduction
- **8 internal helpers** providing single source of truth for common operations
- **100% DRY compliance** in configuration, validation, secure conversion, and file I/O code
- **Zero breaking changes**: All refactoring internal to maintain API stability
Refactoring phases completed:
- Phase 1: Configuration/validation helpers (100 lines saved)
- Phase 2: Secure conversion trait pattern (15 lines saved)
- Phase 2.5: File reading consolidation (5 lines saved)
- Phase 2.6: Format detection consolidation (3 lines saved + removed obsolete helper)
---
## š¤ Contributing
This project thrives on community contributions. Whether it's reporting a bug, suggesting a feature, or writing code, your help is welcome! Please see our task list and contribution guidelines.
## āļø License
This project is licensed under the **MIT License**.