Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve multi-executors RuleTypes flow #26

Merged
merged 8 commits into from
Oct 25, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/yve-bot.core.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion lib/yve-bot.ui.js

Large diffs are not rendered by default.

39 changes: 36 additions & 3 deletions src/core/__tests__/bot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,13 +372,46 @@ test('ruleTypes with multi executors', async () => {
bot.on('end', onEnd).start();
await sleep();
expect(bot.store.get('executors.testStep.currentIdx')).toEqual(undefined);
bot.hear('answer');
await sleep();
expect(bot.store.get('executors.testStep.currentIdx')).toEqual(undefined);
expect(bot.store.get('output.testStep')).toEqual('answer transformed transformed2');
expect(onEnd).toHaveBeenCalledTimes(1);
});

test('ruleTypes with multi executors and waitForUserInput', async () => {
const rules = loadYaml(`
- message: Hello
name: testStep2
type: MultiStep2
`);
const bot = new YveBot(rules, OPTS);
const onEnd = jest.fn();
bot.types.define('MultiStep2', {
executors: [
{
transform: async (answer) => `${answer} transformed`,
},
{
transform: async (answer) => `${answer} transformed2`,
},
bot.executors.WaitForUserInput,
{
transform: async (answer) => `${answer} transformed3`,
}
]
});

bot.on('end', onEnd).start();
await sleep();
expect(bot.store.get('executors.testStep2.currentIdx')).toEqual(undefined);
bot.hear('first answer');
await sleep();
expect(bot.store.get('executors.testStep.currentIdx')).toEqual(1);
expect(bot.store.get('executors.testStep2.currentIdx')).toEqual(3);
bot.hear('second answer');
await sleep();
expect(bot.store.get('executors.testStep.currentIdx')).toEqual(undefined);
expect(bot.store.get('output.testStep')).toEqual('second answer transformed2');
expect(bot.store.get('executors.testStep2.currentIdx')).toEqual(undefined);
expect(bot.store.get('output.testStep2')).toEqual('second answer transformed3');
expect(onEnd).toHaveBeenCalledTimes(1);
});

Expand Down
10 changes: 10 additions & 0 deletions src/core/bot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ import { Store, StoreData } from './store';
import { Controller } from './controller';
import { Actions } from './actions';
import { Types } from './types';
import { Executors } from './executors';
import { Validators } from './validators';

import * as Exceptions from './exceptions';

