spec-pattern
v3.5.0
Published
Specification Pattern in TypeScript
Downloads
5,714
Maintainers
Readme
spec-pattern
Implementation of the Specification Pattern for JavaScript and TypeScript.
Build complex filters and rules easily.
- No external dependencies.
- Fully tested.
- It uses semantic versioning.
- Forks are welcome! See how to contribute.
Installation
$ npm i spec-pattern
Usage
Without syntax sugar
A simple Between rule
import { Between } from 'spec-pattern';
const rating = new Between( 1, 5 );
console.log( rating.isSatisfiedBy( 3 ) ); // true
console.log( rating.isSatisfiedBy( 0 ) ); // true
A little more complex Between rule
import { Between } from 'spec-pattern';
const desiredAgesToAnswerSurvey = new Between( 16, 21 )
.or( new Between( 65, 120 ) );
console.log( desiredAgesToAnswerSurvey.isSatisfiedBy( 18 ) ); // true
console.log( desiredAgesToAnswerSurvey.isSatisfiedBy( 70 ) ); // true
console.log( desiredAgesToAnswerSurvey.isSatisfiedBy( 5 ) ); // false
Composing rules
import { Between, In, GreaterThan } from 'spec-pattern';
const someCrazyRule = new Between( 1, 3 )
.or( new Between( 6, 9 ) )
.or( new In( [ 11, 25, 31 ] ) )
.or( new GreaterThan( 50 ) );
console.log( someCrazyRule.isSatisfiedBy( 2 ) ); // true
console.log( someCrazyRule.isSatisfiedBy( 7 ) ); // true
console.log( someCrazyRule.isSatisfiedBy( 5 ) ); // false
console.log( someCrazyRule.isSatisfiedBy( 11 ) ); // true
console.log( someCrazyRule.isSatisfiedBy( 50 ) ); // false
console.log( someCrazyRule.isSatisfiedBy( 51 ) ); // true
// Printable !
console.log( someCrazyRule.toString() );
// (((between (1, 3) or between (6, 9)) or in [11, 25, 31]) or greater than 50)
Not only numbers
import { StartsWith, Contains } from 'spec-pattern';
const helloWithoutWorld = new StartsWith( 'Hello' )
.andNot( new Contains( 'world' ) );
console.log( helloWithoutWorld.isSatisfiedBy( 'Hello Bob' ) ); // true
console.log( helloWithoutWorld.isSatisfiedBy( 'Hello world' ) ); // false
import { LengthBetween, EqualTo } from 'spec-pattern';
const crazyText = new LengthBetween( 2, 5 )
.andNot( new EqualTo( 'Hello' ) );
console.log( crazyText.isSatisfiedBy( '' ) ); // false
console.log( crazyText.isSatisfiedBy( 'Hi' ) ); // true
console.log( crazyText.isSatisfiedBy( 'Hello' ) ); // false
console.log( crazyText.isSatisfiedBy( 'Howdy' ) ); // true
console.log( crazyText.isSatisfiedBy( 'Hello world' ) ); // false
With syntax sugar
A simple Between rule
import { between } from 'spec-pattern';
const rating = between( 1, 5 );
console.log( rating.isSatisfiedBy( 3 ) ); // true
console.log( rating.isSatisfiedBy( 0 ) ); // true
A little more complex Between rule
import { between } from 'spec-pattern';
const desiredAgesToAnswerSurvey = between( 16, 21 )
.or( between( 65, 120 ) );
console.log( desiredAgesToAnswerSurvey.isSatisfiedBy( 18 ) ); // true
console.log( desiredAgesToAnswerSurvey.isSatisfiedBy( 70 ) ); // true
console.log( desiredAgesToAnswerSurvey.isSatisfiedBy( 5 ) ); // false
Composing rules
import { between, isIn, greaterThan } from 'spec-pattern';
const someCrazyRule = between( 1, 3 )
.or( between( 6, 9 ) )
.or( isIn( [ 11, 25, 31 ] ) )
.or( greaterThan( 50 ) );
console.log( someCrazyRule.isSatisfiedBy( 2 ) ); // true
console.log( someCrazyRule.isSatisfiedBy( 7 ) ); // true
console.log( someCrazyRule.isSatisfiedBy( 5 ) ); // false
console.log( someCrazyRule.isSatisfiedBy( 11 ) ); // true
console.log( someCrazyRule.isSatisfiedBy( 50 ) ); // false
console.log( someCrazyRule.isSatisfiedBy( 51 ) ); // true
// Printable !
console.log( someCrazyRule.toString() );
// (((between (1, 3) or between (6, 9)) or in [11, 25, 31]) or greater than 50)
Not only numbers
import { startsWith, contains } from 'spec-pattern';
const helloWithoutWorld = startsWith( 'Hello' )
.andNot( contains( 'world' ) );
console.log( helloWithoutWorld.isSatisfiedBy( 'Hello Bob' ) ); // true
console.log( helloWithoutWorld.isSatisfiedBy( 'Hello world' ) ); // false
import { lengthBetween, equalTo } from 'spec-pattern';
const crazyText = lengthBetween( 2, 5 )
.andNot( equalTo( 'Hello' ) );
console.log( crazyText.isSatisfiedBy( '' ) ); // false
console.log( crazyText.isSatisfiedBy( 'Hi' ) ); // true
console.log( crazyText.isSatisfiedBy( 'Hello' ) ); // false
console.log( crazyText.isSatisfiedBy( 'Howdy' ) ); // true
console.log( crazyText.isSatisfiedBy( 'Hello world' ) ); // false
Available sugar
There is a corresponding sugar function for every available class. Sugar functions are always named in camelCase.
For instance, sameValueAs()
for the class SameValueAs
.
The only exception is the class In
. Since in
is a reserved word in JavaScript and thus cannot be a function name, the corresponding sugar is isIn
.
Available classes
SameValueAs( value: any )
: equality of values, not of types, not of instancesStrictSameValueAs( value: any )
: equality of values and types, not of instancesEqualTo( value: any )
: equality of values or instances, with==
StrictEqualTo( value: any )
: equality of values and types or of instances, with===
SameTypeAs( value: any )
: equality of typesGreaterThan( value: any )
GreaterThanOrEqualTo( value: any )
LessThan( value: any )
LessThanOrEqualTo( value: any )
Between( min: any, max: any )
In( values: array )
: inside an arrayStartsWith( value: string, ignoreCase: boolean = false )
: string starts withEndsWith( value: string, ignoreCase: boolean = false )
: string ends withContains( value: string, ignoreCase: boolean = false )
: string containsLengthBetween( min: any, max: any )
: string length between two valuesEmpty()
: string is empty or array is emptyMatches( regex: RegExp )
: matches a regular expressionAny( ...specs: Spec )
: composite that takes in multipleSpec
s and performs an orAll( ...specs: Spec )
: composite that takes in multipleSpec
s and performs an and
All these classes extend the abstract class Composite
, which in turn implements the interface Spec
:
export interface Spec< C, T extends C | unknown > {
isSatisfiedBy( candidate: C | T ): boolean;
and( other: Spec< C, T > ): Spec< C, T >;
andNot( other: Spec< C, T > ): Spec< C, T >;
or( other: Spec< C, T > ): Spec< C, T >;
orNot( other: Spec< C, T > ): Spec< C, T >;
xor( other: Spec< C, T > ): Spec< C, T >;
xorNot( other: Spec< C, T > ): Spec< C, T >;
not(): Spec< C, T >;
}
Creating your own class
Create your own class by extending the abstract class Composite
, like in the following example. Of course, you can also extend one of the aforementioned classes or implement the interface Spec
(but why reinventing the wheel, right?).
Let's create a class DifferentFrom
...
...in TypeScript:
import { Composite } from 'spec-pattern';
export class DifferentFrom< C, T extends C | unknown > extends Composite< C, T > {
constructor( private _value: T ) {
super();
}
isSatisfiedBy( candidate: C | T ): boolean {
return this._value != candidate;
}
toString(): string {
return 'different from ' + this._value;
}
}
...or in JavaScript 6+:
import { Composite } from 'spec-pattern';
class DifferentFrom extends Composite {
constructor( value ) {
this.value = value;
}
isSatisfiedBy( candidate ) {
return this.value != candidate;
}
toString() {
return 'different from ' + this.value;
}
}
...or in JavaScript 5+:
var Composite = require( 'spec-pattern' ).Composite;
function DifferentFrom( value ) {
Composite.call( this ); // super()
this.value = value;
this.isSatisfiedBy = function ( candidate ) {
return this.value != candidate;
};
this.toString = function() {
return 'different from ' + this.value;
};
}
DifferentFrom.prototype = Object.create( Composite.prototype );
DifferentFrom.prototype.constructor = DifferentFrom;
That's it! Just three methods: constructor
, isSatisfiedBy
, and toString()
.