I'm wondering what is the idiomatic way to create a specification pattern in Rust?
Let's say there is a WorkingDay
struct and two specifications should be created
IsActiveWorkingDaySpecification
IsInFutureWorkingDaySpecifictaion
My current approach looks like this
struct WorkingDay {
id: uuid::Uuid,
date: chrono::NaiveDate,
is_active: bool,
}
trait Specification<T> {
fn is_satisfied_by(&self, candidate: &T) -> bool;
}
struct IsActiveWorkingDaySpecification;
impl Specification<WorkingDay> for IsActiveWorkingDaySpecification {
fn is_satisfied_by(&self, candidate: &WorkingDay) -> bool {
candidate.is_active == true
}
}
struct IsInFutureWorkingDaySpecification;
impl Specification<WorkingDay> for IsInFutureWorkingDaySpecification {
fn is_satisfied_by(&self, candidate: &WorkingDay) -> bool {
chrono::Utc::now().date().naive_utc() < candidate.date
}
}
fn main() {
let working_day = WorkingDay {
id: uuid::Uuid::new_v4(),
date: chrono::NaiveDate::from_ymd(2077, 11, 24),
is_active: true,
};
let is_active_working_day_specification = IsActiveWorkingDaySpecification {};
let is_future_working_day_specification = IsInFutureWorkingDaySpecification {};
let is_active = is_active_working_day_specification.is_satisfied_by(&working_day);
let is_in_future = is_future_working_day_specification.is_satisfied_by(&working_day);
println!("IsActive: {}", is_active);
println!("IsInFuture: {}", is_in_future);
}
The problem with this code is that the specifications cannot be composed. That is, if specification FutureActiveWorkingDaySpecification
needs to be created it forces manually compare results of existing specifications
// cut
fn main () {
// cut
let is_active_working_day_specification = IsActiveWorkingDaySpecification {};
let is_future_working_day_specification = IsInFutureWorkingDaySpecification {};
let is_active = is_active_working_day_specification.is_satisfied_by(&working_day);
let is_in_future = is_future_working_day_specification.is_satisfied_by(&working_day);
let is_active_and_in_future = is_active && is_in_future; // AndSpecification
let is_active_or_in_future = is_active || is_in_future; // OrSpecification
// cut
}
I would like to achieve something like this, but don't know how
// cut
fn main () {
// cut
let is_active_working_day_specification = IsActiveWorkingDaySpecification {};
let is_future_working_day_specification = IsInFutureWorkingDaySpecification {};
// AndSpecification
let is_active_and_in_future = is_active_working_day_specification
.and(is_future_working_day_specification)
.is_satisfied_by(&working_day);
// OrSpecification
let is_active_or_in_future = is_active_working_day_specification
.or(is_future_working_day_specification)
.is_satisfied_by(&working_day);
// cut
}
This can be done by creating OrSpecification
and AndSpecification
structs which implement the Specification<T>
trait, and then extending Specification<T>
to include or
and and
methods with default implementations which create these:
trait Specification<T>: Sized {
fn is_satisfied_by(&self, candidate: &T) -> bool;
fn and<S>(self, other: S) -> AndSpecification<Self, S> {
AndSpecification {
a: self,
b: other,
}
}
fn or<S>(self, other: S) -> OrSpecification<Self, S> {
OrSpecification {
a: self,
b: other,
}
}
}
struct AndSpecification<A, B> {
a: A,
b: B,
}
impl<T, A, B> Specification<T> for AndSpecification<A, B>
where A: Specification<T>,
B: Specification<T>,
{
fn is_satisfied_by(&self, candidate: &T) -> bool {
self.a.is_satisfied_by(candidate) && self.b.is_satisfied_by(candidate)
}
}
struct OrSpecification<A, B> {
a: A,
b: B,
}
impl<T, A, B> Specification<T> for OrSpecification<A, B>
where A: Specification<T>,
B: Specification<T>,
{
fn is_satisfied_by(&self, candidate: &T) -> bool {
self.a.is_satisfied_by(candidate) || self.b.is_satisfied_by(candidate)
}
}
Note that the requirement of Specification<T>
to implement the Sized
trait is avoidable, it would just require using Box
in various places. For the sake of clarity here I've just chosen the simpler option.