effects-as-data
Advanced tools
Comparing version 2.5.5 to 2.5.6
@@ -15,27 +15,25 @@ 'use strict'; | ||
var g = fn.apply(null, args); | ||
return run(config, handlers, g); | ||
var gen = fn.apply(null, args); | ||
var el = newExecutionLog(); | ||
return run(config, handlers, gen, null, el); | ||
} | ||
function run(config, handlers, fn, input, el) { | ||
var generatorOperation = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 'next'; | ||
var genOperation = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 'next'; | ||
try { | ||
var el1 = getExecutionLog(el); | ||
var _getNextOutput = getNextOutput(fn, input, generatorOperation), | ||
var _getNextOutput = getNextOutput(fn, input, genOperation), | ||
output = _getNextOutput.output, | ||
isList = _getNextOutput.isList, | ||
done = _getNextOutput.done; | ||
if (done) return toPromise(output); | ||
var isList = Array.isArray(output); | ||
var commandsList = toArray(output); | ||
return processCommands(config, handlers, commandsList, el1).then(function (results) { | ||
return processCommands(config, handlers, commandsList, el).then(function (results) { | ||
var unwrappedResults = unwrapResults(isList, results); | ||
el1.step = el1.step + 1; // mutate in place | ||
return run(config, handlers, fn, unwrappedResults, el1); | ||
el.step++; | ||
return run(config, handlers, fn, unwrappedResults, el, 'next'); | ||
}).catch(function (e) { | ||
// ok to mutate? | ||
el1.step = el1.step + 1; // mutate in place | ||
return run(config, handlers, fn, e, el1, 'throw'); | ||
el.step++; | ||
return run(config, handlers, fn, e, el, 'throw'); | ||
}); | ||
@@ -47,4 +45,4 @@ } catch (e) { | ||
function getExecutionLog(el) { | ||
return el || { | ||
function newExecutionLog() { | ||
return { | ||
step: 0 | ||
@@ -65,7 +63,3 @@ }; | ||
return { | ||
output: output, | ||
isList: Array.isArray(output), | ||
done: done | ||
}; | ||
return { output: output, done: done }; | ||
} | ||
@@ -89,3 +83,5 @@ | ||
try { | ||
result = handlers[command.type](command, { call: call, config: config, handlers: handlers }); | ||
var handler = handlers[command.type]; | ||
if (!handler) throw new Error('Handler of type "' + command.type + '" is not registered.'); | ||
result = handler(command, { call: call, config: config, handlers: handlers }); | ||
} catch (e) { | ||
@@ -92,0 +88,0 @@ result = Promise.reject(e); |
@@ -49,2 +49,27 @@ 'use strict'; | ||
}); | ||
test('call should throw error for unregistered handler', async function () { | ||
var fn = regeneratorRuntime.mark(function fn() { | ||
return regeneratorRuntime.wrap(function fn$(_context) { | ||
while (1) { | ||
switch (_context.prev = _context.next) { | ||
case 0: | ||
_context.next = 2; | ||
return { type: 'dne' }; | ||
case 2: | ||
case 'end': | ||
return _context.stop(); | ||
} | ||
} | ||
}, fn, this); | ||
}); | ||
try { | ||
await call({}, handlers, fn); | ||
} catch (e) { | ||
expect(e.message).toEqual('Handler of type "dne" is not registered.'); | ||
return; | ||
} | ||
fail('Did not throw.'); | ||
}); | ||
//# sourceMappingURL=errors.spec.js.map |
@@ -8,3 +8,4 @@ 'use strict'; | ||
handlers = _require2.handlers, | ||
functions = _require2.functions; | ||
functions = _require2.functions, | ||
cmds = _require2.cmds; | ||
@@ -50,2 +51,28 @@ var badHandler = functions.badHandler, | ||
}); | ||
test('handlers should be able to return an array of results', async function () { | ||
var fn = regeneratorRuntime.mark(function fn(a, b) { | ||
var result; | ||
return regeneratorRuntime.wrap(function fn$(_context) { | ||
while (1) { | ||
switch (_context.prev = _context.next) { | ||
case 0: | ||
_context.next = 2; | ||
return [cmds.echo(a), cmds.echo(b)]; | ||
case 2: | ||
result = _context.sent; | ||
return _context.abrupt('return', result); | ||
case 4: | ||
case 'end': | ||
return _context.stop(); | ||
} | ||
} | ||
}, fn, this); | ||
}); | ||
var actual = await call({}, handlers, fn, ['foo', 'bar'], ['foo', 'baz']); | ||
var expected = [['foo', 'bar'], ['foo', 'baz']]; | ||
expect(actual).toEqual(expected); | ||
}); | ||
//# sourceMappingURL=handlers.spec.js.map |
{ | ||
"name": "effects-as-data", | ||
"version": "2.5.5", | ||
"version": "2.5.6", | ||
"description": "Express async workflows using pure functions.", | ||
@@ -5,0 +5,0 @@ "main": "src/index.js", |
247
README.md
@@ -15,8 +15,10 @@ # Effects-as-data | ||
#### First, create a command creator. | ||
This function creates a plain JSON `command` object that effects-as-data will pass to a handler function which will perform the actual HTTP request. The `type` field on the command matches the name of the handler to which it will be passed. | ||
This function creates a plain JSON `command` object that effects-as-data will pass to a handler function which will perform the actual HTTP request. The `type` field on the command matches the name of the handler to which it will be passed (see step 4). *Note* we have not yet actually implemented the function that will actual do the HTTP GET request, we have just defined a `command`. The command is placed on the `cmds` object for convenience. | ||
```js | ||
function httpGetCommand(url) { | ||
return { | ||
type: 'httpGet', | ||
url | ||
const cmds = { | ||
httpGet(url) { | ||
return { | ||
type: 'httpGet', | ||
url | ||
} | ||
} | ||
@@ -26,7 +28,22 @@ } | ||
#### Second, write your business logic. | ||
Effects-as-data uses a generator function's ability to give up execution flow and to pass a value to an outside process using the `yield` keyword. You create `command` objects in your business logic and `yield` them to effects-as-data. | ||
#### Second, test your business logic. | ||
Write a test for `getPeople` function that you are about to create. These tests can be used stand-alone or in any test runner like Jest, Mocha, etc. There are a few ways to test `effects-as-data` functions demonstrated below. | ||
Semantic test example: | ||
```js | ||
const { testFn, args } = require('effects-as-data/test') | ||
testFn(getPeople, () => { | ||
const apiResults = { results: [{ name: 'Luke Skywalker' }] } | ||
return args() | ||
.yieldCmd(cmds.httpGet('https://swapi.co/api/people')).yieldReturns(apiResults) | ||
.returns(['Luke Skywalker']) | ||
})() | ||
``` | ||
#### Third, write your business logic. | ||
Effects-as-data uses a generator function's ability to give up execution flow and to pass a value to an outside process using the `yield` keyword. You create `command` objects in your business logic and `yield` them to `effects-as-data`. It is important to understand that when using effects-as-data that your business logic never actually `httpGet`'s anything. It ONLY creates plain JSON objects and `yield`'s them out (`cmds.httpGet()` simply returns the JSON object from step 1). This is one of the main reasons `effects-as-data` functions are easy to test. | ||
```js | ||
function* getPeople() { | ||
const { results } = yield httpGetCommand('https://swapi.co/api/people') | ||
const { results } = yield cmds.httpGet('https://swapi.co/api/people') | ||
const names = results.map(p => p.name) | ||
@@ -37,12 +54,14 @@ return names | ||
#### Third, create a command handler. | ||
After the `command` object is `yield`ed, effects-as-data will pass it to a handler function that will perform the side-effect producing operation (in this case, an HTTP GET request). | ||
#### Fourth, create a command handler. | ||
After the `command` object is `yield`ed, effects-as-data will pass it to a handler function that will perform the side-effect producing operation (in this case, an HTTP GET request). This is the function mentioned in step 1 that actually performs the HTTP GET request. Notice that the business logic does not call this function directly; the business logic in step 1 simply `yield`s the `httpGet` `command` out, and `effects-as-data` takes care of getting it to the handler. | ||
```js | ||
function httpGetHandler(cmd) { | ||
return fetch(cmd.url).then(r => r.json()) | ||
const handlers = { | ||
httpGet(cmd) { | ||
return fetch(cmd.url).then(r => r.json()) | ||
} | ||
} | ||
``` | ||
#### Fourth, setting up monitoring / telemetry. | ||
The effects-as-data config accepts an `onCommandComplete` callback which will be called every time a `command` completes, giving detailed information about the operation. This data can be logged to the console or sent to a logging service. | ||
#### Fifth, optionally setting up monitoring / telemetry. | ||
The effects-as-data config accepts an `onCommandComplete` callback which will be called every time a `command` completes, giving detailed information about the operation. This data can be logged to the console or sent to a logging service. *Note*, this step is optional. | ||
```js | ||
@@ -56,13 +75,9 @@ const config = { | ||
#### Fifth, wire everything up. | ||
#### Sixth, wire everything up. | ||
This will turn your effects-as-data functions into normal, promise-returning functions. In this case, `functions` will be an object with one key, `getPeople`, which will be a promise-returning function. | ||
```js | ||
const functions = buildFunctions( | ||
config, | ||
{ httpGet: httpGetHandler }, | ||
{ getPeople } | ||
) | ||
const functions = buildFunctions(config, handlers, { getPeople }) | ||
``` | ||
#### Sixth, use your functions. | ||
#### Lastly, use your functions. | ||
Once you have built your functions, you can use them like normal promise-returning functions anywhere in your application. | ||
@@ -81,27 +96,35 @@ ```js | ||
Turn your effects-as-data functions into normal promise-returning functions. | ||
#### Full Example | ||
### Full Example | ||
See full example in the `effects-as-data-examples` repository: [https://github.com/orourkedd/effects-as-data-examples/blob/master/basic/index.js](https://github.com/orourkedd/effects-as-data-examples/blob/master/basic/index.js). | ||
You can run this example by cloning `https://github.com/orourkedd/effects-as-data-examples` and running `npm run basic`. | ||
### Using existing commands and handlers | ||
This example demonstrates using the `effects-as-data-universal` module which contains commands/handler that can be used anywhere Javascript runs. | ||
Full example: [https://github.com/orourkedd/effects-as-data-examples/blob/master/basic-existing-handlers/index.js](https://github.com/orourkedd/effects-as-data-examples/blob/master/basic-existing-handlers/index.js). | ||
Run it: Clone `https://github.com/orourkedd/effects-as-data-examples` and run `npm run basic-existing-handlers`. | ||
```js | ||
const { call, buildFunctions } = require('effects-as-data') | ||
const fetch = require('isomorphic-fetch') | ||
const { testFn, args } = require('effects-as-data/test') | ||
const { cmds, handlers } = require('effects-as-data-universal') | ||
function httpGetCommand(url) { | ||
return { | ||
type: 'httpGet', | ||
url | ||
} | ||
} | ||
function httpGetHandler(cmd) { | ||
return fetch(cmd.url).then(r => r.json()) | ||
} | ||
function* getPeople() { | ||
const { results } = yield httpGetCommand('https://swapi.co/api/people') | ||
const names = results.map(p => p.name) | ||
const { payload } = yield cmds.httpGet('https://swapi.co/api/people') | ||
const names = payload.results.map(p => p.name) | ||
return names | ||
} | ||
// Semantic test style | ||
testFn(getPeople, () => { | ||
const apiResults = { payload: { results: [{ name: 'Luke Skywalker' }] } } | ||
// prettier-ignore | ||
return args() | ||
.yieldCmd(cmds.httpGet('https://swapi.co/api/people')).yieldReturns(apiResults) | ||
.returns(['Luke Skywalker']) | ||
})() | ||
const config = { | ||
@@ -113,7 +136,3 @@ onCommandComplete: telemetry => { | ||
const functions = buildFunctions( | ||
config, | ||
{ httpGet: httpGetHandler }, | ||
{ getPeople } | ||
) | ||
const functions = buildFunctions(config, handlers, { getPeople }) | ||
@@ -130,34 +149,134 @@ functions | ||
### Using existing commands and handlers | ||
### Error handling | ||
This example demonstrates handling errors with `either`. Unlike the above examples, this example has been separated into a few files showing more what production code looks like. | ||
Full example: [https://github.com/orourkedd/effects-as-data-examples/tree/master/error-handling](https://github.com/orourkedd/effects-as-data-examples/tree/master/error-handling). | ||
Run it: Clone `https://github.com/orourkedd/effects-as-data-examples` and run `npm run error-handling`. | ||
Below is the `getPeople` function. Notice the use of `cmds.either`. The `either` handler will process the `httpGet` command, and if the command is successful, will return the response. If the `httpGet` command fails or returns a falsey value, the `either` handler will return `emptyResults`. Because the `either` handler will never throw an exception and will either return a successful result or `emptyResults`, there is no need for an `if` statement to ensure success before the `map`. Using this pattern will reduce the number of code paths and simplify code. | ||
```js | ||
const { call, buildFunctions } = require('../index') | ||
const { cmds, handlers } = require('effects-as-data-universal') | ||
const { cmds } = require('effects-as-data-universal') | ||
// Pure business logic functions | ||
function* getUsers() { | ||
return yield cmds.httpGet('http://example.com/api/users') | ||
function* getPeople() { | ||
const httpGet = cmds.httpGet('https://swapi.co/api/people') | ||
const emptyResults = { payload: { results: [] } } | ||
const { payload } = yield cmds.either(httpGet, emptyResults) | ||
return payload.results.map(p => p.name) | ||
} | ||
function* getUserPosts(userId) { | ||
return yield cmds.httpGet(`http://example.com/api/user/${userId}/posts`) | ||
} | ||
module.exports = getPeople | ||
``` | ||
// Use onCommandComplete to gather telemetry | ||
const config = { | ||
onCommandComplete: telemetry => { | ||
console.log('Telemetry:', telemetry) | ||
} | ||
Tests for the `getPeople` function. These tests are using Jest: | ||
```js | ||
const { cmds } = require('effects-as-data-universal') | ||
const { testFn, args } = require('effects-as-data/test') | ||
const getPeople = require('./get-people') | ||
const testGetPeople = testFn(getPeople) | ||
test( | ||
"getPeople should return a list of people's names", | ||
testGetPeople(() => { | ||
const apiResults = { payload: { results: [{ name: 'Luke Skywalker' }] } } | ||
const httpGet = cmds.httpGet('https://swapi.co/api/people') | ||
const emptyResults = { payload: { results: [] } } | ||
// prettier-ignore | ||
return args() | ||
.yieldCmd(cmds.either(httpGet, emptyResults)).yieldReturns(apiResults) | ||
.returns(['Luke Skywalker']) | ||
}) | ||
) | ||
test( | ||
'getPeople should return an empty list if http get errors out', | ||
testGetPeople(() => { | ||
const apiResults = { payload: { results: [{ name: 'Luke Skywalker' }] } } | ||
const httpGet = cmds.httpGet('https://swapi.co/api/people') | ||
const emptyResults = { payload: { results: [] } } | ||
// prettier-ignore | ||
return args() | ||
.yieldCmd(cmds.either(httpGet, emptyResults)).yieldReturns(emptyResults) | ||
.returns([]) | ||
}) | ||
) | ||
``` | ||
The index file that runs it. `onCommandComplete` is removed for brevity: | ||
```js | ||
const { call, buildFunctions } = require('effects-as-data') | ||
const { handlers } = require('effects-as-data-universal') | ||
const getPeople = require('./get-people') | ||
const functions = buildFunctions({}, handlers, { getPeople }) | ||
functions | ||
.getPeople() | ||
.then(names => { | ||
console.log('Function Results:') | ||
console.log(names.join(', ')) | ||
}) | ||
.catch(console.error) | ||
``` | ||
### Parallelization of commands | ||
Full example: [https://github.com/orourkedd/effects-as-data-examples/tree/master/parallelization](https://github.com/orourkedd/effects-as-data-examples/tree/master/parallelization). | ||
Run it: Clone `https://github.com/orourkedd/effects-as-data-examples` and run `npm run parallelization`. | ||
```js | ||
const { cmds } = require('effects-as-data-universal') | ||
function* getPeople(person1, person2) { | ||
const httpGet1 = cmds.httpGet(`https://swapi.co/api/people/${person1}`) | ||
const httpGet2 = cmds.httpGet(`https://swapi.co/api/people/${person2}`) | ||
const [result1, result2] = yield [httpGet1, httpGet2] | ||
return [result1.payload, result2.payload].map(p => p.name) | ||
} | ||
// Turn effects-as-data functions into normal, | ||
// promise-returning functions | ||
const functions = buildFunctions( | ||
config, | ||
handlers, // command handlers | ||
{ getUsers, getUserPosts } // effects-as-data functions | ||
module.exports = getPeople | ||
``` | ||
Tests for the `getPeople` function. These tests are using Jest: | ||
```js | ||
const { cmds } = require('effects-as-data-universal') | ||
const { testFn, args } = require('effects-as-data/test') | ||
const getPeople = require('./get-people') | ||
const testGetPeople = testFn(getPeople) | ||
test( | ||
"getPeople should return a list of people's names", | ||
testGetPeople(() => { | ||
const apiResult1 = { payload: { name: 'Luke Skywalker' } } | ||
const apiResult2 = { payload: { name: 'C-3PO' } } | ||
const httpGet1 = cmds.httpGet('https://swapi.co/api/people/1') | ||
const httpGet2 = cmds.httpGet('https://swapi.co/api/people/2') | ||
// prettier-ignore | ||
return args(1, 2) | ||
.yieldCmd([httpGet1, httpGet2]).yieldReturns([apiResult1, apiResult2]) | ||
.returns(['Luke Skywalker', 'C-3PO']) | ||
}) | ||
) | ||
``` | ||
// Use the functions like you normally would | ||
functions.getUsers().then(console.log) | ||
The index file that runs it. `onCommandComplete` is removed for brevity: | ||
```js | ||
const { call, buildFunctions } = require('effects-as-data') | ||
const { handlers } = require('effects-as-data-universal') | ||
const getPeople = require('./get-people') | ||
const functions = buildFunctions({}, handlers, { getPeople }) | ||
functions | ||
.getPeople(1, 2) | ||
.then(names => { | ||
console.log('Function Results:') | ||
console.log(names.join(', ')) | ||
}) | ||
.catch(console.error) | ||
``` |
@@ -5,26 +5,22 @@ const { isGenerator, toArray, toPromise } = require('./util') | ||
if (!fn) return Promise.reject(new Error('A function is required.')) | ||
const g = fn.apply(null, args) | ||
return run(config, handlers, g) | ||
const gen = fn.apply(null, args) | ||
const el = newExecutionLog() | ||
return run(config, handlers, gen, null, el) | ||
} | ||
function run(config, handlers, fn, input, el, generatorOperation = 'next') { | ||
function run(config, handlers, fn, input, el, genOperation = 'next') { | ||
try { | ||
const el1 = getExecutionLog(el) | ||
const { output, isList, done } = getNextOutput( | ||
fn, | ||
input, | ||
generatorOperation | ||
) | ||
const { output, done } = getNextOutput(fn, input, genOperation) | ||
if (done) return toPromise(output) | ||
const isList = Array.isArray(output) | ||
const commandsList = toArray(output) | ||
return processCommands(config, handlers, commandsList, el1) | ||
return processCommands(config, handlers, commandsList, el) | ||
.then(results => { | ||
const unwrappedResults = unwrapResults(isList, results) | ||
el1.step = el1.step + 1 // mutate in place | ||
return run(config, handlers, fn, unwrappedResults, el1) | ||
el.step++ | ||
return run(config, handlers, fn, unwrappedResults, el, 'next') | ||
}) | ||
.catch(e => { | ||
// ok to mutate? | ||
el1.step = el1.step + 1 // mutate in place | ||
return run(config, handlers, fn, e, el1, 'throw') | ||
el.step++ | ||
return run(config, handlers, fn, e, el, 'throw') | ||
}) | ||
@@ -36,8 +32,6 @@ } catch (e) { | ||
function getExecutionLog(el) { | ||
return ( | ||
el || { | ||
step: 0 | ||
} | ||
) | ||
function newExecutionLog() { | ||
return { | ||
step: 0 | ||
} | ||
} | ||
@@ -51,7 +45,3 @@ | ||
const { value: output, done } = fn[op](input) | ||
return { | ||
output, | ||
isList: Array.isArray(output), | ||
done | ||
} | ||
return { output, done } | ||
} | ||
@@ -73,3 +63,6 @@ | ||
try { | ||
result = handlers[command.type](command, { call, config, handlers }) | ||
const handler = handlers[command.type] | ||
if (!handler) | ||
throw new Error(`Handler of type "${command.type}" is not registered.`) | ||
result = handler(command, { call, config, handlers }) | ||
} catch (e) { | ||
@@ -76,0 +69,0 @@ result = Promise.reject(e) |
@@ -42,1 +42,14 @@ const { call } = require('../index') | ||
}) | ||
test('call should throw error for unregistered handler', async () => { | ||
const fn = function*() { | ||
yield { type: 'dne' } | ||
} | ||
try { | ||
await call({}, handlers, fn) | ||
} catch (e) { | ||
expect(e.message).toEqual('Handler of type "dne" is not registered.') | ||
return | ||
} | ||
fail('Did not throw.') | ||
}) |
const { call } = require('../index') | ||
const { handlers, functions } = require('./effects') | ||
const { handlers, functions, cmds } = require('./effects') | ||
const { | ||
@@ -42,1 +42,11 @@ badHandler, | ||
}) | ||
test('handlers should be able to return an array of results', async () => { | ||
const fn = function*(a, b) { | ||
const result = yield [cmds.echo(a), cmds.echo(b)] | ||
return result | ||
} | ||
const actual = await call({}, handlers, fn, ['foo', 'bar'], ['foo', 'baz']) | ||
const expected = [['foo', 'bar'], ['foo', 'baz']] | ||
expect(actual).toEqual(expected) | ||
}) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
182676
2950
276