Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Generic Functions and Marker Patterns

Learn how to write reusable functions that work with any marker type using Rust's generics.

The <M> Pattern

When you write <M>, you're saying "this function works with paths of any marker type."

#![allow(unused)]
fn main() {
use strict_path::StrictPath;

// ✅ Generic: works with any marker
fn get_size<M>(path: &StrictPath<M>) -> std::io::Result<u64> {
    Ok(path.metadata()?.len())
}

// Can call with any marker type
let config: StrictPath<ConfigDir> = ...;
let uploads: StrictPath<UserUploads> = ...;

let config_size = get_size(&config)?;   // M = ConfigDir
let upload_size = get_size(&uploads)?;  // M = UserUploads
}

When to Use Generic Functions

Use <M> when:

  • Function logic doesn't care about the specific marker
  • You're building reusable utilities
  • The operation applies to any path type

Use specific markers when:

  • Function requires specific authorization level
  • Business logic depends on path context
  • Type safety prevents mixing different domains

Common Generic Patterns

Pattern 1: Generic Helpers

#![allow(unused)]
fn main() {
/// Read and parse JSON from any path
fn read_json<M, T: serde::de::DeserializeOwned>(
    path: &StrictPath<M>
) -> Result<T, Box<dyn std::error::Error>> {
    let contents = path.read_to_string()?;
    Ok(serde_json::from_str(&contents)?)
}

// Works with any marker
let config: Config = read_json(&config_path)?;
let data: UserData = read_json(&user_path)?;
}

Pattern 2: Generic Validation

#![allow(unused)]
fn main() {
/// Ensure path exists and is a file
fn validate_file<M>(path: &StrictPath<M>) -> std::io::Result<()> {
    if !path.exists() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::NotFound,
            format!("File not found: {}", path.strictpath_display())
        ));
    }
    
    if !path.is_file() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            "Path is not a file"
        ));
    }
    
    Ok(())
}
}

Pattern 3: Generic Logging

#![allow(unused)]
fn main() {
use tracing::info;

/// Log path access for audit trail
fn log_access<M>(path: &StrictPath<M>, operation: &str) {
    info!(
        operation = operation,
        path = %path.strictpath_display(),
        "Path accessed"
    );
}

// Use in your code
log_access(&user_file, "read");
}

Pattern 4: Generic Directory Processing

#![allow(unused)]
fn main() {
/// Count files in directory
fn count_files<M>(dir: &StrictPath<M>) -> std::io::Result<usize> {
    let mut count = 0;
    
    for entry in dir.read_dir()? {
        let entry = entry?;
        if entry.metadata()?.is_file() {
            count += 1;
        }
    }
    
    Ok(count)
}
}

Specific Marker Functions

Sometimes you want exactly the right marker type:

#![allow(unused)]
fn main() {
struct UserData;
struct ConfigDir;

// ✅ Only accepts UserData paths
fn process_user_file(file: &StrictPath<UserData>) -> Result<(), Box<dyn std::error::Error>> {
    let contents = file.read_to_string()?;
    // Process user-specific data
    Ok(())
}

// ✅ Only accepts ConfigDir paths
fn load_config(config: &StrictPath<ConfigDir>) -> Result<AppConfig, Box<dyn std::error::Error>> {
    let contents = config.read_to_string()?;
    Ok(toml::from_str(&contents)?)
}

// ❌ Won't compile: wrong marker type
let user_file: StrictPath<UserData> = ...;
load_config(&user_file); // ERROR: expected ConfigDir, found UserData
}

Generic VirtualPath Functions

The same patterns work for VirtualPath:

#![allow(unused)]
fn main() {
use strict_path::VirtualPath;

/// Generic file writer
fn write_log<M>(path: &VirtualPath<M>, message: &str) -> std::io::Result<()> {
    path.create_parent_dir_all()?;
    path.write(format!("[{}] {}\n", chrono::Utc::now(), message))
}

/// Generic directory validator
fn ensure_directory<M>(path: &VirtualPath<M>) -> std::io::Result<()> {
    if !path.exists() {
        path.create_dir_all()?;
    } else if !path.is_dir() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::AlreadyExists,
            "Path exists but is not a directory"
        ));
    }
    Ok(())
}
}

Passing Paths Through Call Chains

Generic Chain

#![allow(unused)]
fn main() {
// Generic all the way down
fn read_data<M>(path: &StrictPath<M>) -> std::io::Result<Vec<u8>> {
    validate_file(path)?;           // Generic
    log_access(path, "read");        // Generic
    path.read()                      // Read the file
}
}

Specific Chain

