Sling Academy
Home/Rust/Rust - Implementing custom wrappers around Rust’s standard collections for domain logic

Rust - Implementing custom wrappers around Rust’s standard collections for domain logic

Last updated: January 04, 2025

The Rust programming language is known for its performance and memory safety, providing developers with robust features and a powerful type system. Among its features, Rust includes a suite of standard collection types such as Vec, HashMap, and HashSet. These collections are highly optimized for general use. However, there might be cases when you need more control over behaviors—such as enforcing custom constraints or augmenting them with additional domain-specific logic. In this article, we'll learn how to implement custom wrappers around Rust’s standard collections to encapsulate domain logic.

When and Why to Use Custom Wrappers

Custom wrappers are particularly useful when the built-in behavior of a collection does not align perfectly with the requirements of your business logic. By wrapping a standard collection, you can:

  • Guarantee invariants and constraints specific to your application.
  • Add meaningful domain-specific methods that operate on the collection.
  • Improve code readability by tuning the collection interface to your application's needs.

Creating a Custom Wrapper

Imagine an application that needs a collection of user IDs with certain constraints, such as non-zero unique IDs only. Let's wrap a Vec to enforce these constraints:


struct UserIdCollection {
    ids: Vec,
}

impl UserIdCollection {
    /// Create a new, empty collection of User IDs.
    pub fn new() -> Self {
        UserIdCollection { ids: Vec::new() }
    }

    /// Add a user ID if it is non-zero and unique.
    pub fn add_user_id(&mut self, user_id: u32) -> Result<(), String> {
        if user_id == 0 {
            return Err("User ID must be non-zero.".into());
        }
        if self.ids.contains(&user_id) {
            return Err("User ID already exists.".into());
        }
        self.ids.push(user_id);
        Ok(())
    }

    /// Remove a user ID.
    pub fn remove_user_id(&mut self, user_id: u32) {
        self.ids.retain(|&id| id != user_id);
    }

    /// Retrieve the list of user IDs.
    pub fn get_user_ids(&self) -> &Vec {
        &self.ids
    }
}

This custom wrapper not only encapsulates the logic for managing the collection but also enforces the domain-specific constraints of unique, non-zero IDs.

Testing and Using Your Wrapper

Once you have your wrapper in place, it's crucial to test it to ensure it behaves as expected:


fn main() {
    let mut user_ids = UserIdCollection::new();

    // Attempt to add unique user IDs.
    assert_eq!(user_ids.add_user_id(1), Ok(()));
    assert_eq!(user_ids.add_user_id(2), Ok(()));

    // Attempt to add a duplicate user ID; should fail
    assert_eq!(user_ids.add_user_id(1), Err("User ID already exists.".into()));

    // Attempt to add a zero ID; should fail
    assert_eq!(user_ids.add_user_id(0), Err("User ID must be non-zero.".into()));

    // Remove a user ID
    user_ids.remove_user_id(1);
    assert_eq!(user_ids.get_user_ids(), &vec![2]);
}

The above code tests various aspects of our UserIdCollection wrapper to ensure it enforces invariants correctly and interacts with the underlying Vec appropriately.

Advanced Usage

Depending on your needs, you can extend the wrapper with more sophisticated domain-specific methods. For instance, imagine the need to find a user ID based on more complex criteria:


impl UserIdCollection {
    /// Checks if a particular user ID exists
    pub fn contains_id(&self, user_id: u32) -> bool {
        self.ids.contains(&user_id)
    }

    /// Find user IDs greater than a specified value
    pub fn filter_greater_than(&self, threshold: u32) -> Vec {
        self.ids.iter().cloned().filter(|&id| id > threshold).collect()
    }
}

By adding these methods, you're improving the expressiveness of your collection's interface with more tailored functionalities, further aligning your code with domain requirements.

Conclusion

Custom wrappers around Rust’s collections allow you to inject domain logic into your data structures effectively, giving you more power and control over data management. They help bring the language of your code closer to the problem domain, enhancing readability and maintainability. With careful design and thoughtful implementation, custom wrappers can be a vital tool in your Rust programming arsenal.

Next Article: Rust - Inspecting memory usage of vectors and hash maps with built-in methods or external crates

Previous Article: Rust - Safely handling out-of-bounds vector indexing with get and get_mut

Series: Collections in Rust

Rust

You May Also Like

  • E0557 in Rust: Feature Has Been Removed or Is Unavailable in the Stable Channel
  • Network Protocol Handling Concurrency in Rust with async/await
  • Using the anyhow and thiserror Crates for Better Rust Error Tests
  • Rust - Investigating partial moves when pattern matching on vector or HashMap elements
  • Rust - Handling nested or hierarchical HashMaps for complex data relationships
  • Rust - Combining multiple HashMaps by merging keys and values
  • Composing Functionality in Rust Through Multiple Trait Bounds
  • E0437 in Rust: Unexpected `#` in macro invocation or attribute
  • Integrating I/O and Networking in Rust’s Async Concurrency
  • E0178 in Rust: Conflicting implementations of the same trait for a type
  • Utilizing a Reactor Pattern in Rust for Event-Driven Architectures
  • Parallelizing CPU-Intensive Work with Rust’s rayon Crate
  • Managing WebSocket Connections in Rust for Real-Time Apps
  • Downloading Files in Rust via HTTP for CLI Tools
  • Mocking Network Calls in Rust Tests with the surf or reqwest Crates
  • Rust - Designing advanced concurrency abstractions using generic channels or locks
  • Managing code expansion in debug builds with heavy usage of generics in Rust
  • Implementing parse-from-string logic for generic numeric types in Rust
  • Rust.- Refining trait bounds at implementation time for more specialized behavior