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:
- The
EmailValidatorImpl
has a vector of objects implementing theEmailValidator
interface. - In the
is_valid()
method, we useiter()
to iterate over the validators, and theall()
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.