function sanitizeRule(rule: Rule): Rule {
if (typeof rule === 'string') {
return { message: rule };
Expand All @@ -23,6 +26,8 @@ function sanitizeRule(rule: Rule): Rule {
}

export class YveBot {
static exceptions: any;

private handlers: { [handler: string]: Array<() => any> };
private _rules?: Rule[];

Expand All @@ -34,6 +39,7 @@ export class YveBot {

public types: Types;
public actions: Actions;
public executors: Executors;
public validators: Validators;

constructor(rules: Rule[], customOpts?: YveBotOptions) {
Expand Down Expand Up @@ -120,4 +126,8 @@ export class YveBot {

YveBot.prototype.types = new Types;
YveBot.prototype.actions = new Actions;
YveBot.prototype.executors = new Executors;
YveBot.prototype.validators = new Validators;


YveBot.exceptions = Exceptions;
65 changes: 45 additions & 20 deletions src/core/controller.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { Rule, RuleNext, Answer } from '../types';
import { YveBot } from './bot';
import { ValidatorError, InvalidAttributeError, RuleNotFound } from './exceptions';
import {
InvalidAttributeError,
PauseRuleTypeExecutors,
RuleNotFound,
ValidatorError,
} from './exceptions';
import * as utils from './utils';


async function validateAnswer(
answers: Answer | Answer[],
rule: Rule,
Expand Down Expand Up @@ -190,12 +196,36 @@ export class Controller {
return this.bot.store.get(`executors.${rule.name}.currentIdx`) || 0;
}

setRuleExecutorIndex(rule: Rule, value?: any): void {
if (value !== undefined) {
this.bot.store.set(`executors.${rule.name}.currentIdx`, value);
} else {
this.bot.store.unset(`executors.${rule.name}.currentIdx`);
incRuleExecutorIndex(rule: Rule): void {
this.bot.store.set(
`executors.${rule.name}.currentIdx`, this.getRuleExecutorIndex(rule) + 1
);
}

resetRuleExecutorIndex(rule: Rule): void {
this.bot.store.unset(`executors.${rule.name}.currentIdx`);
}

async executeRuleTypeExecutors(rule: Rule, lastAnswer: Answer | Answer[]): Promise<Answer | Answer[]> {
const { bot } = this;
const executorIdx = this.getRuleExecutorIndex(rule);
const executors = bot.types[rule.type].executors;

const executor = executors.slice(executorIdx)[0] || {};
const { transform = (...args) => Promise.resolve(args[0]) } = executor;
const answer = await (
transform(lastAnswer, rule, bot)
.then(answer => validateAnswer(answer, rule, bot, this.getRuleExecutorIndex(rule)))
);

const completed = (this.getRuleExecutorIndex(rule) === executors.length-1);
if (!completed) {
this.incRuleExecutorIndex(rule);
return await this.executeRuleTypeExecutors(rule, answer);
}

this.resetRuleExecutorIndex(rule);
return answer;
}

async receiveMessage(message: Answer | Answer[]): Promise<this> {
Expand All @@ -208,24 +238,19 @@ export class Controller {
}

let answer;
const { executors } = bot.types[rule.type];
const executor = executors[this.getRuleExecutorIndex(rule)] || {};
const { transform = (...args) => Promise.resolve(args[0]) } = executor;
try {
answer = await transform(message, rule, bot).then(answer => validateAnswer(
answer, rule, bot, this.getRuleExecutorIndex(rule)
));

if ( this.getRuleExecutorIndex(rule) < executors.length-1 ) {
this.setRuleExecutorIndex(rule, this.getRuleExecutorIndex(rule)+1);
bot.dispatch('hear');
return this;
}

this.setRuleExecutorIndex(rule);
answer = await this.executeRuleTypeExecutors(rule, message);
} catch (e) {
let expectedError = false;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[minor] renames to expectedReply

if (e instanceof ValidatorError) {
expectedError = true;
await this.sendMessage(e.message, rule);
} if (e instanceof PauseRuleTypeExecutors) {
expectedError = true;
this.incRuleExecutorIndex(rule);
}

if (expectedError) {
bot.dispatch('hear');
return this;
}
Expand Down
5 changes: 5 additions & 0 deletions src/core/exceptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ export function RuleNotFound(name: string, indexes: { [k: string]: number }) {
this.message = `Rule "${name}" not found in indexes`;
this.indexes = indexes;
}

export function PauseRuleTypeExecutors(name: string) {
this.key = 'pauseRuleTypeExecutors';
this.message = `Rule "${name}" must pause execution`;
}
23 changes: 23 additions & 0 deletions src/core/executors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { DefineModule } from './module';
import { PauseRuleTypeExecutors } from './exceptions';
import { Rule, Answer, RuleTypeExecutor } from '../types';


const executors: { [name: string]: RuleTypeExecutor } = {
WaitForUserInput: {
validators: [{
function: (_: Answer, rule: Rule): void => {
throw new PauseRuleTypeExecutors(rule.name);
}
}],
}
};

export class Executors extends DefineModule {
public WaitForUserInput: RuleTypeExecutor;

constructor() {
super();
this.define(executors);
}
}
5 changes: 3 additions & 2 deletions src/core/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { uniq } from 'lodash';
import { DefineModule } from './module';
import { Rule, Answer } from '../types';
import { Rule, Answer, RuleType } from '../types';
import * as utils from './utils';

const types = {

const types: { [name: string]: RuleType } = {
Any: { executors: [{}] },

String: {
Expand Down
15 changes: 15 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import { YveBot } from './core/bot';

export interface YveBotOptions {
enableWaitForSleep?: boolean;
rule?: Rule;
}

export interface RuleType {
executors: RuleTypeExecutor[];
}

export interface RuleTypeExecutor {
validators?: RuleValidator[];
transform?: RuleTypeTransform;
}

export type RuleTypeTransform = (
(value: Answer | Answer[], rule?: Rule, bot?: YveBot) => Promise<Answer | Answer[]>
);

export interface Rule {
name?: string;
type?: string;
Expand Down