Ruby Gem Build Status Maintainability Test Coverage

μ-case (Micro::Case) <!-- omit in toc -->

Create simple and powerful use cases as objects.

The main project goals are:

  1. Easy to use and easy to learn (input >> process >> output).
  2. Promote referential transparency (transforming instead of modifying) and data integrity.
  3. No callbacks (e.g: before, after, around).
  4. Solve complex business logic, by allowing the composition of use cases.
  5. Be fast and optimized (Check out the benchmarks section).

Note: Check out the repo https://github.com/serradura/from-fat-controllers-to-use-cases to see a Rails application that uses this gem to handle its business logic.

Table of Contents <!-- omit in toc -->

Required Ruby version

>= 2.2.0

Dependencies

  1. kind gem.

    A simple type system (at runtime) for Ruby.

    Used to validate method inputs, expose Kind.of.Micro::Case::Result type checker and its activemodel validation module is auto required by u-case/with_activemodel_validation mode.

  2. u-attributes gem.

    This gem allows defining read-only attributes, that is, your objects will have only getters to access their attributes data. 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

Micro::Case - 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

⬆️ Back to Top

Micro::Case::Result - What is a use 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.
  • #value the result value itself.
  • #type a Symbol which gives meaning for the result, this is useful to declare different types of failures or success.
  • #on_success or #on_failure are hook methods that help you define the application flow.
  • #use_case if 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).
  • #then allows if the current result is a success, the then method will allow to applying a new use case for its value.

⬆️ Back to Top

What are the default result types?

Every result has a type and these are the defaults:

  • :ok when success
  • :error/:exception when 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.

⬆️ Back to Top

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

⬆️ Back to Top

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).

⬆️ Back to Top

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 failure hook (without a type) 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)

⬆️ Back to Top

What happens if a result hook was 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 use the Micro::Case::Result#then method?

class ForbidNegativeNumber < Micro::Case
  attribute :number

  def call!
    return Success { attributes } if number >= 0

    Failure { attributes }
  end
end

class Add3 < Micro::Case
  attribute :number

  def call!
    Success { { number: number + 3 } }
  end
end

result1 =
  ForbidNegativeNumber
    .call(number: -1)
    .then(Add3)

result1.type     # :error
result1.value    # {'number' => -1}
result1.failure? # true

# ---

result2 =
  ForbidNegativeNumber
    .call(number: 1)
    .then(Add3)

result2.type     # :ok
result2.value    # {'number' => 4}
result2.success? # true

⬆️ Back to Top

Micro::Case::Flow - How to compose use cases?

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 < Micro::Case
  flow Steps::ConvertTextToNumbers,
       Steps::Double
end

DoubleAllNumbers
  .call(numbers: %w[1 1 b 2 3 4])
  .on_failure { |message| p message } # "numbers must contain only numeric types"

# !------------------------------------ ! #
# ! Deprecated: Micro::Case::Flow mixin ! #
# !-------------------------------------! #

# The code below still works, but it will output a warning message:
# Deprecation: Micro::Case::Flow mixin is being deprecated, please use `Micro::Case` inheritance instead.

class DoubleAllNumbers
  include Micro::Case::Flow

  flow Steps::ConvertTextToNumbers,
       Steps::Double
end

# Note: This feature will be removed in the next major release (3.0)

#-------------------------------------------------------------#
# 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 |_message, 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

⬆️ Back to Top

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.

⬆️ Back to Top

Is it possible a flow accumulates its input and merges each success result to use as the argument of the next use cases?

Answer: Yes, it is! Look at the example below to understand how the data accumulation works inside of the flow execution.

module Users
  class Find < Micro::Case
    attribute :email

    def call!
      user = User.find_by(email: email)

      return Success { { user: user } } if user

      Failure(:user_not_found)
    end
  end
end

module Users
  class ValidatePassword < Micro::Case::Strict
    attributes :user, :password

    def call!
      return Failure(:user_must_be_persisted) if user.new_record?
      return Failure(:wrong_password) if user.wrong_password?(password)

      return Success { attributes(:user) }
    end
  end
end

module Users
  Authenticate = Micro::Case::Flow([
    Find,
    ValidatePassword
  ])
end

Users::Authenticate
  .call(email: '[email protected]', password: 'password')
  .on_success { |result| (result[:user]) }
  .on_failure(:wrong_password) { |result| render status: 401 }
  .on_failure(:user_not_found) { |result| render status: 404 }

First, lets see the attribute of each use case:

class Users::Find < Micro::Case
  attribute :email
end

class Users::ValidatePassword < Micro::Case
  attributes :user, :password
end

As you can see the Users::ValidatePassword expects a user as its input. So, how does it receives the user? It receives the user from the Users::Find success result!

And this, is the power of use cases composition because the output of one flow will compose the input of the next use case in the flow!

