From c2d5bf19b73e2e2627caf15836af49ea34c18846 Mon Sep 17 00:00:00 2001 From: Muthu Kumar Date: Fri, 10 Aug 2018 18:13:17 +0530 Subject: [PATCH] [refactor] Up to 0.3.1 --- .vscode/launch.json | 14 +++++ gunner/index.js | 117 ------------------------------------------ gunner/lib/assertPromise.js | 6 --- gunner/lib/constants.js | 6 --- gunner/lib/expect.js | 67 ------------------------ gunner/lib/runTests.js | 67 ------------------------ index.js | 2 +- package.json | 4 +- sample.test.js | 34 ++++++++++-- shrinkwrap.yaml | 8 +++ src/gunner.js | 122 ++++++++++++++++++++++++++++++++++++++++++++ src/lib/assertPromise.js | 6 +++ src/lib/expect.js | 66 ++++++++++++++++++++++++ src/lib/runTests.js | 74 +++++++++++++++++++++++++++ src/util/index.js | 66 ++++++++++++++++++++++++ src/util/symbols.js | 9 ++++ util/helpers.js | 63 ----------------------- 17 files changed, 400 insertions(+), 331 deletions(-) create mode 100644 .vscode/launch.json delete mode 100644 gunner/index.js delete mode 100644 gunner/lib/assertPromise.js delete mode 100644 gunner/lib/constants.js delete mode 100644 gunner/lib/expect.js delete mode 100644 gunner/lib/runTests.js create mode 100644 src/gunner.js create mode 100644 src/lib/assertPromise.js create mode 100644 src/lib/expect.js create mode 100644 src/lib/runTests.js create mode 100644 src/util/index.js create mode 100644 src/util/symbols.js delete mode 100644 util/helpers.js diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9ab9d8f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "program": "${workspaceFolder}/sample.test.js" + } + ] +} \ No newline at end of file diff --git a/gunner/index.js b/gunner/index.js deleted file mode 100644 index 1e74ab2..0000000 --- a/gunner/index.js +++ /dev/null @@ -1,117 +0,0 @@ -'use strict'; - -const { log } = console; - -Promise = require('bluebird'); - -const _runTests = require('./lib/runTests'); -const _expect = require('./lib/expect'); - -const { stringify, hasProp } = require('../util/helpers'); - -class Gunner { - - constructor (options = {}) { - this.__hooks__ = { - before: { - '@start': [], - '@end': [], - '*': [], - }, - after: { - '*': [], - }, - }; - this.__state__ = []; - this.__tests__ = []; - this.name = options.name; - } - - test (description, test) { - const existing = ( - this.__tests__ - .find(x => x.description === description) - ); - if (existing) - throw new Error(`Test '${description}' already exists!`); - - this.__tests__.push({ - description, - test: () => { - try { - return test(_expect); - } catch (e) { - // If errors are thrown, reject them - return Promise.reject(e); - } - }, - }); - - return this; - } - - before (description, run) { - const hook = { - description, - run, - }; - - this.__hooks__.before[description] - ? this.__hooks__.before[description].push(hook) - : this.__hooks__.before[description] = [ hook ]; - - return this; - } - - after (description, run) { - const hook = { - description, - run, - }; - - this.__hooks__.after[description] - ? this.__hooks__.after[description].push(hook) - : this.__hooks__.after[description] = [ hook ]; - - return this; - } - - run (options = {}) { - const shouldLog = (hasProp(options)('log') && options.log) || !(hasProp(options)('log')); - return _runTests(this) - .then(results => { - if (shouldLog) { - const success = results.filter(r => r.result === 'pass'); - results.passing = success.length; - const successPercent = Math.floor(success.length/results.length * 100); - log( - `\n${success.length} tests passed of ${results.length}`, - `[${successPercent}% success]\n` - ); - results.forEach(r => { - const trace = (options.trace && r.error) - ? `\n Traceback:\n ${stringify(r.error)}` - : ''; - - log(`${r.result === 'pass' ? '✅' : '❌'} ::`, - `${r.description}`, - `${trace}`); - }); - } - - return results; - }) - .then(results => { - if (options.exit) { - if(results.passing < results.length) - process.exit(1); - process.exit(0); - } - return results; - }); - } - -} - -module.exports = Gunner; -module.exports.expect = _expect; diff --git a/gunner/lib/assertPromise.js b/gunner/lib/assertPromise.js deleted file mode 100644 index 6b5e0fe..0000000 --- a/gunner/lib/assertPromise.js +++ /dev/null @@ -1,6 +0,0 @@ -const _assertPromise = (bool, assertion) => { - if(bool && typeof bool.then === 'function') return bool.catch(() => Promise.reject(assertion)); - return bool ? Promise.resolve() : Promise.reject(assertion); -}; - -module.exports = _assertPromise; diff --git a/gunner/lib/constants.js b/gunner/lib/constants.js deleted file mode 100644 index dc5fb3c..0000000 --- a/gunner/lib/constants.js +++ /dev/null @@ -1,6 +0,0 @@ -const constants = { - pass: 'pass', - fail: 'fail', -}; - -module.exports = constants; diff --git a/gunner/lib/expect.js b/gunner/lib/expect.js deleted file mode 100644 index cec83c6..0000000 --- a/gunner/lib/expect.js +++ /dev/null @@ -1,67 +0,0 @@ -const isEq = require('@codefeathers/iseq'); - -const { liftPromise, stringify, isPromise } = require('../../util/helpers'); -const _assertPromise = require('./assertPromise'); - - -const expectPromise = (pred, statement, options = {}) => - toTest => - (...testValues) => - liftPromise( - resolvedValue => _assertPromise( - pred(toTest, ...testValues), - statement(resolvedValue, ...testValues), - ), - toTest, - ) - .catch(rejectedValue => - options.shouldCatch - ? _assertPromise( - pred(toTest, ...testValues), - statement(rejectedValue, ...testValues), - ) - : Promise.reject(rejectedValue) - ); - -const expect = thing => { - - return ({ - done : () => Promise.resolve(), - equal : expectPromise( - (a, b) => a === b, - (a, b) => `${a} is not equal to ${b}`, - )(thing), - deepEqual : expectPromise( - (a, b) => isEq(a, b), - (a, b) => `${stringify(a)} is not deeply equal to ${stringify(b)}`, - )(thing), - isTrue : expectPromise( - a => a === true, - a => `${a} is not true`, - )(thing), - hasProp : expectPromise( - (a, b) => b in a, - (a, b) => `Property ${b} does not exist in ${stringify(a)}`, - )(thing), - hasPair : expectPromise( - (a, b, c) => a[b] === c, - (a, b, c) => `Pair <${b}, ${c}> does not exist in ${stringify(a)}`, - )(thing), - resolvesTo : expectPromise( - (a, b) => isPromise(a) - ? a.then(x => x === b ? Promise.resolve() : Promise.reject()) - : Promise.reject(`${a} was not a Promise`), - (a, b) => `${a} does not resolve to ${b}`, - )(thing), - isPromise : expectPromise( - a => isPromise(a) - ? a.then(() => Promise.resolve()).catch(() => Promise.resolve()) - : Promise.reject(), - a => `${a} is not a Promise`, - { shouldCatch: true }, - )(thing), - }); - -}; - -module.exports = expect; diff --git a/gunner/lib/runTests.js b/gunner/lib/runTests.js deleted file mode 100644 index 2a22fc7..0000000 --- a/gunner/lib/runTests.js +++ /dev/null @@ -1,67 +0,0 @@ -'use strict'; - -const { isPromise } = require('../../util/helpers'); -const { pass, fail } = require('./constants'); - -const runTests = instance => { - - const beforeAll = () => Promise.map( - instance.__hooks__.before['@start'], - hook => hook.run(), - ); - - const beforeEvery = () => Promise.mapSeries( - instance.__hooks__.before['*'] || [], - hook => hook.run(), - ); - - const runner = () => Promise.mapSeries(instance.__tests__, each => { - - const beforeThis = () => Promise.mapSeries( - instance.__hooks__.before[each.description] || [], - hook => hook.run(), - ); - - const afterThis = () => Promise.mapSeries( - instance.__hooks__.after[each.description] || [], - hook => hook.run(), - ); - - return beforeEvery().then(() => beforeThis()).then(() => { - - const pred = each.test(); - - /* There are 4 different cases at play: - 1. A plain expect() is returned. - 2. An array of [ expect() ] is returned - 3. A plain expect() is wrapped in a promise - 4. An array of [ expect() ] is wrapped in a promise. - Here we normalise all of them into something we can process */ - - if (!isPromise(pred) && !(pred && isPromise(pred[0]))) - throw new Error(`Malformed test '${each.description}'`); - const toTest = Array.isArray(pred) - ? Promise.all(pred) - : pred.then(x => Array.isArray(x) ? Promise.all(x) : x); - - return toTest - .then(() => ({ description: each.description, result: pass })) - .catch(e => ({ description: each.description, result: fail, error: e })); - - }) - .then(result => afterThis().then(() => result)); - - }); - - const afterAll = () => Promise.mapSeries( - instance.__hooks__.before['@end'], - hook => hook.run(), - ); - - return beforeAll() - .then(() => runner()) - .then(results => afterAll().then(() => results)); - -}; - -module.exports = runTests; diff --git a/index.js b/index.js index cca91cd..1192c07 100644 --- a/index.js +++ b/index.js @@ -1 +1 @@ -module.exports = require('./gunner'); \ No newline at end of file +module.exports = require('./src/gunner'); \ No newline at end of file diff --git a/package.json b/package.json index 6c21e20..065e1d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@klenty/gunner", - "version": "0.3.0", + "version": "0.3.1", "description": "Zero magic, fast test-runner and assertion framework. No magic globals.", "main": "index.js", "repository": { @@ -22,7 +22,9 @@ "homepage": "https://github.com/klenty/gunner#readme", "dependencies": { "@codefeathers/iseq": "^1.2.1", + "@codefeathers/promise.object": "^0.9.5", "bluebird": "^3.5.1", + "chalk": "^2.4.1", "eslint": "^5.2.0", "json-stringify-safe": "^5.0.1" } diff --git a/sample.test.js b/sample.test.js index 2285f5c..73fef4b 100644 --- a/sample.test.js +++ b/sample.test.js @@ -1,7 +1,17 @@ -const Gunner = require('./gunner'); +/** + * This file contains random tests + * used during development + */ + +const Gunner = require('./index.js'); const gunner = new Gunner({ name: 'sample tests' }); const a = 1; +gunner.before(Gunner.Start, () => console.log('Started tests!')); +gunner.before(Gunner.End, () => console.log('Ended tests!')); +let runCount = 1; +gunner.before('*', () => console.log(`Running test ${runCount++}`)); + gunner.test('should automatically pass', expect => expect().done()); gunner.test(`should be equal`, expect => expect(1).equal(1)); gunner.test(`objects are deep equal`, expect => expect({ a: 1 }).deepEqual({ a: 1 })); @@ -16,6 +26,16 @@ gunner.test('should be a Promise (rejected)', expect => gunner.test('should resolve to 5', expect => expect(Promise.resolve(5)).resolvesTo(5)); +gunner.before( + 'file must have hello as content', + () => console.log('>> starting test! file must have hello as content'), +); + +gunner.after( + 'file must have hello as content', + () => console.log('>> finished test! file must have hello as content'), +); + gunner.test('file must have hello as content', async expect => { const { readFile } = require('fs').promises; const file = await readFile('./hello.txt', { encoding: 'utf8' }); @@ -25,6 +45,12 @@ gunner.test('file must have hello as content', async expect => { ]; }); +gunner.test('(should fail) Value is not a Promise', expect => + expect(5).isPromise()); + +gunner.test('(should fail) Error is not a Promise', expect => + expect(flamethrower()).isPromise()); + gunner.test(`(should fail) objects aren't deeply equal`, expect => expect({a : 1}).deepEqual({ a: 2 })); gunner.test('(should fail) promise must reject', expect => @@ -52,7 +78,9 @@ gunner.test('(should fail) should catch error', expect => { }); gunner.test('(should fail) should not resolve to 5', expect => - expect(Promise.resolve({})).resolvesTo(5)); + expect(Promise.resolve()).resolvesTo(5)); +const trace = process.argv.slice(2).indexOf('--trace') !== -1; +const log = process.argv.slice(2).indexOf('--log') !== -1; -gunner.run({ trace: true}); +gunner.run({ trace, log }); diff --git a/shrinkwrap.yaml b/shrinkwrap.yaml index 229829c..5d831af 100644 --- a/shrinkwrap.yaml +++ b/shrinkwrap.yaml @@ -1,6 +1,8 @@ dependencies: '@codefeathers/iseq': 1.2.1 + '@codefeathers/promise.object': 0.9.5 bluebird: 3.5.1 + chalk: 2.4.1 eslint: 5.2.0 json-stringify-safe: 5.0.1 packages: @@ -8,6 +10,10 @@ packages: dev: false resolution: integrity: sha1-zUHiKGdKZQlWBfKVacbdVtlodsw= + /@codefeathers/promise.object/0.9.5: + dev: false + resolution: + integrity: sha1-YBDLXpC4vhz12WrDHJKr/F8K/zA= /acorn-jsx/4.1.1: dependencies: acorn: 5.7.1 @@ -1043,6 +1049,8 @@ shrinkwrapMinorVersion: 8 shrinkwrapVersion: 3 specifiers: '@codefeathers/iseq': ^1.2.1 + '@codefeathers/promise.object': ^0.9.5 bluebird: ^3.5.1 + chalk: ^2.4.1 eslint: ^5.2.0 json-stringify-safe: ^5.0.1 diff --git a/src/gunner.js b/src/gunner.js new file mode 100644 index 0000000..c0b8ace --- /dev/null +++ b/src/gunner.js @@ -0,0 +1,122 @@ +'use strict'; + +const { log } = console; +const chalk = require('chalk'); + +Promise = require('bluebird'); +Promise.object = require('@codefeathers/promise.object'); + +const _runTests = require('./lib/runTests'); +const _expect = require('./lib/expect'); + +const { stringify, hasProp } = require('./util'); +const symbols = require('./util/symbols'); + +class Gunner { + + constructor (options = {}) { + this.__hooks__ = { + before: { + [symbols.Start]: [], + [symbols.Stop]: [], + '*': [], + }, + after: { + '*': [], + }, + }; + this.__state__ = []; + this.__tests__ = []; + this.name = options.name; + } + + test (description, test) { + const existing = ( + this.__tests__ + .find(x => x.description === description) + ); + if (existing) + throw new Error(`Test '${description}' already exists!`); + + this.__tests__.push({ + description, + test: (state) => { + try { + return test(_expect, state); + } catch (e) { + // If errors are thrown, reject them + return Promise.reject(e); + } + }, + }); + + return this; + } + + before (description, run) { + const hook = { + description, + run, + }; + + this.__hooks__.before[description] + ? this.__hooks__.before[description].push(hook) + : this.__hooks__.before[description] = [ hook ]; + + return this; + } + + after (description, run) { + const hook = { + description, + run, + }; + + this.__hooks__.after[description] + ? this.__hooks__.after[description].push(hook) + : this.__hooks__.after[description] = [ hook ]; + + return this; + } + + run (options = {}) { + const shouldLog = (hasProp(options)('log') && options.log) || !(hasProp(options)('log')); + return _runTests(this) + .then(results => { + if (shouldLog) { + const success = results.filter(r => r.result === 'pass'); + results.passing = success.length; + const successPercent = Math.floor(success.length/results.length * 100); + log( + chalk`\n{green ${success.length}} tests passed of ${results.length}`, + `[${successPercent}% success]\n` + ); + results.forEach(r => { + const trace = (options.trace && r.error) + ? `\n Traceback:\n ${stringify(r.error)}` + : ''; + + log(`${r.result === 'pass' ? chalk`{green ✅}` : chalk`{red ❌}`} ::`, + `${r.description}`, + `${trace}`); + }); + } + + return results; + }) + .then(results => { + if (options.exit) { + if(results.passing < results.length) + process.exit(1); + process.exit(0); + } + return results; + }); + } + +} + +module.exports = Gunner; +module.exports.expect = _expect; +module.exports.Start = symbols.Start; +module.exports.End = symbols.End; diff --git a/src/lib/assertPromise.js b/src/lib/assertPromise.js new file mode 100644 index 0000000..6b5e0fe --- /dev/null +++ b/src/lib/assertPromise.js @@ -0,0 +1,6 @@ +const _assertPromise = (bool, assertion) => { + if(bool && typeof bool.then === 'function') return bool.catch(() => Promise.reject(assertion)); + return bool ? Promise.resolve() : Promise.reject(assertion); +}; + +module.exports = _assertPromise; diff --git a/src/lib/expect.js b/src/lib/expect.js new file mode 100644 index 0000000..bc8afae --- /dev/null +++ b/src/lib/expect.js @@ -0,0 +1,66 @@ +const isEq = require('@codefeathers/iseq'); + +const { liftPromise, stringify, isPromise } = require('../util'); +const _assertPromise = require('./assertPromise'); + +const expectPromise = (pred, statement, options = {}) => + toTest => + (...testValues) => + liftPromise( + resolvedValue => _assertPromise( + pred(toTest, ...testValues), + statement(resolvedValue, ...testValues), + ), + toTest, + ) + .catch(rejectedValue => + options.shouldCatch + ? _assertPromise( + pred(toTest, ...testValues), + statement(rejectedValue, ...testValues), + ) + : Promise.reject(rejectedValue) + ); + +const expect = thing => { + + return ({ + done : () => Promise.resolve(), + equal : expectPromise( + (a, b) => a === b, + (a, b) => `${a} is not equal to ${b}`, + )(thing), + deepEqual : expectPromise( + (a, b) => isEq(a, b), + (a, b) => `${stringify(a)} is not deeply equal to ${stringify(b)}`, + )(thing), + isTrue : expectPromise( + a => a === true, + a => `${a} is not true`, + )(thing), + hasProp : expectPromise( + (a, b) => b in a, + (a, b) => `Property ${b} does not exist in ${stringify(a)}`, + )(thing), + hasPair : expectPromise( + (a, b, c) => a[b] === c, + (a, b, c) => `Pair <${b}, ${c}> does not exist in ${stringify(a)}`, + )(thing), + resolvesTo : expectPromise( + (a, b) => isPromise(a) + ? a.then(x => x === b ? Promise.resolve() : Promise.reject()) + : Promise.reject(`${a} was not a Promise`), + (a, b) => `${a} does not resolve to ${b}`, + )(thing), + isPromise : expectPromise( + a => isPromise(a) + ? a.then(() => Promise.resolve()).catch(() => Promise.resolve()) + : Promise.reject(), + a => `${a} is not a Promise`, + { shouldCatch: true }, + )(thing), + }); + +}; + +module.exports = expect; diff --git a/src/lib/runTests.js b/src/lib/runTests.js new file mode 100644 index 0000000..1fb331f --- /dev/null +++ b/src/lib/runTests.js @@ -0,0 +1,74 @@ +'use strict'; + +const { isPromise } = require('../util'); +const constants = require('../util/symbols'); + +const runTests = instance => { + + const beforeAll = () => Promise.map( + instance.__hooks__.before[constants.Start] || [], + hook => hook.run(), + ); + + const beforeEvery = state => Promise.mapSeries( + instance.__hooks__.before['*'] || [], + hook => hook.run(state), + ); + + const runner = state => Promise.mapSeries(instance.__tests__, each => { + + const beforeThis = state => Promise.mapSeries( + instance.__hooks__.before[each.description] || [], + hook => hook.run(state), + ); + + const afterThis = state => Promise.mapSeries( + instance.__hooks__.after[each.description] || [], + hook => hook.run(state), + ); + + return beforeEvery(state) + .then(newState => ({ ...state, '@every': newState })) + .then(state => Promise.object({ ...state, '@this': beforeThis() })) + .then(state => { + + const pred = each.test(state); + + /* There are 4 different cases at play: + 1. A plain expect() is returned. + 2. An array of [ expect() ] is returned + 3. A plain expect() is wrapped in a promise + 4. An array of [ expect() ] is wrapped in a promise. + Here we normalise all of them into something we can process */ + + if (!isPromise(pred) && !(pred && isPromise(pred[0]))) + throw new Error(`Malformed test '${each.description}'`); + const toTest = Array.isArray(pred) + ? Promise.all(pred) + : pred.then(x => Array.isArray(x) ? Promise.all(x) : x); + + return [ + state, + toTest + .then(() => ({ description: each.description, result: constants.pass })) + .catch(e => ({ description: each.description, result: constants.fail, error: e })), + ]; + + }) + .spread((state, result) => afterThis(state).then(() => result)); + + }); + + const afterAll = state => Promise.mapSeries( + instance.__hooks__.before[constants.End] || [], + hook => hook.run(state, state['@results']), + ); + + return Promise.object({ '@start': beforeAll() }) + .then(state => Promise.object({ ...state, '@results': runner(state)})) + .then(state => Promise.object({ ...state, '@end': afterAll(state) })) + .then(state => state['@results']); + +}; + +module.exports = runTests; diff --git a/src/util/index.js b/src/util/index.js new file mode 100644 index 0000000..9491444 --- /dev/null +++ b/src/util/index.js @@ -0,0 +1,66 @@ +const stringify = require('json-stringify-safe'); + +/* Returns true if a promise is passed */ +const isPromise = prom => prom && (typeof prom.then === 'function'); + +module.exports = { + + /* Returns true if a promise is passed */ + isPromise, + + /* Flattens an array of arrays to an array */ + flatten : arrData => [].concat.apply([], arrData), + + /* Maps a function over an array */ + map : fn => x => x.map(fn), + + /* Returns identity */ + identity : x => x, + + /* Wraps a value in an object with given key */ + wrapWith : x => y => ({ [x] : y }), + + /* Unwraps a value from an object with given key */ + unwrapFrom : x => y => y[x], + + /* Resolves an array of Promises */ + promiseAll : x => Promise.all(x), + + /* Pipe a value or promise through any number of unary functions */ + pipe: (...fns) => + arg => fns.reduce((acc, fn) => + isPromise(acc) + ? acc.then(fn) + : fn(acc), arg), + + /* Pass partial arguments and return a function that accepts the rest */ + partial: (fn, ...args) => (...rest) => fn(...args, ...rest), + + /* Item is in collection */ + isIn : (collection, item) => collection.indexOf(item) !== -1, + + /* Collection contains given path */ + containsPath : (collection, path) => collection.some( + x => path.match(new RegExp(`/${x}/?$`)) + ), + + /* Lift promises into a function */ + liftPromise : (fn, thing) => + isPromise(thing) + ? thing.then(fn) + : fn(thing), + + /* Stringifies object or coerces to string */ + stringify : obj => + typeof obj === 'object' + ? (obj.stack || stringify(obj)) + : obj, + + /* Short circuits with given value on pred. Else calls function */ + short : (pred, shorter) => + fn => value => pred(value) ? shorter(value) : fn(value), + + /* Check if object has given property */ + hasProp : obj => prop => prop in obj, + +}; diff --git a/src/util/symbols.js b/src/util/symbols.js new file mode 100644 index 0000000..602537c --- /dev/null +++ b/src/util/symbols.js @@ -0,0 +1,9 @@ +module.exports = { + + Start : Symbol('Start'), + End : Symbol('End'), + + pass: 'pass', + fail: 'fail', + +}; \ No newline at end of file diff --git a/util/helpers.js b/util/helpers.js deleted file mode 100644 index fabe93d..0000000 --- a/util/helpers.js +++ /dev/null @@ -1,63 +0,0 @@ -const stringify = require('json-stringify-safe'); - -module.exports = { - - /* Returns true if a promise is passed */ - isPromise : prom => prom && (typeof prom.then === 'function'), - - /* Flattens an array of arrays to an array */ - flatten : arrData => [].concat.apply([], arrData), - - /* Maps a function over an array */ - map : fn => x => x.map(fn), - - /* Returns identity */ - identity : x => x, - - /* Wraps a value in an object with given key */ - wrapWith : x => y => ({ [x] : y }), - - /* Unwraps a value from an object with given key */ - unwrapFrom : x => y => y[x], - - /* Resolves an array of Promises */ - promiseAll : x => Promise.all(x), - - /* Pipe a value or promise through any number of unary functions */ - pipe: (...fns) => - arg => fns.reduce((acc, fn) => - typeof acc.then === 'function' - ? acc.then(fn) - : fn(acc), arg), - - /* Pass partial arguments and return a function that accepts the rest */ - partial: (fn, ...args) => (...rest) => fn(...args, ...rest), - - /* Item is in collection */ - isIn : (collection, item) => collection.indexOf(item) !== -1, - - /* Collection contains given path */ - containsPath : (collection, path) => collection.some( - x => path.match(new RegExp(`/${x}/?$`)) - ), - - /* Lift promises into a function */ - liftPromise : (fn, thing) => - typeof thing.then === 'function' - ? thing.then(fn) - : fn(thing), - - /* Stringifies object or coerces to string */ - stringify : obj => - typeof obj === 'object' - ? (obj.stack || stringify(obj)) - : obj, - - /* Short circuits with given value on pred. Else calls function */ - short : (pred, shorter) => - fn => value => pred(value) ? shorter(value) : fn(value), - - /* Check if object has given property */ - hasProp : obj => prop => prop in obj, - -};