Simplified Precision: Unraveling the Simple Specification Pattern in Rust for Expressive Code Design

Photo by Jimmy Chan: https://www.pexels.com/photo/temple-illustration-1404948/

Introduction

Most applications require business rules, such as data validation. It’s crucial to implement these rules in a way that’s flexible, clear, and easy to maintain. The Specification pattern offers a solution, allowing the creation of reusable business rules that can be combined using boolean logic.

Implementation in Rust

In this example, we’ll build a simple and adaptable email validator. 

We start with the EmailValidator trait:

trait EmailValidator {
    fn is_valid(&self, email: &EmailAddress) -> bool;
}

As you can see al this does is try and validate an EmailAddress struct.

This is the EmailAddress struct:

struct EmailAddress {
    user_name: String,
    domain: String,
}

impl EmailAddress {
    fn new(user_name: String, domain: String) -> Self {
        Self {
            user_name,
            domain,
        }
    }
}

To make validation a little easier, we divide the email-addres in a username, the bit before the @-sign, and the domain, the bit after the @-sign.

The UsernameValidator checks whether the user_name has a minimum length:

struct UsernameValidator {
    min_length: usize,
}

impl EmailValidator for UsernameValidator {
    fn is_valid(&self, email: &EmailAddress) -> bool {
        email.user_name.len() >= self.min_length
    }
}

The DomainValidator is used check the minimum length of the domain, and also whether the domain is allowed:

struct DomainValidator {
    min_length: usize,
    allowed_domains: Vec<String>,
}

impl EmailValidator for DomainValidator {
    fn is_valid(&self, email: &EmailAddress) -> bool {
        email.domain.len() >= self.min_length
            && self.allowed_domains.contains(&email.domain)
    }
}

Now we will bring it all together in the EmailValidatorImpl:

struct EmailValidatorImpl {
    validators: Vec<Box<dyn EmailValidator>>,
}

impl EmailValidatorImpl {
    fn new(validators: Vec<Box<dyn EmailValidator>>) -> Self {
        Self { validators }
    }

    fn is_valid(&self, email: &EmailAddress) -> bool {
        self.validators
            .iter()
            .all(|validator| validator.is_valid(email))
    }
}

Some noteworthy points:

  1. The EmailValidatorImpl has a vector of objects implementing the EmailValidator interface.
  2. In the is_valid() method, we use iter() to iterate over the validators, and the all() method to make sure every validator returns true.

Testing

Let’s test it:

fn main() {
    let my_email = EmailAddress::new("test".to_string(), "gmail.com".to_string());
    let local_part_validator = UsernameValidator { min_length: 3 };
    let domain_validator = DomainValidator {
        min_length: 3,
        allowed_domains: vec!["gmail.com".to_string()],
    };
    let email_validator = EmailValidatorImpl::new(vec![
        Box::new(local_part_validator),
        Box::new(domain_validator),
    ]);
    println!("{}", email_validator.is_valid(&my_email));
}

We create an EmailAddress, and two validators. After that we create a validator, and add the two validators to the validators-vectors. Lastly we check our email address.

Conclusion

Implementing business rules with the specification pattern in Rust is straightforward and flexible. Adding new validators is easy; just write the validator and include it in the array. While our example focuses on simplicity, future enhancements could include more informative error messages, a topic for another post.

Leave a Reply

Your email address will not be published. Required fields are marked *