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

Type-Safe Context Separation

Learn how to use marker types to prevent accidentally mixing different storage contexts at compile time. This is one of the most powerful features of strict-path.

The Problem

Applications often have multiple storage areas for different purposes:

  • 🌐 Web assets (CSS, JS, images)
  • 📁 User uploads (documents, photos)
  • ⚙️ Configuration files
  • 🔒 Sensitive data (keys, tokens)

Without type safety, you might accidentally:

  • ❌ Serve a user's private document as a web asset
  • ❌ Write config data to the uploads directory
  • ❌ Read a sensitive key file when expecting a CSS file

The Solution

Use marker types with StrictPath<Marker> and VirtualPath<Marker> to encode context at the type level. The compiler prevents context mixing.

Complete Example

#![allow(unused)]
fn main() {
use strict_path::{PathBoundary, StrictPath, VirtualRoot, VirtualPath};

// Define marker types for different contexts
struct WebAssets;    // CSS, JS, images
struct UserFiles;    // Uploaded documents
struct ConfigData;   // Application configuration

// Functions enforce context via type system
fn serve_asset(path: &StrictPath<WebAssets>) -> Result<Vec<u8>, std::io::Error> {
    path.read()
}

fn process_upload(path: &StrictPath<UserFiles>) -> Result<(), std::io::Error> {
    // Process user-uploaded file
    let content = path.read_to_string()?;
    println!("Processing user file: {} bytes", content.len());
    Ok(())
}

fn load_config(path: &StrictPath<ConfigData>) -> Result<String, std::io::Error> {
    path.read_to_string()
}

fn example_type_safety() -> Result<(), Box<dyn std::error::Error>> {
    // Create context-specific boundaries
    let assets_root: VirtualRoot<WebAssets> = VirtualRoot::try_new("public")?;
    let uploads_root: VirtualRoot<UserFiles> = VirtualRoot::try_new("uploads")?;
    let config_boundary: PathBoundary<ConfigData> = PathBoundary::try_new("config")?;

    // Create paths with proper contexts
    let css: VirtualPath<WebAssets> = assets_root.virtual_join("app.css")?;
    let doc: VirtualPath<UserFiles> = uploads_root.virtual_join("report.pdf")?;
    let cfg: StrictPath<ConfigData> = config_boundary.strict_join("app.toml")?;

    // Type system prevents context mixing
    serve_asset(css.as_unvirtual())?;         // ✅ Correct context
    process_upload(doc.as_unvirtual())?;      // ✅ Correct context  
    load_config(&cfg)?;                       // ✅ Correct context

    // These would be compile errors:
    // serve_asset(doc.as_unvirtual())?;      // ❌ Compile error - wrong context!
    // process_upload(css.as_unvirtual())?;   // ❌ Compile error - wrong context!
    // load_config(css.as_unvirtual())?;      // ❌ Compile error - wrong context!

    Ok(())
}
}

Key Benefits

1. Compile-Time Safety

The compiler catches context mixing errors:

#![allow(unused)]
fn main() {
let css: VirtualPath<WebAssets> = assets_root.virtual_join("app.css")?;
let doc: VirtualPath<UserFiles> = uploads_root.virtual_join("report.pdf")?;

serve_asset(css.as_unvirtual())?;  // ✅ OK
serve_asset(doc.as_unvirtual())?;  // ❌ Compile error!
//          ^^^ expected WebAssets, found UserFiles
}

2. Clear Interfaces

Function signatures document what they accept:

#![allow(unused)]
fn main() {
// This function ONLY accepts web assets
fn serve_asset(path: &StrictPath<WebAssets>) -> Result<Vec<u8>, std::io::Error> {
    // No need to check if this is the right type of file
    // The type system guarantees it
    path.read()
}
}

3. Refactoring Safety

If you change a function's context requirement:

#![allow(unused)]
fn main() {
// Change signature from WebAssets to ConfigData
fn serve_asset(path: &StrictPath<ConfigData>) -> Result<Vec<u8>, std::io::Error> {
    path.read()
}
}

The compiler finds all call sites that need updating. Zero-cost migration!

4. Team Collaboration

New developers can't make context mixing mistakes - the compiler teaches them the correct patterns.