input >> process >> output

Note: Check out these test examples Micro::Case::Flow and Micro::Case::Safe::Flow to see different use cases sharing their own data.

⬆️ Back to Top

How to understand what is happening during a flow execution?

Use Micro::Case::Result#transitions!

Let's use the previous section example to ilustrate how to use this feature.

user_authenticated =
  Users::Authenticate.call(email: '[email protected]', password: user_password)

user_authenticated.transitions
[
  {
    :use_case => {
      :class      => Users::Find,
      :attributes => { :email => "[email protected]" }
    },
    :success => {
      :type  => :ok,
      :value => {
        :user => #<User:0x00007fb57b1c5f88 @email="[email protected]" ...>
      }
    },
    :accessible_attributes => [ :email, :password ]
  },
  {
    :use_case => {
      :class      => Users::ValidatePassword,
      :attributes => {
        :user     => #<User:0x00007fb57b1c5f88 @email="[email protected]" ...>
        :password => "123456"
      }
    },
    :success => {
      :type  => :ok,
      :value => {
        :user => #<User:0x00007fb57b1c5f88 @email="[email protected]" ...>
      }
    },
    :accessible_attributes => [ :email, :password, :user ]
  }
]

The example above shows the output generated by the Micro::Case::Result#transitions. With it is possible to analyze the use cases execution order and what were the given inputs (attributes) and outputs (success.value) in the entire execution.

And look up the accessible_attributes property, because it shows whats attributes are accessible in that flow step. For example, in the last step, you can see that the accessible_attributes increased because of the flow data accumulation.

Note: The Micro::Case::Result#then increments the Micro::Case::Result#transitions.

PS: Use the Micro::Case::Result.disable_transition_tracking global feature toggle to disable this feature (use once) and increase the use cases' performance.

Micro::Case::Result#transitions schema
[
  {
    use_case: {
      class:      <Micro::Case>,# Use case which was executed
      attributes: <Hash>        # (Input) The use case's attributes
    },
    [success:, failure:] => {   # (Output)
      type:  <Symbol>,          # Result type. Defaults:
                                # Success = :ok, Failure = :error/:exception
      value: <Hash>             # The data returned by the use case
    },
    accessible_attributes: <Array>, # Properties that can be accessed by the use case's attributes,
                                    # starting with Hash used to invoke it and which are incremented
                                    # with each result value of the flow's use cases.
  }
]

Is it possible to declare a flow which includes the use case itself?

Answer: Yes, it is! You can use the self.call! macro. e.g:

class ConvertTextToNumber < Micro::Case
  attribute :text

  def call!
    Success { { number: text.to_i } }
  end
end

class ConvertNumberToText < Micro::Case
  attribute :number

  def call!
    Success { { text: number.to_s } }
  end
end

class Double < Micro::Case
  flow ConvertTextToNumber,
       self.call!,
       ConvertNumberToText

  attribute :number

  def call!
    Success { { number: number * 2 } }
  end
end

result = Double.call(text: '4')

result.success? # true
result.value    # "8"

# NOTE: This feature can be used with the Micro::Case::Safe.
#       Checkout the test: test/micro/case/safe/flow/with_classes/using_itself_test.rb

⬆️ Back to Top

Micro::Case::Strict - 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)

⬆️ Back to Top

Micro::Case::Safe - 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.message) # 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.message)
  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

⬆️ Back to Top

Micro::Case::Safe::Flow

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 to declare a safe flow are:

module Users
  Create = Micro::Case::Safe::Flow([
    ProcessParams,
    ValidateParams,
    Persist,
    SendToCRM
  ])
end

# or within classes

module Users
  class Create < Micro::Case::Safe
    flow ProcessParams,
         ValidateParams,
         Persist,
         SendToCRM
  end
end

# !------------------------------------------ ! #
# ! Deprecated: Micro::Case::Safe::Flow mixin ! #
# !-------------------------------------------! #

# The code below still works, but it will output a warning message:
# Deprecation: Micro::Case::Flow mixin is being deprecated, please use `Micro::Case` inheritance instead.

module Users
  class Create
    include Micro::Case::Safe::Flow

    flow ProcessParams, ValidateParams, Persist, SendToCRM
  end
end

# Note: This feature will be removed in the next major release (3.0)

⬆️ Back to Top

Micro::Case::Result#on_exception

In functional programming errors/exceptions are handled as regular data, the idea is to transform the output even when it happens an unexpected behavior. For many, exceptions are very similar to the GOTO statement, jumping the application flow to paths which could be difficult to figure out how things work in a system.

To address this the Micro::Case::Result has a special hook #on_exception to helping you to handle the control flow in the case of exceptions.

Note this feature will work better if you use it with a Micro::Case::Safe use case/flow.

How does it work?

