μ-case (Micro::Case)
Create simple and powerful use cases as objects.
The main goals of this project are:
- Be simple to use and easy to learn (input >> process/transform >> output).
- Promove referential transparency (transforming instead of modifying) and data integrity.
- No callbacks (before, after, around...).
- Solve complex business logic using a composition of use cases.
Table of Contents <!-- omit in toc -->
- μ-case (Micro::Case)
- Required Ruby version
- Dependencies
- Installation
- Usage
- How to define a use case?
- What is a
Micro::Case::Result? - How to compose uses cases to represents complex ones?
- What is a strict use case?
- Is there some feature to auto handle exceptions inside of a use case or flow?
- How to validate use case attributes?
- Examples
- Comparisons
- Benchmarks
- Development
- Contributing
- License
- Code of Conduct
Required Ruby version
>= 2.2.0
Dependencies
This project depends on Micro::Attribute gem. It is used to define the use case attributes.
Installation
Add this line to your application's Gemfile:
gem 'u-case'
And then execute:
$ bundle
Or install it yourself as:
$ gem install u-case
Usage
How to define a use case?
class Multiply < Micro::Case
# 1. Define its input as attributes
attributes :a, :b
# 2. Define the method `call!` with its business logic
def call!
# 3. Wrap the use case result/output using the `Success()` or `Failure()` methods
if a.is_a?(Numeric) && b.is_a?(Numeric)
Success(a * b)
else
Failure { '`a` and `b` attributes must be numeric' }
end
end
end
#==========================#
# Calling a use case class #
#==========================#
# Success result
result = Multiply.call(a: 2, b: 2)
result.success? # true
result.value # 4
# Failure result
bad_result = Multiply.call(a: 2, b: '2')
bad_result.failure? # true
bad_result.value # "`a` and `b` attributes must be numeric"
#-----------------------------#
# Calling a use case instance #
#-----------------------------#
result = Multiply.new(a: 2, b: 3).call
result.value # 6
# Note:
# ----
# The result of a Micro::Case.call
# is an instance of Micro::Case::Result
What is a Micro::Case::Result?
A Micro::Case::Result stores the use cases output data. These are their main methods:
#success?returns true if is a successful result.#failure?returns true if is an unsuccessful result.#valuethe result value itself.#typea Symbol which gives meaning for the result, this is useful to declare different types of failures or success.#on_successor#on_failureare hook methods that help you define the application flow.#use_caseif is a failure result, the use case responsible for it will be accessible through this method. This feature is handy to handle a flow failure (this topic will be covered ahead).
What are the default Micro::Case::Result types?
Every result has a type and these are the defaults:
:okwhen success:error/:exceptionwhen failures
class Divide < Micro::Case
attributes :a, :b
def call!
invalid_attributes.empty? ? Success(a / b) : Failure(invalid_attributes)
rescue => e
Failure(e)
end
private def invalid_attributes
attributes.select { |_key, value| !value.is_a?(Numeric) }
end
end
# Success result
result = Divide.call(a: 2, b: 2)
result.type # :ok
result.value # 1
result.success? # true
result.use_case # raises `Micro::Case::Error::InvalidAccessToTheUseCaseObject: only a failure result can access its own use case`
# Failure result (type == :error)
bad_result = Divide.call(a: 2, b: '2')
bad_result.type # :error
bad_result.value # {"b"=>"2"}
bad_result.failure? # true
bad_result.use_case # #<Divide:0x0000 @__attributes={"a"=>2, "b"=>"2"}, @a=2, @b="2", @__result=#<Micro::Case::Result:0x0000 @use_case=#<Divide:0x0000 ...>, @type=:error, @value={"b"=>"2"}, @success=false>>
# Failure result (type == :exception)
err_result = Divide.call(a: 2, b: 0)
err_result.type # :exception
err_result.value # <ZeroDivisionError: divided by 0>
err_result.failure? # true
err_result.use_case # #<Divide:0x0000 @__attributes={"a"=>2, "b"=>0}, @a=2, @b=0, @__result=#<Micro::Case::Result:0x0000 @use_case=#<Divide:0x0000 ...>, @type=:exception, @value=#<ZeroDivisionError: divided by 0>, @success=false>>
# Note:
# ----
# Any Exception instance which is wrapped by
# the Failure() method will receive `:exception` instead of the `:error` type.
How to define custom result types?
Answer: Use a symbol as the argument of Success(), Failure() methods and declare a block to set their values.
class Multiply < Micro::Case
attributes :a, :b
def call!
return Success(a * b) if a.is_a?(Numeric) && b.is_a?(Numeric)
Failure(:invalid_data) do
attributes.reject { |_, input| input.is_a?(Numeric) }
end
end
end
# Success result
result = Multiply.call(a: 3, b: 2)
result.type # :ok
result.value # 6
result.success? # true
# Failure result
bad_result = Multiply.call(a: 3, b: '2')
bad_result.type # :invalid_data
bad_result.value # {"b"=>"2"}
bad_result.failure? # true
Is it possible to define a custom result type without a block?
Answer: Yes, it is. But only for failure results!
class Multiply < Micro::Case
attributes :a, :b
def call!
return Failure(:invalid_data) unless a.is_a?(Numeric) && b.is_a?(Numeric)
Success(a * b)
end
end
result = Multiply.call(a: 2, b: '2')
result.failure? # true
result.value # :invalid_data
result.type # :invalid_data
result.use_case.attributes # {"a"=>2, "b"=>"2"}
# Note:
# ----
# This feature is handy to handle failures in a flow
# (this topic will be covered ahead).
How to use the result hooks?
As mentioned earlier, the Micro::Case::Result has two methods to improve the flow control. They are: #on_success, on_failure.
The examples below show how to use them:
class Double < Micro::Case
attribute :number
def call!
return Failure(:invalid) { 'the number must be a numeric value' } unless number.is_a?(Numeric)
return Failure(:lte_zero) { 'the number must be greater than 0' } if number <= 0
Success(number * 2)
end
end
#================================#
# Printing the output if success #
#================================#
Double
.call(number: 3)
.on_success { |number| p number }
.on_failure(:invalid) { |msg| raise TypeError, msg }
.on_failure(:lte_zero) { |msg| raise ArgumentError, msg }
# The output because it is a success:
# 6
#=============================#
# Raising an error if failure #
#=============================#
Double
.call(number: -1)
.on_success { |number| p number }
.on_failure { |_result, use_case| puts "#{use_case.class.name} was the use case responsible for the failure" }
.on_failure(:invalid) { |msg| raise TypeError, msg }
.on_failure(:lte_zero) { |msg| raise ArgumentError, msg }
# The outputs will be:
#
# 1. Prints the message: Double was the use case responsible for the failure
# 2. Raises the exception: ArgumentError (the number must be greater than 0)
# Note:
# ----
# The use case responsible for the failure will be accessible as the second hook argument
Why the on_failure result hook exposes a different kind of data?
Answer: To allow you to define how to handle the program flow using some
conditional statement (like an if, case/when).
class Double < Micro::Case
attribute :number
def call!
return Failure(:invalid) unless number.is_a?(Numeric)
return Failure(:lte_zero) { number } if number <= 0
Success(number * 2)
end
end
#=================================#
# Using the result type and value #
#=================================#
Double
.call(-1)
.on_failure do |result, use_case|
case result.type
when :invalid then raise TypeError, 'the number must be a numeric value'
when :lte_zero then raise ArgumentError, "the number `#{result.value}` must be greater than 0"
else raise NotImplementedError
end
end
# The output will be the exception:
#
# ArgumentError (the number `-1` must be greater than 0)
#=====================================================#
# Using decomposition to access result value and type #
#=====================================================#
# The syntax to decompose an Array can be used in methods, blocks and assigments.
# If you doesn't know that, check out:
# https://ruby-doc.org/core-2.2.0/doc/syntax/assignment_rdoc.html#label-Array+Decomposition
#
# And the object exposed in the hook failure can be decomposed using this syntax. e.g:
Double
.call(-2)
.on_failure do |(value, type), use_case|
case type
when :invalid then raise TypeError, 'the number must be a numeric value'
when :lte_zero then raise ArgumentError, "the number `#{value}` must be greater than 0"
else raise NotImplementedError
end
end
# The output will be the exception:
#
# ArgumentError (the number `-2` must be greater than 0)
What happens if a result hook is declared multiple times?
Answer: The hook always will be triggered if it matches the result type.
class Double < Micro::Case
attributes :number
def call!
return Failure(:invalid) { 'the number must be a numeric value' } unless number.is_a?(Numeric)
Success(:computed) { number * 2 }
end
end
result = Double.call(number: 3)
result.value # 6
result.value * 4 # 24
accum = 0
result.on_success { |number| accum += number }
.on_success { |number| accum += number }
.on_success(:computed) { |number| accum += number }
.on_success(:computed) { |number| accum += number }
accum # 24
result.value * 4 == accum # true
How to compose uses cases to represents complex ones?
In this case, this will be a flow (Micro::Case::Flow).
The main idea of this feature is to use/reuse use cases as steps of a new use case.
module Steps
class ConvertTextToNumbers < Micro::Case
attribute :numbers
def call!
if numbers.all? { |value| String(value) =~ /\d+/ }
Success(numbers: numbers.map(&:to_i))
else
Failure('numbers must contain only numeric types')
end
end
end
class Add2 < Micro::Case::Strict
attribute :numbers
def call!
Success(numbers: numbers.map { |number| number + 2 })
end
end
class Double < Micro::Case::Strict
attribute :numbers
def call!
Success(numbers: numbers.map { |number| number * 2 })
end
end
class Square < Micro::Case::Strict
attribute :numbers
def call!
Success(numbers: numbers.map { |number| number * number })
end
end
end
#---------------------------------------------#
# Creating a flow using the collection syntax #
#---------------------------------------------#
Add2ToAllNumbers = Micro::Case::Flow([
Steps::ConvertTextToNumbers,
Steps::Add2
])
result = Add2ToAllNumbers.call(numbers: %w[1 1 2 2 3 4])
p result.success? # true
p result.value # {:numbers => [3, 3, 4, 4, 5, 6]}
#---------------------------------------------------#
# An alternative way to create a flow using classes #
#---------------------------------------------------#
class DoubleAllNumbers
include Micro::Case::Flow
flow Steps::ConvertTextToNumbers, Steps::Double
end
DoubleAllNumbers
.call(numbers: %w[1 1 b 2 3 4])
.on_failure { || p } # "numbers must contain only numeric types"
#-------------------------------------------------------------#
# Another way to create a flow using the composition operator #
#-------------------------------------------------------------#
SquareAllNumbers =
Steps::ConvertTextToNumbers >> Steps::Square
SquareAllNumbers
.call(numbers: %w[1 1 2 2 3 4])
.on_success { |value| p value[:numbers] } # [1, 1, 4, 4, 9, 16]
# Note:
# ----
# When happening a failure, the use case responsible
# will be accessible in the result
result = SquareAllNumbers.call(numbers: %w[1 1 b 2 3 4])
result.failure? # true
result.use_case.is_a?(Steps::ConvertTextToNumbers) # true
result.on_failure do |, use_case|
puts "#{use_case.class.name} was the use case responsible for the failure" # Steps::ConvertTextToNumbers was the use case responsible for the failure
end
Is it possible to compose a use case flow with other ones?
Answer: Yes, it is.
module Steps
class ConvertTextToNumbers < Micro::Case
attribute :numbers
def call!
if numbers.all? { |value| String(value) =~ /\d+/ }
Success(numbers: numbers.map(&:to_i))
else
Failure('numbers must contain only numeric types')
end
end
end
class Add2 < Micro::Case::Strict
attribute :numbers
def call!
Success(numbers: numbers.map { |number| number + 2 })
end
end
class Double < Micro::Case::Strict
attribute :numbers
def call!
Success(numbers: numbers.map { |number| number * 2 })
end
end
class Square < Micro::Case::Strict
attribute :numbers
def call!
Success(numbers: numbers.map { |number| number * number })
end
end
end
Add2ToAllNumbers = Steps::ConvertTextToNumbers >> Steps::Add2
DoubleAllNumbers = Steps::ConvertTextToNumbers >> Steps::Double
SquareAllNumbers = Steps::ConvertTextToNumbers >> Steps::Square
DoubleAllNumbersAndAdd2 = DoubleAllNumbers >> Steps::Add2
SquareAllNumbersAndAdd2 = SquareAllNumbers >> Steps::Add2
SquareAllNumbersAndDouble = SquareAllNumbersAndAdd2 >> DoubleAllNumbers
DoubleAllNumbersAndSquareAndAdd2 = DoubleAllNumbers >> SquareAllNumbersAndAdd2
SquareAllNumbersAndDouble
.call(numbers: %w[1 1 2 2 3 4])
.on_success { |value| p value[:numbers] } # [6, 6, 12, 12, 22, 36]
DoubleAllNumbersAndSquareAndAdd2
.call(numbers: %w[1 1 2 2 3 4])
.on_success { |value| p value[:numbers] } # [6, 6, 18, 18, 38, 66]
Note: You can blend any of the available syntaxes/approaches to create use case flows - examples.
Is it possible a flow accumulates its input and merges each success result to use as the argument of their use cases?
Answer: Yes, it is! Check out these test examples Micro::Case::Flow and Micro::Case::Safe::Flow to see different use cases sharing their own data.
What is a strict use case?
Answer: Is a use case which will require all the keywords (attributes) on its initialization.
class Double < Micro::Case::Strict
attribute :numbers
def call!
Success(numbers.map { |number| number * 2 })
end
end
Double.call({})
# The output will be the following exception:
# ArgumentError (missing keyword: :numbers)
Is there some feature to auto handle exceptions inside of a use case or flow?
Answer: Yes, there is!
Use cases:
Like Micro::Case::Strict the Micro::Case::Safe is another kind of use case. It has the ability to auto intercept any exception as a failure result. e.g:
require 'logger'
AppLogger = Logger.new(STDOUT)
class Divide < Micro::Case::Safe
attributes :a, :b
def call!
return Success(a / b) if a.is_a?(Integer) && b.is_a?(Integer)
Failure(:not_an_integer)
end
end
result = Divide.call(a: 2, b: 0)
result.type == :exception # true
result.value.is_a?(ZeroDivisionError) # true
result.on_failure(:exception) do |exception|
AppLogger.error(exception.) # E, [2019-08-21T00:05:44.195506 #9532] ERROR -- : divided by 0
end
# Note:
# ----
# If you need to handle a specific error,
# I recommend the usage of a case statement. e,g:
result.on_failure(:exception) do |exception, use_case|
case exception
when ZeroDivisionError then AppLogger.error(exception.)
else AppLogger.debug("#{use_case.class.name} was the use case responsible for the exception")
end
end
# Another note:
# ------------
# It is possible to rescue an exception even when is a safe use case.
# Examples: https://github.com/serradura/u-case/blob/5a85fc238b63811a32737493dc6c59965f92491d/test/micro/case/safe_test.rb#L95-L123
Flows:
As the safe use cases, safe flows can intercept an exception in any of its steps. These are the ways to define one:
module Users
Create = ProcessParams & ValidateParams & Persist & SendToCRM
end
# Note:
# The ampersand is based on the safe navigation operator. https://ruby-doc.org/core-2.6/doc/syntax/calling_methods_rdoc.html#label-Safe+navigation+operator
# The alternatives are:
module Users
class Create
include Micro::Case::Safe::Flow
flow ProcessParams, ValidateParams, Persist, SendToCRM
end
end
# or
module Users
Create = Micro::Case::Safe::Flow([
ProcessParams,
ValidateParams,
Persist,
SendToCRM
])
end
How to validate use case attributes?
Requirement:
To do this your application must have the activemodel >= 3.2 as a dependency.
#
# By default, if your application has the activemodel as a dependency,
# any kind of use case can use it to validate their attributes.
#
class Multiply < Micro::Case
attributes :a, :b
validates :a, :b, presence: true, numericality: true
def call!
return Failure(:validation_error) { {errors: self.errors} } unless valid?
Success(number: a * b)
end
end
#
# But if do you want an automatic way to fail
# your use cases on validation errors, you can use:
# In some file. e.g: A Rails initializer
require 'u-case/with_validation' # or require 'micro/case/with_validation'
# In the Gemfile
gem 'u-case', require: 'u-case/with_validation'
# Using this approach, you can rewrite the previous example with less code. e.g:
class Multiply < Micro::Case
attributes :a, :b
validates :a, :b, presence: true, numericality: true
def call!
Success(number: a * b)
end
end
# Note:
# ----
# After requiring the validation mode, the
# Micro::Case::Strict and Micro::Case::Safe classes will inherit this new behavior.
If I enable the auto validation, is it possible to disable it only in some specific use case classes?
Answer: Yes, it is. To do this, you only need to use the disable_auto_validation macro. e.g:
require 'u-case/with_validation'
class Multiply < Micro::Case
disable_auto_validation
attribute :a
attribute :b
validates :a, :b, presence: true, numericality: true
def call!
Success(number: a * b)
end
end
Multiply.call(a: 2, b: 'a')
# The output will be the following exception:
# TypeError (String can't be coerced into Integer)
Examples
- Rescuing an exception inside of use cases
-
An example of flow in how to define steps to sanitize, validate, and persist some input data.
-
A more complex example which use rake tasks to demonstrate how to handle user data, and how to use different failures type to control the program flow.
Comparisons
Check it out implementations of the same use case with different gems/abstractions.
Benchmarks
interactor VS u-case
https://github.com/serradura/u-case/tree/master/benchmarks/interactor

Development
After checking out the repo, run bin/setup to install dependencies. Then, run ./test.sh to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/serradura/u-case. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the Micro::Case project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.