Real-World Pattern: Multi-Context Web Server

use strict_path::{PathBoundary, StrictPath, VirtualRoot, VirtualPath};

struct WebAssets;
struct UserUploads;
struct ServerConfig;

struct WebServer {
    assets: VirtualRoot<WebAssets>,
    uploads: VirtualRoot<UserUploads>,
    config: PathBoundary<ServerConfig>,
}

impl WebServer {
    fn new() -> Result<Self, Box<dyn std::error::Error>> {
        Ok(Self {
            assets: VirtualRoot::try_new("public")?,
            uploads: VirtualRoot::try_new("uploads")?,
            config: PathBoundary::try_new("config")?,
        })
    }
    
    // This can ONLY serve web assets
    fn serve_static_file(&self, path: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
        let asset: VirtualPath<WebAssets> = self.assets.virtual_join(path)?;
        Ok(self.read_asset(asset.as_unvirtual())?)
    }
    
    // Helper enforces WebAssets context
    fn read_asset(&self, path: &StrictPath<WebAssets>) -> std::io::Result<Vec<u8>> {
        path.read()
    }
    
    // This can ONLY handle user uploads
    fn save_upload(&self, filename: &str, content: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
        let upload: VirtualPath<UserUploads> = self.uploads.virtual_join(filename)?;
        self.write_upload(upload.as_unvirtual(), content)?;
        Ok(())
    }
    
    // Helper enforces UserUploads context
    fn write_upload(&self, path: &StrictPath<UserUploads>, content: &[u8]) -> std::io::Result<()> {
        path.create_parent_dir_all()?;
        path.write(content)
    }
    
    // This can ONLY read config files
    fn load_config(&self, name: &str) -> Result<String, Box<dyn std::error::Error>> {
        let cfg: StrictPath<ServerConfig> = self.config.strict_join(name)?;
        Ok(self.read_config(&cfg)?)
    }
    
    // Helper enforces ServerConfig context
    fn read_config(&self, path: &StrictPath<ServerConfig>) -> std::io::Result<String> {
        path.read_to_string()
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let server = WebServer::new()?;
    
    // Each method can only access its designated context
    let css = server.serve_static_file("app.css")?;
    server.save_upload("document.pdf", b"PDF content")?;
    let config = server.load_config("server.toml")?;
    
    // These would be impossible to mess up due to type safety:
    // - Can't serve an upload as a static file
    // - Can't save a config as an upload
    // - Can't read an asset as config
    
    Ok(())
}

Advanced: Permission Markers

Combine resource markers with permission markers using tuples:

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

// Resource markers
struct Documents;
struct DatabaseFiles;

// Permission markers
struct ReadOnly;
struct ReadWrite;

// Type-safe permission enforcement
fn read_document(path: &StrictPath<(Documents, ReadOnly)>) -> std::io::Result<String> {
    path.read_to_string()
}

fn write_document(
    path: &StrictPath<(Documents, ReadWrite)>,
    content: &str,
) -> std::io::Result<()> {
    path.write(content)
}

fn backup_database(
    source: &StrictPath<(DatabaseFiles, ReadOnly)>,
    dest: &StrictPath<(DatabaseFiles, ReadWrite)>,
) -> std::io::Result<()> {
    let data = source.read()?;
    dest.write(&data)
}

fn example_permissions() -> Result<(), Box<dyn std::error::Error>> {
    let docs_ro: PathBoundary<(Documents, ReadOnly)> = 
        PathBoundary::try_new("documents")?;
    let docs_rw: PathBoundary<(Documents, ReadWrite)> = 
        PathBoundary::try_new("documents")?;
    
    let file_ro = docs_ro.strict_join("report.txt")?;
    let file_rw = docs_rw.strict_join("report.txt")?;
    
    // Can read from read-only
    read_document(&file_ro)?;
    
    // Can't write to read-only - compile error!
    // write_document(&file_ro, "new content")?;  // ❌ Compile error!
    
    // Can write to read-write
    write_document(&file_rw, "new content")?;
    
    Ok(())
}
}

Advanced: Authorization Markers

Use change_marker() after authorization checks:

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

struct UserFiles;
struct ReadOnly;
struct ReadWrite;

