Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop'
Browse files Browse the repository at this point in the history
  • Loading branch information
maciejzasada committed Apr 23, 2017
2 parents 054ea7f + 22f9190 commit dd7dad9
Show file tree
Hide file tree
Showing 19 changed files with 434 additions and 31 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ script:
- npm test
after_script:
- npm run cover
- npm run report-coverage
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ A flow consists of three elements:

## I/O

`flowchat` provides [Observable](http://reactivex.io/documentation/observable.html) `input` and `output`, making no assumptions on where the conversation input comes from and where the output should go. Using [Observables](http://reactivex.io/documentation/observable.html) for input and output also allows for their easy and modular mapping.
`flowchat` provides [Subject](http://reactivex.io/documentation/subject.html) `input`, `output` and `state`, making no assumptions on where the conversation input comes from and where the output should go, as well as how you persist the conversation state. Using [Subject](http://reactivex.io/documentation/subject.html) for input and output also allows for their easy and modular mapping.

## The Gist

Expand Down Expand Up @@ -53,9 +53,12 @@ let sessionId = Math.random();

bot.flow('/hello', ...helloFlow);

bot.output.subscribe(({ data, state, sessionId }) => console.log(data));
bot.state.subscribe(newState => console.log('state:', newState));
bot.output.subscribe(({ data, sessionId }) => console.log('data:', data));

bot.input.onNext({ data: 'hello', state: { saidHello: false }, sessionId }); // logs "Hello, user!"
bot.input.onNext({ data: 'hello', state: { saidHello: false }, sessionId });
// logs "state: { saidHello: true }"
// logs "Hello, user!"

```

Expand Down
13 changes: 0 additions & 13 deletions lib/effects.js

This file was deleted.

3 changes: 3 additions & 0 deletions lib/effects/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './input';
export * from './run';
export * from './send';
5 changes: 5 additions & 0 deletions lib/effects/input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { put } from 'redux-saga/effects';

export const input = function* (data, state, sessionId) {
yield put({ type: 'input', data, state, sessionId });
}
5 changes: 5 additions & 0 deletions lib/effects/run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { put } from 'redux-saga/effects';

export const run = function* (flow, data, state, sessionId) {
yield put({ type: 'run', flow, data, state, sessionId });
}
5 changes: 5 additions & 0 deletions lib/effects/send.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { put } from 'redux-saga/effects';

export const send = function* (data, sessionId) {
yield put({ type: 'send', data, sessionId });
}
27 changes: 17 additions & 10 deletions lib/flowchat.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ export class Flowchat {
constructor() {
const self = this;

this.input = new Subject();
this.inputSubscription = null;
this.setInput(this.input);
this.setInput(new Subject());
this.state = new Subject();
this.output = new Subject();
this.activators = [];
this.paths = [];
Expand All @@ -31,8 +31,8 @@ export class Flowchat {
// Set up internal sagas.

// Send.
this.sendSaga = function* sendSaga({ data, state, sessionId }) {
self.send(data, state, sessionId);
this.sendSaga = function* sendSaga({ data, sessionId }) {
self.send(data, sessionId);
}
this.sendSagaRunner = function* sendSagaRunner() {
yield takeEvery('send', self.sendSaga);
Expand Down Expand Up @@ -60,15 +60,21 @@ export class Flowchat {

/* public */
setInput(input) {
if (this.inputSubscription) {
if (this.input) {
this.input.dispose();
this.inputSubscription.dispose();
}
this.inputSubscription = input.subscribe(this.receive.bind(this));
this.input = input;
this.inputSubscription = this.input.subscribe(this.receive.bind(this));
}

flow(flowPath, activator, reducer, saga) {
if (arguments.length !== 4) {
throw new Error('flow requires exactly 4 arguments');
}

if (Flowchat.RESTRICTED_FLOWS.indexOf(flowPath) !== -1) {
throw new Error(`Flow ${flowPath} is restricted for internal use.`);
throw new Error(`Flow ${flowPath} is restricted for internal use`);
}

this.paths.push(flowPath);
Expand All @@ -95,7 +101,7 @@ export class Flowchat {
}

// Activate flows.
const activationPromises = this.activators.map(activator => activator(data, state));
const activationPromises = this.activators.map(activator => activator(data, state, sessionId));
return Promise.all(activationPromises)
.then(toActivate => {
const activeIndices = toActivate
Expand All @@ -114,6 +120,7 @@ export class Flowchat {

run(flow, data, state, sessionId) {
const newState = this.reduce(flow, data, state, sessionId)
this.state.onNext(newState);
this.store.dispatch({ type: flow, data, state: newState, sessionId });
}

Expand All @@ -129,8 +136,8 @@ export class Flowchat {
return state;
}

send(data, state, sessionId) {
this.output.onNext({ data, state, sessionId });
send(data, sessionId) {
this.output.onNext({ data, sessionId });
}

}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@
"scripts": {
"prepublish": "npm run compile",
"test": "npm run compile && babel-node --plugins transform-es2015-arrow-functions node_modules/.bin/tape test/*.js",
"compile": "babel --out-dir dist lib/**.js",
"compile": "rm -rf dist && babel --out-dir dist lib/**.js lib/**/*.js",
"lint": "eslint lib",
"cover": "npm run compile && babel-node --plugins transform-es2015-arrow-functions node_modules/.bin/istanbul cover node_modules/.bin/tape -- test/*.js && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
"cover": "npm run compile && babel-node --plugins transform-es2015-arrow-functions node_modules/.bin/istanbul cover node_modules/.bin/tape -- test/*.js",
"report-coverage": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js",
"example:cli": "npm run compile && babel-node --plugins transform-es2015-arrow-functions node_modules/.bin/tape examples/cli"
},
"main": "./dist/lib"
Expand Down
112 changes: 112 additions & 0 deletions test/test-activator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
'use strict';

import Test from 'tape';

import { Flowchat } from '../dist/lib';

Test('test activator', (t) => {

t.test('activator receives input, state and sessionId', (t) => {
t.plan(3);
const mockInput = Math.random();
const mockState = {rand: Math.random()};
const mockSessionId = Math.random();
const activator = (input, state, sessionId) => {
t.equal(mockInput, input);
t.deepEqual(mockState, state);
t.equal(mockSessionId, sessionId);
}
const reducer = (input, state) => state;
const saga = function* () {};
const flow = [activator, reducer, saga];
const bot = new Flowchat();
bot.flow('/test', ...flow);
bot.input.onNext({ data: mockInput, state: mockState, sessionId: mockSessionId });
});

t.test('send, input and run are restricted flow names', (t) => {
t.plan(3);
const bot = new Flowchat();
const activator = () => true;
const reducer = (input, state) => state;
const saga = function* () {};
const mockFlow = [activator, reducer, saga];
const throw1 = () => bot.flow('send', ...mockFlow);
const throw2 = () => bot.flow('input', ...mockFlow);
const throw3 = () => bot.flow('run', ...mockFlow);
t.throws(throw1);
t.throws(throw2);
t.throws(throw3);
});

t.test('flows run when activator returns true', (t) => {
t.plan(1);
const bot = new Flowchat();
const activator = () => true;
const reducer = (input, state) => state;
const saga = function* () {
t.ok(true);
};
const flow = [activator, reducer, saga];
bot.flow('/test', ...flow);
bot.input.onNext({ data: 'test', state: {}, sessionId: 1 });
});

t.test('flows do not run when activator returns false', (t) => {
t.plan(1);
const bot = new Flowchat();
const activator = () => false;
const reducer = (input, state) => state;
const saga = function* () {
t.ok(false);
};
const flow = [activator, reducer, saga];
bot.flow('/test', ...flow);
bot.input.onNext({ data: 'test', state: {}, sessionId: 1 });
setTimeout(() => {
t.ok(true);
}, 10);
});

t.test('flows run when activator returns a promise that resolves to true', (t) => {
t.plan(1);
const bot = new Flowchat();
const activator = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve(true);
}, 10);
});
}
const reducer = (input, state) => state;
const saga = function* () {
t.ok(true);
};
const flow = [activator, reducer, saga];
bot.flow('/test', ...flow);
bot.input.onNext({ data: 'test', state: {}, sessionId: 1 });
});

t.test('flows do not run when activator returns a promise that resolves to false', (t) => {
t.plan(1);
const bot = new Flowchat();
const activator = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve(false);
}, 10);
});
}
const reducer = (input, state) => state;
const saga = function* () {
t.ok(false);
};
const flow = [activator, reducer, saga];
bot.flow('/test', ...flow);
bot.input.onNext({ data: 'test', state: {}, sessionId: 1 });
setTimeout(() => {
t.ok(true);
}, 20);
});

});
32 changes: 32 additions & 0 deletions test/test-effect-input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use strict';

import Test from 'tape';

import { Flowchat, input as inputEffect } from '../dist/lib';

Test('test input effect', (t) => {

t.test('input effect emulates new chat bot input', (t) => {
t.plan(1);
const bot = new Flowchat();

const activator1 = (input) => input === 'one';
const reducer1 = (input, state) => state;
const saga1 = function* (input, state, sessionId) {
yield inputEffect('two', state, sessionId);
}
const flow1 = [activator1, reducer1, saga1];
bot.flow('/one', ...flow1);

const activator2 = (input) => input === 'two';
const reducer2 = (input, state) => state;
const saga2 = function* () {
t.ok(true);
}
const flow2 = [activator2, reducer2, saga2];
bot.flow('/two', ...flow2);

bot.input.onNext({ data: 'one', state: {}, sessionId: 1 });
});

});
37 changes: 37 additions & 0 deletions test/test-effect-run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use strict';

import Test from 'tape';

import { Flowchat, run } from '../dist/lib';

Test('test run effect', (t) => {

t.test('run effect unconditionally runs another flow, skipping the activator', (t) => {
t.plan(2);
const bot = new Flowchat();
let reducer2Run = false;

const activator1 = (input) => input === 'one';
const reducer1 = (input, state) => state;
const saga1 = function* (input, state, sessionId) {
yield run('/two', 'test', state, sessionId);
}
const flow1 = [activator1, reducer1, saga1];
bot.flow('/one', ...flow1);

const activator2 = () => {
t.ok(!reducer2Run); // this activator should only be run once, at the initial input
reducer2Run = true;
return false;
}
const reducer2 = (input, state) => state;
const saga2 = function* () {
t.ok(true);
}
const flow2 = [activator2, reducer2, saga2];
bot.flow('/two', ...flow2);

bot.input.onNext({ data: 'one', state: {}, sessionId: 1 });
});

});
28 changes: 28 additions & 0 deletions test/test-effect-send.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';

import Test from 'tape';

import { Flowchat, send } from '../dist/lib';

Test('test output', (t) => {

t.test('send effect sends output', (t) => {
t.plan(2);
const bot = new Flowchat();
const mockOutput = {this: {is: 'a test'}};
const mockSessionId = Math.random();
const activator = () => true;
const reducer = (input, state) => state;
const saga = function* (input, state, sessionId) {
yield send(mockOutput, sessionId);
}
const flow = [activator, reducer, saga];
bot.flow('/test', ...flow);
bot.output.subscribe(({ data, sessionId }) => {
t.deepEqual(data, mockOutput);
t.equal(sessionId, mockSessionId);
});
bot.input.onNext({ data: 'test', state: {}, sessionId: mockSessionId });
});

});
21 changes: 21 additions & 0 deletions test/test-flow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict';

import Test from 'tape';

import { Flowchat } from '../dist/lib';

Test('test flow', (t) => {

t.test('flows run in order, one after another', (t) => {
t.plan(1);
// TODO: implement
t.ok(true);
});

t.test('next flow reducer receives state updated by previous reducer', (t) => {
t.plan(1);
// TODO: implement
t.ok(true);
});

});
Loading

0 comments on commit dd7dad9

Please sign in to comment.