{"id":18576152,"url":"https://github.com/thiagodp/spec-pattern","last_synced_at":"2025-04-10T08:31:06.274Z","repository":{"id":150584528,"uuid":"124640201","full_name":"thiagodp/spec-pattern","owner":"thiagodp","description":"Specification design pattern for JavaScript and TypeScript with bonus classes","archived":false,"fork":false,"pushed_at":"2022-05-19T22:02:00.000Z","size":523,"stargazers_count":60,"open_issues_count":0,"forks_count":4,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-03-24T19:03:04.615Z","etag":null,"topics":["builder","design-pattern","filter","javascript","matcher","matching","pattern","rule","spec","specification","specification-pattern","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/thiagodp.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"contributing.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-03-10T09:17:19.000Z","updated_at":"2025-03-11T12:25:05.000Z","dependencies_parsed_at":"2023-06-11T08:00:37.420Z","dependency_job_id":null,"html_url":"https://github.com/thiagodp/spec-pattern","commit_stats":null,"previous_names":[],"tags_count":13,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thiagodp%2Fspec-pattern","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thiagodp%2Fspec-pattern/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thiagodp%2Fspec-pattern/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/thiagodp%2Fspec-pattern/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/thiagodp","download_url":"https://codeload.github.com/thiagodp/spec-pattern/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248185332,"owners_count":21061498,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["builder","design-pattern","filter","javascript","matcher","matching","pattern","rule","spec","specification","specification-pattern","typescript"],"created_at":"2024-11-06T23:23:49.003Z","updated_at":"2025-04-10T08:31:06.262Z","avatar_url":"https://github.com/thiagodp.png","language":"TypeScript","readme":"[![npm (tag)](https://img.shields.io/npm/v/spec-pattern?color=green\u0026label=NPM\u0026style=for-the-badge)](https://github.com/thiagodp/spec-pattern/releases)\n[![License](https://img.shields.io/npm/l/spec-pattern.svg?style=for-the-badge\u0026color=green)](https://github.com/thiagodp/spec-pattern/blob/master/LICENSE.txt)\n[![npm](https://img.shields.io/npm/dt/spec-pattern?style=for-the-badge\u0026color=green)](https://www.npmjs.com/package/spec-pattern)\n\n# spec-pattern\n\nImplementation of the [Specification Pattern](https://en.wikipedia.org/wiki/Specification_pattern) for JavaScript and TypeScript.\n\n\u003e Build complex filters and rules easily.\n\n- No external dependencies;\n- Fully [tested](https://github.com/thiagodp/spec-pattern/blob/master/__tests__/);\n- [Semantic versioning](https://semver.org);\n- *Forks are welcome!* See [how to contribute](contributing.md).\n\n## Installation\n\n```bash\n$ npm i spec-pattern\n```\n\n## Usage\n\n### Without syntax sugar\n\n#### A simple Between rule\n ```js\nimport { Between } from 'spec-pattern';\n\nconst rating = new Between( 1, 5 );\n\nconsole.log( rating.isSatisfiedBy( 3 ) ); // true\nconsole.log( rating.isSatisfiedBy( 0 ) ); // false\n```\n\n\n#### A little more complex Between rule\n ```js\nimport { Between } from 'spec-pattern';\n\nconst desiredAgesToAnswerSurvey = new Between( 16, 21 )\n\t.or( new Between( 65, 120 ) );\n\nconsole.log( desiredAgesToAnswerSurvey.isSatisfiedBy( 18 ) ); // true\nconsole.log( desiredAgesToAnswerSurvey.isSatisfiedBy( 70 ) ); // true\nconsole.log( desiredAgesToAnswerSurvey.isSatisfiedBy( 5 ) ); // false\n```\n\n#### Composing rules\n ```js\nimport { Between, In, GreaterThan } from 'spec-pattern';\n\nconst someCrazyRule = new Between( 1, 3 )\n    .or( new Between( 6, 9 ) )\n    .or( new In( [ 11, 25, 31 ] ) )\n    .or( new GreaterThan( 50 ) );\n\nconsole.log( someCrazyRule.isSatisfiedBy( 2 ) ); // true\nconsole.log( someCrazyRule.isSatisfiedBy( 7 ) ); // true\nconsole.log( someCrazyRule.isSatisfiedBy( 5 ) ); // false\nconsole.log( someCrazyRule.isSatisfiedBy( 11 ) ); // true\nconsole.log( someCrazyRule.isSatisfiedBy( 50 ) ); // false\nconsole.log( someCrazyRule.isSatisfiedBy( 51 ) ); // true\n\n// Printable !\nconsole.log( someCrazyRule.toString() );\n// (((between (1, 3) or between (6, 9)) or in [11, 25, 31]) or greater than 50)\n```\n\n#### Not only numbers\n```js\nimport { StartsWith, Contains } from 'spec-pattern';\n\nconst helloWithoutWorld = new StartsWith( 'Hello' )\n    .andNot( new Contains( 'world' ) );\n\nconsole.log( helloWithoutWorld.isSatisfiedBy( 'Hello Bob' ) ); // true\nconsole.log( helloWithoutWorld.isSatisfiedBy( 'Hello world' ) ); // false\n```\n\n```js\nimport { LengthBetween, EqualTo } from 'spec-pattern';\n\nconst crazyText = new LengthBetween( 2, 5 )\n    .andNot( new EqualTo( 'Hello' ) );\n\nconsole.log( crazyText.isSatisfiedBy( '' ) ); // false\nconsole.log( crazyText.isSatisfiedBy( 'Hi' ) ); // true\nconsole.log( crazyText.isSatisfiedBy( 'Hello' ) ); // false\nconsole.log( crazyText.isSatisfiedBy( 'Howdy' ) ); // true\nconsole.log( crazyText.isSatisfiedBy( 'Hello world' ) ); // false\n```\n\n\n### With syntax sugar\n\n#### A simple Between rule\n ```js\nimport { between } from 'spec-pattern';\n\nconst rating = between( 1, 5 );\n\nconsole.log( rating.isSatisfiedBy( 3 ) ); // true\nconsole.log( rating.isSatisfiedBy( 0 ) ); // false\n```\n\n#### A little more complex Between rule\n ```js\nimport { between } from 'spec-pattern';\n\nconst desiredAgesToAnswerSurvey = between( 16, 21 )\n\t.or( between( 65, 120 ) );\n\nconsole.log( desiredAgesToAnswerSurvey.isSatisfiedBy( 18 ) ); // true\nconsole.log( desiredAgesToAnswerSurvey.isSatisfiedBy( 70 ) ); // true\nconsole.log( desiredAgesToAnswerSurvey.isSatisfiedBy( 5 ) ); // false\n```\n\n#### Composing rules\n ```js\nimport { between, isIn, greaterThan } from 'spec-pattern';\n\nconst someCrazyRule = between( 1, 3 )\n    .or( between( 6, 9 ) )\n    .or( isIn( [ 11, 25, 31 ] ) )\n    .or( greaterThan( 50 ) );\n\nconsole.log( someCrazyRule.isSatisfiedBy( 2 ) ); // true\nconsole.log( someCrazyRule.isSatisfiedBy( 7 ) ); // true\nconsole.log( someCrazyRule.isSatisfiedBy( 5 ) ); // false\nconsole.log( someCrazyRule.isSatisfiedBy( 11 ) ); // true\nconsole.log( someCrazyRule.isSatisfiedBy( 50 ) ); // false\nconsole.log( someCrazyRule.isSatisfiedBy( 51 ) ); // true\n\n// Printable !\nconsole.log( someCrazyRule.toString() );\n// (((between (1, 3) or between (6, 9)) or in [11, 25, 31]) or greater than 50)\n```\n\n#### Not only numbers\n```js\nimport { startsWith, contains } from 'spec-pattern';\n\nconst helloWithoutWorld = startsWith( 'Hello' )\n    .andNot( contains( 'world' ) );\n\nconsole.log( helloWithoutWorld.isSatisfiedBy( 'Hello Bob' ) ); // true\nconsole.log( helloWithoutWorld.isSatisfiedBy( 'Hello world' ) ); // false\n```\n\n```js\nimport { lengthBetween, equalTo } from 'spec-pattern';\n\nconst crazyText = lengthBetween( 2, 5 )\n    .andNot( equalTo( 'Hello' ) );\n\nconsole.log( crazyText.isSatisfiedBy( '' ) ); // false\nconsole.log( crazyText.isSatisfiedBy( 'Hi' ) ); // true\nconsole.log( crazyText.isSatisfiedBy( 'Hello' ) ); // false\nconsole.log( crazyText.isSatisfiedBy( 'Howdy' ) ); // true\nconsole.log( crazyText.isSatisfiedBy( 'Hello world' ) ); // false\n```\n\n\n## Available sugar\n\nThere is a corresponding sugar function for every available class. Sugar functions are always named in _camelCase_.\nFor instance, `sameValueAs()` for the class `SameValueAs`.\nThe 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`.\n\n\n## Available classes\n\n- `SameValueAs( value: any )`: equality of values, not of types, not of instances\n- `StrictSameValueAs( value: any )`: equality of values and types, not of instances\n- `EqualTo( value: any )`: equality of values or instances, with `==`\n- `StrictEqualTo( value: any )`: equality of values and types or of instances, with `===`\n- `SameTypeAs( value: any )`: equality of types\n- `GreaterThan( value: any )`\n- `GreaterThanOrEqualTo( value: any )`\n- `LessThan( value: any )`\n- `LessThanOrEqualTo( value: any )`\n- `Between( min: any, max: any )`\n- `In( values: array )`: inside an array\n- `StartsWith( value: string, ignoreCase: boolean = false )`: string starts with\n- `EndsWith( value: string, ignoreCase: boolean = false )`: string ends with\n- `Contains( value: string, ignoreCase: boolean = false )`: string contains\n- `LengthBetween( min: any, max: any )`: string length between two values\n- `Empty()`: string is empty or array is empty\n- `Matches( regex: RegExp )`: matches a regular expression\n- `Any( ...specs: Spec )`: composite that takes in multiple `Spec`s and performs an or\n- `All( ...specs: Spec )`: composite that takes in multiple `Spec`s and performs an and\n\nAll these classes extend the abstract class `Composite`, which in turn implements the interface `Spec`:\n\n```typescript\nexport interface Spec\u003c C, T extends C | unknown \u003e {\n\n    isSatisfiedBy( candidate: C | T ): boolean;\n\n    and( other: Spec\u003c C, T \u003e ): Spec\u003c C, T \u003e;\n\n    andNot( other: Spec\u003c C, T \u003e ): Spec\u003c C, T \u003e;\n\n    or( other: Spec\u003c C, T \u003e ): Spec\u003c C, T \u003e;\n\n    orNot( other: Spec\u003c C, T \u003e ): Spec\u003c C, T \u003e;\n\n    xor( other: Spec\u003c C, T \u003e ): Spec\u003c C, T \u003e;\n\n    xorNot( other: Spec\u003c C, T \u003e ): Spec\u003c C, T \u003e;\n\n    not(): Spec\u003c C, T \u003e;\n\n}\n```\n\n## Creating your own class\n\nCreate 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?)*.\n\nLet's create a class `DifferentFrom` ...\n\n*...in TypeScript:*\n```typescript\nimport { Composite } from 'spec-pattern';\n\nexport class DifferentFrom\u003c C, T extends C | unknown \u003e extends Composite\u003c C, T \u003e {\n\n    constructor( private _value: T ) {\n        super();\n    }\n\n    isSatisfiedBy( candidate: C | T ): boolean {\n        return this._value != candidate;\n    }\n\n    toString(): string {\n        return 'different from ' + this._value;\n    }\n\n}\n```\n\n*...or in JavaScript 6+:*\n```js\nimport { Composite } from 'spec-pattern';\n\nclass DifferentFrom extends Composite {\n\n    constructor( value ) {\n        this.value = value;\n    }\n\n    isSatisfiedBy( candidate ) {\n        return this.value != candidate;\n    }\n\n    toString() {\n        return 'different from ' + this.value;\n    }\n}\n```\n\n\n*...or in JavaScript 5+:*\n```js\nvar Composite  = require( 'spec-pattern' ).Composite;\n\nfunction DifferentFrom( value ) {\n\n    Composite.call( this ); // super()\n\n    this.value = value;\n\n    this.isSatisfiedBy = function ( candidate ) {\n        return this.value != candidate;\n    };\n\n    this.toString = function() {\n        return 'different from ' + this.value;\n    };\n}\n\nDifferentFrom.prototype = Object.create( Composite.prototype );\nDifferentFrom.prototype.constructor = DifferentFrom;\n```\n\n*That's it!* Just three methods: `constructor`, `isSatisfiedBy`, and `toString()`.\n\n## License\n\n[MIT](LICENSE) © [Thiago Delgado Pinto](https://github.com/thiagodp)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthiagodp%2Fspec-pattern","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fthiagodp%2Fspec-pattern","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fthiagodp%2Fspec-pattern/lists"}