Roman Mahotskyi
Roman Mahotskyi

Reputation: 6625

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
}

Upvotes: 1

Views: 249

Answers (2)

Holloway
Holloway

Reputation: 7367

You can add default methods to the Specification trait that return combination structs, eg AndCombination

trait Specification<T> {
    fn is_satisfied_by(&self, candidate: &T) -> bool;
    fn and<O: Specification<T>>(self, other: O) -> AndCombination<Self, O>
    where Self: Sized {
        AndCombination(self, other)
    }
}

struct AndCombination<L, R>(L,R);

impl<T, L, R> Specification<T> for AndCombination<L,R>
    where L: Specification<T>,
        R: Specification<T>
{
    fn is_satisfied_by(&self, candidate: &T) -> bool {
        self.0.is_satisfied_by(candidate) && self.1.is_satisfied_by(candidate)
    }
}

These can then be combined to create composite specifications

let active_future = IsActive.and(IsInFuture);
println!("Day is active in future: {}", active_future.is_satisfied_by(&working_day));

Similar structs can be created for Or and Not.

Playground Example here

Upvotes: 2

frankplow
frankplow

Reputation: 512

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.

Upvotes: 1

Related Questions