effects-as-data
Advanced tools
Comparing version 2.14.0 to 2.14.1
{ | ||
"name": "effects-as-data", | ||
"version": "2.14.0", | ||
"version": "2.14.1", | ||
"description": | ||
@@ -5,0 +5,0 @@ "A micro abstraction layer for Javascript that makes writing, testing, and monitoring side-effects easy.", |
151
README.md
# Effects-as-data | ||
Effects-as-data is a micro abstraction layer for Javascript that makes writing, [testing](https://github.com/orourkedd/effects-as-data#second-test-your-business-logic), and [monitoring](https://github.com/orourkedd/effects-as-data#fifth-optionally-setting-up-monitoring--telemetry) side-effects easy. | ||
Effects-as-data is a micro abstraction layer for Javascript that makes writing, [testing](#testing), and [monitoring](#telemetry) side-effects easy. | ||
@@ -11,2 +11,3 @@ * Using effects-as-data can dramatically reduce the time it takes to deliver tested code. | ||
* Anywhere you can use promises, you can use effects-as-data. | ||
* The effects-as-data runtime is 100% stateless. | ||
@@ -19,2 +20,3 @@ ## Table of Contents | ||
* [Getting Starting Using Existing Commands and Handlers](#getting-starting-using-existing-commands-and-handlers) | ||
* [Calling an Effects-as-data Function](#calling-an-effects-as-data-function) | ||
* [Creating Your Own Commands and Handlers](#creating-your-own-commands-and-handlers) | ||
@@ -37,5 +39,5 @@ * [Error handling](#error-handling) | ||
In comes effects-as-data. Effects-as-data is a runtime that allows you to write pure functions that merely declare side effects (commands). Effects-as-data uses [generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators). | ||
In comes effects-as-data. Effects-as-data is a runtime that allows you to write pure functions that merely declare side effects (commands). The runtime will take care of handling the command. In effects-as-data, you write these pure functions using [generators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators). | ||
Why generators? A generator is like a "function with multiple returns". The power of a generator is that it can hold a lexical scope between these returns and normal control flow operators can be used. This gives us the power to write very complex side effect production operations as a pure function. | ||
Why generators? A generator is like a "function with multiple returns". The power of a generator is that it can hold a lexical scope between these returns and normal control flow operators can be used. This gives you the power to write very complex side effect production operations as a pure function. | ||
@@ -58,3 +60,3 @@ ### Think in terms of inputs and outputs only | ||
### 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 (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. This command represents the `data` in `effects-as-data`. [See Working Code](https://github.com/orourkedd/effects-as-data-examples/blob/master/basic/cmds.js) | ||
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`. This command represents the `data` in `effects-as-data`. [See Working Code](https://github.com/orourkedd/effects-as-data-examples/blob/master/basic/cmds.js) | ||
```js | ||
@@ -76,3 +78,3 @@ // cmds.js | ||
### 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. [See Working Code](https://github.com/orourkedd/effects-as-data-examples/blob/master/basic/functions.spec.js) | ||
Write a test for the `getPeople` function (that you are about to create). Effects-as-data tests are compatible with any test runner like Jest, Mocha, etc. For more on testing, see [Testing](#testing). | ||
@@ -102,3 +104,3 @@ Semantic test example: | ||
### 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. [See Working Code](https://github.com/orourkedd/effects-as-data-examples/blob/master/basic/functions.js) | ||
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](#first-create-a-command-creator)). This is one of the main reasons `effects-as-data` functions are easy to test. [See Working Code](https://github.com/orourkedd/effects-as-data-examples/blob/master/basic/functions.js) | ||
```js | ||
@@ -121,3 +123,3 @@ // functions.js | ||
### 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. The handler does the `effect` in `effects-as-data`. [See Working Code](https://github.com/orourkedd/effects-as-data-examples/blob/master/basic/handlers.js) | ||
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](#first-create-a-command-creator) that actually performs the HTTP GET request. Notice that the business logic does not call this function directly; the business logic in [Step 1](#first-create-a-command-creator) simply `yield`-s the `httpGet` `command` out, and `effects-as-data` takes care of getting it to the handler. The handler does the `effect` in `effects-as-data`. [See Working Code](https://github.com/orourkedd/effects-as-data-examples/blob/master/basic/handlers.js) | ||
```js | ||
@@ -137,3 +139,3 @@ // handlers.js | ||
### 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. | ||
The effects-as-data config accepts several lifecycle callbacks which will be called when a function is called and completes and when a command is called and 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 | ||
@@ -191,32 +193,19 @@ // Normally this will be in index.js (see below) | ||
See full example in the `effects-as-data-examples` repository: [https://github.com/orourkedd/effects-as-data-examples/blob/master/basic](https://github.com/orourkedd/effects-as-data-examples/blob/master/basic). | ||
See full example in the `effects-as-data-examples` repository: [effects-as-data-examples](https://github.com/orourkedd/effects-as-data-examples/blob/master/basic). | ||
## Getting Starting 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. | ||
This example demonstrates using the `effects-as-data-universal` module which contains commands/handlers that can be used anywhere Javascript runs. | ||
```js | ||
const { call, buildFunctions } = require('effects-as-data') | ||
const { testFn, args } = require('effects-as-data/test') | ||
const { buildFunctions } = require('effects-as-data') | ||
const { cmds, handlers } = require('effects-as-data-universal') | ||
function* getPeople() { | ||
const { payload } = yield cmds.httpGet('https://swapi.co/api/people') | ||
const names = payload.results.map(p => p.name) | ||
const { results } = yield cmds.httpGet('https://swapi.co/api/people') | ||
const names = results.map(p => p.name) | ||
return names | ||
} | ||
// Semantic test style | ||
testFn(getPeople, () => { | ||
const apiResults = { payload: { results: [{ name: 'Luke Skywalker' }] } } | ||
return args() | ||
.yieldCmd(cmds.httpGet('https://swapi.co/api/people')).yieldReturns(apiResults) | ||
.returns(['Luke Skywalker']) | ||
})() | ||
const config = { /* lifecycle callbacks, etc */ } | ||
const config = { | ||
onCommandComplete: telemetry => { | ||
console.log('Telemetry (from onCommandComplete):', telemetry) | ||
} | ||
} | ||
const functions = buildFunctions(config, handlers, { getPeople }) | ||
@@ -234,2 +223,30 @@ | ||
## Calling an Effects-as-data Function | ||
There are two ways to call an effects-as-data function. | ||
The first is to use `buildFunctions()` which will turn multiple effects-as-data functions in to normal, promise-returning functions. You can see an example of this in [Getting Starting Using Existing Commands and Handlers](#getting-starting-using-existing-commands-and-handlers). | ||
The second way is to use the call function: | ||
```js | ||
const { call } = require('effects-as-data') | ||
const { cmds, handlers } = require('effects-as-data-universal') | ||
function* getPeople() { | ||
const { results } = yield cmds.httpGet('https://swapi.co/api/people') | ||
const names = results.map(p => p.name) | ||
return names | ||
} | ||
const config = { /* lifecycle callbacks, etc */ } | ||
call(config, handlers, getPeople, /* arg1, arg2, etc */) | ||
.then(names => { | ||
console.log('\n') | ||
console.log('Function Results:') | ||
console.log(names.join(', ')) | ||
}) | ||
.catch(console.error) | ||
``` | ||
## Creating Your Own Commands and Handlers | ||
@@ -239,12 +256,59 @@ | ||
### Anatomy of a handler | ||
A handler is simply a function that receives a command object and does something with it. | ||
A few notes on handlers: | ||
1. Effects-as-data will handle errors from handlers so they should throw or reject when things go wrong. | ||
1. Handlers can return a promise, return a normal value (string, number, object, etc), return nothing, or throw an error. | ||
1. Handlers can call another effects-as-data functions (see below). | ||
#### Simple Handler | ||
```js | ||
function httpGet(cmd) { | ||
return fetch(cmd.url).then(r => r.json()); | ||
} | ||
``` | ||
#### Simple Handler Returning a Number | ||
```js | ||
// notice that the cmd is not even used | ||
function now(cmd) { | ||
return Date.now() | ||
} | ||
``` | ||
#### Handler that calls another effects-as-data function | ||
This example is taken from the [either](https://github.com/orourkedd/effects-as-data-universal/blob/master/src/handlers/either.js) hander in [effects-as-data-universal](https://github.com/orourkedd/effects-as-data-universal). | ||
Notice that all handlers receive an object as a second argument. This object has a reference to the [call](#calling-an-effects-as-data-function) function, the current effects-as-data config and handlers. | ||
```js | ||
// notice that the command has a cmd property | ||
function either({ cmd, defaultValue }, { call, config, handlers }) { | ||
return call(config, handlers, function*() { | ||
try { | ||
const result = yield cmd | ||
return result || defaultValue | ||
} catch (e) { | ||
return defaultValue | ||
} | ||
}) | ||
} | ||
``` | ||
## Error handling | ||
Below are various examples of error handling with effects-as-data. It is important to note that effects-as-data will catch any error thrown by your effects-as-data function or thrown by handler and will: | ||
Below are various examples of error handling with effects-as-data. It is important to note that effects-as-data will catch any error thrown by your effects-as-data function or thrown by a handler and will: | ||
1. Pass the error to the `onCommandComplete` or `onCallComplete` lifecycle callbacks. This means you don't have to do any logging in your business logic. | ||
1. Reject the promise created by effects-as-data around your running function and pass the error out. | ||
1. You don't have to write a test to verify that an error is handled ([unless you are doing something specific in a `catch` block](#using-trycatch-like-in-asyncawait)) | ||
By default errors should act just like they do in `async/await`. Things get fun, however, when you use command modifiers like [either](#using-cmdseither) or [retry](#using-cmdsretry). Using command modifiers can add sophisticated error handling to your code without adding complexity. Pro tip: Because command modifiers are not really a thing (I just call them that because they run other commands), they are all composable. | ||
Because the runtime handles errors for you, you don't have to write a test to verify that an error is handled ([unless you are doing something specific in a `catch` block](#using-trycatch-like-in-asyncawait)) | ||
By default errors should act just like they do in `async/await`. Things get fun, however, when you use command modifiers like [either](#using-cmdseither) or [retry](#using-cmdsretry). Using command modifiers can add sophisticated error handling to your code without adding complexity. | ||
NOTE: you can easily write your own command modifiers. Follow the example of `either` here: [either cmd](https://github.com/orourkedd/effects-as-data-universal/blob/master/src/cmds/either.js), [either handler](https://github.com/orourkedd/effects-as-data-universal/blob/master/src/handlers/either.js). | ||
@@ -296,3 +360,3 @@ | ||
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 `defaultResults`. Because the `either` handler will never throw an exception and will either return a successful result or `defaultResults`, 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. | ||
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 `defaultResults`. Because the `either` handler will never throw an exception and will either return a successful result or `defaultResults`, there is no need for an `if` or a `try/catch` statement to ensure success before the `map`. Using this pattern will reduce the number of code paths and simplify code. | ||
@@ -397,3 +461,3 @@ See Working Example: [https://github.com/orourkedd/effects-as-data-examples/tree/master/misc-examples). | ||
test( | ||
'getPeople should return an empty list if all retried fail', | ||
'getPeople should return an empty list if all retries fail', | ||
testGetPeople(() => { | ||
@@ -495,3 +559,4 @@ const apiResults = { results: [{ name: 'Luke Skywalker' }] } | ||
{config, handlers, fn: [Function: getPeople], args} | ||
] | ||
], | ||
// ... whatever other arbitrary values you put onto the effects-as-data config | ||
} | ||
@@ -532,3 +597,4 @@ } | ||
{config, handlers, fn: [Function: getPeople], args} | ||
] | ||
], | ||
// ... whatever other arbitrary values you put onto the effects-as-data config | ||
} | ||
@@ -571,3 +637,4 @@ } | ||
{config, handlers, fn: [Function: getPeople], args} | ||
] | ||
], | ||
// ... whatever other arbitrary values you put onto the effects-as-data config | ||
} | ||
@@ -617,3 +684,4 @@ } | ||
{config, handlers, fn: [Function: getPeople], args} | ||
] | ||
], | ||
// ... whatever other arbitrary values you put onto the effects-as-data config | ||
} | ||
@@ -626,3 +694,3 @@ } | ||
Testing in effects-as-data is really easy, even for complex asynchronous operations. This is because effects-as-data functions are pure functions and only output JSON objects. Effects-as-data tests don't make assertions; they simply declare a data-structure and the test runner validates that the inputs and outputs in the data structure match the inputs and outputs of the function. | ||
Testing in effects-as-data is easy, even for complex asynchronous operations. This is because effects-as-data functions are pure functions and only output JSON objects. Effects-as-data tests don't make assertions; they simply declare a data-structure and the test runner validates that the inputs and outputs in the data structure match the inputs and outputs of the function. | ||
@@ -653,5 +721,6 @@ Below are a few examples of testing with effects-as-data: | ||
it('should get a person return his/her name', testGetPerson(() => { | ||
const person = { name: 'C-3P0'} | ||
return args(2) | ||
.yieldCmd(cmds.httpGet(`https://swapi.co/api/people/2`)).yieldReturns({ name: 'C-3P0'}) | ||
.returns('C-3P0') | ||
.yieldCmd(cmds.httpGet(`https://swapi.co/api/people/2`)).yieldReturns(person) | ||
.returns(person.name) | ||
})) | ||
@@ -666,3 +735,3 @@ }) | ||
const cmds = require('effects-as-data-universal') | ||
const { postgres } = require('effects-as-data-postgres') | ||
const { postgres } = require('some-effects-as-data-postgres-module') | ||
@@ -680,3 +749,3 @@ function* getPeopleWithSameName(id) { | ||
const cmds = require('effects-as-data-universal') | ||
const { postgres } = require('effects-as-data-postgres') | ||
const { postgres } = require('some-effects-as-data-postgres-module') | ||
const getPeopleWithSameName = require('./get-person') | ||
@@ -683,0 +752,0 @@ |
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
259897
857