#container #typemap #heterogeneous #type-safe #key-value-store #send-sync

bin+lib sovran-typemap

A thread-safe heterogeneous container with type-safety

5 releases

new 0.4.0 Mar 1, 2025
0.3.3 Feb 25, 2025
0.3.2 Feb 25, 2025
0.3.1 Feb 25, 2025
0.3.0 Feb 25, 2025

#421 in Rust patterns

Download history 16/week @ 2025-02-19 290/week @ 2025-02-26

306 downloads per month

MIT license

55KB
441 lines

sovran-typemap

Crates.io Documentation License: MIT

A thread-safe, type-safe heterogeneous container library for Rust.

sovran-typemap provides a flexible way to store different types in a single container while maintaining type-safety through runtime checks. This is particularly useful for applications that need to share state between components without requiring all components to know about all types.

Key Features

  • Type-safe: Values are checked at runtime to ensure type correctness
  • Thread-safe: Built on Arc<Mutex<_>> for safe concurrent access
  • Ergonomic API: Simple methods with closures for storing, retrieving, and modifying values
  • Flexible: Supports any type that implements Any + Send + Sync with any hashable key type
  • Comprehensive Error Handling: Detailed error types for better debugging and recovery
  • No macros: Pure runtime solution without complex macro magic
  • No Unsafe Code: Relies entirely on safe Rust with no unsafe blocks

Installation

Add this to your Cargo.toml:

[dependencies]
sovran-typemap = "0.3"

Basic Usage

use sovran_typemap::{TypeStore, StoreError};

fn main() -> Result<(), StoreError> {
    // Create a new store with string keys
    let store = TypeStore::<String>::new();

    // Store values of different types
    store.set("number".to_string(), 42i32)?;
    store.set("text".to_string(), "Hello, world!".to_string())?;
    store.set("data".to_string(), vec![1, 2, 3, 4, 5])?;

    // Retrieve values in a type-safe way
    let num = store.get::<i32>(&"number".to_string())?;
    let text = store.get::<String>(&"text".to_string())?;

    println!("Number: {}", num);
    println!("Text: {}", text);

    // Handle errors properly
    match store.get::<bool>(&"nonexistent".to_string()) {
        Ok(value) => println!("Value: {}", value),
        Err(StoreError::KeyNotFound) => println!("Key doesn't exist"),
        Err(StoreError::TypeMismatch) => println!("Type doesn't match"),
        Err(e) => println!("Other error: {}", e),
    }

    Ok(())
}

Using with_mut to Modify Values In-Place

use sovran_typemap::{TypeStore, StoreError};
use std::collections::HashMap;

fn main() -> Result<(), StoreError> {
    let store = TypeStore::<String>::new();

    // Initialize a counter map
    let mut counters = HashMap::new();
    counters.insert("visits".to_string(), 0);
    store.set("counters".to_string(), counters)?;

    // Update a counter in-place
    store.with_mut(&"counters".to_string(), |counters: &mut HashMap<String, i32>| {
        let visits = counters.entry("visits".to_string()).or_insert(0);
        *visits += 1;
    })?;

    // Add a new counter
    store.with_mut(&"counters".to_string(), |counters: &mut HashMap<String, i32>| {
        counters.insert("api_calls".to_string(), 1);
    })?;

    // Read current values
    let visit_count = store.with(&"counters".to_string(), |counters: &HashMap<String, i32>| {
        counters.get("visits").copied().unwrap_or(0)
    })?;

    println!("Visit count: {}", visit_count);

    Ok(())
}

Sharing State Between Components

use sovran_typemap::{TypeStore, StoreError};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};

struct UserService {
    store: Arc<TypeStore<String>>,
}

struct LogService {
    store: Arc<TypeStore<String>>,
}

impl UserService {
    fn new(store: Arc<TypeStore<String>>) -> Self {
        Self { store }
    }

    fn get_user_count(&self) -> Result<usize, StoreError> {
        self.store.with(&"users".to_string(), |users: &Vec<String>| {
            users.len()
        })
    }

    fn add_user(&self, username: String) -> Result<(), StoreError> {
        // Initialize users vector if it doesn't exist yet
        if !self.store.contains_key(&"users".to_string())? {
            self.store.set("users".to_string(), Vec::<String>::new())?;
        }

        // Add a user
        self.store.with_mut(&"users".to_string(), |users: &mut Vec<String>| {
            users.push(username);
        })
    }
}

impl LogService {
    fn new(store: Arc<TypeStore<String>>) -> Self {
        Self { store }
    }

    fn log(&self, message: String) -> Result<(), StoreError> {
        // Initialize logs if they don't exist
        if !self.store.contains_key(&"logs".to_string())? {
            self.store.set("logs".to_string(), Vec::<String>::new())?;
        }

        // Add log entry with timestamp
        self.store.with_mut(&"logs".to_string(), |logs: &mut Vec<String>| {
            let now = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_secs();
            logs.push(format!("[{}] {}", now, message));
        })
    }

    fn get_recent_logs(&self, count: usize) -> Result<Vec<String>, StoreError> {
        self.store.with(&"logs".to_string(), |logs: &Vec<String>| {
            logs.iter()
               .rev()
               .take(count)
               .cloned()
               .collect()
        })
    }
}

fn main() -> Result<(), StoreError> {
    // Create a shared store
    let store = Arc::new(TypeStore::<String>::new());

    // Create services that share the store
    let user_service = UserService::new(Arc::clone(&store));
    let log_service = LogService::new(Arc::clone(&store));

    // Use the services
    user_service.add_user("alice".to_string())?;
    log_service.log("User alice added".to_string())?;

    user_service.add_user("bob".to_string())?;
    log_service.log("User bob added".to_string())?;

    // Get information from both services
    println!("User count: {}", user_service.get_user_count()?);
    
    println!("Recent logs:");
    for log in log_service.get_recent_logs(5)? {
        println!("  {}", log);
    }

    Ok(())
}

Error Handling

The library provides detailed error types to help with error handling:

use sovran_typemap::{TypeStore, StoreError};

fn main() {
    let store = TypeStore::<String>::new();

    // Set a value for demonstration
    if let Err(e) = store.set("config".to_string(), vec!["setting1", "setting2"]) {
        eprintln!("Failed to store config: {}", e);
        return;
    }

    // Try to get a value with the wrong type
    match store.get::<String>(&"config".to_string()) {
        Ok(value) => println!("Config: {}", value),
        Err(StoreError::KeyNotFound) => println!("Config key not found"),
        Err(StoreError::TypeMismatch) => println!("Config is not a String"),
        Err(StoreError::LockError) => println!("Failed to acquire lock"),
    }

    // Try to access a non-existent key
    match store.get::<i32>(&"settings".to_string()) {
        Ok(value) => println!("Setting: {}", value),
        Err(StoreError::KeyNotFound) => println!("Settings key not found"),
        Err(e) => println!("Other error: {}", e),
    }
}

Available Methods

  • new() - Create a new empty TypeStore
  • set(key, value) - Store a value of any type
  • set_with(key, closure) - Store a value generated by a closure
  • get<T>(key) - Get a clone of a value
  • with<T, F, R>(key, closure) - Access a value with a read-only closure
  • with_mut<T, F, R>(key, closure) - Access a value with a read-write closure
  • remove(key) - Remove a value
  • contains_key(key) - Check if a key exists
  • keys() - Get all keys
  • len() - Get the number of items
  • is_empty() - Check if the store is empty

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contribution

Contributions are welcome! Please feel free to submit a Pull Request.

Dependencies

~1MB
~23K SLoC