workspace_tools

Crates.ioworkspace_tools
lib.rsworkspace_tools
version0.5.0
created_at2025-08-10 06:26:51.680956+00
updated_at2025-09-23 14:01:57.105819+00
descriptionReliable workspace-relative path resolution for Rust projects. Automatically finds your workspace root and provides consistent file path handling regardless of execution context. Features memory-safe secret management, configuration loading with validation, and resource discovery.
homepagehttps://github.com/Wandalen/workspace_tools
repositoryhttps://github.com/Wandalen/workspace_tools
max_upload_size
id1788589
size640,309
Wandalen (Wandalen)

documentation

https://docs.rs/workspace_tools

README

workspace_tools

Crates.io Documentation MIT License Build Status

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.

// āŒ 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.

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:

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.

Click to see a complete `main.rs` example
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( () )
}

3. Run Your Application

Run your code from different directories to see workspace_tools in action:

# 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.

#[ 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.

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.

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.

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.

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:

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.

// 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:

# 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.

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

// 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/

Configuration Loading

// 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

// 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

// 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

šŸ¤ 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.

Commit count: 0

cargo fmt