class Divide < Micro::Case::Safe
  attributes :a, :b

  def call!
    Success(division: a / b)
  end
end

Divide
  .call(a: 2, b: 0)
  .on_success { |result| puts result[:division] }
  .on_exception(TypeError) { puts 'Please, use only numeric attributes.' }
  .on_exception(ZeroDivisionError) { |_error| puts "Can't divide a number by 0." }
  .on_exception { |_error, _use_case| puts 'Oh no, something went wrong!' }

# Output:
# -------
# Can't divide a number by 0
# Oh no, something went wrong!

Divide.
  .call(a: 2, b: '2').
  .on_success { |result| puts result[:division] }
  .on_exception(TypeError) { puts 'Please, use only numeric attributes.' }
  .on_exception(ZeroDivisionError) { |_error| puts "Can't divide a number by 0." }
  .on_exception { |_error, _use_case| puts 'Oh no, something went wrong!' }

# Output:
# -------
# Please, use only numeric attributes.
# Oh no, something went wrong!

As you can see, this hook has the same behavior of result.on_failure(:exception), but, the ideia here is to have a better communication in the code, making an explicit reference when some failure happened because of an exception.

⬆️ Back to Top

u-case/with_activemodel_validation - How to validate use case attributes?

Requirement:

To do this your application must have the activemodel >= 3.2, < 6.1.0 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_activemodel_validation' # or require 'micro/case/with_validation'

# In the Gemfile
gem 'u-case', require: 'u-case/with_activemodel_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 enabled the auto validation, is it possible to disable it only in 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_activemodel_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)

⬆️ Back to Top

Kind::Validator

The kind gem has a module to enable the validation of data type through ActiveModel validations. So, when you require the 'u-case/with_activemodel_validation', this module will require the Kind::Validator.

The example below shows how to validate the attributes data types.

class Todo::List::AddItem < Micro::Case
  attributes :user, :params

  validates :user, kind: User
  validates :params, kind: ActionController::Parameters

  def call!
    todo_params = Todo::Params.to_save(params)

    todo = user.todos.create(todo_params)

    Success { { todo: todo} }
  rescue ActionController::ParameterMissing => e
    Failure(:parameter_missing) { { message: e.message } }
  end
end

Benchmarks

Micro::Case

Best overall

The table below contains the average between the Success results and Failure results benchmarks.

Gem / Abstraction Iterations per second Comparison
Micro::Case 116629.7 The Fastest
Dry::Monads 101796.3 1.14x slower
Interactor 21230.5 5.49x slower
Trailblazer::Operation 16466.6 7.08x slower
Dry::Transaction 5069.5 23.00x slower

Success results

Gem / Abstraction Iterations per second Comparison
Dry::Monads 139352.5 The Fastest
Micro::Case 124749.4 1.12x slower
Interactor 28974.4 4.81x slower
Trailblazer::Operation 17275.6 8.07x slower
Dry::Transaction 5571.7 25.01x slower
Show the full benchmark/ips results. ```ruby # Warming up -------------------------------------- # Interactor 2.865k i/100ms # Trailblazer::Operation # 1.686k i/100ms # Dry::Monads 13.389k i/100ms # Dry::Transaction 551.000 i/100ms # Micro::Case 11.984k i/100ms # Micro::Case::Strict 9.102k i/100ms # Micro::Case::Safe 11.747k i/100ms # Calculating ------------------------------------- # Interactor 28.974k (± 2.7%) i/s - 146.115k in 5.046703s # Trailblazer::Operation # 17.276k (± 1.8%) i/s - 87.672k in 5.076609s # Dry::Monads 139.353k (± 2.5%) i/s - 709.617k in 5.095599s # Dry::Transaction 5.572k (± 3.6%) i/s - 28.101k in 5.050376s # Micro::Case 124.749k (± 1.9%) i/s - 635.152k in 5.093310s # Micro::Case::Strict 93.417k (± 4.8%) i/s - 473.304k in 5.081341s # Micro::Case::Safe 120.607k (± 3.2%) i/s - 610.844k in 5.070394s # Comparison: # Dry::Monads: 139352.5 i/s # Micro::Case: 124749.4 i/s - 1.12x slower # Micro::Case::Safe: 120607.3 i/s - 1.16x slower # Micro::Case::Strict: 93417.3 i/s - 1.49x slower # Interactor: 28974.4 i/s - 4.81x slower # Trailblazer::Operation: 17275.6 i/s - 8.07x slower # Dry::Transaction: 5571.7 i/s - 25.01x slower ```

https://github.com/serradura/u-case/blob/master/benchmarks/use_case/with_success_result.rb

Failure results

