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