diff --git a/README.md b/README.md index 0eae890..3a5cc51 100644 --- a/README.md +++ b/README.md @@ -326,6 +326,8 @@ Feature of Alexia that helps you to control flow of the intents. To understand i By defining the action you enable transition from one intent to another. When no actions are specified, every intent transition is allowed. +Action properties `from` and `to` can be defined as `string` (one intent), `array` (multiple intents) or `'*'` (all intents). + Each action could have condition to check whether the transition should be handled or the fail method should be invoked. If no fail method is defined `app.defaultActionFail()` is invoked when condition of handling is not met or the action (transition) is not defined. ```javascript @@ -335,7 +337,7 @@ app.action({ to: 'intent1' }); -// Allow transition from start intent to `intent2`. +// Allow transition from `@start` intent to `intent2`. app.action({ from: '@start', to: 'intent2' @@ -349,6 +351,12 @@ app.action({ fail: (slots, attrs) => 'Sorry, your pin is invalid' }); +// Allow transition from `intent2` to `intent3` and also `intent4`. +app.action({ + from: 'intent2', + to: ['intent3', 'intent4'] +}); + // Set default fail handler app.defaultActionFail(() => 'Sorry, your request is invalid'); ``` diff --git a/src/create-app.js b/src/create-app.js index b05080d..95f063f 100644 --- a/src/create-app.js +++ b/src/create-app.js @@ -129,17 +129,30 @@ module.exports = (name, options) => { * Creates action * @param {string} action - Action object * @param {string} action.from - Name of the intent to allow transition from - * @param {string} action.to - Name of th eintent to allow transition to + * @param {string} action.to - Name of the intent to allow transition to * @param {function} action.if - Function returning boolean whether this transition should be handled. * @param {function} action.fail - Handler to be called if `action.if` returned `false` */ app.action = (action) => { - app.actions.push({ - from: typeof (action.from) === 'string' ? action.from : action.from.name, - to: typeof (action.to) === 'string' ? action.to : action.to.name, - if: action.if, - fail: action.fail - }); + if (Array.isArray(action.from)) { + action.from.forEach(fromItem => { + if (Array.isArray(action.to)) { + action.to.forEach(toItem => { + addAction(app.actions, fromItem, toItem, action.if, action.fail); + }); + } else { + addAction(app.actions, fromItem, action.to, action.if, action.fail); + } + }); + } else { + if (Array.isArray(action.to)) { + action.to.forEach(toItem => { + addAction(app.actions, action.from, toItem, action.if, action.fail); + }); + } else { + addAction(app.actions, action.from, action.to, action.if, action.fail); + } + } }; /** @@ -191,3 +204,20 @@ module.exports = (name, options) => { return app; }; + +/** + * Adds action to app's actions array + * @param {object} actions - Actions object of alexia app + * @param {string} from - Name of the intent to allow transition from + * @param {string} to - Name of the intent to allow transition to + * @param {function} condition - Function returning boolean whether this transition should be handled + * @param {function} fail - Handler to be called if `condition` returned `false` + */ +const addAction = (actions, from, to, condition, fail) => { + actions.push({ + from: typeof (from) === 'string' ? from : from.name, + to: typeof (to) === 'string' ? to : to.name, + if: condition, + fail: fail + }); +}; diff --git a/src/handle-request.js b/src/handle-request.js index 2d39d72..43f7761 100644 --- a/src/handle-request.js +++ b/src/handle-request.js @@ -84,7 +84,7 @@ const callHandler = (handler, slots, attrs, app, data, done) => { }; /** - * Checks for `actions` presence to help us with Alexa coznversation workflow configuration + * Checks for `actions` presence to help us with Alexa conversation workflow configuration * * 1) no actions: just call the intent.handler method without any checks * 2) with actions: check if action for current intent transition is found @@ -165,7 +165,7 @@ const createCardObject = (card) => { */ const getShouldEndSession = (intentOptions, appOptions) => { if (!intentOptions || intentOptions.end === undefined) { - if(!appOptions || appOptions.shouldEndSessionByDefault === undefined){ + if (!appOptions || appOptions.shouldEndSessionByDefault === undefined) { return true; } else { return appOptions.shouldEndSessionByDefault; diff --git a/test/contacts.spec.js b/test/contacts.spec.js new file mode 100644 index 0000000..c039cb7 --- /dev/null +++ b/test/contacts.spec.js @@ -0,0 +1,36 @@ +'use strict'; +const expect = require('chai').expect; +const app = require('./test-apps/contacts-app'); +const alexia = require('..'); + +describe('action app handler', () => { + + it('should handle OpenContactList -> NewContact and return correct outputSpeech', () => { + const request = alexia.createIntentRequest('NewContact', null, {previousIntent: 'OpenContactList'}); + app.handle(request, (response) => { + expect(response.response.outputSpeech.text).to.equal('Please insert value for name or phone number of contact.'); + }); + }); + + it('should handle NewContact -> SetName and return correct outputSpeech', () => { + const request = alexia.createIntentRequest('SetName', null, {previousIntent: 'NewContact'}); + app.handle(request, (response) => { + expect(response.response.outputSpeech.text).to.equal('Name was saved.'); + }); + }); + + it('should handle SetName -> CloseContactList and return correct outputSpeech', () => { + const request = alexia.createIntentRequest('CloseContactList', null, {previousIntent: 'SetName'}); + app.handle(request, (response) => { + expect(response.response.outputSpeech.text).to.equal('See you next time.'); + }); + }); + + it('should not handle OpenContactList -> SetName and return correct outputSpeech', () => { + const request = alexia.createIntentRequest('SetName', null, {previousIntent: 'OpenContactList'}); + app.handle(request, (response) => { + expect(response.response.outputSpeech.text).to.equal('Sorry, your command is invalid'); + }); + }); + +}); diff --git a/test/test-apps/contacts-app.js b/test/test-apps/contacts-app.js new file mode 100644 index 0000000..5d4b61d --- /dev/null +++ b/test/test-apps/contacts-app.js @@ -0,0 +1,45 @@ +// short app for testing action's transitions saved in arrays +'use strict'; +const alexia = require('../..'); +const app = alexia.createApp('ContactsApp'); + +app.intent('OpenContactList', 'Open contact list', () => { + return 'You can list your contacts, add new one or change already saved ones.'; +}); + +app.intent('NewContact', 'Create new contact', () => { + return 'Please insert value for name or phone number of contact.'; +}); + +app.intent('ChangeContact', 'Change name for Glogo contact', () => { + return 'Please insert new value for name or phone number.'; +}); + +app.intent('SetName', 'Set name to Misyak', () => { + return 'Name was saved.'; +}); + +app.intent('SetNumber', 'Set number to {number:NUMBER}', () => { + return 'Number was saved.'; +}); + +app.intent('CloseContactList', 'Close contact list', () => { + return 'See you next time.'; +}); + +app.action({ + from: 'OpenContactList', + to: ['NewContact', 'ChangeContact'] +}); + +app.action({ + from: ['NewContact', 'ChangeContact'], + to: ['SetName', 'SetNumber'] +}); + +app.action({ + from: ['NewContact', 'ChangeContact', 'SetName', 'SetNumber'], + to: 'CloseContactList' +}); + +module.exports = app;