module Slayer # ResultMatcher is the object passed to the block of a {Command.call}. The ResultMatcher # allows the block-author to specify which piece of logic they would like to invoke # based on the state of the {Result} object. # # In the event that multiple blocks match the {Result}, only the most specific # matching block will be invoked. Status matches take precedence over default matches. # If there are two blocks with a matching status, the pass/fail block takes precedence # over the all block. # # == Matching based on success or failure # # The ResultMatcher matches calls to {#pass} to a {Result} that returns +true+ # for {Result#success?}, calls to {#fail} to a {Result} that returns +true+ # for {Result#failure?}, and calls to {#all} to a {Result} in either state. # # A matching call to {#pass} or {#fail} takes precedence over matching calls to {#all} # # == Matching based on status # # Additionally, the ResultMatcher can also match by the {Result#status}. If a status # or statuses is passed to {#pass}, {#fail}, or {#all}, these will only be invoked if the # status of the {Result} matches the passed in status. # # If the default block is the same as the block for one of the statuses the status +:default+ # can be used to indicate which block should be used as the default. Successful status matches # take precedence over default matchers. # # == Both pass and fail must be handled # # If the block form of a {Command.call} is invoked, both the block must handle the default # status for both a {Result#success?} and a {Result#failure?}. If both are not handled, # the matching block will not be invoked and a {CommandResultNotHandledError} will be # raised. # # @example Matcher invokes the matching pass block, with precedence given to {#pass} and {#fail} # # Call produces a successful Result # SuccessCommand.call do |m| # m.pass { puts "Pass!" } # m.fail { puts "Fail!" } # m.all { puts "All!" } # will never be invoked, due to both a pass and fail response existing # end # # => prints "Pass!" # # @example Matcher invokes the matching status of the result object, or the default # # Call produces a successful Result with status :ok # SuccessCommand.call do |m| # m.pass(:ok) { puts "Pass, OK!" } # m.pass { puts "Pass, default!" } # m.fail { puts "Fail!" } # end # # => prints "Pass, OK!" # # # Call produces a successful Result with status :created # SuccessCommand.call do |m| # m.pass(:ok) { puts "Pass, OK!" } # m.pass { puts "Pass, default!" } # m.fail { puts "Fail!" } # end # # => prints "Pass, default!" # # @example Matcher invokes the explicitly indicated default block # # Call produces a successful Result with status :created # SuccessCommand.call do |m| # m.pass(:ok, :default) { puts "Pass, OK!" } # m.pass(:great) { puts "Pass, default!" } # m.fail { puts "Fail!" } # end # # => prints "Pass, OK!" # # @example Matcher must handle both pass and fail defaults. # # Call produces a successful Result with status :ok # SuccessCommand.call do |m| # m.pass(:ok) { puts "Pass, OK!"} # m.fail { puts "Fail!" } # end # # => raises CommandResultNotHandledError (because no default pass was provided) # # # Call produces a successful Result with status :ok # SuccessCommand.call do |m| # m.pass(:ok, :default) { puts "Pass, OK!"} # m.fail { puts "Fail!" } # end # # => prints "Pass, OK!" # # # Call produces a successful Result with status :ok # SuccessCommand.call do |m| # m.pass(:ok) { puts "Pass, OK!"} # m.all { puts "All!" } # end # # => prints "Pass, OK!" # # # Call produces a successful Result with status :ok # SuccessCommand.call do |m| # m.pass(:ok, :default) { puts "Pass, OK!"} # end # # => raises CommandResultNotHandledError (because no default fail was provided) class ResultMatcher attr_reader :result, :command # @api private def initialize(result, command) @result = result @command = command @status = result.status || :default @handled_default_pass = false @handled_default_fail = false # These are set to false if they are never set. If they are set to `nil` that # means the block intentionally passed `nil` as the block to be executed. @matching_block = false @matching_all = false @default_block = false @default_all = false @ensure_block = false end # Provide a block that should be invoked if the {Result} is a success. # # @param statuses [Array<status>] Statuses that should be compared to the {Result}. If # any of provided statuses match the {Result} this block will be considered a match. # The symbol +:default+ can also be used to indicate that this should match any {Result} # not matched by other matchers. # # If no value is provided for statuses it defaults to +:default+. def pass(*statuses, &block) statuses << :default if statuses.empty? @handled_default_pass ||= statuses.include?(:default) block_is_match = @result.success? && statuses.include?(@status) block_is_default = @result.success? && statuses.include?(:default) @matching_block = block if block_is_match @default_block = block if block_is_default end # Provide a block that should be invoked if the {Result} is a failure. # # @param statuses [Array<status>] Statuses that should be compared to the {Result}. If # any of provided statuses match the {Result} this block will be considered a match. # The symbol +:default+ can also be used to indicate that this should match any {Result} # not matched by other matchers. # # If no value is provided for statuses it defaults to +:default+. def fail(*statuses, &block) statuses << :default if statuses.empty? @handled_default_fail ||= statuses.include?(:default) block_is_match = @result.failure? && statuses.include?(@status) block_is_default = @result.failure? && statuses.include?(:default) @matching_block = block if block_is_match @default_block = block if block_is_default end # Provide a block that should be invoked for any {Result}. This has a lower precedence that # either {#pass} or {#fail}. # # @param statuses [Array<status>] Statuses that should be compared to the {Result}. If # any of provided statuses match the {Result} this block will be considered a match. # The symbol +:default+ can also be used to indicate that this should match any {Result} # not matched by other matchers. # # If no value is provided for statuses it defaults to +:default+. def all(*statuses, &block) statuses << :default if statuses.empty? @handled_default_pass ||= statuses.include?(:default) @handled_default_fail ||= statuses.include?(:default) block_is_match = statuses.include?(@status) block_is_default = statuses.include?(:default) @matching_all = block if block_is_match @default_all = block if block_is_default end # Provide a block that should be always be invoked after other blocks have executed. This block # will be invoked even if the other block raises an error. def ensure(&block) @ensure_block = block end # @return Whether both the pass and the fail defaults have been handled. # # @api private def handled_defaults? return @handled_default_pass && @handled_default_fail end # Executes the provided block that best matched the {Result}. If no block matched # nothing is executed # # @api private def execute_matching_block if @matching_block != false # nil should pass this test run_block(@matching_block) elsif @matching_all != false run_block(@matching_all) elsif @default_block != false run_block(@default_block) elsif @default_all run_block(@default_all) end end # Executes the ensure block if one exists. # @api private def execute_ensure_block run_block(@ensure_block) if @ensure_block != false # nil should pass this test end private def run_block(block) block.call(@result.value, @result, @command) if block # explicit nil should fail this test end end end