diff --git a/eg/reply-async/README.md b/eg/reply-async/README.md
new file mode 100644
index 0000000..12eb158
--- /dev/null
+++ b/eg/reply-async/README.md
@@ -0,0 +1,47 @@
+# Reply-async
+
+This example demonstrates using replyAsync function. You should use **replyAsync** instead of **reply** if you have subroutines that return promises.
+
+## Running the example
+
+```bash
+npm install && node weatherman.js
+```
+
+Refer to weatherman.rive for the list of supported commmands
+
+## Using async subroutines
+
+Whenever you have a subroutine that needs to call some sort of asynchronous function in order to return a value back to the script, you should use promises:
+
+```javascript
+var rs = new RiveScript();
+rs.setSubroutine("asyncHelper", function(rs, args) {
+ // Rivescript comes bundled with RSVP.js which you can
+ // access through RiveScript.Promise alias
+ // but you are free to use your own Promise implementation
+ return new rs.Promise(function(resolve, reject) {
+ resolve('hello there');
+ });
+})
+```
+
+Async reponses in Rive come in 2 flavors:
+
+```javascript
+// using promises
+rs.replyAsync(username, message).then(function(reply) {
+ // good to go
+}).catch(function(error){
+ // something went wrong
+});
+
+// or using callbacks
+rs.replyAsync(username, message, this, function(error, reply) {
+ if (!error) {
+ // you can use reply here
+ } else {
+ // something went wrong, error has more info
+ }
+});
+```
\ No newline at end of file
diff --git a/eg/reply-async/package.json b/eg/reply-async/package.json
new file mode 100644
index 0000000..c8655d8
--- /dev/null
+++ b/eg/reply-async/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "rive-weatherman",
+ "version": "1.0.0",
+ "description": "Your weather report",
+ "main": "weatherman.js",
+ "dependencies": {
+ "colors": "^1.1.2",
+ "request": "^2.69.0"
+ }
+}
diff --git a/eg/reply-async/weatherman.js b/eg/reply-async/weatherman.js
new file mode 100644
index 0000000..aa4ebba
--- /dev/null
+++ b/eg/reply-async/weatherman.js
@@ -0,0 +1,125 @@
+// Asynchronous Objects Example
+// See the accompanying README.md for details.
+
+// Run this demo: `node weatherman.js`
+
+var readline = require("readline");
+var request = require("request");
+var colors = require('colors');
+
+// This would just be require("rivescript") if not for running this
+// example from within the RiveScript project.
+var RiveScript = require("../../lib/rivescript");
+var rs = new RiveScript();
+
+var getWeather = function(location, callback) {
+ request.get({
+ url: "http://api.openweathermap.org/data/2.5/weather",
+ qs: {
+ q: location,
+ APPID: "6460241df9136925432064ac70416d05"
+ },
+ json: true
+ }, function(error, response) {
+ if (response.statusCode !== 200) {
+ callback.call(this, response.body);
+ } else {
+ callback.call(this, null, response.body);
+ }
+ });
+};
+
+
+rs.setSubroutine("getWeather", function (rs, args) {
+ return new rs.Promise(function(resolve, reject) {
+ getWeather(args.join(' '), function(error, data){
+ if(error) {
+ reject(error);
+ } else {
+ resolve(data.weather[0].description);
+ }
+ });
+ });
+});
+
+rs.setSubroutine("checkForRain", function(rs, args) {
+ return new rs.Promise(function(resolve, reject) {
+ getWeather(args.join(' '), function(error, data){
+ if(error) {
+ console.error('');
+ reject(error);
+ } else {
+ var rainStatus = data.rain ? 'yup :(' : 'nope';
+ resolve(rainStatus);
+ }
+ });
+ });
+});
+
+// Create a prototypical class for our own chatbot.
+var AsyncBot = function(onReady) {
+ var self = this;
+
+ // Load the replies and process them.
+ rs.loadFile("weatherman.rive", function() {
+ rs.sortReplies();
+ onReady();
+ });
+
+ // This is a function for delivering the message to a user. Its actual
+ // implementation could vary; for example if you were writing an IRC chatbot
+ // this message could deliver a private message to a target username.
+ self.sendMessage = function(username, message) {
+ // This just logs it to the console like "[Bot] @username: message"
+ console.log(
+ ["[Brick Tamland]", message].join(": ").underline.green
+ );
+ };
+
+ // This is a function for a user requesting a reply. It just proxies through
+ // to RiveScript.
+ self.getReply = function(username, message, callback) {
+ return rs.replyAsync(username, message, self).then(function(reply){
+ callback.call(this, null, reply);
+ }).catch(function(error) {
+ callback.call(this, error);
+ });
+ }
+};
+
+// Create and run the example bot.
+var bot = new AsyncBot(function() {
+ // Drop into an interactive shell to get replies from the user.
+ // If this were something like an IRC bot, it would have a message
+ // handler from the server for when a user sends a private message
+ // to the bot's nick.
+ var rl = readline.createInterface({
+ input: process.stdin,
+ output: process.stdout
+ });
+
+ rl.setPrompt("> ");
+ rl.prompt();
+ rl.on("line", function(cmd) {
+ // If this was an IRC bot, imagine "nick" came from the server as the
+ // sending user's IRC nickname.
+ nick = "Soandso";
+ console.log("[" + nick + "] " + cmd);
+
+ // Handle commands.
+ if (cmd === "/quit") {
+ process.exit(0);
+ } else {
+ reply = bot.getReply(nick, cmd, function(error, reply){
+ if (error) {
+ bot.sendMessage(nick, "Oops. The weather service is not cooperating!");
+ } else {
+ bot.sendMessage(nick, reply);
+ }
+ rl.prompt();
+ });
+ }
+ }).on("close", function() {
+ process.exit(0);
+ });
+});
diff --git a/eg/reply-async/weatherman.rive b/eg/reply-async/weatherman.rive
new file mode 100644
index 0000000..9fcbea7
--- /dev/null
+++ b/eg/reply-async/weatherman.rive
@@ -0,0 +1,10 @@
+! sub how's = how is
+! sub is it going to = will it
+! sub is it gonna = will it
+! sub is it raining = will it rain
+
++ how is [the] weather in *
+- getWeather
+
++ will it rain in *
+- checkForRain
\ No newline at end of file
diff --git a/package.json b/package.json
index 38ac98b..6f3e4af 100644
--- a/package.json
+++ b/package.json
@@ -25,10 +25,9 @@
"lib/lang/javascript.js"
],
"directories": {
- "lib": "lib",
- "doc": "docs"
+ "lib": "lib",
+ "doc": "docs"
},
- "main": "lib/rivescript.js",
"repository": {
"type": "git",
"url": "https://github.com/aichaos/rivescript-js.git"
@@ -67,6 +66,8 @@
"scripts": {
"prepublish": "grunt dist",
"test": "grunt test"
+ },
+ "dependencies": {
+ "rsvp": "^3.1.0"
}
-
}
diff --git a/src/brain.coffee b/src/brain.coffee
index 49ece6c..403e6a9 100644
--- a/src/brain.coffee
+++ b/src/brain.coffee
@@ -9,6 +9,7 @@
# Brain logic for RiveScript
utils = require("./utils")
inherit_utils = require("./inheritance")
+RSVP = require("rsvp")
##
# Brain (RiveScript master)
@@ -36,7 +37,7 @@ class Brain
#
# Fetch a reply for the user.
##
- reply: (user, msg, scope) ->
+ reply: (user, msg, scope, async) ->
@say "Asked to reply to [#{user}] #{msg}"
# Store the current user's ID.
@@ -60,6 +61,17 @@ class Brain
else
reply = @_getReply(user, msg, "normal", 0, scope)
+ reply = @processCallTags(reply, scope, async)
+
+ if not utils.isAPromise(reply)
+ @onAfterReply(msg, user, reply)
+ else
+ reply.then (result) =>
+ @onAfterReply(msg, user, result)
+
+ return reply
+
+ onAfterReply: (msg, user, reply) ->
# Save their reply history
@master._users[user].__history__.input.pop()
@master._users[user].__history__.input.unshift(msg)
@@ -69,7 +81,88 @@ class Brain
# Unset the current user ID.
@_currentUser = undefined
- return reply
+ ##
+ # string|Promise processCallTags (string reply, Object scope, bool async)
+ #
+ # Process tags in the preprocessed reply string.
+ # If async is true, processCallTags can handle asynchronous subroutines
+ # and it returns a promise, other wise a string is returned
+ ##
+ processCallTags: (reply, scope, async) ->
+ reply = reply.replace(/\{__call__\}/g, "")
+ reply = reply.replace(/\{\/__call__\}/g, "")
+ callRe = /(.+?)<\/call>/ig
+
+ giveup = 0
+ matches = {}
+ promises = []
+
+ while true
+ giveup++
+ if giveup >= 50
+ @warn "Infinite loop looking for call tag!"
+ break
+
+ match = callRe.exec(reply)
+
+ if not match
+ break
+
+ text = utils.strip(match[1])
+ parts = text.split(/\s+/)
+ obj = parts.shift()
+ args = parts
+
+ matches[match[1]] =
+ text: text
+ obj: obj
+ args: args
+
+ # go through all the object calls and run functions
+ for k,data of matches
+ output = ""
+ if @master._objlangs[data.obj]
+ # We do. Do we have a handler for it?
+ lang = @master._objlangs[data.obj]
+ if @master._handlers[lang]
+ # We do.
+ output = @master._handlers[lang].call(@master, data.obj, data.args, scope)
+ else
+ output = "[ERR: No Object Handler]"
+ else
+ output = "[ERR: Object Not Found]"
+
+ # if we get a promise back and we are not in the async mode,
+ # leave an error message to suggest using an async version of rs
+ # otherwise, keep promises tucked into a list where we can check on
+ # them later
+ if utils.isAPromise(output)
+ if async
+ promises.push
+ promise: output
+ text: k
+ continue
+ else
+ output = "[ERR: Using async routine with reply: use replyAsync instead]"
+
+ reply = @._replaceCallTags(k, output, reply)
+
+ if not async
+ return reply
+ else
+ # wait for all the promises to be resolved and
+ # return a resulting promise with the final reply
+ return new RSVP.Promise (resolve, reject) =>
+ RSVP.all(p.promise for p in promises).then (results) =>
+ for i in [0...results.length]
+ reply = @_replaceCallTags(promises[i].text, results[i], reply)
+
+ resolve(reply)
+ .catch (reason) =>
+ reject(reason)
+
+ _replaceCallTags: (callSignature, callResult, reply) ->
+ return reply.replace(new RegExp("" + utils.quotemeta(callSignature) + "", "i"), callResult)
##
# string _getReply (string user, string msg, string context, int step, scope)
@@ -544,6 +637,8 @@ class Brain
# string[] botstars, int step, scope)
#
# Process tags in a reply element.
+ # XX: All the tags get processed here except for tags that have
+ # a separate subroutine (refer to processCallTags for more info)
##
processTags: (user, msg, reply, st, bst, step, scope) ->
# Prepare the stars and botstars.
@@ -746,38 +841,6 @@ class Brain
reply = reply.replace(new RegExp("\\{@" + utils.quotemeta(target) + "\\}", "i"), subreply)
match = reply.match(/\{@(.+?)\}/)
- # Object caller
- reply = reply.replace(/\{__call__\}/g, "")
- reply = reply.replace(/\{\/__call__\}/g, "")
- match = reply.match(/(.+?)<\/call>/i)
- giveup = 0
- while match
- giveup++
- if giveup >= 50
- @warn "Infinite loop looking for call tag!"
- break
-
- text = utils.strip(match[1])
- parts = text.split(/\s+/)
- obj = parts.shift()
- args = parts
-
- # Do we know this object?
- output = ""
- if @master._objlangs[obj]
- # We do. Do we have a handler for it?
- lang = @master._objlangs[obj]
- if @master._handlers[lang]
- # We do.
- output = @master._handlers[lang].call(@master, obj, args, scope)
- else
- output = "[ERR: No Object Handler]"
- else
- output = "[ERR: Object Not Found]"
-
- reply = reply.replace(new RegExp("" + utils.quotemeta(match[1]) + "", "i"), output)
- match = reply.match(/(.+?)<\/call>/i)
-
return reply
##
diff --git a/src/rivescript.coffee b/src/rivescript.coffee
index 64edfa7..67800f6 100644
--- a/src/rivescript.coffee
+++ b/src/rivescript.coffee
@@ -16,6 +16,7 @@ utils = require "./utils"
sorting = require "./sorting"
inherit_utils = require "./inheritance"
JSObjectHandler = require "./lang/javascript"
+RSVP = require("rsvp")
##
# RiveScript (hash options)
@@ -115,6 +116,23 @@ class RiveScript
version: ->
return VERSION
+ ##
+ # Promise Promise
+ #
+ # Alias for RSVP.Promise
+ #
+ # You can use shortcut in your async subroutines
+ #
+ # ```javascript
+ # rs.setSubroutine("asyncHelper", function (rs, args) {
+ # return new rs.Promise(function (resolve, reject) {
+ # resolve(42);
+ # });
+ # });
+ # ```
+ ##
+ Promise: RSVP.Promise
+
##
# private void runtime ()
#
@@ -755,4 +773,41 @@ class RiveScript
reply: (user, msg, scope) ->
return @brain.reply(user, msg, scope)
+ ##
+ # Promise replyAsync (string username, string message [[, scope], callback])
+ #
+ # Asyncronous version of reply. Use replyAsync if at least one of the subroutines
+ # used with tag returns a promise
+ #
+ # Example: using promises
+ #
+ # ```javascript
+ # rs.replyAsync(user, message).then(function(reply) {
+ # console.log("Bot>", reply);
+ # }).catch(function(error) {
+ # console.error("Error: ", error);
+ # });
+ # ```
+ #
+ # Example: using the callback
+ #
+ # ```javascript
+ # rs.replyAsync(username, msg, this, function(error, reply) {
+ # if (!error) {
+ # console.log("Bot>", reply);
+ # } else {
+ # console.error("Error: ", error);
+ # }
+ # });
+ # ```
+ ##
+ replyAsync: (user, msg, scope, callback) ->
+ reply = @brain.reply(user, msg, scope, true)
+ if callback
+ reply.then (result) =>
+ callback.call @, null, result
+ .catch (error) =>
+ callback.call @, error, null
+ return reply
+
module.exports = RiveScript
diff --git a/src/utils.coffee b/src/utils.coffee
index 7c7b5e6..569d64c 100644
--- a/src/utils.coffee
+++ b/src/utils.coffee
@@ -131,3 +131,14 @@ exports.clone = (obj) ->
copy[key] = exports.clone(obj[key])
return copy
+
+##
+# boolean isAPromise (object)
+#
+# Determines if obj looks like a promise
+##
+exports.isAPromise = (obj) ->
+ return obj and obj.then and obj.catch and obj.finally and
+ typeof obj.then is 'function' and
+ typeof obj.catch is 'function' and
+ typeof obj.finally is 'function'
\ No newline at end of file
diff --git a/test/test-rivescript.coffee b/test/test-rivescript.coffee
index 441bf75..18457c9 100644
--- a/test/test-rivescript.coffee
+++ b/test/test-rivescript.coffee
@@ -805,17 +805,16 @@ exports.test_js_string_in_setSubroutine = (test) ->
exports.test_function_in_setSubroutine = (test) ->
bot = new TestCase(test, """
- + *
+ + my name is *
- hello personhelper
""")
- input = "hello there"
+ input = "my name is Rive"
bot.rs.setSubroutine("helper", (rs, args) ->
- test.ok(args.length is 2)
+ test.ok(args.length is 1)
test.equal(rs, bot.rs)
- test.equal(args[0], "hello")
- test.equal(args[1], "there")
+ test.equal(args[0], "rive")
test.done()
)
@@ -832,4 +831,128 @@ exports.test_function_in_setSubroutine_return_value = (test) ->
)
bot.reply("hello", "hello person")
- test.done()
\ No newline at end of file
+ test.done()
+
+exports.test_promises_in_objects = (test) ->
+ bot = new TestCase(test, """
+ + my name is *
+ - hello there helperWithPromise with a anotherHelperWithPromise
+ """)
+
+ input = "my name is Rive"
+
+ bot.rs.setSubroutine("helperWithPromise", (rs, args) ->
+ test.ok(args.length is 1)
+ test.equal(args[0], "rive")
+ return new rs.Promise((resolve, reject) ->
+ resolve("stranger")
+ )
+ )
+
+ bot.rs.setSubroutine("anotherHelperWithPromise", (rs, args) ->
+ return new rs.Promise((resolve, reject) ->
+ setTimeout () ->
+ resolve("delay")
+ , 1000
+ )
+ )
+
+ bot.rs.replyAsync(bot.username, input).then (reply) ->
+ test.equal(reply, "hello there stranger with a delay")
+ test.done()
+
+exports.test_replyAsync_supports_callbacks = (test) ->
+ bot = new TestCase(test, """
+ + my name is *
+ - hello there asyncHelper
+ """)
+
+ input = "my name is Rive"
+
+ bot.rs.setSubroutine("asyncHelper", (rs, args) ->
+ return new rs.Promise((resolve, reject) ->
+ resolve("stranger")
+ )
+ )
+
+ bot.rs.replyAsync(bot.username, input, null, (error, reply) ->
+ test.ok(!error)
+ test.equal(reply, "hello there stranger")
+ test.done()
+ )
+
+exports.test_use_reply_with_async_subroutines = (test) ->
+ bot = new TestCase(test, """
+ + my name is *
+ - hello there asyncHelper
+ """)
+
+ bot.rs.setSubroutine("asyncHelper", (rs, args) ->
+ return new rs.Promise((resolve, reject) ->
+ resolve("stranger")
+ )
+ )
+
+ bot.reply("my name is Rive", "hello there [ERR: Using async routine with reply: use replyAsync instead]")
+ test.done()
+
+exports.test_errors_in_async_subroutines_with_callbacks = (test) ->
+ bot = new TestCase(test, """
+ + my name is *
+ - hello there asyncHelper
+ """)
+
+ errorMessage = "Something went terribly wrong"
+
+ bot.rs.setSubroutine("asyncHelper", (rs, args) ->
+ return new rs.Promise((resolve, reject) ->
+ reject(new Error(errorMessage))
+ )
+ )
+
+ bot.rs.replyAsync(bot.username, "my name is Rive", null, (error, reply) ->
+ test.ok(error)
+ test.equal(error.message, errorMessage)
+ test.ok(!reply)
+ test.done()
+ )
+
+exports.test_errors_in_async_subroutines_with_promises = (test) ->
+ bot = new TestCase(test, """
+ + my name is *
+ - hello there asyncHelper
+ """)
+
+ errorMessage = "Something went terribly wrong"
+
+ bot.rs.setSubroutine("asyncHelper", (rs, args) ->
+ return new rs.Promise((resolve, reject) ->
+ reject(new Error(errorMessage))
+ )
+ )
+
+ bot.rs.replyAsync(bot.username, "my name is Rive").catch (error) ->
+ test.ok(error)
+ test.equal(error.message, errorMessage)
+ test.done()
+
+exports.test_async_and_sync_subroutines_together = (test) ->
+ bot = new TestCase(test, """
+ + my name is *
+ - hello there asyncHelperexclaim
+ """)
+
+
+ bot.rs.setSubroutine("exclaim", (rs, args) ->
+ return "!"
+ )
+
+ bot.rs.setSubroutine("asyncHelper", (rs, args) ->
+ return new rs.Promise((resolve, reject) ->
+ resolve("stranger")
+ )
+ )
+
+ bot.rs.replyAsync(bot.username, "my name is Rive").then (reply) ->
+ test.equal(reply, "hello there stranger!")
+ test.done()
\ No newline at end of file