Allow Dry::Matcher blocks to be shared/nested

We’re using Dry::Transaction extensively within our Rails application, specifially for controller actions and background jobs. Most of our controller actions invoke a Transaction, and then render a response/status based on the Result of the Transaction. The bulk of Transactions invoked from the controller action have a similar set of steps (eg :authorize, :validate, etc…). To dry up the error handling code, we extracted the common failure handler to a separate method:

# Controller action
def update
  txn.call(params) do |on|
    on.success           { ... }
    on.failure(:special) { ... }
    on.failure           { handle_common_failures(on) }
  end
end

# Shared helper
def handle_common_failures(on)
  on.failure(:authorize) { render status: :not_found}
  on.failure(:validate)  { |errors| render error: errors, status: :unprocessable_entity }
  on.failure do |err|
    status = case err
             when ActiveRecord::RecordInvalid, Dry::Schema::Result
               :unprocessable_entity
             when ActiveRecord::RecordNotFound
               :not_found
             else
               :forbidden
             end

    render error: err, status: status
  end
end

However, this doesn’t work out-of-the box with Dry::Matchers, because the @output of each branch gets checked if its defined? (dry-matcher/evaluator.rb at master · dry-rb/dry-matcher · GitHub). What’s happening is that in the controller action the on.failure gets invoked, which defines @output (to nil) then yields to our helper. Then when we try to invoke a different set of matchers in the helper, @output is already defined, so none of those matcher branches are attempted.

We’ve monkey-patched Dry::Matcher::Evaluator#method_missing to separately check if we’ve “completed” a matcher branch (with the @matched ivar), rather than if it has been started:

class Dry::Matcher::Evaluator 
  def method_missing(name, *args, &block) 
    kase = @cases.fetch(name) { return super }

    @unhandled_cases.delete name

    return @output if @matched

    kase.(@result, args) do |result|
      @output = yield(result)
      @matched = true
      @output
    end
  end
end

We’ve been running with this monkey-patch for years (June 2018 according to the git history). I’m wondering if I opened a PR with the implementation in our monkey-patch, is it something that would be accepted upstream?

Couldn’t you call handle_common_failures unnested?

def update
  txn.call(params) do |on|
    on.success           { ... }
    on.failure(:special) { ... }
    handle_common_failures(on)
  end
end

Yeah, I suppose that’s an option. It lacks the symmetry of having each branch of the matcher within a block in the controller action, though. Also, I simplified what we’re actually doing for the purposes of this question, I debated if I should go with the simplest possible example, or the more Rails-centric code we’re actually using. Instead of the helper, we use an ActionController::Renderer called :failure that takes the matcher, so the symmetry of all the render ... blocks reads very nicely:

def update
  txn.call(params) do |on|
    on.success           { |post| render post }
    on.failure(:special) { |msg|  render text: msg, status: :forbidden }
    on.failure           { |_|    render failure: on  }
  end
end

Honestly, since the Ruby language has added pattern-matching, I would expect dry-matcher to be deprecated and mothballed.