#![allow(unused)]
fn main() {
struct TenantData;

// Specific markers maintained through chain
fn load_tenant_data(path: &StrictPath<TenantData>) -> Result<Data, Error> {
    validate_tenant_path(path)?;     // Takes StrictPath<TenantData>
    parse_tenant_data(path)?;        // Takes StrictPath<TenantData>
    decrypt_tenant_data(path)        // Takes StrictPath<TenantData>
}

// Each function in chain requires TenantData marker
fn validate_tenant_path(path: &StrictPath<TenantData>) -> Result<(), Error> { ... }
fn parse_tenant_data(path: &StrictPath<TenantData>) -> Result<(), Error> { ... }
fn decrypt_tenant_data(path: &StrictPath<TenantData>) -> Result<Data, Error> { ... }
}

Returning Generic Paths

You can return paths with preserved markers:

#![allow(unused)]
fn main() {
use strict_path::PathBoundary;

/// Find config file in directory
fn find_config<M>(dir: &PathBoundary<M>) -> Option<StrictPath<M>> {
    for entry in dir.read_dir().ok()? {
        let entry = entry.ok()?;
        let name = entry.file_name();
        
        if name.to_string_lossy().ends_with(".conf") {
            return dir.strict_join(&name).ok();
        }
    }
    None
}

// Marker type flows through
let config_dir: PathBoundary<ConfigDir> = ...;
let found: Option<StrictPath<ConfigDir>> = find_config(&config_dir);
}

Generic with Constraints

You can constrain markers with trait bounds:

#![allow(unused)]
fn main() {
/// Marker must implement Send + Sync
fn parallel_process<M: Send + Sync>(
    paths: Vec<StrictPath<M>>
) -> Vec<std::io::Result<String>> {
    use rayon::prelude::*;
    
    paths.par_iter()
        .map(|p| p.read_to_string())
        .collect()
}

/// Marker must implement custom trait
trait Auditable {
    fn audit_log_name() -> &'static str;
}

fn audit_access<M: Auditable>(path: &StrictPath<M>) {
    println!("Accessing {} path: {}", 
        M::audit_log_name(), 
        path.strictpath_display()
    );
}
}

Common Mistakes

❌ Unnecessary Generic Constraint

#![allow(unused)]
fn main() {
// Bad: overly restrictive
fn read_size<M: Sized>(path: &StrictPath<M>) -> std::io::Result<u64> {
    Ok(path.metadata()?.len())
}

// Good: no constraint needed
fn read_size<M>(path: &StrictPath<M>) -> std::io::Result<u64> {
    Ok(path.metadata()?.len())
}
}

❌ Mixing Markers Unsafely

#![allow(unused)]
fn main() {
// Bad: forces marker type conversion
fn bad_mix<M1, M2>(src: &StrictPath<M1>, dst: &StrictPath<M2>) {
    // Won't compile: can't copy between different marker types
    src.strict_copy(dst); // ERROR: marker type mismatch
}

// Good: require same marker
fn good_copy<M>(src: &StrictPath<M>, dst: &StrictPath<M>) {
    src.strict_copy(dst); // ✅ OK: both have marker M
}
}

❌ Losing Marker Type

#![allow(unused)]
fn main() {
// Bad: loses type information
fn process(path: &StrictPath<()>) { ... }

// Good: preserve marker
fn process<M>(path: &StrictPath<M>) { ... }
}

Best Practices

✅ Start Generic, Add Constraints as Needed

#![allow(unused)]
fn main() {
// Start here
fn process<M>(path: &StrictPath<M>) { ... }

// Add trait bounds only if needed
fn process<M: Send + Sync>(path: &StrictPath<M>) { ... }

// Use specific markers only when required for business logic
fn process_user(path: &StrictPath<UserData>) { ... }
}

✅ Document Marker Expectations

#![allow(unused)]
fn main() {
/// Process any file in any boundary.
///
/// Generic over marker type `M` because the operation
/// doesn't depend on specific authorization or context.
fn process_file<M>(file: &StrictPath<M>) -> std::io::Result<()> {
    // ...
}

/// Load user-specific configuration.
///
/// Requires `UserData` marker to enforce that only
/// user-scoped paths can be processed here.
fn load_user_config(file: &StrictPath<UserData>) -> Result<Config> {
    // ...
}
}

✅ Use Generic for Utilities, Specific for Domain Logic

#![allow(unused)]
fn main() {
// ✅ Generic utility
fn file_size<M>(path: &StrictPath<M>) -> std::io::Result<u64> { ... }

// ✅ Specific domain logic
fn charge_storage_fees(path: &StrictPath<BillingData>) -> Result<Amount> { ... }
}

Summary

  • <M> means "works with any marker"
  • Use generics for reusable utilities
  • Use specific markers for domain-specific logic
  • The compiler prevents marker type mismatches
  • Marker types flow through call chains automatically
  • Add trait bounds only when needed