diff --git a/.gitignore b/.gitignore index e675df6..f5576b3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ /spec/reports/ /tmp/ .vscode +coverage diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..5e4293f --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,8 @@ +inherit_from: .rubocop_todo.yml +require: rubocop-rspec + +RSpec/MultipleExpectations: + Max: 5 + +RSpec/ExampleLength: + Max: 8 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 0000000..81f6e36 --- /dev/null +++ b/.rubocop_todo.yml @@ -0,0 +1,237 @@ +# This configuration was generated by +# `rubocop --auto-gen-config` +# on 2018-09-05 23:55:24 +0530 using RuboCop version 0.58.2. +# The point is for the user to remove these configuration records +# one by one as the offenses are removed from the code base. +# Note that changes in the inspected code, or installation of new +# versions of RuboCop, may require this file to be generated again. + +# Offense count: 5 +# Cop supports --auto-correct. +# Configuration parameters: Include, TreatCommentsAsGroupSeparators. +# Include: **/*.gemspec +Gemspec/OrderedDependencies: + Exclude: + - 'bot_framework.gemspec' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyleAlignWith, AutoCorrect, Severity. +# SupportedStylesAlignWith: start_of_line, def +Layout/DefEndAlignment: + Exclude: + - 'lib/bot_framework/models/audio_card.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Layout/EmptyLinesAroundArguments: + Exclude: + - 'examples/messenger/bot.rb' + +# Offense count: 3 +# Cop supports --auto-correct. +Layout/LeadingBlankLines: + Exclude: + - 'bot_framework.gemspec' + - 'examples/ruby_conf/bot.rb' + - 'lib/bot_framework/models/entity.rb' + +# Offense count: 3 +# Cop supports --auto-correct. +Layout/SpaceAfterComma: + Exclude: + - 'lib/bot_framework/dialogs/reg_exp_recognizer.rb' + - 'spec/bot_framework/dialogs/reg_exp_recognizer_spec.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters. +# SupportedStyles: space, no_space +# SupportedStylesForEmptyBraces: space, no_space +Layout/SpaceInsideBlockBraces: + Exclude: + - 'spec/bot_framework/dialogs/reg_exp_recognizer_spec.rb' + +# Offense count: 8 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces. +# SupportedStyles: space, no_space, compact +# SupportedStylesForEmptyBraces: space, no_space +Layout/SpaceInsideHashLiteralBraces: + Exclude: + - 'lib/bot_framework/dialogs/reg_exp_recognizer.rb' + - 'spec/bot_framework/dialogs/reg_exp_recognizer_spec.rb' + +# Offense count: 3 +Lint/DuplicateMethods: + Exclude: + - 'lib/bot_framework/bot.rb' + - 'lib/bot_framework/connector.rb' + - 'lib/bot_framework/token_validator.rb' + +# Offense count: 3 +Lint/EmptyWhen: + Exclude: + - 'lib/bot_framework/dialogs/entity_recognizer.rb' + - 'lib/bot_framework/dialogs/luis_recognizer.rb' + +# Offense count: 4 +Lint/UriEscapeUnescape: + Exclude: + - 'lib/bot_framework/api_base.rb' + +# Offense count: 4 +Lint/UselessAssignment: + Exclude: + - 'examples/dialog/basic_first_run.rb' + - 'lib/bot_framework/dialogs/entity_recognizer.rb' + - 'lib/bot_framework/models/object.rb' + +# Offense count: 12 +Metrics/AbcSize: + Max: 38 + +# Offense count: 2 +# Configuration parameters: CountComments, ExcludedMethods. +# ExcludedMethods: refine +Metrics/BlockLength: + Max: 168 + +# Offense count: 1 +# Configuration parameters: CountBlocks. +Metrics/BlockNesting: + Max: 5 + +# Offense count: 2 +# Configuration parameters: CountComments. +Metrics/ClassLength: + Max: 129 + +# Offense count: 9 +Metrics/CyclomaticComplexity: + Max: 11 + +# Offense count: 14 +# Configuration parameters: CountComments. +Metrics/MethodLength: + Max: 34 + +# Offense count: 4 +Metrics/PerceivedComplexity: + Max: 13 + +# Offense count: 4 +Naming/AccessorMethodName: + Exclude: + - 'lib/bot_framework/bot_state.rb' + - 'lib/bot_framework/connector.rb' + +# Offense count: 2 +Naming/BinaryOperatorParameterName: + Exclude: + - 'lib/bot_framework/models/base.rb' + +# Offense count: 2 +# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames. +# AllowedNames: io, id, to, by, on, in, at, ip +Naming/UncommunicativeMethodParamName: + Exclude: + - 'lib/bot_framework/models/base.rb' + +# Offense count: 1 +# Configuration parameters: AllowedChars. +Style/AsciiComments: + Exclude: + - 'lib/bot_framework/token_validator.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle. +# SupportedStyles: braces, no_braces, context_dependent +Style/BracesAroundHashParameters: + Exclude: + - 'spec/bot_framework/dialogs/reg_exp_recognizer_spec.rb' + +# Offense count: 1 +Style/CommentedKeyword: + Exclude: + - 'lib/bot_framework/models/base.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. +# SupportedStyles: assign_to_condition, assign_inside_condition +Style/ConditionalAssignment: + Exclude: + - 'lib/bot_framework/dialogs/reg_exp_recognizer.rb' + +# Offense count: 2 +Style/DateTime: + Exclude: + - 'lib/bot_framework/dialogs/entity_recognizer.rb' + - 'lib/bot_framework/models/base.rb' + +# Offense count: 28 +Style/Documentation: + Enabled: false + +# Offense count: 8 +# Cop supports --auto-correct. +Style/ExpandPathArguments: + Exclude: + - 'bot_framework.gemspec' + - 'examples/dialog/basic_first_run.rb' + - 'examples/echo/config.ru' + - 'examples/messenger/config.ru' + - 'examples/ruby_conf/config.ru' + - 'examples/simple_regex/config.ru' + - 'examples/stock/config.ru' + - 'spec/spec_helper.rb' + +# Offense count: 4 +# Configuration parameters: MinBodyLength. +Style/GuardClause: + Exclude: + - 'lib/bot_framework/dialogs/entity_recognizer.rb' + - 'lib/bot_framework/models/attachment_info.rb' + - 'lib/bot_framework/server.rb' + +# Offense count: 4 +# Cop supports --auto-correct. +Style/IfUnlessModifier: + Exclude: + - 'lib/bot_framework/dialogs/entity_recognizer.rb' + - 'lib/bot_framework/dialogs/luis_recognizer.rb' + - 'lib/bot_framework/models/base.rb' + +# Offense count: 1 +Style/MixinUsage: + Exclude: + - 'examples/dialog/basic_first_run.rb' + +# Offense count: 3 +# Cop supports --auto-correct. +# Configuration parameters: AllowAsExpressionSeparator. +Style/Semicolon: + Exclude: + - 'lib/bot_framework/dialogs/luis_recognizer.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. +# SupportedStyles: single_quotes, double_quotes +Style/StringLiterals: + Exclude: + - 'lib/bot_framework/dialogs/reg_exp_recognizer.rb' + +# Offense count: 1 +# Cop supports --auto-correct. +Style/UnneededCondition: + Exclude: + - 'lib/bot_framework/dialogs/reg_exp_recognizer.rb' + +# Offense count: 87 +# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. +# URISchemes: http, https +Metrics/LineLength: + Max: 150 diff --git a/.travis.yml b/.travis.yml index d181203..0b67c83 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,5 +4,8 @@ rvm: - 2.3.1 - 2.4.0 - 2.4.1 -before_install: gem install bundler + - 2.5.1 +before_install: + - gem install bundler + - export TZ=Asia/Kolkata script: bundle exec rspec spec diff --git a/README.md b/README.md index f022439..528819a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # BotFramework [![Join the chat at https://gitter.im/botframework-ruby/Lobby](https://badges.gitter.im/botframework-ruby/Lobby.svg)](https://gitter.im/botframework-ruby/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Open Source Helpers](https://www.codetriage.com/tachyons/botframework-ruby/badges/users.svg)](https://www.codetriage.com/tachyons/botframework-ruby) [![Build Status](https://travis-ci.org/tachyons/botframework-ruby.svg?branch=master)](https://travis-ci.org/tachyons/botframework-ruby) [![Build status](https://ci.appveyor.com/api/projects/status/fdbwdbvv3viswwec?svg=true)](https://ci.appveyor.com/project/tachyons/botframework-ruby) diff --git a/appveyor.yml b/appveyor.yml index b3b5515..eb76afa 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -11,6 +11,7 @@ environment: install: - SET PATH=C:\Ruby%ruby_version%\bin;%PATH% + - SET TZ=Asia/Kolkata - gem install bundler --no-document -v 1.14.6 - bundle install --retry=3 diff --git a/bot_framework.gemspec b/bot_framework.gemspec index 65cc8a9..9a26cd9 100644 --- a/bot_framework.gemspec +++ b/bot_framework.gemspec @@ -24,6 +24,9 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'rake', '~> 10.0' spec.add_development_dependency 'rspec', '~> 3.0' spec.add_development_dependency 'pry' + spec.add_development_dependency 'simplecov' + spec.add_development_dependency 'rubocop-rspec' + spec.add_development_dependency 'timecop' spec.add_dependency 'oauth2' spec.add_dependency 'jwt' diff --git a/lib/bot_framework.rb b/lib/bot_framework.rb index 5d10542..0597905 100644 --- a/lib/bot_framework.rb +++ b/lib/bot_framework.rb @@ -8,12 +8,15 @@ require 'bot_framework/errors' require 'bot_framework/util' require 'bot_framework/connector' +require 'bot_framework/configuration' require 'bot_framework/api_base' require 'bot_framework/conversation' require 'bot_framework/bot_state' require 'bot_framework/token_validator' require 'bot_framework/bot' require 'bot_framework/server' + +require 'bot_framework/errors/configuration' # Models require 'bot_framework/models/base' require 'bot_framework/models/activity' @@ -52,14 +55,26 @@ module BotFramework class << self - attr_accessor :connector + attr_writer :configuration + end - def configure(*args, &block) - @connector = Connector.new(*args, &block) - end + def self.configuration + @configuration ||= Configuration.new + end + + def self.reset + @configuration = Configuration.new + end + + def self.configure + yield(configuration) + end + + def self.logger + @logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT) + end - def logger - @logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT) - end + def self.connector + @connector ||= Connector.new(configuration) end end diff --git a/lib/bot_framework/bot.rb b/lib/bot_framework/bot.rb index 2724700..b94cbd0 100644 --- a/lib/bot_framework/bot.rb +++ b/lib/bot_framework/bot.rb @@ -1,7 +1,6 @@ module BotFramework class Bot class << self - extend Gem::Deprecate attr_accessor :recognizers def on(event, &block) @@ -12,12 +11,6 @@ def on_intent(intent, &block) intent_callbacks[intent] = block end - def recognizer=(recognizer) - warn 'DEPRECATED: Use add_recognizer method instead' - add_recognizer(recognizer) - end - deprecate :recognizer=, :add_recognizer, 2016, 5 - def add_recognizer(recognizer) recognizers << recognizer end diff --git a/lib/bot_framework/configuration.rb b/lib/bot_framework/configuration.rb new file mode 100644 index 0000000..93003bb --- /dev/null +++ b/lib/bot_framework/configuration.rb @@ -0,0 +1,21 @@ +module BotFramework + class Configuration + attr_writer :app_id, :app_secret + + def initialize + @app_id = nil + @app_secret = nil + end + + def app_id + raise Errors::Configuration, 'Bot Framework app_id missing! See the documentation for configuration settings.' unless @app_id + @app_id + end + + def app_secret + raise Errors::Configuration, 'Bot Framework app_secret missing! See the documentation for configuration settings.' unless @app_secret + @app_secret + end + + end +end diff --git a/lib/bot_framework/connector.rb b/lib/bot_framework/connector.rb index 0c1eef1..bfaaa02 100644 --- a/lib/bot_framework/connector.rb +++ b/lib/bot_framework/connector.rb @@ -16,10 +16,9 @@ class Connector STATE_END_POINT = 'https://state'.freeze ISSUER_DOMAINS = ['sts.windows.net', 'api.botframework.com', 'login.microsoftonline.com'].freeze - def initialize(options = {}) - @app_id = options[:app_id] - @app_secret = options[:app_secret] - yield(self) if block_given? + def initialize(config) + @app_id = config.app_id + @app_secret = config.app_secret end def client diff --git a/lib/bot_framework/console_connector.rb b/lib/bot_framework/console_connector.rb deleted file mode 100644 index 7e26d1a..0000000 --- a/lib/bot_framework/console_connector.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'observer' -require 'nio' -module BotFramework - class ConsoleConnector - include Observable - def listen - BotFramework.logger.info 'Listening' - loop do - line = Readline.readline('> ') - break if line.nil? || line == 'quit' - Readline::HISTORY.push(line) - BotFramework.logger.info "You typed: #{line}" - end - self - end - end -end diff --git a/lib/bot_framework/dialogs/action_set.rb b/lib/bot_framework/dialogs/action_set.rb deleted file mode 100644 index c185f23..0000000 --- a/lib/bot_framework/dialogs/action_set.rb +++ /dev/null @@ -1,66 +0,0 @@ -module BotFramework - module Dialogs - class ActionSet - attr_accessor :actions - - def initialize - @actions = {} - end - - def clone(copy_to = nil) - obj = copy_to || ActionSet.new - obj.trigger = trigger - actions.each do |name| - object.actions[name] = actions[name] - end - obj - end - - def add_dialog_trigger(actions, dialog_id) - if trigger - trigger.localization_namespace = dialog_id.split(':').first - actions.begin_dialog_action(dialog_id, dialog_id, trigger) - end - end - - def find_action_routes(context, callback); end - - def select_action_route(session, route); end - - def dialog_interrupted(session, dialog_id, dialog_ags); end - - def begin_dialog_action(name, id, options = {}); end - - def end_conversation_action(name, message, options); end - - def reload_action(name, message, options); end - - def cancel_action(name, message, options) - action(name, options) do |args, session| - if options[:confirm_prompt] - session.begin_dialog('BotBuilder:ConfirmCancel', localization_namespace: nil, - confirm_prompt: nil, - dialog_index: nil, - msg: message) - elsif message.present? - session.send_localized(args.library_name, message) - else - session.cancel_dialog(args.dialog_index) - end - end - end - - def select_action_routes; end - - def trigger_action; end - - private - - def action(_name, _options = {}) - yield - end - - def recognize_action(message); end - end - end -end diff --git a/lib/bot_framework/dialogs/entity_recognizer.rb b/lib/bot_framework/dialogs/entity_recognizer.rb index 6b953bd..bb2312b 100644 --- a/lib/bot_framework/dialogs/entity_recognizer.rb +++ b/lib/bot_framework/dialogs/entity_recognizer.rb @@ -3,8 +3,8 @@ module BotFramework module Dialogs class EntityRecognizer DATE_REGEX = /^\d{4}-\d{2}-\d{2}/i - YES_REGEX = /^(1|y|yes|yep|sure|ok|true)(\W|$)/i - NO_REGX = /^(2|n|no|nope|not|false)(\W|$)/i + YES_REGEX = /^(1|y|yes|yep|sure|ok|true|yeah)(\W|$)/i + NO_REGX = /^(2|n|no|nope|not|false|never)(\W|$)/i NUMBER_REGEX = /[+-]?(?:\d+\.?\d*|\d*\.?\d+)/ ORDINAL_WORDS_REGEX = /first|second|third|fourth|fifth|sixth|seventh|eigth|ninth|tenth/ def initialize(*args); end diff --git a/lib/bot_framework/dialogs/reg_exp_recognizer.rb b/lib/bot_framework/dialogs/reg_exp_recognizer.rb index d2ee92b..c001113 100644 --- a/lib/bot_framework/dialogs/reg_exp_recognizer.rb +++ b/lib/bot_framework/dialogs/reg_exp_recognizer.rb @@ -1,4 +1,3 @@ -require_relative 'action_set' module BotFramework module Dialogs class RegExpRecognizer diff --git a/lib/bot_framework/errors/configuration.rb b/lib/bot_framework/errors/configuration.rb new file mode 100644 index 0000000..0298f92 --- /dev/null +++ b/lib/bot_framework/errors/configuration.rb @@ -0,0 +1,5 @@ +module BotFramework + module Errors + class Configuration < StandardError; end + end +end diff --git a/lib/bot_framework/events/event_emitter.rb b/lib/bot_framework/events/event_emitter.rb deleted file mode 100644 index 6b8f9a2..0000000 --- a/lib/bot_framework/events/event_emitter.rb +++ /dev/null @@ -1,6 +0,0 @@ -module BotFramework - module Events - class EventEmitter - end - end -end diff --git a/lib/bot_framework/message.rb b/lib/bot_framework/message.rb deleted file mode 100644 index 48f5c64..0000000 --- a/lib/bot_framework/message.rb +++ /dev/null @@ -1,80 +0,0 @@ -module BotFramework - class Message - TEXT_FORMAT = { - plain: 'plain', - markdown: 'markdown', - xml: 'xml' - }.freeze - ATTACHMENT_LAYOUT = { - list: 'list', - carousel: 'carousel' - }.freeze - - def initialize(_session = nil) - @data = {} - @data[:type] = 'consts.MessageType' # FIXME - @data[:agent] = 'consts.agent' - if @session - m = @session.message - @data[:source] = m[:source] if m[:source] - @data[:text_locale] = m[:text_locale] if m[:text_locale] - @data[:address] = m[:address] if m[:address] - end - end - - def text_locale(locale) - @data[:text_locale] = locale - self - end - - def text_format(style) - @data[:text_format] = style - self - end - - def text(text, *_args) - @data[:text] = text.present? ? format_text(text) : '' - self - end - - def ntext(msg, _msg_plural, count) - fmt = count == 1 ? self.class.random_prompt(msg) : self.class.random_prmpt(message_plural) - fmt = @session.get_text(fmt) if @session - @data[:text] = fmt, count # FIXME - self - end - - def compose(prompts, *args) - if prompts - @data[:text] = Message.compose_prompt(@session, prompts, *args) - self - end - end - - def summary; end - - def attachment_layout; end - - def attachments; end - - def add_attachment; end - - def entities; end - - def add_entity; end - - def address; end - - def timestamp; end - - def source_event; end - - def to_message; end - - class << self - def random_prompt; end - - def compose_prompt; end - end - end -end diff --git a/lib/bot_framework/prompt.rb b/lib/bot_framework/prompt.rb deleted file mode 100644 index a57127f..0000000 --- a/lib/bot_framework/prompt.rb +++ /dev/null @@ -1,54 +0,0 @@ -module BotFramework - class Prompt < Dialog - @@options = { - recognizer: BotFramework::SimplePromptRecognizer.new, - prompt_after_action: true - } - - @@default_retry_prompt = { - text: 'default_text', - number: 'default_number', - confirm: 'daefault_confirm', - choice: 'default_choice', - time: 'default_time', - attachment: 'default_file' - } - - def self.configure(_options); end - - def begin(session, options = {}) - options[:prompt_after_action] = options[:prompt_after_action] || options[:prompt_after_action] - options[:retry_count] = 0 - options.each do |option| - # Store in dialog data - end - send_prompt(session, options) - end - - def reply_received(session, args = {}); end - - def dialog_resumed; end - - def recognize; end - - def send_prompt(session, options); end - - def create_prompt; end - - def self.text; end - - def self.number; end - - def self.confirm; end - - def self.choice; end - - def self.time; end - - def self.attachment; end - - def self.disambiguate; end - - private - end -end diff --git a/lib/bot_framework/session.rb b/lib/bot_framework/session.rb deleted file mode 100644 index 2c56077..0000000 --- a/lib/bot_framework/session.rb +++ /dev/null @@ -1,405 +0,0 @@ -module BotFramework - class Session # < Events::EventEmitter - attr_accessor :library, :message, :user_data, :conversation_data, :private_conversation_data, - :session_state, :dialog_state, :localizer - def initialize(options) - super - @library = options[:library] - @localizer = options[:localizer] - @msg_sent = false - @is_reset = false - @last_send_time = Time.now # TODO: Move to proc? - @batch = [] - @batch_timer = Timers::Group.new - @batch_started = false - @sending_batch = false - @in_middleware = false - @_locale = nil - - @options = options || { - on_save: nil, - on_send: nil, - library: nil, - localizer: nil, - middleware: nil, - dialog_id: nil, - dialog_args: nil, - auto_batch_delay: nil, - dialog_error_message: nil, - actions: nil - } - @auto_batch_delay = 250 unless options[:auto_batch_delay].is_a? Integer - @timers = Timers::Group.new - end - - def to_recognize_context - { - message: message, - user_data: user_data, - conversation_data: conversation_data, - private_conversation_data: private_conversation_data, - localizer: localizer, - dialog_stack: dialog_stack, - preferred_locale: preferred_locale, - get_text: get_text, - nget_text: nget_text, - locale: preferred_locale - } - end - - def dispatch(session_state, message) - index = 0 - session = self - now = Time.now - middleware = @options[:middleware] || [] - - _next = lambda do - handler = middleware[index] if index < middleware.length - if handler - index += 1 - handler(session, _next) - else - @in_middleware = false - @session_state[:last_acess] = now - done - end - end - session_state ||= { call_stack: [], last_acess: Time.now, version: 0.0 } - # Making sure that dialog is properly initialized - cur = cur_dialog - self.dialog_data = cur.state if cur - # Dispatch message - message ||= { text: '' } - message[:type] ||= 'message' - # Ensure that localized prompts are loaded - # TODO - self - end - - def error(_error) - logger.info 'Session error' - if options[:dialog_error_message] - end_conversation(options[:dialog_error_message]) - else - # TODO: Add localisation - locale = preferred_locale - end_conversation 'Error in conversation' - end - - # TODO: Log error - end - - def preferred_locale(locale = nil) - if locale - @_locale = locale - @user_data['BotBuilder.Data.PreferredLocale'] = locale if @user_data - @localizer.load if @localizer # TODO - elsif !@_locale - if @user_data && @user_data['BotBuilder.Data.PreferredLocale'] - @_locale = @user_data['BotBuilder.Data.PreferredLocale'] - elsif @message && @message[:text_locale] - @_locale = @message[:text_locale] - elsif @localizer - @_locale = @localizer[:default_locale] - end - end - @_locale - end - - # Gets and formats localized text string - def gettext(message_id, options = {}) - # TODO - # stub - end - - # Gets and formats the singular/plural form of a localized text string. - def ngettext(message_id, message_id_plural, count); end - - # Manually save current session state - def save - logger.info 'Session.save' - start_batch - self - end - - def send(message, args = []) - args.unshift(@cur_library_name, message) - send_localized(args, message) - end - - def send_localized(_localization_namspace, message, _args = []) - # TODO: Incomplete - @msg_sent = true - m = { text: message } - prepare_message(m) - @batch << m - self - end - - # Sends a typing indicator to the user - def send_typing - @msg_sent = true - m = { type: 'typing' } - m = prepare_message(m) - @batch.push(m) - logger.info 'Send Typing' - send_batch - end - - def send_batch - logger.info "Sending batch with elements #{@batch.count}" - return if sending_batch? - # TODO: timer - @batch_timer = nil - batch = @batch - @batch_started = false - @sending_batch = true - cur = cur_dialog - cur.state = @dialog_data - options[:on_save] = proc do |err| - if err - @sending_batch = false - case (err.code || '') - when 'EBADMSG' - when 'EMSGSIZE' - # Something went wrong , let's reset everything - @user_data = {} - @batch = [] - end_conversation(@options[:dialog_error_message] || 'Something went wrong and we need to startover') - end - yield(err) if block_given? - else - if batch.length - options[:on_send].call(batch) do |err| - @sending_batch = false - start_batch if @batch_started # What is this :o - yield(err) if block_given? - end - else - @sending_batch = false - start_batch if @batch_started - yield(err) if block_given? - end - end - end - end - - # Begin a new dialog - def begin_dialog(id, args = nil) - logger.info "Beginning new dialog #{id}" - id = resolve_dialog_id(id) - dialog = find_dialog(id) - raise "Dialog #{id} Not found" unless dialog - push_dialog(id: id, state: {}) - start_batch - dialog.begin(self, args) - self - end - - def replace_dialog(id, args = nil) - logger.info "Session replace dialog #{id}" - id = resolve_dialog_id(id) - dialog = find_dialog(id) - raise "Dialog #{id} Not found" unless dialog - - # Update the stack and start dialog - pop_dialog - push_dialog(id: id, state: {}) - start_batch - dialog.begin(self, args) - self - end - - # End conversation with the user - def end_conversation(message = nil, _args = {}) - if message - # TODO: sent message - end - # Clear private conversation data - @private_conversation_data = {} - - # Clear stack and save - logger.info 'End conversation' - ss = @session_state - ss.call_stack = [] - send_batch - self - end - - def end_dialog(message = nil, args = {}) - if message - # TODO: end_dialog_with_result - end - cur = cur_dialog - if cur - if message - m = if (message.is_a? String) || (message.is_a? Array) - create_message(cur_library_name, message, args) - else - message - end - @msg_sent = true - prepare_message(m) - @batch.push(m) - end - - # Pop the dialog of the stack and resume the parent - logger.info 'Session.end_dialog' - child_id = cur.id - cur = pop_dialog - start_batch - if cur - dialog = find_dialog(cur.id) - if dialog - dialog.dialog_resumed(self, resumed: :completed, response: true, child_id: child_id) - else - # Bad dialog !! , we should never reach here - raise "Can't resume , missing parent" - end - end - end - end - - # Ends current dialog and returns a value to the caller - def end_dialog_with_result(result) - # Validate call stack - cur = cur_dialog - if cur - # Validate the result - result ||= {} - result[:resumed] ||= :completed - result.child_id = cur.id - logger.info 'Session.end_dialog_with_result' - - # Pop dialog of the stack and resume parent dialog - cur = pop_dialog - start_batch - if cur - dialog = find_dialog(cur.id) - if dialog - dialog.dialog_resumed(self, result) - else - # Bad dialog state - raise "Can't resume , missing parent dialog" - end - end - end - self - end - - def cancel_dialog; end - - def reset; end - - ############### Dialog Stack Management ############################33 - - # Gets and Sets current dialog stack - def dialog_stack(new_stack = nil) - if new_stack - # Update stack and dialog data - - stack = @session_state[:call_stack] = new_stack || [] - @dialog_data = stack.empty? nil || stack.last - else - # Copy over dialog data to slack - stack = @session_state[:call_stack] || [] - stack.last.state = @dialog_data || {} - end - stack - end - - # Clears the current Dialog stack - def clear_dialog_stack - @session_state[:call_stack] = [] - @dialog_data = nil - end - - # Enumerates all a stacks dialog entries in either a forward or reverse direction. - def self.forEachDialogStackEntry(stack) - stack.each { |item| yield(item) } - end - - # Searches a dialog stack for a specific dialog, in either a forward or reverse direction, returning its index. - def self.findEachDialogStackEntry(stack, dialog_id) - stack.each_with_index do |item, index| - return index if item[:id] = dialog_id - end - -1 - end - - # Returns a active stack entry or nil - def self.active_dialog_stack_entry(stack) - stack.last || nil - end - - # Pushes a new dialog into stack and return it as active dialog - def self.push_dialog_stack_entry(statck, entry) - entry[:state] ||= {} - statck ||= [] - stack.push(entry) - entry - end - - # Pop active dialog out of the stack - def self.pop_dialog_stack_entry(stack) - stack.pop if stack - Session.active_dialog_stack_entry(stack) - end - - # Deletes all dialog stack entries starting with the specified index and returns the new active dialog. - def self.prune_dialog_stack(_stack, _start); end - - # Ensures that all of the entries on a dialog stack reference valid dialogs within a library hierarchy. - def self.validate_dialog_stack(_stack, _root); end - - ## Message routing - ######################### - - # Dispatches handling of the current message to the active dialog stack entry. - def route_to_active_dialog; end - - private - - def start_batch - @batch_started = true - unless @sending_batch - @batch_timer.cancel if @batch_timer - # TODO: send_batch after config[:auto_batch_delay] seconds - @batch_timer = @timers.after(config[:auto_batch_delay]) do - send_batch - end - end - end - - def create_message; end - - def prepare_message(msg) - msg[:type] ||= 'message' - msg[:address] ||= message[:address] - msg[:text_locale] ||= message[:text_locale] - msg - end - - def vget_text; end - - def validate_call_stack; end - - def resolve_dialog_id; end - - def cur_library_name; end - - def find_dialog; end - - def push_dialog; end - - def pop_dialog; end - - def delete_dialogs; end - - def cur_dialog - ss = @session_sate - cur = ss.call_stack.last unless ss.call_stack.empty? - cur - end - end -end diff --git a/lib/bot_framework/universal_bot.rb b/lib/bot_framework/universal_bot.rb deleted file mode 100644 index e89003b..0000000 --- a/lib/bot_framework/universal_bot.rb +++ /dev/null @@ -1,7 +0,0 @@ -module BotFramework - class UniversalBot - def initialize(session, default_dialog); end - - def dialog(name, dialogs); end - end -end diff --git a/spec/bot_framework/bot_spec.rb b/spec/bot_framework/bot_spec.rb index 053807d..27c2e83 100644 --- a/spec/bot_framework/bot_spec.rb +++ b/spec/bot_framework/bot_spec.rb @@ -1,10 +1,39 @@ require 'spec_helper' describe BotFramework::Bot do + subject(:bot) { described_class } + + let(:hook) { proc { |_args| } } + it '.add_recognizer' do - expect(BotFramework::Bot.recognizers).to be_empty - recognizer = double(:recognizer) - BotFramework::Bot.add_recognizer(recognizer) - expect(BotFramework::Bot.recognizers).to eq([recognizer]) + expect(described_class.recognizers).to be_empty + recognizer = instance_double('recognizer') + described_class.add_recognizer(recognizer) + expect(described_class.recognizers).to eq([recognizer]) + end + + it 'register event callback' do + bot.on('created', &hook) + expect(bot.hooks['created']).to be_eql(hook) + end + + it 'register intent callback' do + bot.on_intent('get_name', &hook) + expect(bot.intent_callbacks['get_name']).to be_eql(hook) + end + + describe '.trigger' do + let(:hook) { proc { |args| args } } + + it 'returns false when event is not registered' do + expect(bot.trigger('created')).to be_falsy + end + + it 'execute the hook if the event is already registered' do + response = instance_spy('response') + hook = proc { response } + bot.on('created', &hook) + expect(bot.trigger('created')).to be_eql(response) + end end end diff --git a/spec/bot_framework/configuration_spec.rb b/spec/bot_framework/configuration_spec.rb new file mode 100644 index 0000000..ac7681e --- /dev/null +++ b/spec/bot_framework/configuration_spec.rb @@ -0,0 +1,25 @@ +RSpec.describe BotFramework::Configuration do + context 'with configuration block' do + it 'returns correct app_id' do + expect(BotFramework.configuration.app_id).to be_eql(ENV['BOT_FRAMEWORK_ACCESS']) + end + + it 'returns correct app secret' do + expect(BotFramework.configuration.app_secret).to be_eql(ENV['BOT_FRAMEWORK_SECRET']) + end + end + + context 'without config block' do + before do + BotFramework.reset + end + + it 'returns correct app_id' do + expect { BotFramework.configuration.app_id }.to raise_error(BotFramework::Errors::Configuration) + end + + it 'returns correct app secret' do + expect { BotFramework.configuration.app_secret }.to raise_error(BotFramework::Errors::Configuration) + end + end +end diff --git a/spec/bot_framework/dialogs/entity_recognizer_spec.rb b/spec/bot_framework/dialogs/entity_recognizer_spec.rb new file mode 100644 index 0000000..9899b4c --- /dev/null +++ b/spec/bot_framework/dialogs/entity_recognizer_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' +require 'timecop' +require './lib/bot_framework/dialogs/entity_recognizer.rb' + +RSpec.describe BotFramework::Dialogs::EntityRecognizer do + describe '.recognize_time' do + before do + Timecop.freeze(Time.utc(2018)) + end + + it 'returns parsed time' do + expect(described_class.recognize_time('yesterday')).to be_eql(entity: '2017-12-31 12:00:00 +0530', + resolution: { + resolution_type: 'chronic.time', + time: Time.parse('2017-12-31 12:00:00.000000000 +0530') + }, + type: 'chronic.time') + end + end + + describe '.parse_number' do + it 'parse number from a string' do + expect(described_class.parse_number('an apple and 10 oranges')).to be_eql('10') + end + end + + describe '.parse_boolean' do + it 'return true for positive responses' do + %w[yes Yeah sure Ok].each do |response| + expect(described_class.parse_boolean(response)).to be_eql(true) + end + end + + it 'returns false for negative responses' do + %w[No never nope not false].each do |response| + expect(described_class.parse_boolean(response)).to be_eql(false) + end + end + + it 'returns nil for neutral response' do + %w[maybe vayad].each do |response| + expect(described_class.parse_boolean(response)).to be_nil + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e52f2ee..766d059 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,2 +1,117 @@ +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +require 'simplecov' + +SimpleCov.start + $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) require 'bot_framework' +require 'pry' + +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + # # This allows you to limit a spec run to individual examples or groups + # # you care about by tagging them with `:focus` metadata. When nothing + # # is tagged with `:focus`, all examples get run. RSpec also provides + # # aliases for `it`, `describe`, and `context` that include `:focus` + # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + # config.filter_run_when_matching :focus + # + # # Allows RSpec to persist some state between runs in order to support + # # the `--only-failures` and `--next-failure` CLI options. We recommend + # # you configure your source control system to ignore this file. + # config.example_status_persistence_file_path = "spec/examples.txt" + # + # # Limits the available syntax to the non-monkey patched syntax that is + # # recommended. For more details, see: + # # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + # config.disable_monkey_patching! + # + # # This setting enables warnings. It's recommended, but in some cases may + # # be too noisy due to issues in dependencies. + # config.warnings = true + # + # # Many RSpec users commonly either run the entire suite or an individual + # # file, and it's useful to allow more verbose output when running an + # # individual spec file. + # if config.files_to_run.one? + # # Use the documentation formatter for detailed output, + # # unless a formatter has already been configured + # # (e.g. via a command-line flag). + # config.default_formatter = "doc" + # end + # + # # Print the 10 slowest examples and example groups at the + # # end of the spec run, to help surface which specs are running + # # particularly slow. + # config.profile_examples = 10 + # + # # Run specs in random order to surface order dependencies. If you find an + # # order dependency and want to debug it, you can fix the order by providing + # # the seed, which is printed after each run. + # # --seed 1234 + # config.order = :random + # + # # Seed global randomization in this process using the `--seed` CLI option. + # # Setting this allows you to use `--seed` to deterministically reproduce + # # test failures related to randomization by passing the same `--seed` value + # # as the one that triggered the failure. + # Kernel.srand config.seed + + ENV['BOT_FRAMEWORK_ACCESS'] ||= 'app_id' + ENV['BOT_FRAMEWORK_SECRET'] ||= 'app_secret' + ENV['TZ'] ||= 'Asia/Kolkata' + + config.before(:all) do + BotFramework.configure do |conf| + conf.app_id = ENV['BOT_FRAMEWORK_ACCESS'] + conf.app_secret = ENV['BOT_FRAMEWORK_SECRET'] + end + end +end