fn authenticate_and_upgrade(
    path: StrictPath<(UserFiles, ReadOnly)>,
    user_has_write_access: bool,
) -> Result<StrictPath<(UserFiles, ReadWrite)>, &'static str> {
    if user_has_write_access {
        // Authorization succeeded - change marker to encode permission
        Ok(path.change_marker())
    } else {
        Err("Access denied")
    }
}

fn write_file(path: &StrictPath<(UserFiles, ReadWrite)>, content: &[u8]) -> std::io::Result<()> {
    path.write(content)
}

// Usage:
let boundary: PathBoundary<(UserFiles, ReadOnly)> = 
    PathBoundary::try_new("uploads")?;
let file_ro = boundary.strict_join("document.txt")?;

// Can't write yet - read-only marker
// write_file(&file_ro, b"data")?;  // ❌ Compile error!

// After authorization, upgrade to read-write
if let Ok(file_rw) = authenticate_and_upgrade(file_ro, check_permissions()) {
    write_file(&file_rw, b"data")?;  // ✅ Now allowed
}
}

See the Authorization & Permissions chapter for more details.

Shared Logic Across Contexts

Use generics when logic applies to any context:

#![allow(unused)]
fn main() {
// Generic over marker type - works with any context
fn get_file_size<M>(path: &StrictPath<M>) -> std::io::Result<u64> {
    path.metadata().map(|m| m.len())
}

// Works with any marker
let asset_size = get_file_size(&css_file)?;
let upload_size = get_file_size(&upload_file)?;
let config_size = get_file_size(&config_file)?;
}

Best Practices

1. Name Markers After Resources

#![allow(unused)]
fn main() {
struct UserDocuments;   // ✅ Clear
struct Documents;       // ⚠️  Which documents?
struct MyMarker;        // ❌ Meaningless
}

2. Use Tuples for Multi-Dimensional Context

#![allow(unused)]
fn main() {
StrictPath<(ResourceType, PermissionLevel)>
StrictPath<(UserFiles, ReadWrite)>
}

3. Keep Markers Simple

#![allow(unused)]
fn main() {
// ✅ Simple, zero-size
struct WebAssets;

// ❌ Don't add fields
struct WebAssets {
    size_limit: usize,  // Wrong - use runtime checks
}
}

4. Document Marker Meaning

#![allow(unused)]
fn main() {
/// Marker for publicly-accessible web assets
/// (CSS, JavaScript, images, fonts)
struct WebAssets;

/// Marker for user-uploaded files
/// (documents, photos, videos)
struct UserUploads;
}

Integration Tips

With Web Frameworks

#![allow(unused)]
fn main() {
// Axum route handlers
async fn serve_asset(
    Path(asset_path): Path<String>,
) -> Result<Vec<u8>, StatusCode> {
    let assets: VirtualRoot<WebAssets> = get_assets_root();
    let asset = assets.virtual_join(&asset_path)
        .map_err(|_| StatusCode::BAD_REQUEST)?;
    
    read_asset(asset.as_unvirtual())
        .map_err(|_| StatusCode::NOT_FOUND)
}

fn read_asset(path: &StrictPath<WebAssets>) -> std::io::Result<Vec<u8>> {
    path.read()
}
}

With Async Runtimes

Type safety works with async code too:

#![allow(unused)]
fn main() {
async fn read_asset_async(path: &StrictPath<WebAssets>) -> std::io::Result<Vec<u8>> {
    tokio::fs::read(path.interop_path()).await
}
}

Common Patterns

Pattern 1: Service with Multiple Contexts

#![allow(unused)]
fn main() {
struct AppService {
    assets: VirtualRoot<WebAssets>,
    uploads: VirtualRoot<UserFiles>,
    config: PathBoundary<ConfigData>,
}
}

Pattern 2: Generic Helpers

#![allow(unused)]
fn main() {
fn exists<M>(path: &StrictPath<M>) -> bool {
    path.exists()
}
}

Pattern 3: Marker Transformation

#![allow(unused)]
fn main() {
fn authorize<R>(
    path: StrictPath<(R, ReadOnly)>,
) -> Result<StrictPath<(R, ReadWrite)>, Error> {
    // Check permissions...
    Ok(path.change_marker())
}
}

Next Steps