# frozen_string_literal: true require_relative "./router_node" require_relative "../helpers/queue" require_relative "../helpers/response_execution_block" require_relative "../helpers/input_output_pair" require_relative "../logging/composite_logger" module Kanal module Core module Router # Router serves as a container class for # root node of router nodes, also as somewhat # namespace. Basically class router stores all the # router nodes and have a name. class Router include Helpers include Logging::Logger attr_reader :name, :core, :output_ready_block def initialize(name, core) logger.info "Initializing" @name = name @core = core @root_node = nil @default_node = nil @default_error_node = nil default_error_response do if core.plugin_registered? :batteries body "Unfortunately, error happened. Please consider contacting the creator of this bot to provide information about the circumstances of this error." else raise "Error occurred and there is no way to inform end user about it. You can override error response with router.error_response method or register :batteries plugin so default response will populate the .body output parameter" end end @error_node = nil @response_execution_queue = Queue.new @input_output_pair_queue = Queue.new @output_ready_block = nil @core.hooks.register(:output_ready) # arg _this = self _input_output_pair_queue = @input_output_pair_queue # Attaching to the queue hook to capture enqueued output and move it # forward in the workflow: apply hooks in :output_before_ready and pass it to # the end consumer which uses .output_ready(&block) @input_output_pair_queue.hooks.attach :item_queued do |input_output_pair| _this.logger.debug "Calling output_ready block for input ##{input_output_pair.input.__id__} and output #{input_output_pair.output.__id__}. Output body is: '#{input_output_pair.output.body}'" begin _this.core.hooks.call :output_before_returned, input_output_pair.input, input_output_pair.output rescue => e _this.logger.error "Problem with some hooks inside :output_before_returned. More info: #{e.full_message}" return end begin _this.output_ready_block.call input_output_pair.output _input_output_pair_queue.remove(input_output_pair) rescue => e _input_output_pair_queue.remove(input_output_pair) _this.logger.error "Problem occurred in block provided in .output_ready(&block), here is more info: #{e.full_message}" end end end def configure(&block) logger.info "Configuring" # Root node does not have parent @root_node ||= RouterNode.new router: self, parent: nil, root: true @root_node.instance_eval(&block) end def default_response(&block) logger.info "Setting default response" if @default_node logger.fatal "Attempted to set default_response for a second time" raise "default node for router #{@name} already defined" end @default_node = RouterNode.new parent: nil, router: self, default: true @default_node.respond(&block) end def error_response(&block) raise "error node for router #{@name} already defined" if @error_node @error_node = RouterNode.new parent: nil, router: self, error: true @error_node.respond(&block) end # Method allows to pass output with input directly to the end step, # when outputs are processed after router. Meaning it will not go # through router, but will have all hooks in :output_before_returned executed for it. # Input is needed for hooks to work properly. # # WARNING: this method AVOIDS router whatsoever # Why use this method? If you want router to give out (through output_ready(&block) method) output without # passing through router but with hooks called (many plugins use hooks to alter the output) - you # use this method. # # This method adds input-output pair into input_output_pair_queue for it to be processed in item_queued hook # # param [Kanal::Core::Output::Output] output # param [Kanal::Core::Input::Input] input # def provide_output_with_input(output, input) @input_output_pair_queue.enqueue InputOutputPair.new input, output end # # Method allows to pass output directly to the end output # consumer (code which uses .output_ready(&block). Without passing router and # without passing hooks attached to :output_before_ready # # WARNING: output provided via this method will avoid any alteration by the # router inner machinery and will go directly to the output consumer # # Why use this method? If you want to pass output with your manually defined # properties to the output consumer (which uses .output_ready(&block)) # without any alternations by the router system. # # This method calls output_ready_block on pre-formed output right away # # param [Kanal::Core::Output] output # def provide_output(output) @output_ready_block.call output end # Main method for creating output(s) if it is found or going to default output def consume_input(input) logger.debug "Consuming input #{input.__id__}." # Checking if default node with output exists throw error if not unless @default_node logger.fatal "Attempted to consume input with no default response set" raise "Please provide default response for router before you try and throw input against it ;)" end unless @root_node logger.fatal "Attempted to consume input but router is not configured" raise "You did not actually .configure router, didn't you? There is no even root node! Use .configure method" end unless @root_node.children? logger.fatal "Attempted to consume input but router does not have any routes" raise "Hey your router actually does not have ANY routes to work with. Did you even try adding them?" end unless @output_ready_block logger.fatal "Attempted to consume input but output_ready block is not set" raise "You must provide block via .output_ready for router to function properly" end @core.hooks.call :input_before_router, input node = test_input_against_router_node input, @root_node # No result means no route node was found for that input # using default response node ||= @default_node response_blocks = node.response_blocks error_node = @error_node || @default_error_node response_execution_blocks = response_blocks.map { |rb| ResponseExecutionBlock.new rb, input, @default_error_node, @error_node } response_execution_blocks.each do |reb| @response_execution_queue.enqueue reb end process_response_execution_queue end def process_response_execution_queue until @response_execution_queue.empty? response_execution = @response_execution_queue.dequeue response_execution.execute core, @input_output_pair_queue end end def output_ready(&block) logger.info "Setting output_ready block" @output_ready_block = block end # Recursive method for searching router nodes def test_input_against_router_node(input, router_node) # Allow root node because it does not have any conditions and does not have # any responses, but it does have children. Well, it should have children... # Basically: # if router_node is root - proceed with code # if router_node is not root and condition is not met - stop right here. # Cannot proceed inside of this node. return if !router_node.root? && !router_node.condition_met?(input, @core) # Check if node has children first. Router node with children SHOULD NOT HAVE RESPONSE. # There is an exception for this case so don't worry, it's not protected only by # this comment if router_node.children? node = nil router_node.children.each do |c| node = test_input_against_router_node input, c break if node end node elsif router_node.response? # Router node without children can have response router_node end end def routes_info_string error_and_default_response_data = "Router Debug Info: Default response: #{variable_defined @default_node} Error response: #{variable_defined @error_node} Routes: " router_data = get_data @root_node error_and_default_response_data + router_data end private :test_input_against_router_node def default_error_response(&block) @default_error_node = RouterNode.new parent: nil, router: self, error: true @default_error_node.respond(&block) end def get_data(node, depth = 0) if node != @root_node data = get_node_data_string node else data = "" end if node.children? node.children.each do |c| c.children? ? symbol = "↳ " : symbol = "⏺ " data = data + (" " * depth) + symbol + get_data(c, depth + 1) end end data end def variable_defined(variable) variable.nil? ? "not defined" : "defined" end def get_node_data_string(node) data = "on :#{node.instance_variable_get(:@condition_pack_name)}, #{node.instance_variable_get(:@condition_name)}: #{node.instance_variable_get(:@condition_argument)}" data = data + ", responses_total: #{node.instance_variable_get(:@response_blocks).count}" if node.instance_variable_get(:@response_blocks).count > 0 data = data + " " data end end end end end