rustspecification-pattern

How to implement specification pattern in Rust?


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

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
}

Solution

  • 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.