├── .eslintrc.js ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── logo └── logo_s.png ├── package-lock.json ├── package.json ├── prettier.config.js ├── src ├── getPropertyByPath.js ├── index.js └── lib │ ├── isFunction.js │ └── isPromise.js └── test ├── examples.js └── test.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ["prettier"], 3 | parserOptions: { 4 | ecmaVersion: 8, 5 | }, 6 | env: { 7 | browser: true, 8 | commonjs: true, 9 | es6: true, 10 | node: true, 11 | mocha: true, 12 | }, 13 | extends: ["plugin:prettier/recommended"], 14 | rules: { 15 | "prettier/prettier": "error", 16 | "one-var": ["error", "never"], 17 | "one-var-declaration-per-line": ["error", "always"], 18 | "operator-assignment": ["error", "always"], 19 | "operator-linebreak": [ 20 | "error", 21 | "before", 22 | { 23 | overrides: { 24 | "=": "none", 25 | }, 26 | }, 27 | ], 28 | "max-statements-per-line": [ 29 | "error", 30 | { 31 | max: 1, 32 | }, 33 | ], 34 | eqeqeq: ["error", "always"], 35 | "no-multiple-empty-lines": [ 36 | "error", 37 | { 38 | max: 1, 39 | }, 40 | ], 41 | "no-unexpected-multiline": "error", 42 | "no-unreachable": "error", 43 | "no-extra-boolean-cast": "off", 44 | "no-unused-vars": "error", 45 | "no-empty": "error", 46 | "no-useless-escape": "off", 47 | "comma-spacing": [ 48 | "error", 49 | { 50 | before: false, 51 | after: true, 52 | }, 53 | ], 54 | "no-undef": "error", 55 | "no-console": "warn", 56 | "block-spacing": ["error", "always"], 57 | yoda: "error", 58 | "arrow-spacing": "error", 59 | "func-call-spacing": ["error", "never"], 60 | "function-call-argument-newline": ["error", "consistent"], 61 | "no-extra-semi": "error", 62 | "no-control-regex": "error", 63 | "no-global-assign": "error", 64 | "no-redeclare": "error", 65 | "no-cond-assign": "error", 66 | "key-spacing": [ 67 | "error", 68 | { 69 | beforeColon: false, 70 | }, 71 | ], 72 | "array-bracket-spacing": ["error", "never"], 73 | "array-bracket-newline": ["error", "consistent"], 74 | "array-element-newline": ["error", "consistent"], 75 | "spaced-comment": [ 76 | "error", 77 | "always", 78 | { 79 | exceptions: ["-", "+"], 80 | }, 81 | ], 82 | "object-curly-spacing": ["error", "always"], 83 | "object-curly-newline": [ 84 | "error", 85 | { 86 | consistent: true, 87 | }, 88 | ], 89 | "object-property-newline": [ 90 | "error", 91 | { 92 | allowAllPropertiesOnSameLine: true, 93 | }, 94 | ], 95 | curly: "error", 96 | "no-mixed-operators": "error", 97 | "arrow-body-style": ["error", "as-needed"], 98 | "arrow-parens": "error", 99 | "no-confusing-arrow": "error", 100 | "prefer-arrow-callback": "error", 101 | "max-len": [ 102 | "error", 103 | { 104 | code: 120, 105 | tabWidth: 4, 106 | }, 107 | ], 108 | "array-callback-return": "error", 109 | "brace-style": ["error", "1tbs"], 110 | "no-prototype-builtins": "off", 111 | "prefer-const": "error", 112 | camelcase: [ 113 | "error", 114 | { 115 | properties: "never", 116 | ignoreDestructuring: false, 117 | }, 118 | ], 119 | "no-var": "error", 120 | "class-methods-use-this": "error", 121 | "default-param-last": "error", 122 | "no-alert": "error", 123 | "no-caller": "error", 124 | "comma-dangle": [ 125 | "error", 126 | { 127 | arrays: "always-multiline", 128 | objects: "always-multiline", 129 | imports: "always-multiline", 130 | exports: "always-multiline", 131 | functions: "always-multiline", 132 | }, 133 | ], 134 | "no-constructor-return": "error", 135 | "no-else-return": "error", 136 | "dot-location": ["error", "property"], 137 | "max-classes-per-file": ["error", 1], 138 | semi: ["error", "always"], 139 | "semi-spacing": [ 140 | "error", 141 | { 142 | before: false, 143 | after: true, 144 | }, 145 | ], 146 | "semi-style": ["error", "last"], 147 | "padded-blocks": [ 148 | "error", 149 | "never", 150 | { 151 | allowSingleLineBlocks: true, 152 | }, 153 | ], 154 | "space-before-blocks": "error", 155 | "space-in-parens": ["error", "never"], 156 | "space-infix-ops": "error", 157 | "no-multi-spaces": "error", 158 | "space-unary-ops": [ 159 | "error", 160 | { 161 | words: true, 162 | nonwords: false, 163 | overrides: {}, 164 | }, 165 | ], 166 | "switch-colon-spacing": [ 167 | "error", 168 | { 169 | after: true, 170 | before: false, 171 | }, 172 | ], 173 | "dot-notation": "error", 174 | "padding-line-between-statements": [ 175 | "error", 176 | { 177 | blankLine: "always", 178 | prev: "expression", 179 | next: "block-like", 180 | }, 181 | { 182 | blankLine: "always", 183 | prev: "block-like", 184 | next: "expression", 185 | }, 186 | { 187 | blankLine: "always", 188 | prev: "*", 189 | next: "import", 190 | }, 191 | { 192 | blankLine: "always", 193 | prev: "*", 194 | next: "return", 195 | }, 196 | { 197 | blankLine: "always", 198 | prev: "import", 199 | next: "*", 200 | }, 201 | { 202 | blankLine: "any", 203 | prev: "import", 204 | next: "import", 205 | }, 206 | { 207 | blankLine: "always", 208 | prev: ["const", "let", "var"], 209 | next: "*", 210 | }, 211 | { 212 | blankLine: "always", 213 | prev: "*", 214 | next: ["const", "let", "var"], 215 | }, 216 | { 217 | blankLine: "any", 218 | prev: ["const", "let", "var"], 219 | next: ["const", "let", "var"], 220 | }, 221 | ], 222 | }, 223 | }; 224 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | ### License 4 | 5 | [ISC](https://en.wikipedia.org/wiki/ISC_license) (Very similar to MIT) 6 | 7 | ### Hostility towards anyone trying to help by reporting bugs or asking questions 8 | 9 | None. 10 | 11 | ### Pull requests? 12 | 13 | Who doesn't love them? 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Expected Behavior 2 | 3 | 4 | ### Actual Behavior 5 | 6 | 7 | ### Steps to Reproduce the Problem 8 | 9 | 1. 10 | 1. 11 | 1. 12 | 13 | ### Specifications 14 | 15 | - Version: 16 | - Platform (Node, web, electron...): 17 | - Platform version: 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Motivation and Context 7 | 8 | 9 | 10 | ## How Has This Been Tested? 11 | 12 | 13 | 14 | 15 | ## Screenshots (if appropriate): 16 | 17 | ## Types of changes 18 | 19 | - [ ] Bug fix (non-breaking change which fixes an issue) 20 | - [ ] New feature (non-breaking change which adds functionality) 21 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 22 | 23 | ## Checklist: 24 | 25 | 26 | - [ ] All tests pass. 27 | - [ ] Test coverage is 100%. 28 | - [ ] I used prettier and eslint, which made me follow the style of this project. 29 | - [ ] My change requires a change to the documentation. 30 | - [ ] I have updated the documentation accordingly. 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | .vscode/launch.json 35 | .nyc_output 36 | /.idea/ 37 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "14" 4 | after_success: npm run coverage -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project owner. You can find the contact details on [egeozcan.com](https://egeozcan.com). The project owner will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project owner is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Yavuz Ege Özcan 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, 10 | OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, 11 | DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS 12 | ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS 13 | SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [![PPIPE](logo/logo_s.png)](https://github.com/egeozcan/ppipe) 2 | 3 | [![build](https://travis-ci.org/egeozcan/ppipe.svg?branch=master)](https://travis-ci.org/egeozcan/ppipe) 4 | [![Coverage Status](https://coveralls.io/repos/github/egeozcan/ppipe/badge.svg?branch=master)](https://coveralls.io/github/egeozcan/ppipe?branch=master) 5 | [![npm](https://img.shields.io/npm/v/ppipe.svg)](https://www.npmjs.com/package/ppipe) 6 | [![npm](https://img.shields.io/npm/dt/ppipe.svg)](https://www.npmjs.com/package/ppipe) 7 | [![license](https://img.shields.io/github/license/egeozcan/ppipe.svg)](https://github.com/egeozcan/ppipe/blob/master/LICENSE) 8 | [![DeepScan Grade](https://deepscan.io/api/projects/565/branches/916/badge/grade.svg)](https://deepscan.io/dashboard/#view=project&pid=565&bid=916) 9 | 10 | _Please note that this library is considered "done". It is still maintained and will be in the foreseeable future, but, 11 | other than adding Typescript support, no new functionality will be added. At least, there is no plan to do so. 12 | This library has an extensive test suite with 100% coverage, and it is used by at least a few well-established 13 | projects in production. The mythical "production-ready" seems to be reached :)_ 14 | 15 | _All bug reports and suggestions are still welcome!_ 16 | 17 | **pipes values through functions**, an alternative to using the 18 | [proposed pipe operator](https://github.com/mindeavor/es-pipeline-operator) ( |> 19 | ) for ES. 20 | 21 | [Demo available on RunKit](https://runkit.com/egeozcan/ppipe). 22 | 23 | Supports functions returning promises too. In that case, the result of the chain 24 | will also be a promise. This is similar to the proposed support for await in the 25 | chained functions. 26 | 27 | ## Installation 28 | 29 | `npm install ppipe` 30 | 31 | ## Problems ppipe solves 32 | 33 | Let's assume you have these functions: 34 | 35 | ```javascript 36 | const add = (x, y) => x + y; 37 | const square = x => x * x; 38 | const divide = (x, y) => x / y; 39 | const double = x => x + x; 40 | ``` 41 | 42 | How do you pass the results from one to another? 43 | 44 | ```javascript 45 | //good old single line solution 46 | add(divide(square(double(add(1, 1))), 8), 1); 47 | //try to get creative with variable names? 48 | const incremented = add(1, 1); 49 | const doubled = double(incremented); 50 | //... 51 | ``` 52 | 53 | An ideal solution would have been having a pipe operator (|>) but we don't have 54 | it. Here is where ppipe comes in. 55 | 56 | _Order of arguments can be manipulated using the \_ property of ppipe function. 57 | The result of the previous function is inserted to its place if it exists in the 58 | arguments. It can also occur more than once if you want to pass the same 59 | parameter more than once. If, and only if, \_ doesn't exist among the arguments, 60 | the piped value will be inserted at the end._ 61 | 62 | ```javascript 63 | const ppipe = require("ppipe"); 64 | const _ = ppipe._; 65 | ppipe(1)(add, 1)(double)(square)(divide, _, 8)(add, 1)(); // 3 66 | ``` 67 | 68 | If that is too lisp-y, you can also use ".pipe". 69 | 70 | ```javascript 71 | ppipe(1) 72 | .pipe(add, 1) 73 | .pipe(double) 74 | .pipe(square) 75 | .pipe(divide, _, 8) 76 | .pipe(add, 1)(); // 3 77 | ``` 78 | 79 | And then you receive some new "requirements", which end up making the "double" 80 | function async... 81 | 82 | ```javascript 83 | async function asyncDouble(x) { 84 | const result = x * 2; 85 | await someAPICall(result); 86 | return result; 87 | } 88 | ``` 89 | 90 | Here are the changes you need to make: 91 | 92 | ```javascript 93 | await ppipe(1) 94 | .pipe(add, 1) 95 | .pipe(asyncDouble) 96 | .pipe(square) 97 | .pipe(divide, _, 8) 98 | .pipe(add, 1); //3 (you can also use .then and .catch) 99 | ``` 100 | 101 | Yes, ppipe automatically turns the end result into a promise, if one or more 102 | functions in the chain return a promise. It also waits for the resolution and 103 | passes the unwrapped value to the next function. You can also catch the errors 104 | with `.catch` like a standard promise or use try/catch in an async function. You 105 | meet the requirements and keep the code tidy. 106 | 107 | For consistency, the `.then` and `.catch` methods are always available, so you 108 | don't have to care if any function in the chain is async as long as you use 109 | those. 110 | 111 | So, later you receive some new "requirements", which make our now infamous 112 | double function return an object: 113 | 114 | ```javascript 115 | async function asyncComplexDouble(x) { 116 | const result = x * 2; 117 | const someInfo = await someAPICall(result); 118 | return { result, someInfo }; 119 | } 120 | ``` 121 | 122 | Still not a problem: 123 | 124 | ```javascript 125 | await ppipe(1) 126 | .pipe(add, 1) 127 | .pipe(asyncComplexDouble) 128 | //pipe._ is also a proxy which saves the property accesses to pluck the prop from the 129 | //previous function's result later 130 | .pipe(square, _.result) 131 | .pipe(divide, _, 8) 132 | .pipe(add, 1); //3 133 | 134 | //well, if you think that might not be clear, you can write it like this, too 135 | await ppipe(1) 136 | .pipe(add, 1) 137 | .pipe(asyncComplexDouble) 138 | .pipe(x => x.result) 139 | .pipe(square) 140 | .pipe(divide, _, 8) 141 | .pipe(add, 1); //3 142 | 143 | //this also works 144 | await ppipe(1) 145 | .pipe(add, 1) 146 | .pipe(asyncComplexDouble) 147 | //promises will be unboxed and properties will be returned as getter functions 148 | //the methods will be available in the chain as well, as shown in the next example 149 | .result() 150 | .pipe(square) 151 | .pipe(divide, _, 8) 152 | .pipe(add, 1); //3 153 | ``` 154 | 155 | Let's go one step further; what if you need to access a method from the result? 156 | 157 | ```javascript 158 | async function advancedDouble(x) { 159 | const result = x * 2; 160 | const someInfo = await someAPICall(result); 161 | return { 162 | getResult() { 163 | return result; 164 | }, 165 | someInfo 166 | }; 167 | } 168 | ``` 169 | 170 | There you go: 171 | 172 | ```javascript 173 | await ppipe(1) 174 | .pipe(add, 1) 175 | .pipe(advancedDouble) 176 | .getResult() 177 | .pipe(square) 178 | .pipe(divide, _, 8) 179 | .pipe(add, 1); //3 180 | ``` 181 | 182 | ## Some More Examples 183 | 184 | It is possible to expand the iterable result 185 | 186 | ```javascript 187 | const addAll = (...x) => x.reduce((a, b) => a + b, 0) 188 | ppipe([1,2,3]).map(x => x + 1).pipe(addAll, ..._)(); //9 189 | ``` 190 | 191 | It is possible to reach array members: 192 | 193 | ```javascript 194 | await ppipe(10) 195 | .pipe(asyncComplexDoubleArray) 196 | .pipe((x, y) => x + y, _[1], _[2]); 197 | ``` 198 | 199 | Also object properties: 200 | 201 | ```javascript 202 | ppipe(10) 203 | .pipe(x => ({multipliers: [10,20], value: x})) 204 | .pipe((x, y) => x * y, _.multipliers[0], _.value)(); //100 205 | ``` 206 | 207 | And you can omit the function altogether if you just want to extract values: 208 | 209 | ```javascript 210 | ppipe({multipliers: [10,20], value: 10}).pipe(_.value)(); //10 211 | await ppipe({multipliers: [10,20], value: 10}).pipe(_.value); //10 212 | ``` 213 | 214 | And as you've seen before, you can always omit the ".pipe", as long as you 215 | know how to keep ASI in check: 216 | 217 | ```javascript 218 | ppipe({multipliers: [10,20], value: 10})(_.value)(); //10 219 | await ppipe({multipliers: [10,20], value: 10})(_.value); //10 220 | ``` 221 | 222 | ## Advanced Functionality 223 | 224 | ### Chain Methods / Properties 225 | 226 | You can use these from the chain (after creating one with `ppipe(val)`). 227 | 228 | #### .with(ctx) 229 | 230 | Calls the following function in chain with the given `this` value (ctx). After 231 | calling `.with` the chain can be continued with the methods from the ctx. 232 | 233 | ```javascript 234 | class Example { 235 | constructor(myInt) { 236 | this.foo = Promise.resolve(myInt); 237 | } 238 | addToFoo(x) { 239 | return this.foo.then(foo => foo + x); 240 | } 241 | } 242 | await ppipe(10).with(new Example(5)).addToFoo(_); //15 243 | ``` 244 | 245 | Look at the test/test.js for more examples. 246 | 247 | #### .val 248 | 249 | Gets the current value from the chain. Will be a promise if any function in the 250 | chain returns a promise. Calling the chain with no parameters achieves the same 251 | result. 252 | 253 | ### Extending Ppipe 254 | 255 | You can create an extended instance of ppipe via `.extend`. 256 | 257 | ```javascript 258 | const newPipe = ppipe.extend({ 259 | divide (x, y) { 260 | return x / y; 261 | }, 262 | log(...params) { 263 | console.log(...params); 264 | return params[params.length - 1]; 265 | } 266 | }); 267 | const res = await newPipe(10) 268 | .pipe(x => x + 1) 269 | .divide(_, 11) 270 | .log("here is our x: ") //logs "here is our x: 1" 271 | .pipe(x => x + 1) // 2 272 | ``` 273 | 274 | You can also call `.extend` on the extended ppipes. It will create a new ppipe 275 | with the new and existing extensions merged. 276 | 277 | ## Testing 278 | 279 | All the functionality is tested, with 100% coverage. This is also integrated in the build process. 280 | 281 | To run the tests yourself, clone the repository, install the dev dependencies, and run the npm test command. 282 | 283 | `npm install` 284 | 285 | `npm test` 286 | 287 | ## Contributing 288 | 289 | See 290 | [CONTRIBUTING](https://github.com/egeozcan/ppipe/blob/master/.github/CONTRIBUTING.md). 291 | 292 | ## Changelog 293 | 294 | * v2.5.0 - placeholder can be the only argument to the .pipe, for just extracting a property or path 295 | * v2.4.0 - allow deep property extraction via the placeholder 296 | (\_.deeply.nested.prop) (test: should be able to extract array members) 297 | * v2.3.0 - now supports expanding the placeholder (...\_) (test: should support 298 | expanding the array result) 299 | 300 | ## Caveats 301 | 302 | * This library was not written with performance in mind. So, it makes next to no 303 | sense to use it in, say, a tight loop. Use in a web-server should be fine as 304 | long as you don't have tight response-time requirements. General rule of 305 | thumb: Test it before putting it into prod. There are a lot of tests written 306 | for ppipe but none of them measure performance. I may improve the performance 307 | in the future (some low-hanging fruits) but I'd rather avoid making any 308 | guarantees. Well, there is one good news: 309 | [Chrome team is working on performance improvements to the Proxy](https://v8project.blogspot.de/2017/10/optimizing-proxies.html) 310 | which will very positively affect ppipe performance. 311 | 312 | * It uses ES6 Proxies to do its magic. Proxies are not back-portable. 1.x.x 313 | versions of ppipe didn't use proxies. So you can try using an older version 314 | with a transpiler if evergreen sounds alien to you. 315 | [Here](https://github.com/egeozcan/ppipe/blob/1888e9269be90f549d5c00002f7e800598c6d539/index.js) 316 | is an older stable version without value extracting and context change 317 | support. 318 | 319 | * ppipe is not typed. No type definition exists for TypeScript nor Flow. I 320 | actually love TypeScript and would support it but the lack of variadic generic 321 | type parameters make it next to impossible to provide type definitions for 322 | ppipe. More can be read 323 | [here](https://github.com/Microsoft/TypeScript/issues/5453). Also, ppipe is as 324 | dynamic as it gets, giving the ability to access virtual properties/methods 325 | which may belong to the provided context, the processed value or any of the 326 | possible extensions. 327 | [TypeScripts Type System is Turing Complete](https://github.com/Microsoft/TypeScript/issues/14833), 328 | so, maybe there is a way to type all of this but I really need help about 329 | that. 330 | -------------------------------------------------------------------------------- /logo/logo_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/egeozcan/ppipe/14e5f6b9b84f7a4012f04bb8b89cf2d5b84e47ca/logo/logo_s.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ppipe", 3 | "version": "2.6.5", 4 | "description": "piping without the operator support", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "nyc --reporter=text --reporter=html mocha test", 8 | "coverage": "nyc report --reporter=text-lcov | coveralls", 9 | "lint": "eslint --fix src/" 10 | }, 11 | "keywords": [ 12 | "piping", 13 | "promise", 14 | "chain", 15 | "async", 16 | "pipe", 17 | "pipe-operator", 18 | "pipes-values" 19 | ], 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/egeozcan/ppipe.git" 23 | }, 24 | "author": "Yavuz Ege Özcan", 25 | "license": "ISC", 26 | "devDependencies": { 27 | "chai": "^4.2.0", 28 | "coveralls": "^3.1.0", 29 | "eslint": "^8.12.0", 30 | "eslint-config-prettier": "^8.5.0", 31 | "eslint-plugin-prettier": "^4.0.0", 32 | "mocha": "^9.2.2", 33 | "nyc": "^15.1.0", 34 | "prettier": "^2.6.1" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | trailingComma: "es5", 3 | tabWidth: 4, 4 | useTabs: true, 5 | semi: true, 6 | singleQuote: false, 7 | printWidth: 120, 8 | }; 9 | -------------------------------------------------------------------------------- /src/getPropertyByPath.js: -------------------------------------------------------------------------------- 1 | // Mostly taken from https://stackoverflow.com/a/6491621/300011 2 | const isPromise = require("./lib/isPromise"); 3 | 4 | module.exports = function getPropertyByPath(object, accessString) { 5 | // convert indexes to properties 6 | accessString = accessString.replace(/\[(\w+)\]/g, ".$1"); 7 | // strip a leading dot 8 | accessString = accessString.replace(/^\./, ""); 9 | 10 | const properties = accessString.split("."); 11 | 12 | for (let i = 0, n = properties.length; i < n; ++i) { 13 | const property = properties[i]; 14 | 15 | object = isPromise(object) ? object.then((x) => getProperty(property, x)) : getProperty(property, object); 16 | } 17 | 18 | return object; 19 | }; 20 | 21 | function getProperty(property, object) { 22 | if (object === undefined) { 23 | return; 24 | } 25 | 26 | let call = false; 27 | 28 | if (property.endsWith("()")) { 29 | call = true; 30 | property = property.substr(0, property.length - 2); 31 | } 32 | if (property in object) { 33 | return call ? object[property]() : object[property]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const isFn = require("./lib/isFunction"); 2 | const getPropertyByPath = require("./getPropertyByPath"); 3 | const isPromise = require("./lib/isPromise"); 4 | 5 | const unitFn = (x) => x; 6 | const isUndef = (val) => typeof val === "undefined"; 7 | const truthy = (val) => !isUndef(val) && val !== null; 8 | 9 | function createPpipe(extensions = {}) { 10 | function ppipe(val, thisVal, err) { 11 | function pipe(fn, ...params) { 12 | if (isUndef(fn)) { 13 | if (truthy(err)) { 14 | throw err; 15 | } 16 | 17 | return val; 18 | } 19 | 20 | if (!isFn(fn)) { 21 | if (fn instanceof Placeholder && params.length === 0) { 22 | params = [fn]; 23 | fn = unitFn; 24 | } else { 25 | throw new Error("first parameter to a pipe should be a function or a single placeholder"); 26 | } 27 | } 28 | 29 | const callResultFn = (value) => { 30 | let replacedPlaceHolder = false; 31 | 32 | for (let i = params.length; i >= 0; i--) { 33 | const pholdr = params[i]; 34 | 35 | if (!(pholdr instanceof Placeholder)) { 36 | continue; 37 | } 38 | 39 | replacedPlaceHolder = true; 40 | 41 | const replacedParam = !pholdr.prop ? value : getPropertyByPath(value, pholdr.prop); 42 | 43 | pholdr.expandTarget === true 44 | ? params.splice(i, 1, ...replacedParam) 45 | : params.splice(i, 1, replacedParam); 46 | } 47 | if (!replacedPlaceHolder) { 48 | params.splice(params.length, 0, value); 49 | } 50 | 51 | return fn.call(thisVal, ...params); 52 | }; 53 | let res; 54 | 55 | if (isPromise(val)) { 56 | res = val.then(callResultFn); 57 | } else { 58 | try { 59 | res = truthy(err) ? undefined : callResultFn(val); 60 | } catch (e) { 61 | err = e; 62 | } 63 | } 64 | 65 | return ppipe(res, undefined, err); 66 | } 67 | 68 | const piped = new Proxy(pipe, { 69 | get(target, name) { 70 | switch (name) { 71 | case "then": 72 | case "catch": { 73 | const res = truthy(err) ? Promise.reject(err) : Promise.resolve(val); 74 | 75 | return (...params) => (name === "then" ? res.then(...params) : res.catch(...params)); 76 | } 77 | case "val": 78 | if (truthy(err)) { 79 | throw err; 80 | } 81 | 82 | return val; 83 | case "with": 84 | return (ctx) => { 85 | thisVal = ctx; 86 | 87 | return piped; 88 | }; 89 | case "pipe": 90 | return piped; 91 | case "bind": 92 | case "call": 93 | case "apply": 94 | return (...params) => pipe[name](...params); 95 | } 96 | 97 | if (isPromise(val)) { 98 | return (...params) => 99 | piped((x) => { 100 | if (isUndef(x[name])) { 101 | throw new TypeError(`${name} is not defined on ${x}`); 102 | } 103 | 104 | return isFn(x[name]) ? x[name](...params) : x[name]; 105 | }); 106 | } 107 | 108 | const fnExistsInCtx = truthy(thisVal) && isFn(thisVal[name]); 109 | const valHasProp = !fnExistsInCtx && !isUndef(val[name]); 110 | const extensionWithNameExists = !fnExistsInCtx && !valHasProp && isFn(extensions[name]); 111 | 112 | if (fnExistsInCtx || valHasProp || extensionWithNameExists) { 113 | const ctx = fnExistsInCtx ? thisVal : valHasProp ? val : extensions; 114 | 115 | return (...params) => 116 | piped((...replacedParams) => { 117 | const newParams = fnExistsInCtx || extensionWithNameExists ? replacedParams : params; 118 | 119 | return !isFn(ctx[name]) ? ctx[name] : ctx[name](...newParams); 120 | }, ...params); 121 | } 122 | }, 123 | }); 124 | 125 | return piped; 126 | } 127 | 128 | return Object.assign(ppipe, { 129 | extend(newExtensions) { 130 | return createPpipe(Object.assign(newExtensions, extensions)); 131 | }, 132 | _, 133 | }); 134 | } 135 | class Placeholder { 136 | *[Symbol.iterator]() { 137 | yield new Placeholder(this.prop, true); 138 | } 139 | 140 | constructor(prop, expandTarget) { 141 | this.prop = prop; 142 | this.expandTarget = expandTarget; 143 | } 144 | } 145 | 146 | const placeholderProxy = (prop = undefined, expandTarget = false) => 147 | new Proxy(new Placeholder(prop, expandTarget), { 148 | get(target, name) { 149 | if (name === Symbol.iterator || Object.getOwnPropertyNames(target).includes(name)) { 150 | return target[name]; 151 | } 152 | 153 | return placeholderProxy([prop, name].filter((x) => !!x).join(".")); 154 | }, 155 | }); 156 | 157 | const _ = placeholderProxy(); 158 | 159 | module.exports = createPpipe(); 160 | -------------------------------------------------------------------------------- /src/lib/isFunction.js: -------------------------------------------------------------------------------- 1 | module.exports = (val) => typeof val === "function"; 2 | -------------------------------------------------------------------------------- /src/lib/isPromise.js: -------------------------------------------------------------------------------- 1 | const isFn = require("./isFunction"); 2 | 3 | module.exports = (val) => val && isFn(val.then); 4 | -------------------------------------------------------------------------------- /test/examples.js: -------------------------------------------------------------------------------- 1 | let assert = require("chai").assert; 2 | let ppipe = require("../src/index.js"); 3 | 4 | const add = (x, y) => x + y; 5 | const square = x => x * x; 6 | const divide = (x, y) => x / y; 7 | const double = x => x * 2; 8 | 9 | let _ = ppipe._; 10 | 11 | const delay = fn => (...args) => 12 | new Promise(resolve => setTimeout(() => resolve(fn.apply(null, args)), 10)); 13 | const someAPICall = delay(x => x); 14 | 15 | describe("check readme", function() { 16 | it("first example", function() { 17 | const res = ppipe(1)(add, 1)(double)(square)(divide, ppipe._, 8)(add, 1)(); 18 | assert.equal(res, add(divide(square(double(add(1, 1))), 8), 1)); 19 | }); 20 | 21 | it("second example", function() { 22 | const res = ppipe(1) 23 | .pipe(add, 1) 24 | .pipe(double) 25 | .pipe(square) 26 | .pipe(divide, _, 8) 27 | .pipe(add, 1)(); 28 | assert.equal(res, add(divide(square(double(add(1, 1))), 8), 1)); 29 | }); 30 | 31 | it("third example", async function() { 32 | async function asyncDouble(x) { 33 | const result = x * 2; 34 | await someAPICall(result); 35 | return result; 36 | } 37 | const res = await ppipe(1) 38 | .pipe(add, 1) 39 | .pipe(asyncDouble) 40 | .pipe(square) 41 | .pipe(divide, _, 8) 42 | .pipe(add, 1); 43 | assert.equal(res, add(divide(square(double(add(1, 1))), 8), 1)); 44 | }); 45 | 46 | it("fourth example", async function() { 47 | async function asyncComplexDouble(x) { 48 | const result = x * 2; 49 | const someInfo = await someAPICall(result); 50 | return { 51 | result, 52 | someInfo, 53 | getResultPlus: y => result + y 54 | }; 55 | } 56 | const res = await ppipe(1) 57 | .pipe(add, 1) 58 | .pipe(asyncComplexDouble) 59 | .pipe(square, _.result) 60 | .pipe(divide, _, 8) 61 | .pipe(add, 1); 62 | assert.equal(res, add(divide(square(double(add(1, 1))), 8), 1)); 63 | const res2 = await ppipe(1) 64 | .pipe(add, 1) 65 | .pipe(asyncComplexDouble) 66 | .result() 67 | .pipe(asyncComplexDouble) 68 | .getResultPlus(2) 69 | .pipe(square) 70 | .pipe(divide, _, 8) 71 | .pipe(add, 1) 72 | .pipe(add, -2.5); 73 | assert.equal(11, res2); 74 | assert.equal(res, add(divide(square(double(add(1, 1))), 8), 1)); 75 | }); 76 | 77 | it("fourth example async result", async function() { 78 | async function asyncComplexDouble(x) { 79 | const result = x * 2; 80 | const someInfo = await someAPICall(result); 81 | //go wild with deferring 82 | return Promise.resolve({ 83 | result, 84 | someInfo, 85 | //go wilder with deferring 86 | getResultPlusAsync: y => 87 | new Promise(resolve => setTimeout(() => resolve(result + y), 10)) 88 | }); 89 | } 90 | const res3 = await ppipe(1) 91 | .pipe(add, 1) 92 | .pipe(asyncComplexDouble) 93 | .result() 94 | .pipe(asyncComplexDouble) 95 | .getResultPlusAsync(2) 96 | .pipe(square) 97 | .pipe(divide, _, 8) 98 | .pipe(add, 1) 99 | .pipe(add, -2.5); 100 | assert.equal(11, res3); 101 | }); 102 | 103 | it("fifth example", async function() { 104 | async function advancedDouble(x) { 105 | const result = x * 2; 106 | const someInfo = await someAPICall(result); 107 | return { 108 | getResult() { 109 | return result; 110 | }, 111 | someInfo 112 | }; 113 | } 114 | const res = await ppipe(1) 115 | .pipe(add, 1) 116 | .pipe(advancedDouble) 117 | .getResult() 118 | .pipe(square) 119 | .pipe(divide, _, 8) 120 | .pipe(add, 1); 121 | assert.equal(res, add(divide(square(double(add(1, 1))), 8), 1)); 122 | const res2 = await ppipe(1) 123 | .pipe(add, 1) 124 | .pipe(x => Promise.resolve(x)) 125 | //.pipe((...params) => (console.log(params), params[0])) 126 | .pipe(advancedDouble) 127 | .getResult() 128 | .toFixed(2) 129 | .pipe(parseInt) 130 | .pipe(square) 131 | .pipe(divide, _, 8) 132 | .pipe(add, 1); 133 | assert.equal(res2, 3); 134 | }); 135 | 136 | it("sixth example", async function() { 137 | class Example { 138 | constructor(myInt) { 139 | this.foo = Promise.resolve(myInt); 140 | } 141 | addToFoo(x) { 142 | return this.foo.then(foo => foo + x); 143 | } 144 | } 145 | const res = await ppipe(10) 146 | .with(new Example(5)) 147 | .addToFoo(_); 148 | assert.equal(res, 15); 149 | const res2 = await ppipe(10) 150 | .with(new Example(5)) 151 | .addToFoo(); 152 | assert.equal(res2, 15); 153 | }); 154 | 155 | it("seventh example", async function() { 156 | let logged = false; 157 | const newPipe = ppipe.extend({ 158 | divide(x, y) { 159 | return x / y; 160 | }, 161 | log(...params) { 162 | logged = true; 163 | assert.equal(params[params.length - 1], 1); 164 | return params[params.length - 1]; 165 | } 166 | }); 167 | const res = await newPipe(10) 168 | .pipe(x => x + 1) 169 | .divide(_, 11) 170 | .log("here is our x: ") 171 | .pipe(x => x + 1); 172 | assert.equal(res, 2); 173 | assert.equal(logged, true); 174 | }); 175 | }); 176 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* eslint quotes: "off" */ 2 | 3 | let assert = require("chai").assert; 4 | let ppipe = require("../src/index.js"); 5 | 6 | function doubleSay(str) { 7 | return str + ", " + str; 8 | } 9 | function capitalize(str) { 10 | return str[0].toUpperCase() + str.substring(1); 11 | } 12 | function delay(fn) { 13 | return function() { 14 | let args = arguments; 15 | return new Promise(resolve => 16 | setTimeout(() => resolve(fn.apply(null, args)), 10) 17 | ); 18 | }; 19 | } 20 | function exclaim(str) { 21 | return str + "!"; 22 | } 23 | function join() { 24 | let arr = Array.from(arguments); 25 | return arr.join(", "); 26 | } 27 | function quote(str) { 28 | return '"' + str + '"'; 29 | } 30 | 31 | let _ = ppipe._; 32 | 33 | describe("ppipe", function() { 34 | let message = "hello"; 35 | it("should correctly pass the params to the first fn", function() { 36 | assert.equal(ppipe(message)(doubleSay).val, doubleSay(message)); 37 | }); 38 | 39 | it("should throw if accessing val from a pipe that contains an error", function() { 40 | let caught = false; 41 | try { 42 | ppipe(message)(() => { 43 | throw new Error("foo"); 44 | })(doubleSay).val; 45 | } catch (error) { 46 | caught = error.message === "foo"; 47 | } 48 | assert.equal(caught, true); 49 | }); 50 | 51 | it("should throw if ending a pipe that contains an error", function() { 52 | let caught = false; 53 | try { 54 | ppipe(message)(() => { 55 | throw new Error("foo"); 56 | })(doubleSay)(); 57 | } catch (error) { 58 | caught = error.message === "foo"; 59 | } 60 | assert.equal(caught, true); 61 | }); 62 | 63 | it("should fail promise if ending an async pipe that contains an error even when the deferred value comes later", async function() { 64 | let caught = false; 65 | try { 66 | await ppipe(message)(() => { 67 | throw new Error("foo"); 68 | })(() => Promise.resolve(message))(doubleSay).then(x => x); 69 | } catch (error) { 70 | caught = error.message === "foo"; 71 | } 72 | assert.equal(caught, true); 73 | }); 74 | 75 | it("should not touch the error as long as it exists when an undefined prop is called", async function() { 76 | let caught = false; 77 | try { 78 | var error = new Error("oh noes"); 79 | await ppipe()(() => Promise.reject(error)).weCantKnowIfThisMethodExists(); 80 | } catch (error) { 81 | caught = error.message === "oh noes"; 82 | } 83 | assert.equal(caught, true); 84 | }); 85 | 86 | it("should not continue a sync chain if a method is missing", function() { 87 | let caught = false; 88 | try { 89 | ppipe("foo")(x => x).weKnowThisMethodIsMissing(); 90 | } catch (error) { 91 | caught = true; 92 | } 93 | assert.equal(caught, true); 94 | }); 95 | 96 | it("should error with missing method if no errors exist in ctx and missing method is called", async function() { 97 | let caught = false; 98 | try { 99 | await ppipe("foo")(x => Promise.resolve(x)).weKnowThisMethodIsMissing(); 100 | } catch (error) { 101 | caught = true; 102 | } 103 | assert.equal(caught, true); 104 | }); 105 | 106 | it("should throw if a non-function is passed as the first argument", function() { 107 | let caught = false; 108 | try { 109 | ppipe(message)({})(doubleSay)(); 110 | } catch (error) { 111 | const expectedErrorMessage = 112 | "first parameter to a pipe should be a function or a single placeholder"; 113 | caught = error.message === expectedErrorMessage; 114 | } 115 | assert.equal(caught, true); 116 | }); 117 | 118 | it("should correctly pass the params to the second fn", function() { 119 | assert.equal( 120 | ppipe(message)(doubleSay)(exclaim).val, 121 | exclaim(doubleSay(message)) 122 | ); 123 | }); 124 | 125 | it("should correctly insert parameters", function() { 126 | assert.equal( 127 | ppipe(message)(doubleSay)(join, _, "I said")(exclaim).val, 128 | exclaim(join(doubleSay(message), "I said")) 129 | ); 130 | }); 131 | 132 | it("should insert parameters at the end when no placeholder exists", function() { 133 | assert.equal( 134 | ppipe(message)(doubleSay)(join, "I said")(exclaim).val, 135 | exclaim(join("I said", doubleSay(message))) 136 | ); 137 | }); 138 | 139 | it("should correctly insert parameters on multiple functions", function() { 140 | assert.equal( 141 | ppipe(message)(doubleSay)(join, _, "I said")(exclaim)( 142 | join, 143 | "and suddenly", 144 | _, 145 | "without thinking" 146 | ).val, 147 | join( 148 | "and suddenly", 149 | exclaim(join(doubleSay(message), "I said")), 150 | "without thinking" 151 | ) 152 | ); 153 | }); 154 | 155 | it("should return the value when no function is passed", function() { 156 | assert.equal( 157 | ppipe(message)(doubleSay)(join, _, "I said")(exclaim)( 158 | join, 159 | "and suddenly", 160 | _, 161 | "without thinking" 162 | )(), 163 | join( 164 | "and suddenly", 165 | exclaim(join(doubleSay(message), "I said")), 166 | "without thinking" 167 | ) 168 | ); 169 | }); 170 | 171 | let result = "Hello!"; 172 | 173 | it("should wrap promise factories in the middle of the chain", function() { 174 | return ppipe(message)(Promise.resolve.bind(Promise))(delay(capitalize))( 175 | exclaim 176 | ).then(res => { 177 | return assert.equal(result, res); 178 | }); 179 | }); 180 | 181 | it("should wrap promise factories at the end of the chain", function() { 182 | return ppipe(message)(capitalize)(delay(exclaim)).then(res => { 183 | return assert.equal(result, res); 184 | }); 185 | }); 186 | 187 | it("should wrap promises in the beginning of the chain", function() { 188 | return ppipe(Promise.resolve(message))(capitalize)(exclaim).then(res => { 189 | return assert.equal(result, res); 190 | }); 191 | }); 192 | 193 | it("should wrap multiple promise factories and promises in chain", function() { 194 | return ppipe(Promise.resolve(message))(delay(capitalize))( 195 | delay(exclaim) 196 | ).then(res => { 197 | return assert.equal(result, res); 198 | }); 199 | }); 200 | 201 | it("should simulate promises even when value is not delayed", function() { 202 | return ppipe(message)(capitalize)(exclaim).then(res => { 203 | return assert.equal(result, res); 204 | }); 205 | }); 206 | 207 | it("should be able to insert promise values as parameters", function() { 208 | return ppipe(message)(doubleSay)(delay(quote))(delay(join), _, "I said")( 209 | join, 210 | "and suddenly", 211 | _, 212 | "without thinking" 213 | )(delay(exclaim))(exclaim).then(res => 214 | assert.equal( 215 | 'and suddenly, "hello, hello", I said, without thinking!!', 216 | res 217 | ) 218 | ); 219 | }); 220 | 221 | it("should be able to insert promise values more than once", function() { 222 | return ppipe(message)(doubleSay)(delay(quote))( 223 | delay(join), 224 | _, 225 | "test", 226 | _, 227 | _, 228 | _, 229 | "test" 230 | )(delay(exclaim))(exclaim).then(res => 231 | assert.equal( 232 | '"hello, hello", test, "hello, hello", "hello, hello", "hello, hello", test!!', 233 | res 234 | ) 235 | ); 236 | }); 237 | 238 | it("should be able to insert selected properties from promise values more than once", function() { 239 | return ppipe(message) 240 | .pipe(doubleSay) 241 | .pipe(delay(quote)) 242 | .pipe(x => ({ foo: x, bar: x.toUpperCase() })) 243 | .pipe(delay(join), _.foo, _.foo, _.foo, _.bar) 244 | .pipe(delay(exclaim)) 245 | .pipe(exclaim) 246 | .then(res => 247 | assert.equal( 248 | '"hello, hello", "hello, hello", "hello, hello", "HELLO, HELLO"!!', 249 | res 250 | ) 251 | ); 252 | }); 253 | 254 | it("should be awaitable", async function() { 255 | const res = await ppipe(message) 256 | .pipe(doubleSay) 257 | .pipe(delay(quote)) 258 | .pipe(x => ({ foo: x, bar: x.toUpperCase() })) 259 | .pipe(delay(join), _.foo, _.foo, _.foo, _.bar) 260 | .pipe(delay(exclaim)) 261 | .pipe(exclaim); 262 | assert.equal( 263 | '"hello, hello", "hello, hello", "hello, hello", "HELLO, HELLO"!!', 264 | res 265 | ); 266 | }); 267 | 268 | it("should pass the errors when rejected", function() { 269 | let caught = false; 270 | return ppipe(message) 271 | .pipe(doubleSay) 272 | .pipe(delay(quote)) 273 | .pipe(x => ({ foo: x, bar: x.toUpperCase() })) 274 | .pipe(delay(join), _.foo, _.foo, _.foo, _.bar) 275 | .pipe(() => Promise.reject(new Error("oh noes"))) 276 | .pipe(delay(exclaim)) 277 | .pipe(exclaim) 278 | .catch(() => { 279 | caught = true; 280 | }) 281 | .then(() => assert(caught, true)); 282 | }); 283 | 284 | it("should pass the errors when thrown", function() { 285 | let caught = false; 286 | return ppipe(message) 287 | .pipe(doubleSay) 288 | .pipe(delay(quote)) 289 | .pipe(x => ({ foo: x, bar: x.toUpperCase() })) 290 | .pipe(delay(join), _.foo, _.foo, _.foo, _.bar) 291 | .pipe(() => { 292 | throw new Error("oh noes"); 293 | }) 294 | .someMethodOfThePotentialResultIWantedToCallIfThereWasntAnError() 295 | .pipe(delay(exclaim)) 296 | .pipe(exclaim) 297 | .catch(() => { 298 | caught = true; 299 | }) 300 | .then(() => assert(caught, true)); 301 | }); 302 | 303 | it("should have catchable async errors", function() { 304 | let caught = false; 305 | try { 306 | ppipe(message) 307 | .pipe(doubleSay) 308 | .pipe(() => { 309 | throw new Error("oh noes"); 310 | }) 311 | .someMethodOfThePotentialResultIWantedToCallIfThereWasntAnError() 312 | .pipe(delay(exclaim)); 313 | } catch (error) { 314 | caught = true; 315 | } 316 | assert(caught, true); 317 | }); 318 | 319 | it("should be able to access result prototype methods", function() { 320 | return ppipe([1, 2, 3]) 321 | .map(i => i + 1) 322 | .pipe(x => x.reduce((x, y) => x + y, 0)) 323 | .then(res => { 324 | return assert.equal(9, res); 325 | }); 326 | }); 327 | 328 | it("should be able to revert to chaining and back from prototype methods", function() { 329 | const divide = (x, y) => x / y; 330 | return ( 331 | ppipe("dummy")(() => [1, 2, 3]) 332 | .map(i => i + 1) 333 | /*reduce: 9, divide: 9/3 == 3*/ 334 | .reduce((x, y) => x + y, 0) 335 | .pipe(divide, _, 3) 336 | .then(x => assert.equal(3, x)) 337 | ); 338 | }); 339 | 340 | it("should be able to access properties of the result", function() { 341 | const divide = (x, y) => x / y; 342 | return ppipe("dummy")(() => [1, 2, 3]) 343 | .map(i => i + 1) 344 | .length() 345 | .pipe(divide, _, 3) 346 | .then(x => assert.equal(1, x)); 347 | }); 348 | 349 | it("should return itself via .pipe", function() { 350 | const divide = (x, y) => x / y; 351 | return ( 352 | ppipe("dummy")(() => [1, 2, 3]) 353 | .map(i => i + 1) 354 | /*reduce: 9, divide: 9/3 == 3*/ 355 | .reduce((x, y) => x + y, 0) 356 | .pipe(divide, _, 3) 357 | .then(x => assert.equal(3, x)) 358 | ); 359 | }); 360 | 361 | class Test { 362 | constructor(initial) { 363 | this.value = initial; 364 | } 365 | increment() { 366 | this.value = this.value + 1; 367 | return this; 368 | } 369 | square() { 370 | this.value = this.value * this.value; 371 | return this; 372 | } 373 | add(x) { 374 | this.value = this.value + x.value; 375 | return this; 376 | } 377 | doWeirdStuff(x, y) { 378 | return this.value * 100 + x * 10 + y; 379 | } 380 | } 381 | 382 | it("should be able to switch context by using 'with'", () => { 383 | const startVal = new Test(5); 384 | ppipe(startVal) 385 | .square() 386 | .increment() 387 | .with(new Test(9)) 388 | .add() 389 | .with(new Test(1)) 390 | .doWeirdStuff(_.value, _.value) 391 | .with(assert) 392 | .equal(_, 485); 393 | const secondStartVal = new Test(5); 394 | const res = ppipe(secondStartVal) 395 | .square() 396 | .increment() 397 | .with(new Test(9)) 398 | .add() 399 | .with(new Test(1)) 400 | .doWeirdStuff(_.value, _.value); 401 | assert.equal(res.val, 485); 402 | }); 403 | 404 | it("should keep the context gained with 'with' after a 'pipe'", () => { 405 | const startVal = new Test(5); 406 | const res = ppipe(startVal) 407 | .square() 408 | .increment() 409 | .with(new Test(9)) 410 | .add() 411 | .with(new Test(1)) 412 | .pipe(Test.prototype.doWeirdStuff, _.value, _.value).val; 413 | assert.equal(res, 485); 414 | 415 | const secondStartVal = new Test(5); 416 | const res2 = ppipe(secondStartVal) 417 | .square() 418 | .increment() 419 | .with(new Test(9)) 420 | .add() 421 | .with(new Test(1)) 422 | .pipe(secondStartVal.doWeirdStuff, _.value, _.value).val; 423 | assert.equal(res2, 485); 424 | }); 425 | 426 | it("should not mess with the promises", async () => { 427 | const startVal = new Test(5); 428 | const res = await ppipe(startVal) 429 | .square() 430 | .increment() 431 | .with(new Test(9)) 432 | .then(x => 10 * x.value) 433 | .catch(() => { 434 | throw new Error("should not be reachable"); 435 | }); 436 | const res2 = await ppipe(res).pipe((x, y) => x + y, 1); 437 | assert.equal(res2, 261); 438 | }); 439 | 440 | it("should support binding, applying and calling", async () => { 441 | const res = await ppipe(10) 442 | .call(null, x => x + 1) 443 | .apply(null, [x => Promise.resolve(x)]) 444 | .bind(null, (x, y) => x / y)(_, 10); 445 | assert.equal(res, 1.1); 446 | }); 447 | 448 | it("should support extensions", async () => { 449 | const newPipe = ppipe.extend({ 450 | assertEqAndIncrement: (x, y) => { 451 | assert.equal(x, y); 452 | return x + 1; 453 | } 454 | }); 455 | const res = await newPipe(10) 456 | .pipe(x => x + 1) 457 | .assertEqAndIncrement(_, 11); 458 | assert.equal(res, 12); 459 | }); 460 | 461 | it("should support re-extending an extended ppipe", async () => { 462 | const newPipe = ppipe.extend({ 463 | assertEqAndIncrement: (x, y) => { 464 | assert.equal(x, y); 465 | return x + 1; 466 | } 467 | }); 468 | const newerPipe = newPipe.extend({ 469 | divide: (x, y) => { 470 | return x / y; 471 | } 472 | }); 473 | const res = await newerPipe(10) 474 | .pipe(x => x + 1) 475 | .assertEqAndIncrement(_, 11) 476 | .divide(_, 12); 477 | assert.equal(res, 1); 478 | }); 479 | 480 | it("should support expanding the array result", async () => { 481 | const addAll = (...params) => { 482 | return params.reduce((a, b) => a + b, 0); 483 | }; 484 | const res = await ppipe(1) 485 | .pipe(x => [x, 2, 3]) 486 | .pipe(addAll, ..._, 4); 487 | assert.equal(res, 10); 488 | }); 489 | 490 | it("should support expanding the array property of result", async () => { 491 | const addAll = (...params) => { 492 | return params.reduce((a, b) => a + b, 0); 493 | }; 494 | const res = await ppipe(1) 495 | .pipe(x => ({ test: [x, 2, 3] })) 496 | .pipe(addAll, ..._.test, 4); 497 | assert.equal(res, 10); 498 | }); 499 | 500 | it("should support expanding the deep array property of result", async () => { 501 | const addAll = (...params) => { 502 | return params.reduce((a, b) => a + b, 0); 503 | }; 504 | const res = await ppipe(1) 505 | .pipe(x => ({ test: { test: [x, 2, 3] } })) 506 | .pipe(addAll, ..._.test.test, 4); 507 | assert.equal(res, 10); 508 | }); 509 | 510 | it("should return undefined when getting not existing deep properties", async () => { 511 | const res = await ppipe(1) 512 | .pipe(x => ({ test: { test: [x, 2, 3] } })) 513 | .pipe(x => x, _.test.test.foo.bar); 514 | assert.equal(res, undefined); 515 | }); 516 | 517 | it("should use unit fn with no defined and a single param", async () => { 518 | const res = await ppipe(1) 519 | .pipe(x => ({ test: { test: [x, 2, 3] } })) 520 | .pipe(_.test.test.foo.bar); 521 | assert.equal(res, undefined); 522 | const res2 = await ppipe(1) 523 | .pipe(x => ({ test: { test: [x, 2, 3] } })) 524 | .pipe(_.test.test[0]); 525 | assert.equal(res2, 1); 526 | }); 527 | 528 | it("should be able to extract array members", async () => { 529 | async function asyncComplexDoubleArray(x) { 530 | const result = x * 2; 531 | const input = x; 532 | return [await Promise.resolve(0), result, input, 20]; //some API designed by a mad scientist 533 | } 534 | const addAll = (...params) => { 535 | return params.reduce((a, b) => a + b, 0); 536 | }; 537 | const res = await ppipe(5) 538 | .pipe(asyncComplexDoubleArray) 539 | .pipe(addAll, _[1], _[2]); 540 | assert.equal(res, 15); 541 | const res2 = await ppipe(5) 542 | .pipe(asyncComplexDoubleArray) 543 | .pipe(addAll, _["[1]"], _["[2]"]); 544 | assert.equal(res2, 15); 545 | const res3 = await ppipe(5) 546 | .pipe(asyncComplexDoubleArray) 547 | .pipe(x => ({ test: x, foo: "bar" })) 548 | .pipe(addAll, _["test[1]"], _.test[2], _.test["3"]); 549 | assert.equal(res3, 35); 550 | const res4 = await ppipe(5) 551 | .pipe(asyncComplexDoubleArray) 552 | .pipe(x => ({ test: () => ({ innerTest: x }), foo: "bar" })) 553 | .pipe( 554 | addAll, 555 | _["test().innerTest[1]"], 556 | _["test().innerTest"][2], 557 | ..._["test()"].innerTest 558 | ); 559 | assert.equal(res4, 50); 560 | const res5 = await ppipe(5) 561 | .pipe(asyncComplexDoubleArray) 562 | .pipe(x => ({ test: () => ({ innerTest: x }), foo: "bar" })) 563 | .test() 564 | .innerTest() 565 | .pipe(addAll, _[1], _[2]); 566 | assert.equal(res5, 15); 567 | }); 568 | it("should be able to extract promised values", async () => { 569 | const getXAsync = x => 570 | new Promise(res => setTimeout(() => res({ result: x }), 1)); 571 | const aComplexResult = { a: { complex: getXAsync.bind(null, 2) } }; 572 | const res = await ppipe(aComplexResult) 573 | .pipe(_.a["complex()"].result) 574 | .pipe(x => x * x); 575 | assert.equal(res, 4); 576 | const res2 = await ppipe(aComplexResult) 577 | .pipe(_.a["complex().result"]) 578 | .pipe(x => x * x); 579 | assert.equal(res2, 4); 580 | const res3 = await ppipe(aComplexResult) 581 | .a() 582 | .complex() 583 | .result() 584 | .pipe(x => x * x); 585 | assert.equal(res3, 4); 586 | const plainCrazyResult = { 587 | a: { complex: getXAsync.bind(null, aComplexResult) } 588 | }; 589 | const res4 = await ppipe(plainCrazyResult) 590 | .a() 591 | .complex() 592 | .result() 593 | .a() 594 | .complex() 595 | .result() 596 | .pipe(x => x * x); 597 | assert.equal(res4, 4); 598 | const res5 = await ppipe(plainCrazyResult) 599 | .pipe(_.a["complex()"].result.a["complex()"].result) 600 | .pipe(x => x * x); 601 | assert.equal(res5, 4); 602 | }); 603 | }); 604 | --------------------------------------------------------------------------------