Gem / Abstraction Iterations per second Comparison
Micro::Case 108510.0 The Fastest
Dry::Monads 64240.1 1.69x slower
Trailblazer::Operation 15657.7 6.93x slower
Interactor 13486.7 8.05x slower
Dry::Transaction 4567.3 23.76x slower
Show the full benchmark/ips results. ```ruby # Warming up -------------------------------------- # Interactor 1.331k i/100ms # Trailblazer::Operation # 1.544k i/100ms # Dry::Monads 6.343k i/100ms # Dry::Transaction 456.000 i/100ms # Micro::Case 10.429k i/100ms # Micro::Case::Strict 8.109k i/100ms # Micro::Case::Safe 10.280k i/100ms # Calculating ------------------------------------- # Interactor 13.487k (± 1.9%) i/s - 67.881k in 5.035059s # Trailblazer::Operation # 15.658k (± 1.6%) i/s - 78.744k in 5.030427s # Dry::Monads 64.240k (± 1.8%) i/s - 323.493k in 5.037461s # Dry::Transaction 4.567k (± 1.3%) i/s - 23.256k in 5.092699s # Micro::Case 108.510k (± 2.3%) i/s - 542.308k in 5.000605s # Micro::Case::Strict 83.527k (± 1.4%) i/s - 421.668k in 5.049245s # Micro::Case::Safe 105.641k (± 3.7%) i/s - 534.560k in 5.067836s # Comparison: # Micro::Case: 108510.0 i/s # Micro::Case::Safe: 105640.6 i/s - same-ish: difference falls within error # Micro::Case::Strict: 83526.8 i/s - 1.30x slower # Dry::Monads: 64240.1 i/s - 1.69x slower # Trailblazer::Operation: 15657.7 i/s - 6.93x slower # Interactor: 13486.7 i/s - 8.05x slower # Dry::Transaction: 4567.3 i/s - 23.76x slower ```

https://github.com/serradura/u-case/blob/master/benchmarks/use_case/with_failure_result.rb


Micro::Case::Flow

Gems / Abstraction Success results Failure results
Micro::Case::Flow The Fastest The Fastest
Micro::Case::Safe::Flow 0x slower 0x slower
Interactor::Organizer 1.47x slower 5.51x slower

* The Dry::Monads, Dry::Transaction, Trailblazer::Operation are out of this analysis because all of them doesn't have this kind of feature.

Success results - Show the full benchmark/ips results. ```ruby # Warming up -------------------------------------- # Interactor::Organizer 4.880k i/100ms # Micro::Case::Flow 7.035k i/100ms # Micro::Case::Safe::Flow 7.059k i/100ms # Calculating ------------------------------------- # Interactor::Organizer 50.208k (± 1.3%) i/s - 253.760k in 5.055099s # Micro::Case::Flow 73.791k (± 0.9%) i/s - 372.855k in 5.053311s # Micro::Case::Safe::Flow 73.314k (± 1.1%) i/s - 367.068k in 5.007473s # Comparison: # Micro::Case::Flow: 73790.7 i/s # Micro::Case::Safe::Flow: 73313.7 i/s - same-ish: difference falls within error # Interactor::Organizer: 50207.7 i/s - 1.47x slower ```
Failure results - Show the full benchmark/ips results. ```ruby # Warming up -------------------------------------- # Interactor::Organizer 2.372k i/100ms # Micro::Case::Flow 12.802k i/100ms # Micro::Case::Safe::Flow 12.673k i/100ms # Calculating ------------------------------------- # Interactor::Organizer 24.522k (± 2.0%) i/s - 123.344k in 5.032159s # Micro::Case::Flow 135.122k (± 1.7%) i/s - 678.506k in 5.022903s # Micro::Case::Safe::Flow 133.980k (± 1.4%) i/s - 671.669k in 5.014181s # Comparison: # Micro::Case::Flow: 135122.0 i/s # Micro::Case::Safe::Flow: 133979.8 i/s - same-ish: difference falls within error # Interactor::Organizer: 24521.8 i/s - 5.51x slower ```

https://github.com/serradura/u-case/tree/master/benchmarks/flow

Comparisons

Check it out implementations of the same use case with different gems/abstractions.

⬆️ Back to Top

Examples

1️⃣ Rails App (API)

This project shows different kinds of architecture (one per commit), and in the last one, how to use the Micro::Case gem to handle the application business logic.

Link: https://github.com/serradura/from-fat-controllers-to-use-cases

2️⃣ CLI calculator

Rake tasks to demonstrate how to handle user data, and how to use different failure types to control the program flow.

Link: https://github.com/serradura/u-case/tree/master/examples/calculator

3️⃣ Users creation

An example of a use case flow that define steps to sanitize, validate, and persist its input data.

Link: https://github.com/serradura/u-case/blob/master/examples/users_creation.rb

4️⃣ Rescuing exception inside of the use cases

Link: https://github.com/serradura/u-case/blob/master/examples/rescuing_exceptions.rb

⬆️ Back to Top

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.