dry-rails controller helpers

So, there’s a few things going on here. This is basically a copy-paste of the example code in the website, but the example doesn’t actually show you what "users.create" is aside from mentioning that it’s a dry-monad operation object.

The on.success block, however, does not come from dry-monads but dry-matcher, specifically the result matcher

Let me sketch out quickly what the rest of the owl might look like to help you understand how these pieces work together.

require "dry/monads"

module Users
  Class Create
    # adds Success(), Failure(), and wraps
    # new methods in do-notation
    include Dry::Monads[:result, :do]

    # this would be registered in your container
    # from elsewhere. `Deps` is dry-auto_inject
    # module builder
    include Deps["users.repo"]

    def call(params)
      # validate params, like
      # params = yield validate(params)
      # see dry-schema and dry-validation

      # create user record in persistence layer
      # replace "repo" with whatever persistence
      # you happen to use
      user = repo.create(params)

      # return successful result
      Success(user)
    rescue PersistenceError => err # whatever base-level error makes sense for a persistence failure
      # I always return Failures as a tuple for reasons that will become clear later
      # This is merely shorthand for Failure([:persist, err])
      Failure[:persist, err]
    end
  end
end

This is a very basic monadic command object that accepts the user params, creates a user record in the database, and returns a result object. (I’m glossing over why you would want to structure it this way – but there are good reasons I can get into if you want)

Result objects are great for composing commands together, because you just yield their return value to unwrap the monad (and it will halt and return a failure). But, dealing with monads in regular code can become a chore.

resolve("users.create")
  .bind { |user|
    render json: user
  }
  .or { |(type, err)|
    render json: { code: type, errors: err.message }, status: :unprocessable_entity
  }

This is where Dry::Matcher comes in.

require "dry/matcher"
require "dry/matcher/result_matcher"
require "dry/monads"

module Users
  class Create
    # unfortunately, these two are order-dependent!
    include Dry::Monads[:result, :do]
    include Dry::Matcher.for(:call, with: Dry::Matcher::ResultMatcher)
    include Deps["users.repo"]

    def call(params)
      # same as before
    end
  end
end

that is what allows you to use the matcher syntax

resolve("users.create").(safe_params[:user]) do |on|
  # matches Success monad result
  on.success do |user|
    render json: user
  end

  # matches first value of the failure tuple
  # you can have a separate matcher for each
  # kind of failure, or you can write a generic
  # matcher
  on.failure :persist do |_, err|
    render status: :unprocessable_entity
  end

  # catch-all failure matcher
  # you should always have one so your matchers
  # are exhaustive
  on.failure do |err|
    logger.warn "unhandled error: #{err.inspect}"
    render status: :internal_server_error
  end
end

In Ruby 3 I generally prefer pattern-matching to dry-matcher

case resolve("users.create").(safe_params[:user])
in Success(user)
  render json: user
in Failure[:persist, StandardError => err]
  logger.warn err.inspect
  render status: :unprocessable_entity
in Failure(err)
  logger.warn err.inspect
  render status: :internal_server_error
end

So that is a long-winded way of saying, this is nothing inherent to how monad operations work, it’s a general-purpose matcher tool that you can use in any class whenever you want. You can even define your own matchers with different sets of callbacks. It can be used to match whatever objects you want, not just monad types.