export interface IBaseSpecification { isSatisfiedBy(candidate: T): Promise; } export interface ICompositeSpecification extends IBaseSpecification { and(other: ICompositeSpecification): ICompositeSpecification; andNot(other: ICompositeSpecification): ICompositeSpecification; or(other: ICompositeSpecification): ICompositeSpecification; orNot(other: ICompositeSpecification): ICompositeSpecification; not(): ICompositeSpecification; } export abstract class CompositeSpecification implements ICompositeSpecification { abstract isSatisfiedBy(candidate: T): Promise; public and(other: ICompositeSpecification): ICompositeSpecification { return new AndSpecification(this, other); } public andNot(other: ICompositeSpecification): ICompositeSpecification { return new AndNotSpecification(this, other); } public or(other: ICompositeSpecification): ICompositeSpecification { return new OrSpecification(this, other); } public orNot(other: ICompositeSpecification): ICompositeSpecification { return new OrNotSpecification(this, other); } public not(): ICompositeSpecification { return new NotSpecification(this); } } class AndSpecification extends CompositeSpecification { constructor( private readonly left: ICompositeSpecification, private readonly right: ICompositeSpecification ) { super(); } public async isSatisfiedBy(candidate: T): Promise { const leftResult = await this.left.isSatisfiedBy(candidate); if (!leftResult) return false; const rightResult = await this.right.isSatisfiedBy(candidate); return rightResult; } toString(): string { return `(${this.left.toString()} and ${this.right.toString()})`; } } class AndNotSpecification extends AndSpecification { public async isSatisfiedBy(candidate: T): Promise { return (await super.isSatisfiedBy(candidate)) !== true; } toString(): string { return `not ${super.toString()}`; } } class OrSpecification extends CompositeSpecification { constructor( private readonly left: ICompositeSpecification, private readonly right: ICompositeSpecification ) { super(); } public async isSatisfiedBy(candidate: T): Promise { const leftResult = await this.left.isSatisfiedBy(candidate); if (leftResult) return true; const rightResult = await this.right.isSatisfiedBy(candidate); return rightResult; } toString(): string { return `(${this.left.toString()} or ${this.right.toString()})`; } } class OrNotSpecification extends OrSpecification { public async isSatisfiedBy(candidate: T): Promise { return (await super.isSatisfiedBy(candidate)) !== true; } toString(): string { return `not ${super.toString()}`; } } export class NotSpecification extends CompositeSpecification { constructor(private readonly spec: ICompositeSpecification) { super(); } public async isSatisfiedBy(candidate: T): Promise { return !this.spec.isSatisfiedBy(candidate); } toString(): string { return `(not ${this.spec.toString()})`; } } export class RangeSpecification extends CompositeSpecification { private readonly min: T; private readonly max: T; private readonly compareFn: (a: T, b: T) => number; constructor(min: T, max: T, compareFn: (a: T, b: T) => number) { super(); this.min = min; this.max = max; this.compareFn = compareFn; } public async isSatisfiedBy(candidate: T): Promise { return this.compareFn(candidate, this.min) >= 0 && this.compareFn(candidate, this.max) <= 0; } toString(): string { return `range [${this.min}, ${this.max}]`; } }