├── .travis.yml ├── .gitignore ├── .editorconfig ├── package.json ├── test ├── helper.js └── test.js ├── index.js └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.12" 5 | - "iojs" 6 | - "4" 7 | - "5" 8 | - "6" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | node_modules 4 | build 5 | *.node 6 | components 7 | coverage 8 | *.swp 9 | .idea 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = false -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pipeline-js", 3 | "description": "Hassle free pipeline design pattern implementation", 4 | "version": "1.0.2", 5 | "main": "index.js", 6 | "author": "Kamran Ahmed ", 7 | "keywords": [ 8 | "design-pattern", 9 | "pipeline", 10 | "pipeline-js" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/kamranahmedse/pipeline" 15 | }, 16 | "scripts": { 17 | "test": "mocha" 18 | }, 19 | "dependencies": {}, 20 | "devDependencies": { 21 | "assert": "^1.4.1", 22 | "mocha": "^3.0.2", 23 | "q": "^1.4.1" 24 | }, 25 | "engines": { 26 | "node": ">= 0.10" 27 | }, 28 | "license": "MIT" 29 | } 30 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | var q = require('q'); 2 | 3 | module.exports = { 4 | 5 | doubleSay: function (sayWhat) { 6 | return sayWhat + ', ' + sayWhat; 7 | }, 8 | 9 | capitalize: function (text) { 10 | return text[0].toUpperCase() + text.substring(1); 11 | }, 12 | 13 | exclaim: function (text) { 14 | return text + '!'; 15 | }, 16 | 17 | capitalizeAsync: function (text) { 18 | var deferred = q.defer(); 19 | deferred.resolve(text[0].toUpperCase() + text.substring(1)); 20 | return deferred.promise; 21 | }, 22 | 23 | doubleSayAsync: function (sayWhat) { 24 | var deferred = q.defer(); 25 | deferred.resolve(sayWhat + ', ' + sayWhat); 26 | return deferred.promise; 27 | }, 28 | 29 | exclaimAsync: function (text) { 30 | var deferred = q.defer(); 31 | deferred.resolve(text + '!'); 32 | return deferred.promise; 33 | } 34 | }; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Creates a new pipeline. Optionally pass an array of stages 5 | * 6 | * @param presetStages[] 7 | * @constructor 8 | */ 9 | function Pipeline(presetStages) { 10 | // Stages for the pipeline, either received through 11 | // the constructor or the pipe method in prototype 12 | this.stages = presetStages || []; 13 | } 14 | 15 | /** 16 | * Adds a new stage. Stage can be a function or some literal value. In case 17 | * of literal values. That specified value will be passed to the next stage and the 18 | * output from last stage gets ignored 19 | * 20 | * @param stage 21 | * @returns {Pipeline} 22 | */ 23 | Pipeline.prototype.pipe = function (stage) { 24 | this.stages.push(stage); 25 | 26 | return this; 27 | }; 28 | 29 | /** 30 | * Processes the pipeline with passed arguments 31 | * 32 | * @param args 33 | * @returns {*} 34 | */ 35 | Pipeline.prototype.process = function (args) { 36 | 37 | // Output is same as the passed args, if 38 | // there are no stages in the pipeline 39 | if (this.stages.length === 0) { 40 | return args; 41 | } 42 | 43 | // Set the stageOutput to be args 44 | // as there is no output to start with 45 | var stageOutput = args; 46 | 47 | this.stages.forEach(function (stage, counter) { 48 | 49 | // Output from the last stage was promise 50 | if (stageOutput && typeof stageOutput.then === 'function') { 51 | // Call the next stage only when the promise is fulfilled 52 | stageOutput = stageOutput.then(stage); 53 | } else { 54 | 55 | // Otherwise, call the next stage with the last stage output 56 | if (typeof stage === 'function') { 57 | stageOutput = stage(stageOutput); 58 | } else { 59 | stageOutput = stage; 60 | } 61 | } 62 | 63 | }); 64 | 65 | return stageOutput; 66 | }; 67 | 68 | module.exports = Pipeline; 69 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var assert = require('assert'); 4 | var Pipeline = require('../'); 5 | var helpers = require('./helper'); 6 | 7 | describe('pipeline', function () { 8 | 9 | // Helpers for normal testing 10 | var doubleSay = helpers.doubleSay, 11 | capitalize = helpers.capitalize, 12 | exclaim = helpers.exclaim; 13 | 14 | // Helpers for async testing 15 | var doubleSayAsync = helpers.doubleSayAsync, 16 | capitalizeAsync = helpers.capitalizeAsync, 17 | exclaimAsync = helpers.exclaimAsync; 18 | 19 | it('can process pipelines created using pipe', function (done) { 20 | 21 | var excited = new Pipeline(); 22 | 23 | excited.pipe(doubleSay) 24 | .pipe(capitalize) 25 | .pipe(exclaim); 26 | 27 | assert.equal(excited.process('hello'), 'Hello, hello!'); 28 | 29 | done(); 30 | }); 31 | 32 | it('can process pipelines passed using constructor', function (done) { 33 | 34 | var excited = new Pipeline([ 35 | doubleSay, 36 | capitalize, 37 | exclaim 38 | ]); 39 | 40 | assert.equal(excited.process('hello'), 'Hello, hello!'); 41 | 42 | done(); 43 | }); 44 | 45 | it('can reuse pipelines', function (done) { 46 | 47 | var excited = new Pipeline(); 48 | 49 | excited.pipe(doubleSay) 50 | .pipe(capitalize) 51 | .pipe(exclaim); 52 | 53 | assert.equal(excited.process('hello'), 'Hello, hello!'); 54 | assert.equal(excited.process('world'), 'World, world!'); 55 | assert.equal(excited.process('it works'), 'It works, it works!'); 56 | 57 | done(); 58 | }); 59 | 60 | it('can process any pipelines with all stages returning promises', function (done) { 61 | var excited = new Pipeline([ 62 | doubleSayAsync, 63 | capitalizeAsync, 64 | exclaimAsync 65 | ]); 66 | 67 | excited.process('hello') 68 | .then(function (value) { 69 | assert.equal(value, 'Hello, hello!'); 70 | }) 71 | .catch(function (error) { 72 | }); 73 | 74 | done(); 75 | }); 76 | 77 | 78 | it('can process any pipelines with some stages returning promises and some not', function (done) { 79 | var excited = new Pipeline([ 80 | doubleSayAsync, 81 | capitalize, 82 | exclaim 83 | ]); 84 | 85 | excited.process('hello') 86 | .then(function (value) { 87 | assert.equal(value, 'Hello, hello!'); 88 | }) 89 | .catch(function (error) { 90 | }); 91 | 92 | done(); 93 | }); 94 | 95 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pipeline-js 2 | 3 | ![Build Status](https://travis-ci.org/kamranahmedse/pipeline-js.svg) 4 | ![Package Version](https://img.shields.io/npm/v/pipeline-js.svg) 5 | 6 | > Hassle free pipeline pattern implementation with the support for sync and async stages 7 | 8 | 9 | ## Introduction 10 | 11 | Pipeline JS allows you to implement the pipeline pattern while creating **reusable pipelines** in your Javascript applications. You can create pipelines consisting of one or more stages once and then **process them using different payloads**. Pipeline processing is initiated by some payload and this payload will be passed from stage to stage in order to complete the required process. 12 | 13 | ### General Pipeline Example 14 | 15 | To demonstrate it using an example, consider a request made to access user by id; the pipeline may consist of stages including `getUserById`, `transformUser`, `convertToJson` and return. And for each next stage, the input comes from the last stage i.e. 16 | 17 | ```javascript 18 | ->(User ID)>>getUserById() 19 | ->(User Detail)>>transformUser() 20 | ->(Transformed Detail)>>convertToJson() 21 | ->(User Detail.json)>>return 22 | ``` 23 | 24 | ### Sample Programmatic implementation 25 | 26 | While using Pipeline JS, it can be written programmatically as 27 | 28 | ```javascript 29 | // First syntax 30 | var userPipeline = new Pipeline([ 31 | getUserById, 32 | transformUser, 33 | convertToJson 34 | ]); 35 | 36 | // Alternatively, you may write the same using the pipe method 37 | var userPipeline = (new Pipeline()).pipe(getUserById) 38 | .pipe(transformUser) 39 | .pipe(convertToJson); 40 | 41 | // Then this pipeline can be used with any payload i.e. 42 | var userJson = userPipeline.process(10); // JSON detail for the user with ID 10 43 | var userJson = userPipeline.process(23); // JSON detail for the user with ID 23 44 | ``` 45 | 46 | Where **stages shown above can be anything invokable**. The sample implementation for the stages may be something like below 47 | 48 | ```javascript 49 | // For example, methods from some objects 50 | var getUserById = UserModel.getUserById, 51 | transformUser = Transformers.transformUser, 52 | convertToJson = Utility.convertToJson; 53 | 54 | // Or functions 55 | var getUserById = function (userId) { 56 | //.. 57 | return promise; 58 | }; 59 | 60 | var transformUser = function (userDetail) { 61 | // .. 62 | return transformedObject; 63 | }; 64 | 65 | var convertToJson = function(userDetail) { 66 | // .. 67 | return jsonString; 68 | }; 69 | 70 | ``` 71 | 72 | Using pipeline will not only allow constructing reusable pipelines but also **result in comparatively cleaner and readable code** i.e. consider the following example 73 | 74 | ```javascript 75 | var output = JSON.stringify(transformFilters(getSelectedFilters())); 76 | 77 | // It can be transformed to 78 | var pipeline = new Pipeline([ 79 | getSelectedFilters, 80 | transformFilters, 81 | JSON.stringify 82 | ]); 83 | 84 | // Or maybe use the alternate syntax 85 | var pipeline = (new Pipeline()).pipe(getSelectedFilters) 86 | .pipe(transformFilters) 87 | .pipe(JSON.stringify); 88 | 89 | // Get the output by processing the pipeline 90 | var output = pipeline.process(); 91 | ``` 92 | 93 | ## Installation 94 | 95 | Run the below command to install using NPM 96 | 97 | ``` 98 | npm install --save pipeline-js 99 | ``` 100 | 101 | ## Usage 102 | 103 | Operations in a pipeline i.e. stages can be **anything that is callable** i.e. closures and anything that's invokable is good. 104 | 105 | In order to create a pipeline, you can either pass the stages as an array parameter to constructor 106 | 107 | ```javascript 108 | var pipeline = require('pipeline-js'); 109 | 110 | // Creating pipeline using constructor parameterå 111 | var pipeline = new Pipeline([ 112 | invokableStage1, 113 | invokableStage2, 114 | invokableStage3 115 | ]); 116 | 117 | // Process pipeline with payload1 118 | var output1 = pipeline.process(payload1); 119 | var output2 = pipeline.process(payload2); 120 | ``` 121 | 122 | Or you can create a pipeline object and call the `pipe` method on it to successively add the invokable stages 123 | 124 | ```javascript 125 | var pipeline = require('pipeline-js'); 126 | 127 | // Creating pipeline using pipe method 128 | var pipeline = (new Pipeline()).pipe(invokableStage1) 129 | .pipe(invokableStage2) 130 | .pipe(invokableStage3 131 | 132 | // Process pipeline with payload1 133 | var output1 = pipeline.process(payload1); 134 | var output2 = pipeline.process(payload2); 135 | ``` 136 | 137 | 138 | ## Sync/Async Usage 139 | 140 | The only difference between the synchronous usage and asynchronous usage is how the output is handled. For both types of usages, **pipelines are created the same way**. The difference is when you call the `process()` method, if the pipeline has all the stages returning concrete output the process method returns concrete value, however if any of the stages returns a promise then the `process` method returns promise and you will have to use `.then()` to get the output. 141 | 142 | Examples for both the sync and async usage are given below 143 | 144 | ## Sync Example 145 | 146 | > How to use when all the stages return concrete values? 147 | 148 | > If none of the stages return promise then `process(payload)` will return concrete value 149 | 150 | ```javascript 151 | var addOne = function (x) { 152 | return x + 1; 153 | }; 154 | 155 | var square = function (x) { 156 | return x * x; 157 | }; 158 | 159 | var minusTwo = function (x) { 160 | return x - 2; 161 | }; 162 | 163 | // Without pipeline 164 | // - Not reusable 165 | // - Not that clean 166 | var output1 = minusTwo(square(addOne(10))); 167 | var output2 = minusTwo(square(addOne(10))); 168 | 169 | // With Pipeline 170 | // Reusable with different payload 171 | // Cleaner 172 | var someFormula = new Pipeline([ 173 | addOne, 174 | square, 175 | minusTwo 176 | ]); 177 | 178 | var result = someFormula.process(10); // 10 + 1 => 11 179 | // 11 * 11 => 121 180 | // 121 - 2 => 119 181 | console.log(result); // (int) 119 182 | 183 | var result = someFormula.process(20); // 20 + 1 => 21 184 | // 21 * 21 => 441 185 | // 441 - 2 => 339 186 | console.log(result); // (int) 339 187 | ``` 188 | 189 | Or maybe you can write the same example as 190 | 191 | ```javascript 192 | var someFormula = (new Pipeline()).pipe(addOne) 193 | .pipe(square) 194 | .pipe(minusTwo); 195 | 196 | var output1 = someFormula.process(20); 197 | var output2 = someFormula.process(20); 198 | 199 | ``` 200 | 201 | ## Async Example 202 | 203 | > How to use when one or all of the stages return promise? 204 | 205 | > If any single of the stages returns a promise, `process(payload)` will return a promise 206 | 207 | ```javascript 208 | var Pipeline = require('pipeline-js'); 209 | 210 | // Gets the user by ID and returns promise 211 | var getUserById = function (userId) { 212 | var q = q.defer(); 213 | // .. 214 | return q.promise; 215 | }; 216 | 217 | // Transforms the user 218 | var transformUser = function (userDetail) { 219 | return { 220 | name: userDetail.name, 221 | email: userDetail.email, 222 | password: '*****' 223 | }; 224 | }; 225 | 226 | // Converts to JSON 227 | var createJson = function (object) { 228 | return JSON.stringify(object); 229 | }; 230 | 231 | var pipeline = new Pipeline([ 232 | getUserById, // Returns promise 233 | transformUser, 234 | createJson 235 | ]); 236 | 237 | // process() will return promise; since one of the stages returns a promise 238 | var output = pipeline.process(142) 239 | .then(function(userJson){ 240 | console.log(userJson); // (string) {"name": "John Doe", "email": "johndoe@gmail.com", "password": "****"} 241 | }) 242 | .catch(function(error) { 243 | console.log(error); 244 | }); 245 | 246 | var output = pipeline.process(263) // promise will be returned 247 | .then(function(userJson){ 248 | console.log(userJson); // (string) {"name": "Jane Doe", "email": "janedoe@gmail.com", "password": "****"} 249 | }) 250 | .catch(function(error) { 251 | console.log(error); 252 | }); 253 | ``` 254 | 255 | Altneratively, 256 | 257 | ```javascript 258 | // Same pipeline using `pipe` method 259 | var pipeline = (new Pipeline()).pipe(getUserById) // Returns promise 260 | .pipe(transformUser) 261 | .pipe(createJson); 262 | 263 | var output = pipeline.process(142) // promise will be returned; since one of the stages returns a promise 264 | .then(function(userJson){ 265 | console.log(userJson); // (string) {"name": "John Doe", "email": "johndoe@gmail.com", "password": "****"} 266 | }) 267 | .catch(function(error) { 268 | console.log(error); 269 | }); 270 | ``` 271 | 272 | ## Sidenote 273 | 274 | You may also want to check this [pipeline proposal](https://github.com/mindeavor/es-pipeline-operator) 275 | 276 | ## Contribution 277 | 278 | Feel free to fork, extend, create issues, create PRs or spread the word. 279 | 280 | ## License 281 | 282 | MIT © [Kamran Ahmed](http://kamranahmed.info) 283 | --------------------------------------------------------------------------------