Represente casos de uso de forma simples e poderosa ao escrever código modular, expressivo e sequencialmente lógico.
Principais objetivos deste projeto:
Nota: Verifique o repo https://github.com/serradura/from-fat-controllers-to-use-cases para ver uma aplicação Ruby on Rails que utiliza esta gem para resolver as regras de negócio.
Micro::Case - Como definir um caso de uso?Micro::Case::Result - O que é o resultado de um caso de uso?
Micro::Case::Result#then?
Micro::Cases::Flow - Como compor casos de uso?
Micro::Case::Strict - O que é um caso de uso estrito?Micro::Case::Safe - Existe algum recurso para lidar automaticamente com exceções dentro de um caso de uso ou fluxo?
u-case/with_activemodel_validation - Como validar os atributos do caso de uso?
Micro::Case.config| u-case | branch | ruby | activemodel | u-attributes- |
|---|---|---|---|---|
| unreleased | main | >= 2.2.0 | >= 3.2, < 7.0 | >= 2.7, < 3.0 |
| 4.5.1 | v4.x | >= 2.2.0 | >= 3.2, < 7.0 | >= 2.7, < 3.0 |
| 3.1.0 | v3.x | >= 2.2.0 | >= 3.2, < 6.1 | ~> 1.1 |
| 2.6.0 | v2.x | >= 2.2.0 | >= 3.2, < 6.1 | ~> 1.1 |
| 1.1.0 | v1.x | >= 2.2.0 | >= 3.2, < 6.1 | ~> 1.1 |
Nota: O activemodel é uma dependência opcional, esse módulo que pode ser habilitado para validar os atributos dos casos de uso.
Gem kind.
Sistema de tipos simples (em runtime) para Ruby.
É usado para validar os inputs de alguns métodos do u-case, além de expor um validador de tipos através do activemodel validation (veja como habilitar).
u-attributes gem.
Essa gem permite definir atributos de leitura (read-only), ou seja, os seus objetos só terão getters para acessar os dados dos seus atributos. Ela é usada para definir os atributos dos casos de uso.
Adicione essa linha ao Gemfile da sua aplicação:
gem 'u-case', '~> 4.5.1'
E então execute:
$ bundle
Ou instale manualmente:
$ gem install u-case
class Multiply < Micro::Case
# 1. Defina o input como atributos
attributes :a, :b
# 2. Defina o método `call!` com a regra de negócio
def call!
# 3. Envolva o resultado do caso de uso com os métodos `Success(result: *)` ou `Failure(result: *)`
if a.is_a?(Numeric) && b.is_a?(Numeric)
Success result: { number: a * b }
else
Failure result: { message: '`a` and `b` attributes must be numeric' }
end
end
end
#===========================#
# Executando um caso de uso #
#===========================#
# Resultado de sucesso
result = Multiply.call(a: 2, b: 2)
result.success? # true
result.data # { number: 4 }
# Resultado de falha
bad_result = Multiply.call(a: 2, b: '2')
bad_result.failure? # true
bad_result.data # { message: "`a` and `b` attributes must be numeric" }
# Nota:
# ----
# O resultado de um Micro::Case.call é uma instância de Micro::Case::Result
Um Micro::Case::Result armazena os dados de output de um caso de uso. Esses são seus métodos:
#success? retorna true se for um resultado de sucesso.#failure? retorna true se for um resultado de falha.#use_case retorna o caso de uso responsável pelo resultado. Essa funcionalidade é útil para lidar com falhas em flows (esse tópico será abordado mais a frente).#type retorna um Symbol que dá significado ao resultado, isso é útil para declarar diferentes tipos de falha e sucesso.#data os dados do resultado (um Hash).#[] e #values_at são atalhos para acessar as propriedades do #data.#key? retorna true se a chave estiver present no #data.#value? retorna true se o valor estiver present no #data.#slice retorna um novo Hash que inclui apenas as chaves fornecidas. Se as chaves fornecidas não existirem, um Hash vazio será retornado.#on_success or #on_failure são métodos de hooks que te auxiliam a definir o fluxo da aplicação.#then este método permite aplicar novos casos de uso ao resultado atual se ele for sucesso. A ideia dessa feature é a criação de fluxos dinâmicos.#transitions retorna um array com todas as transformações que um resultado teve durante um flow.Nota: por conta de retrocompatibilidade, você pode usar o método
#valuecomo um alias para o método#data.
Todo resultado tem um tipo (#type), e estes são os valores padrões:
:ok em casos de sucesso;:error ou :exception em casos de falhas.class Divide < Micro::Case
attributes :a, :b
def call!
if invalid_attributes.empty?
Success result: { number: a / b }
else
Failure result: { invalid_attributes: invalid_attributes }
end
rescue => exception
Failure result: exception
end
private def invalid_attributes
attributes.select { |_key, value| !value.is_a?(Numeric) }
end
end
# Resultado de sucesso
result = Divide.call(a: 2, b: 2)
result.type # :ok
result.data # { number: 1 }
result.success? # true
result.use_case # #<Divide:0x0000 @__attributes={"a"=>2, "b"=>2}, @a=2, @b=2, @__result=...>
# Resultado de falha (type == :error)
bad_result = Divide.call(a: 2, b: '2')
bad_result.type # :error
bad_result.data # { invalid_attributes: { "b"=>"2" } }
bad_result.failure? # true
bad_result.use_case # #<Divide:0x0000 @__attributes={"a"=>2, "b"=>"2"}, @a=2, @b="2", @__result=...>
# Resultado de falha (type == :exception)
err_result = Divide.call(a: 2, b: 0)
err_result.type # :exception
err_result.data # { exception: <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>
# Nota:
# ----
# Toda instância de Exception será envolvida pelo método
# Failure(result: *) que receberá o tipo `:exception` ao invés de `:error`.
Resposta: Use um Symbol com argumento dos métodos Success(), Failure() e declare o result: keyword para definir os dados do resultado.
class Multiply < Micro::Case
attributes :a, :b
def call!
if a.is_a?(Numeric) && b.is_a?(Numeric)
Success result: { number: a * b }
else
Failure :invalid_data, result: {
attributes: attributes.reject { |_, input| input.is_a?(Numeric) }
}
end
end
end
# Resultado de sucesso
result = Multiply.call(a: 3, b: 2)
result.type # :ok
result.data # { number: 6 }
result.success? # true
# Resultado de falha
bad_result = Multiply.call(a: 3, b: '2')
bad_result.type # :invalid_data
bad_result.data # { attributes: {"b"=>"2"} }
bad_result.failure? # true
Resposta: Sim, é possível. Mas isso terá um comportamento especial por conta dos dados do resultado ser um hash com o tipo definido como chave e true como o valor.
class Multiply < Micro::Case
attributes :a, :b
def call!
if a.is_a?(Numeric) && b.is_a?(Numeric)
Success result: { number: a * b }
else
Failure(:invalid_data)
end
end
end
result = Multiply.call(a: 2, b: '2')
result.failure? # true
result.data # { :invalid_data => true }
result.type # :invalid_data
result.use_case.attributes # {"a"=>2, "b"=>"2"}
# Nota:
# ----
# Essa funcionalidade será muito útil para lidar com resultados de falha de um Flow
# (este tópico será coberto em breve).
Como mencionando anteriormente, o Micro::Case::Result tem dois métodos para melhorar o controle do fluxo da aplicação. São eles:
#on_success, on_failure.
Os exemplos abaixo os demonstram em uso:
class Double < Micro::Case
attribute :number
def call!
return Failure :invalid, result: { msg: 'number must be a numeric value' } unless number.is_a?(Numeric)
return Failure :lte_zero, result: { msg: 'number must be greater than 0' } if number <= 0
Success result: { number: number * 2 }
end
end
#================================#
# Imprimindo o output se sucesso #
#================================#
Double
.call(number: 3)
.on_success { |result| p result[:number] }
.on_failure(:invalid) { |result| raise TypeError, result[:msg] }
.on_failure(:lte_zero) { |result| raise ArgumentError, result[:msg] }
# O output será:
# 6
#===================================#
# Lançando um erro em caso de falha #
#===================================#
Double
.call(number: -1)
.on_success { |result| p result[:number] }
.on_failure { |_result, use_case| puts "#{use_case.class.name} was the use case responsible for the failure" }
.on_failure(:invalid) { |result| raise TypeError, result[:msg] }
.on_failure(:lte_zero) { |result| raise ArgumentError, result[:msg] }
# O output será:
#
# 1. Imprimirá a mensagem: Double was the use case responsible for the failure
# 2. Lançará a exception: ArgumentError (the number must be greater than 0)
# Nota:
# ----
# O caso de uso responsável estará sempre acessível como o segundo argumento do hook
Resposta: Para permitir que você defina o controle de fluxo da aplicação usando alguma estrutura condicional como um if ou case when.
class Double < Micro::Case
attribute :number
def call!
return Failure(:invalid) unless number.is_a?(Numeric)
return Failure :lte_zero, result: attributes(:number) if number <= 0
Success result: { number: number * 2 }
end
end
Double
.call(number: -1)
.on_failure do |result, use_case|
case result.type
when :invalid then raise TypeError, "number must be a numeric value"
when :lte_zero then raise ArgumentError, "number `#{result[:number]}` must be greater than 0"
else raise NotImplementedError
end
end
# O output será uma exception:
#
# ArgumentError (number `-1` must be greater than 0)
Nota: O mesmo que foi feito no exemplo anterior poderá ser feito com o hook
#on_success!
A sintaxe para decompor um Array pode ser usada na declaração de variáveis e nos argumentos de métodos/blocos. Se você não sabia disso, confira a documentação do Ruby.
# O objeto exposto em hook sem um tipo é um Micro::Case::Result e ele pode ser decomposto. Exemplo:
Double
.call(number: -2)
.on_failure do |(data, type), use_case|
case type
when :invalid then raise TypeError, 'number must be a numeric value'
when :lte_zero then raise ArgumentError, "number `#{data[:number]}` must be greater than 0"
else raise NotImplementedError
end
end
# O output será a exception:
#
# ArgumentError (the number `-2` must be greater than 0)
Nota: O que mesmo pode ser feito com o
#on_successhook!
Resposta: Se o tipo do resultado for identificado o hook será sempre executado.
class Double < Micro::Case
attributes :number
def call!
if number.is_a?(Numeric)
Success :computed, result: { number: number * 2 }
else
Failure :invalid, result: { msg: 'number must be a numeric value' }
end
end
end
result = Double.call(number: 3)
result.data # { number: 6 }
result[:number] * 4 # 24
accum = 0
result
.on_success { |result| accum += result[:number] }
.on_success { |result| accum += result[:number] }
.on_success(:computed) { |result| accum += result[:number] }
.on_success(:computed) { |result| accum += result[:number] }
accum # 24
result[:number] * 4 == accum # true
Este método permite você criar fluxos dinâmicos. Com ele, você pode adicionar novos casos de uso ou fluxos para continuar a transformação de um resultado. Exemplo:
class ForbidNegativeNumber < Micro::Case
attribute :number
def call!
return Success result: attributes if number >= 0
Failure result: attributes
end
end
class Add3 < Micro::Case
attribute :number
def call!
Success result: { number: number + 3 }
end
end
result1 =
ForbidNegativeNumber
.call(number: -1)
.then(Add3)
result1.data # {'number' => -1}
result1.failure? # true
# ---
result2 =
ForbidNegativeNumber
.call(number: 1)
.then(Add3)
result2.data # {'number' => 4}
result2.success? # true
Nota: este método altera o
Micro::Case::Result#transitions.
Ele passará o próprio resultado (uma instância do Micro::Case::Result) como argumento do bloco, e retornará o output do bloco ao invés dele mesmo. e.g:
class Add < Micro::Case
attributes :a, :b
def call!
if Kind.of?(Numeric, a, b)
Success result: { sum: a + b }
else
Failure(:attributes_arent_numbers)
end
end
end
# --
success_result =
Add
.call(a: 2, b: 2)
.then { |result| result.success? ? result[:sum] : 0 }
puts success_result # 4
# --
failure_result =
Add
.call(a: 2, b: '2')
.then { |result| result.success? ? result[:sum] : 0 }
puts failure_result # 0
Passe um Hash como segundo argumento do método Micro::Case::Result#then.
Todo::FindAllForUser
.call(user: current_user, params: params)
.then(Paginate)
.then(Serialize::PaginatedRelationAsJson, serializer: Todo::Serializer)
.on_success { |result| render_json(200, data: result[:todos]) }
Chamamos de fluxo uma composição de casos de uso. A ideia principal desse recurso é usar/reutilizar casos de uso como etapas de um novo caso de uso. Exemplo:
module Steps
class ConvertTextToNumbers < Micro::Case
attribute :numbers
def call!
if numbers.all? { |value| String(value) =~ /\d+/ }
Success result: { numbers: numbers.map(&:to_i) }
else
Failure result: { message: 'numbers must contain only numeric types' }
end
end
end
class Add2 < Micro::Case::Strict
attribute :numbers
def call!
Success result: { numbers: numbers.map { |number| number + 2 } }
end
end
class Double < Micro::Case::Strict
attribute :numbers
def call!
Success result: { numbers: numbers.map { |number| number * 2 } }
end
end
class Square < Micro::Case::Strict
attribute :numbers
def call!
Success result: { numbers: numbers.map { |number| number * number } }
end
end
end
#-----------------------------------------#
# Criando um flow com Micro::Cases.flow() #
#-----------------------------------------#
Add2ToAllNumbers = Micro::Cases.flow([
Steps::ConvertTextToNumbers,
Steps::Add2
])
result = Add2ToAllNumbers.call(numbers: %w[1 1 2 2 3 4])
result.success? # true
result.data # {:numbers => [3, 3, 4, 4, 5, 6]}
#--------------------------------#
# Criando um flow usando classes #
#--------------------------------#
class DoubleAllNumbers < Micro::Case
flow Steps::ConvertTextToNumbers,
Steps::Double
end
DoubleAllNumbers.
call(numbers: %w[1 1 b 2 3 4]).
on_failure { |result| puts result[:message] } # "numbers must contain only numeric types"
Ao ocorrer uma falha, o caso de uso responsável ficará acessível no resultado. Exemplo:
result = DoubleAllNumbers.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
Resposta: Sim, é possível.
module Steps
class ConvertTextToNumbers < Micro::Case
attribute :numbers
def call!
if numbers.all? { |value| String(value) =~ /\d+/ }
Success result: { numbers: numbers.map(&:to_i) }
else
Failure result: { message: 'numbers must contain only numeric types' }
end
end
end
class Add2 < Micro::Case::Strict
attribute :numbers
def call!
Success result: { numbers: numbers.map { |number| number + 2 } }
end
end
class Double < Micro::Case::Strict
attribute :numbers
def call!
Success result: { numbers: numbers.map { |number| number * 2 } }
end
end
class Square < Micro::Case::Strict
attribute :numbers
def call!
Success result: { numbers: numbers.map { |number| number * number } }
end
end
end
DoubleAllNumbers =
Micro::Cases.flow([Steps::ConvertTextToNumbers, Steps::Double])
SquareAllNumbers =
Micro::Cases.flow([Steps::ConvertTextToNumbers, Steps::Square])
DoubleAllNumbersAndAdd2 =
Micro::Cases.flow([DoubleAllNumbers, Steps::Add2])
SquareAllNumbersAndAdd2 =
Micro::Cases.flow([SquareAllNumbers, Steps::Add2])
SquareAllNumbersAndDouble =
Micro::Cases.flow([SquareAllNumbersAndAdd2, DoubleAllNumbers])
DoubleAllNumbersAndSquareAndAdd2 =
Micro::Cases.flow([DoubleAllNumbers, SquareAllNumbersAndAdd2])
SquareAllNumbersAndDouble
.call(numbers: %w[1 1 2 2 3 4])
.on_success { |result| p result[:numbers] } # [6, 6, 12, 12, 22, 36]
DoubleAllNumbersAndSquareAndAdd2
.call(numbers: %w[1 1 2 2 3 4])
.on_success { |result| p result[:numbers] } # [6, 6, 18, 18, 38, 66]
Nota: Você pode mesclar qualquer approach para criar flows - exemplos.
Resposta: Sim, é possível! Veja o exemplo abaixo para entender como funciona o acúmulo de dados dentro da execução de um fluxo.
module Users
class FindByEmail < Micro::Case
attribute :email
def call!
user = User.find_by(email: email)
return Success result: { 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 result: attributes(:user)
end
end
end
module Users
Authenticate = Micro::Cases.flow([
FindByEmail,
ValidatePassword
])
end
Users::Authenticate
.call(email: 'somebody@test.com', password: 'password')
.on_success { |result| sign_in(result[:user]) }
.on_failure(:wrong_password) { render status: 401 }
.on_failure(:user_not_found) { render status: 404 }
Primeiro, vamos ver os atributos usados por cada caso de uso:
class Users::FindByEmail < Micro::Case
attribute :email
end
class Users::ValidatePassword < Micro::Case
attributes :user, :password
end
Como você pode ver, Users::ValidatePassword espera um usuário como sua entrada. Então, como ele recebe o usuário?
R: Ele recebe o usuário do resultado de sucesso Users::FindByEmail!
E este é o poder da composição de casos de uso porque o output de uma etapa irá compor a entrada do próximo caso de uso no fluxo!
input >> processamento >> output
Nota: Verifique esses exemplos de teste Micro::Cases::Flow e Micro::Cases::Safe::Flow para ver diferentes casos de uso tendo acesso aos dados de um fluxo.
Use Micro::Case::Result#transitions!
Vamos usar os exemplos da seção anterior para ilustrar como utilizar essa feature.
user_authenticated =
Users::Authenticate.call(email: 'rodrigo@test.com', password: user_password)
user_authenticated.transitions
[
{
:use_case => {
:class => Users::FindByEmail,
:attributes => { :email => "rodrigo@test.com" }
},
:success => {
:type => :ok,
:result => {
:user => #<User:0x00007fb57b1c5f88 @email="rodrigo@test.com" ...>
}
},
:accessible_attributes => [ :email, :password ]
},
{
:use_case => {
:class => Users::ValidatePassword,
:attributes => {
:user => #<User:0x00007fb57b1c5f88 @email="rodrigo@test.com" ...>
:password => "123456"
}
},
:success => {
:type => :ok,
:result => {
:user => #<User:0x00007fb57b1c5f88 @email="rodrigo@test.com" ...>
}
},
:accessible_attributes => [ :email, :password, :user ]
}
]
O exemplo acima mostra a saída gerada pelas Micro::Case::Result#transitions.
Com ele é possível analisar a ordem de execução dos casos de uso e quais foram os inputs fornecidos ([:attributes]) e outputs ([:success][:result]) em toda a execução.
E observe a propriedade accessible_attributes, ela mostra quais atributos são acessíveis nessa etapa do fluxo. Por exemplo, na última etapa, você pode ver que os atributos accessible_attributes aumentaram devido ao acúmulo de fluxo de dados.
Nota: O
Micro::Case::Result#thenincrementa oMicro::Case::Result#transitions.
[
{
use_case: {
class: <Micro::Case>,# Caso de uso que será executado
attributes: <Hash> # (Input) Os atributos do caso de uso
},
[success:, failure:] => { # (Output)
type: <Symbol>, # Tipo do resultado. Padrões:
# Success = :ok, Failure = :error or :exception
result: <Hash> # Os dados retornados pelo resultado do use case
},
accessible_attributes: <Array>, # Propriedades que podem ser acessadas pelos atributos do caso de uso,
# começando com Hash usado para invocá-lo e que são incrementados
# com os valores de resultado de cada caso de uso do fluxo.
}
]
Resposta: Sim! Você pode usar o Micro::Case.config para fazer isso. Link para essa seção.
Resposta: Sim! Você pode usar a macro self ou self.call!. Exemplo:
class ConvertTextToNumber < Micro::Case
attribute :text
def call!
Success result: { number: text.to_i }
end
end
class ConvertNumberToText < Micro::Case
attribute :number
def call!
Success result: { text: number.to_s }
end
end
class Double < Micro::Case
flow ConvertTextToNumber,
self.call!,
ConvertNumberToText
attribute :number
def call!
Success result: { number: number * 2 }
end
end
result = Double.call(text: '4')
result.success? # true
result[:number] # "8"
Note: Essa funcionalidade pode ser usada com Micro::Case::Safe. Verifique esse teste para ver um example: https://github.com/serradura/u-case/blob/714c6b658fc6aa02617e6833ddee09eddc760f2a/test/micro/case/safe/with_inner_flow_test.rb
Resposta: é um tipo de caso de uso que exigirá todas as palavras-chave (atributos) em sua inicialização.
class Double < Micro::Case::Strict
attribute :numbers
def call!
Success result: { numbers: numbers.map { |number| number * 2 } }
end
end
Double.call({})
# O output será:
# ArgumentError (missing keyword: :numbers)
Micro::Case::Safe - Existe algum recurso para lidar automaticamente com exceções dentro de um caso de uso ou fluxo?Sim, assim como Micro::Case::Strict, o Micro::Case::Safe é outro tipo de caso de uso. Ele tem a capacidade de interceptar automaticamente qualquer exceção como um resultado de falha. Exemplo:
require 'logger'
AppLogger = Logger.new(STDOUT)
class Divide < Micro::Case::Safe
attributes :a, :b
def call!
if a.is_a?(Integer) && b.is_a?(Integer)
Success result: { number: a / b}
else
Failure(:not_an_integer)
end
end
end
result = Divide.call(a: 2, b: 0)
result.type == :exception # true
result.data # { exception: #<ZeroDivisionError...> }
result[:exception].is_a?(ZeroDivisionError) # true
result.on_failure(:exception) do |result|
AppLogger.error(result[:exception].message) # E, [2019-08-21T00:05:44.195506 #9532] ERROR -- : divided by 0
end
Se você precisar lidar com um erro específico, recomendo o uso de uma instrução case. Exemplo:
result.on_failure(:exception) do |data, use_case|
case exception = data[: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
Note: É possível resgatar uma exceção mesmo quando é um caso de uso seguro. Exemplos: https://github.com/serradura/u-case/blob/714c6b658fc6aa02617e6833ddee09eddc760f2a/test/micro/case/safe_test.rb#L90-L118
Como casos de uso seguros, os fluxos seguros podem interceptar uma exceção em qualquer uma de suas etapas. Estas são as maneiras de definir um:
module Users
Create = Micro::Cases.safe_flow([
ProcessParams,
ValidateParams,
Persist,
SendToCRM
])
end
Definindo dentro das classes:
module Users
class Create < Micro::Case::Safe
flow ProcessParams,
ValidateParams,
Persist,
SendToCRM
end
end
Na programação funcional os erros/exceções são tratados como dados comuns, a ideia é transformar a saída mesmo quando ocorre um comportamento inesperado. Para muitos, as exceções são muito semelhantes à instrução GOTO, pulando o fluxo do programa para caminhos que podem ser difíceis de descobrir como as coisas funcionam em um sistema.
Para resolver isso, o Micro::Case::Result tem um hook especial #on_exception para ajudá-lo a lidar com o fluxo de controle no caso de exceções.
Note: essa funcionalidade funcionará melhor se for usada com um flow ou caso de uso
Micro::Case::Safe.
Como ele funciona?
class Divide < Micro::Case::Safe
attributes :a, :b
def call!
Success result: { 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!
Como você pode ver, este hook tem o mesmo comportamento de result.on_failure(:exception), mas, a ideia aqui é ter uma melhor comunicação no código, fazendo uma referência explícita quando alguma falha acontecer por causa de uma exceção.
Requisitos:
Para fazer isso a sua aplicação deverá ter o activemodel >= 3.2, < 6.1.0 como dependência.
Por padrão, se a sua aplicação tiver o ActiveModel como uma dependência, qualquer tipo de caso de uso pode fazer uso dele para validar seus atributos.
class Multiply < Micro::Case
attributes :a, :b
validates :a, :b, presence: true, numericality: true
def call!
return Failure :invalid_attributes, result: { errors: self.errors } if invalid?
Success result: { number: a * b }
end
end
Mas se você deseja uma maneira automática de falhar seus casos de uso em erros de validação, você poderá fazer:
gem 'u-case', require: 'u-case/with_activemodel_validation'
Micro::Case.config para habilitar ele. Link para essa seção.Usando essa abordagem, você pode reescrever o exemplo anterior com menos código. Exemplo:
require 'u-case/with_activemodel_validation'
class Multiply < Micro::Case
attributes :a, :b
validates :a, :b, presence: true, numericality: true
def call!
Success result: { number: a * b }
end
end
Nota: Após habilitar o modo de validação, as classes
Micro::Case::StricteMicro::Case::Safeirão herdar este novo comportamento.
Resposta: Sim, é possível. Para fazer isso, você só precisará usar a macro disable_auto_validation. Exemplo:
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 result: { number: a * b }
end
end
Multiply.call(a: 2, b: 'a')
# O output será:
# TypeError (String can't be coerced into Integer)
A gem kind possui um módulo para habilitar a validação do tipo de dados através do ActiveModel validations. Então, quando você fizer o require do 'u-case/with_activemodel_validation', este módulo também irá fazer o require do Kind::Validator.
O exemplo abaixo mostra como validar os tipos de atributos.
class Todo::List::AddItem < Micro::Case
attributes :user, :params
validates :user, kind: User
validates :params, kind: ActionController::Parameters
def call!
todo_params = params.require(:todo).permit(:title, :due_at)
todo = user.todos.create(todo_params)
Success result: { todo: todo }
rescue ActionController::ParameterMissing => e
Failure :parameter_missing, result: { message: e.message }
end
end
A ideia deste recurso é permitir a configuração de algumas funcionalidades/módulos do u-case.
Eu recomendo que você use apenas uma vez em sua base de código. Exemplo: Em um inicializador do Rails.
Você pode ver abaixo todas as configurações disponíveis com seus valores padrão:
Micro::Case.config do |config|
# Use ActiveModel para auto-validar os atributos dos seus casos de uso.
config.enable_activemodel_validation = false
# Use para habilitar/desabilitar o `Micro::Case::Results#transitions`.
config.enable_transitions = true
end
| Gem / Abstração | Iterações por segundo | Comparação |
|---|---|---|
| Dry::Monads | 315635.1 | O mais rápido |
| Micro::Case | 75837.7 | 4.16x mais lento |
| Interactor | 59745.5 | 5.28x mais lento |
| Trailblazer::Operation | 28423.9 | 11.10x mais lento |
| Dry::Transaction | 10130.9 | 31.16x mais lento |
# Warming up --------------------------------------
# Interactor 5.711k i/100ms
# Trailblazer::Operation
# 2.283k i/100ms
# Dry::Monads 31.130k i/100ms
# Dry::Transaction 994.000 i/100ms
# Micro::Case 7.911k i/100ms
# Micro::Case::Safe 7.911k i/100ms
# Micro::Case::Strict 6.248k i/100ms
# Calculating -------------------------------------
# Interactor 59.746k (±29.9%) i/s - 274.128k in 5.049901s
# Trailblazer::Operation
# 28.424k (±15.8%) i/s - 141.546k in 5.087882s
# Dry::Monads 315.635k (± 6.1%) i/s - 1.588M in 5.048914s
# Dry::Transaction 10.131k (± 6.4%) i/s - 50.694k in 5.025150s
# Micro::Case 75.838k (± 9.7%) i/s - 379.728k in 5.052573s
# Micro::Case::Safe 75.461k (±10.1%) i/s - 379.728k in 5.079238s
# Micro::Case::Strict 64.235k (± 9.0%) i/s - 324.896k in 5.097028s
# Comparison:
# Dry::Monads: 315635.1 i/s
# Micro::Case: 75837.7 i/s - 4.16x (± 0.00) slower
# Micro::Case::Safe: 75461.3 i/s - 4.18x (± 0.00) slower
# Micro::Case::Strict: 64234.9 i/s - 4.91x (± 0.00) slower
# Interactor: 59745.5 i/s - 5.28x (± 0.00) slower
# Trailblazer::Operation: 28423.9 i/s - 11.10x (± 0.00) slower
# Dry::Transaction: 10130.9 i/s - 31.16x (± 0.00) slower
https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/use_case/success_results.
| Gem / Abstração | Iterações por segundo | Comparação |
|---|---|---|
| Dry::Monads | 135386.9 | O mais rápido |
| Micro::Case | 73489.3 | 1.85x mais lento |
| Trailblazer::Operation | 29016.4 | 4.67x mais lento |
| Interactor | 27037.0 | 5.01x mais lento |
| Dry::Transaction | 8988.6 | 15.06x mais lento |
# Warming up --------------------------------------
# Interactor 2.626k i/100ms
# Trailblazer::Operation 2.343k i/100ms
# Dry::Monads 13.386k i/100ms
# Dry::Transaction 868.000 i/100ms
# Micro::Case 7.603k i/100ms
# Micro::Case::Safe 7.598k i/100ms
# Micro::Case::Strict 6.178k i/100ms
# Calculating -------------------------------------
# Interactor 27.037k (±24.9%) i/s - 128.674k in 5.102133s
# Trailblazer::Operation 29.016k (±12.4%) i/s - 145.266k in 5.074991s
# Dry::Monads 135.387k (±15.1%) i/s - 669.300k in 5.055356s
# Dry::Transaction 8.989k (± 9.2%) i/s - 45.136k in 5.084820s
# Micro::Case 73.247k (± 9.9%) i/s - 364.944k in 5.030449s
# Micro::Case::Safe 73.489k (± 9.6%) i/s - 364.704k in 5.007282s
# Micro::Case::Strict 61.980k (± 8.0%) i/s - 308.900k in 5.014821s
# Comparison:
# Dry::Monads: 135386.9 i/s
# Micro::Case::Safe: 73489.3 i/s - 1.84x (± 0.00) slower
# Micro::Case: 73246.6 i/s - 1.85x (± 0.00) slower
# Micro::Case::Strict: 61979.7 i/s - 2.18x (± 0.00) slower
# Trailblazer::Operation: 29016.4 i/s - 4.67x (± 0.00) slower
# Interactor: 27037.0 i/s - 5.01x (± 0.00) slower
# Dry::Transaction: 8988.6 i/s - 15.06x (± 0.00) slower
https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/use_case/failure_results.
| Gem / Abstração | Resultados de sucesso | Resultados de falha |
|---|---|---|
Micro::Case::Result pipe method | 80936.2 i/s | 78280.4 i/s |
Micro::Case::Result then method | 0x mais lento | 0x mais lento |
| Micro::Cases.flow | 0x mais lento | 0x mais lento |
| Micro::Case class with an inner flow | 1.72x mais lento | 1.68x mais lento |
| Micro::Case class including itself as a step | 1.93x mais lento | 1.87x mais lento |
| Interactor::Organizer | 3.33x mais lento | 3.22x mais lento |
* As gems Dry::Monads, Dry::Transaction, Trailblazer::Operation estão fora desta análise por não terem esse tipo de funcionalidade.
# Warming up --------------------------------------
# Interactor::Organizer 1.809k i/100ms
# Micro::Cases.flow([]) 7.808k i/100ms
# Micro::Case flow in a class 4.816k i/100ms
# Micro::Case including the class 4.094k i/100ms
# Micro::Case::Result#| 7.656k i/100ms
# Micro::Case::Result#then 7.138k i/100ms
# Calculating -------------------------------------
# Interactor::Organizer 24.290k (±24.0%) i/s - 113.967k in 5.032825s
# Micro::Cases.flow([]) 74.790k (±11.1%) i/s - 374.784k in 5.071740s
# Micro::Case flow in a class 47.043k (± 8.0%) i/s - 235.984k in 5.047477s
# Micro::Case including the class 42.030k (± 8.5%) i/s - 208.794k in 5.002138s
# Micro::Case::Result#| 80.936k (±15.9%) i/s - 398.112k in 5.052531s
# Micro::Case::Result#then 71.459k (± 8.8%) i/s - 356.900k in 5.030526s
# Comparison:
# Micro::Case::Result#|: 80936.2 i/s
# Micro::Cases.flow([]): 74790.1 i/s - same-ish: difference falls within error
# Micro::Case::Result#then: 71459.5 i/s - same-ish: difference falls within error
# Micro::Case flow in a class: 47042.6 i/s - 1.72x (± 0.00) slower
# Micro::Case including the class: 42030.2 i/s - 1.93x (± 0.00) slower
# Interactor::Organizer: 24290.3 i/s - 3.33x (± 0.00) slower
# Warming up --------------------------------------
# Interactor::Organizer 1.734k i/100ms
# Micro::Cases.flow([]) 7.515k i/100ms
# Micro::Case flow in a class 4.636k i/100ms
# Micro::Case including the class 4.114k i/100ms
# Micro::Case::Result#| 7.588k i/100ms
# Micro::Case::Result#then 6.681k i/100ms
# Calculating -------------------------------------
# Interactor::Organizer 24.280k (±24.5%) i/s - 112.710k in 5.013334s
# Micro::Cases.flow([]) 74.999k (± 9.8%) i/s - 375.750k in 5.055777s
# Micro::Case flow in a class 46.681k (± 9.3%) i/s - 236.436k in 5.105105s
# Micro::Case including the class 41.921k (± 8.9%) i/s - 209.814k in 5.043622s
# Micro::Case::Result#| 78.280k (±12.6%) i/s - 386.988k in 5.022146s
# Micro::Case::Result#then 68.898k (± 8.8%) i/s - 347.412k in 5.080116s
# Comparison:
# Micro::Case::Result#|: 78280.4 i/s
# Micro::Cases.flow([]): 74999.4 i/s - same-ish: difference falls within error
# Micro::Case::Result#then: 68898.4 i/s - same-ish: difference falls within error
# Micro::Case flow in a class: 46681.0 i/s - 1.68x (± 0.00) slower
# Micro::Case including the class: 41920.8 i/s - 1.87x (± 0.00) slower
# Interactor::Organizer: 24280.0 i/s - 3.22x (± 0.00) slower
https://github.com/serradura/u-case/blob/main/benchmarks/perfomance/flow/
Clone este repositório e acesse a sua pasta, então execute os comandos abaixo:
Casos de uso
ruby benchmarks/perfomance/use_case/failure_results.rb ruby benchmarks/perfomance/use_case/success_results.rb
Flows
ruby benchmarks/perfomance/flow/failure_results.rb ruby benchmarks/perfomance/flow/success_results.rb
Casos de uso
./benchmarks/memory/use_case/success/with_transitions/analyze.sh ./benchmarks/memory/use_case/success/without_transitions/analyze.sh
Flows
./benchmarks/memory/flow/success/with_transitions/analyze.sh ./benchmarks/memory/flow/success/without_transitions/analyze.sh
Confira as implementações do mesmo caso de uso com diferentes gems/abstrações.
Um exemplo de fluxo que define etapas para higienizar, validar e persistir seus dados de entrada. Ele tem todas as abordagens possíveis para representar casos de uso com a gem
u-case.Link: https://github.com/serradura/u-case/blob/main/examples/users_creation
Este projeto mostra diferentes tipos de arquitetura (uma por commit), e na última, como usar a gem
Micro::Casepara lidar com a lógica de negócios da aplicação.Link: https://github.com/serradura/from-fat-controllers-to-use-cases
Rake tasks para demonstrar como lidar com os dados do usuário e como usar diferentes tipos de falha para controlar o fluxo do programa.
Link: https://github.com/serradura/u-case/tree/main/examples/calculator
Link: https://github.com/serradura/u-case/blob/main/examples/rescuing_exceptions.rb
Após fazer o checking out do repo, execute bin/setup para instalar dependências. Então, execute ./test.sh para executar os testes. Você pode executar bin/console para ter um prompt interativo que permitirá você experimenta-lá.
Para instalar esta gem em sua máquina local, execute bundle exec rake install. Para lançar uma nova versão, atualize o número da versão em version.rb e execute bundle exec rake release, que criará uma tag git para a versão, enviará git commits e tags e enviará o arquivo .gempara rubygems.org.
Reportar bugs e solicitar pull requests são bem-vindos no GitHub em https://github.com/serradura/u-case. Este projeto pretende ser um espaço seguro e acolhedor para colaboração, e espera-se que os colaboradores sigam o código de conduta do Covenant do Contribuidor.
A gem está disponível como código aberto nos termos da licença MIT.
Espera-se que todos que interagem com o codebase do projeto Micro::Case, issue trackers, chat rooms and mailing lists sigam o código de conduta.