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.