├── .tool-versions ├── test ├── micro │ ├── case │ │ ├── MWRF │ │ │ ├── README.txt │ │ │ ├── users_entity.rb │ │ │ ├── shared_assertions.rb │ │ │ └── steps │ │ │ │ ├── 01_test.rb │ │ │ │ ├── 02_test.rb │ │ │ │ ├── 03_test.rb │ │ │ │ ├── 04_using_static_composition_via_inner_flow_test.rb │ │ │ │ ├── 04_using_static_composition_test.rb │ │ │ │ └── 04_using_variable_composition_test.rb │ │ ├── with_activemodel_validation │ │ │ ├── MWRF │ │ │ │ ├── README.txt │ │ │ │ ├── users_entity.rb │ │ │ │ ├── shared_assertions.rb │ │ │ │ └── steps │ │ │ │ │ ├── 01_test.rb │ │ │ │ │ ├── 02_test.rb │ │ │ │ │ ├── 04_using_static_composition_via_inner_flow_test.rb │ │ │ │ │ ├── 03_test.rb │ │ │ │ │ ├── 04_using_static_composition_test.rb │ │ │ │ │ └── 04_using_variable_composition_test.rb │ │ │ ├── safe │ │ │ │ ├── disable_auto_validation_test.rb │ │ │ │ ├── base_test.rb │ │ │ │ ├── classes_with_flows │ │ │ │ │ ├── last_step_with_validation_test.rb │ │ │ │ │ ├── first_step_with_validation_test.rb │ │ │ │ │ └── all_steps_with_validation_test.rb │ │ │ │ └── strict_test.rb │ │ │ ├── disable_auto_validation_test.rb │ │ │ ├── config_test.rb │ │ │ ├── base_test.rb │ │ │ ├── classes_with_flows │ │ │ │ ├── last_step_with_validation_test.rb │ │ │ │ ├── first_step_with_validation_test.rb │ │ │ │ └── all_steps_with_validation_test.rb │ │ │ └── strict_test.rb │ │ ├── version_test.rb │ │ ├── then_test.rb │ │ ├── utils │ │ │ └── hashes_test.rb │ │ ├── safe │ │ │ └── with_inner_flow_test.rb │ │ ├── with_inner_flow_test.rb │ │ ├── wrong_usage_test.rb │ │ ├── strict │ │ │ └── safe_test.rb │ │ ├── strict_test.rb │ │ ├── transaction │ │ │ └── activerecord_test.rb │ │ └── safe_test.rb │ └── cases │ │ ├── flow │ │ ├── then_test.rb │ │ ├── di_test.rb │ │ ├── collection_test.rb │ │ └── blend_test.rb │ │ ├── safe │ │ └── flow │ │ │ ├── di_test.rb │ │ │ └── collection_test.rb │ │ └── map_test.rb ├── support │ ├── todoing │ │ ├── app │ │ │ └── models │ │ │ │ ├── users │ │ │ │ ├── authenticate.rb │ │ │ │ ├── find.rb │ │ │ │ ├── validate_password.rb │ │ │ │ └── create.rb │ │ │ │ ├── user_todo_list │ │ │ │ ├── add_item.rb │ │ │ │ └── mark_item_as_done.rb │ │ │ │ ├── todos │ │ │ │ ├── find_with_user.rb │ │ │ │ └── create.rb │ │ │ │ ├── todo.rb │ │ │ │ └── user.rb │ │ ├── boot.rb │ │ └── lib │ │ │ └── inactive_record.rb │ ├── steps.rb │ └── jobs │ │ ├── base.rb │ │ └── safe.rb └── test_helper.rb ├── lib ├── u-case.rb ├── u-case │ └── with_activemodel_validation.rb └── micro │ ├── case │ ├── version.rb │ ├── strict.rb │ ├── safe.rb │ ├── result │ │ ├── transitions.rb │ │ └── wrapper.rb │ ├── utils.rb │ ├── config.rb │ ├── with_activemodel_validation.rb │ └── error.rb │ ├── cases │ ├── error.rb │ ├── utils.rb │ ├── safe │ │ └── flow.rb │ ├── map.rb │ └── flow.rb │ └── cases.rb ├── assets ├── ucase_brand_v1.png └── ucase_logo_v1.png ├── examples ├── calculator │ ├── Gemfile │ ├── assets │ │ └── usage.gif │ ├── calc │ │ ├── transform_into_numbers.rb │ │ ├── normalize_args.rb │ │ └── operation.rb │ ├── README.md │ └── Rakefile ├── rescuing_exceptions.rb └── users_creation │ ├── 01.rb │ ├── 02b.rb │ ├── 03.rb │ ├── 02a.rb │ └── 04a.rb ├── .gitignore ├── .vscode └── settings.json ├── bin ├── setup └── console ├── benchmarks ├── examples │ ├── flow │ │ └── add_five_with │ │ │ ├── u-case │ │ │ ├── add1.rb │ │ │ ├── convert_text_to_number.rb │ │ │ └── flow │ │ │ │ ├── collection.rb │ │ │ │ ├── collection_in_a_class.rb │ │ │ │ ├── using_result_pipes.rb │ │ │ │ ├── using_result_thens.rb │ │ │ │ └── including_the_class.rb │ │ │ ├── all.rb │ │ │ └── interactor.rb │ └── use_case │ │ └── multiply_with │ │ ├── all.rb │ │ ├── u-case.rb │ │ ├── u-case │ │ ├── safe.rb │ │ └── strict.rb │ │ ├── trailblazer_operation.rb │ │ ├── interactor.rb │ │ ├── dry_transaction.rb │ │ └── dry_monads.rb ├── memory │ ├── flow │ │ └── success │ │ │ ├── with_transitions │ │ │ ├── analyze.sh │ │ │ ├── u_case_v2-6-0.rb │ │ │ ├── u_case_v3-1-0.rb │ │ │ └── u_case_v4-1-0.rb │ │ │ └── without_transitions │ │ │ ├── analyze.sh │ │ │ ├── u_case_v2-0-0.rb │ │ │ ├── u_case_v2-6-0.rb │ │ │ ├── interactor.rb │ │ │ ├── u_case_v3-1-0.rb │ │ │ └── u_case_v4-1-0.rb │ └── use_case │ │ ├── failure │ │ ├── with_transitions │ │ │ ├── analyze.sh │ │ │ ├── u_case_v2-6-0.rb │ │ │ ├── u_case_v3-1-0.rb │ │ │ └── u_case_v4-1-0.rb │ │ └── without_transitions │ │ │ ├── u_case_v2-0-0.rb │ │ │ ├── interactor.rb │ │ │ ├── analyze.sh │ │ │ ├── u_case_v2-6-0.rb │ │ │ ├── u_case_v3-1-0.rb │ │ │ ├── u_case_v4-1-0.rb │ │ │ └── trailblazer_operations.rb │ │ └── success │ │ ├── with_transitions │ │ ├── analyze.sh │ │ ├── u_case_v2-6-0.rb │ │ ├── u_case_v3-1-0.rb │ │ └── u_case_v4-1-0.rb │ │ └── without_transitions │ │ ├── u_case_v2-0-0.rb │ │ ├── analyze.sh │ │ ├── interactor.rb │ │ ├── u_case_v2-6-0.rb │ │ ├── u_case_v3-1-0.rb │ │ ├── u_case_v4-1-0.rb │ │ └── trailblazer_operations.rb └── perfomance │ ├── use_case │ ├── call_use_cases.rb │ ├── failure_results.rb │ └── success_results.rb │ └── flow │ ├── call_flows.rb │ ├── failure_results.rb │ └── success_results.rb ├── Rakefile ├── Gemfile ├── gemfiles ├── rails_5.2 │ └── Gemfile ├── rails_6.0 │ └── Gemfile ├── rails_6.1 │ └── Gemfile ├── rails_7.0 │ └── Gemfile ├── rails_7.1 │ └── Gemfile └── rails_edge │ └── Gemfile ├── LICENSE.txt ├── u-case.gemspec ├── comparisons ├── u-case.rb └── interactor.rb ├── .github └── workflows │ └── ci.yml └── CODE_OF_CONDUCT.md /.tool-versions: -------------------------------------------------------------------------------- 1 | ruby 2.6.5 2 | -------------------------------------------------------------------------------- /test/micro/case/MWRF/README.txt: -------------------------------------------------------------------------------- 1 | MWRF = Make it Work, Right and Fast 2 | -------------------------------------------------------------------------------- /lib/u-case.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'micro/case' 4 | -------------------------------------------------------------------------------- /assets/ucase_brand_v1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serradura/u-case/HEAD/assets/ucase_brand_v1.png -------------------------------------------------------------------------------- /assets/ucase_logo_v1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serradura/u-case/HEAD/assets/ucase_logo_v1.png -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/MWRF/README.txt: -------------------------------------------------------------------------------- 1 | MWRF = Make it Work, Right and Fast 2 | -------------------------------------------------------------------------------- /examples/calculator/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rake' 4 | 5 | gem 'u-case', '~> 4.1.0' 6 | -------------------------------------------------------------------------------- /examples/calculator/assets/usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serradura/u-case/HEAD/examples/calculator/assets/usage.gif -------------------------------------------------------------------------------- /lib/u-case/with_activemodel_validation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'micro/case/with_activemodel_validation' 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | Gemfile.lock 10 | tags 11 | -------------------------------------------------------------------------------- /lib/micro/case/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Micro 4 | class Case 5 | VERSION = '4.5.2'.freeze 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.enabled": true, 3 | "cSpell.ignoreWords": [ 4 | "paambaati", 5 | "resultset", 6 | "simplecov" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /test/support/todoing/app/models/users/authenticate.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Users 4 | Authenticate = Micro::Cases.flow([ 5 | Find, 6 | ValidatePassword 7 | ]) 8 | end 9 | -------------------------------------------------------------------------------- /test/micro/case/version_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Micro::Case::VersionTest < Minitest::Test 4 | def test_that_it_has_a_version_number 5 | refute_nil ::Micro::Case::VERSION 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/support/todoing/app/models/user_todo_list/add_item.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module UserTodoList 4 | AddItem = Micro::Cases.flow([ 5 | Users::Authenticate, 6 | Todos::Create 7 | ]) 8 | end 9 | -------------------------------------------------------------------------------- /benchmarks/examples/flow/add_five_with/u-case/add1.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Add1 < Micro::Case 4 | attribute :number 5 | 6 | def call! 7 | Success result: number + 1 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList["test/**/*_test.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /benchmarks/examples/use_case/multiply_with/all.rb: -------------------------------------------------------------------------------- 1 | [ 2 | 'dry_monads', 3 | 'dry_transaction', 4 | 'interactor', 5 | 'trailblazer_operation', 6 | 'u-case', 7 | 'u-case/safe', 8 | 'u-case/strict' 9 | ].each { |file| require_relative(file) } 10 | -------------------------------------------------------------------------------- /lib/micro/cases/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Micro 4 | module Cases 5 | 6 | module Error 7 | class InvalidUseCases < ArgumentError 8 | def initialize; super('argument must be a collection of `Micro::Case` classes'.freeze); end 9 | end 10 | end 11 | 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /benchmarks/examples/flow/add_five_with/u-case/convert_text_to_number.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class ConvertTextToNumber < Micro::Case 4 | attribute :text 5 | 6 | def call! 7 | return Success(result: text.to_i) if text =~ /\d+/ 8 | 9 | Failure result: { text: 'must be an integer value' } 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /test/micro/case/MWRF/users_entity.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Micro::Case::MWRF 4 | module Users 5 | class Entity 6 | include Micro::Attributes.with(:initialize) 7 | 8 | attributes :id, :name, :email 9 | 10 | def persisted? 11 | !id.nil? 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/micro/case/strict.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Micro 4 | class Case 5 | class Strict < ::Micro::Case 6 | include Micro::Attributes::Features::Initialize::Strict 7 | 8 | class Safe < ::Micro::Case::Safe 9 | include Micro::Attributes::Features::Initialize::Strict 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/support/todoing/app/models/users/find.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Users 4 | class Find < Micro::Case::Strict 5 | attribute :email 6 | 7 | def call! 8 | user = User.find_by_email(email) 9 | 10 | return Success result: { user: user } if user 11 | 12 | Failure(:user_not_found) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /benchmarks/examples/flow/add_five_with/all.rb: -------------------------------------------------------------------------------- 1 | [ 2 | 'interactor', 3 | 'u-case/convert_text_to_number', 4 | 'u-case/add1', 5 | 'u-case/flow/collection', 6 | 'u-case/flow/collection_in_a_class', 7 | 'u-case/flow/including_the_class', 8 | 'u-case/flow/using_result_pipes', 9 | 'u-case/flow/using_result_thens' 10 | ].each { |file| require_relative(file) } 11 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/MWRF/users_entity.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Micro::Case::MWRF::WithValidation 4 | module Users 5 | class Entity 6 | include Micro::Attributes.with(:initialize) 7 | 8 | attributes :id, :name, :email 9 | 10 | def persisted? 11 | !id.nil? 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /benchmarks/examples/flow/add_five_with/u-case/flow/collection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AddFiveWith 4 | module MicroCase 5 | module Flow 6 | 7 | Collection = Micro::Cases.flow([ 8 | ConvertTextToNumber, 9 | Add1, 10 | Add1, 11 | Add1, 12 | Add1, 13 | Add1 14 | ]) 15 | 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /benchmarks/examples/use_case/multiply_with/u-case.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MultiplyWith 4 | class MicroCase < Micro::Case 5 | attributes :a, :b 6 | 7 | def call! 8 | if a.is_a?(Numeric) && b.is_a?(Numeric) 9 | Success result: { number: a * b } 10 | else 11 | Failure(:invalid_data) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /benchmarks/memory/flow/success/with_transitions/analyze.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd $(dirname $0) 4 | 5 | echo u_case_v4-1-0.rb 6 | echo '----------------' 7 | ruby u_case_v4-1-0.rb | head -n 3 8 | 9 | echo u_case_v3-1-0.rb 10 | echo '----------------' 11 | ruby u_case_v3-1-0.rb | head -n 3 12 | 13 | echo u_case_v2-6-0.rb 14 | echo '----------------' 15 | ruby u_case_v2-6-0.rb | head -n 3 16 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/failure/with_transitions/analyze.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd $(dirname $0) 4 | 5 | echo u_case_v4-1-0.rb 6 | echo '----------------' 7 | ruby u_case_v4-1-0.rb | head -n 3 8 | 9 | echo u_case_v3-1-0.rb 10 | echo '----------------' 11 | ruby u_case_v3-1-0.rb | head -n 3 12 | 13 | echo u_case_v2-6-0.rb 14 | echo '----------------' 15 | ruby u_case_v2-6-0.rb | head -n 3 16 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/success/with_transitions/analyze.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd $(dirname $0) 4 | 5 | echo u_case_v4-1-0.rb 6 | echo '----------------' 7 | ruby u_case_v4-1-0.rb | head -n 3 8 | 9 | echo u_case_v3-1-0.rb 10 | echo '----------------' 11 | ruby u_case_v3-1-0.rb | head -n 3 12 | 13 | echo u_case_v2-6-0.rb 14 | echo '----------------' 15 | ruby u_case_v2-6-0.rb | head -n 3 16 | -------------------------------------------------------------------------------- /benchmarks/examples/use_case/multiply_with/u-case/safe.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MultiplyWith 4 | class MicroCaseSafe < Micro::Case::Safe 5 | attributes :a, :b 6 | 7 | def call! 8 | if a.is_a?(Numeric) && b.is_a?(Numeric) 9 | Success result: { number: a * b } 10 | else 11 | Failure(:invalid_data) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /benchmarks/examples/use_case/multiply_with/trailblazer_operation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MultiplyWith 4 | class Trailblazer < ::Trailblazer::Operation 5 | step :calculate 6 | 7 | private 8 | 9 | def calculate(options, a:, b:) 10 | if a.is_a?(Numeric) && b.is_a?(Numeric) 11 | options[:number] = a * b 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /benchmarks/examples/use_case/multiply_with/u-case/strict.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MultiplyWith 4 | class MicroCaseStrict < Micro::Case::Strict 5 | attributes :a, :b 6 | 7 | def call! 8 | if a.is_a?(Numeric) && b.is_a?(Numeric) 9 | Success result: { number: a * b } 10 | else 11 | Failure(:invalid_data) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "micro/case" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /test/support/todoing/app/models/todos/find_with_user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Todos 4 | class FindWithUser < Micro::Case::Strict 5 | attributes :user, :todo_id 6 | 7 | def call! 8 | todo = Todo.find_by_id_and_user_id(todo_id, user.id) 9 | 10 | return Success result: { todo: todo } if todo 11 | 12 | Failure(:todo_not_found) 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /test/support/todoing/app/models/todos/create.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Todos 4 | class Create < Micro::Case::Strict 5 | attributes :user, :description 6 | 7 | def call! 8 | todo = Todo.new(user_id: user.id, description: description) 9 | 10 | return Failure(:invalid_attributes) unless todo.save 11 | 12 | Success result: { todo: todo } 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /benchmarks/examples/flow/add_five_with/u-case/flow/collection_in_a_class.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AddFiveWith 4 | module MicroCase 5 | module Flow 6 | 7 | class CollectionInAClass < Micro::Case 8 | flow ConvertTextToNumber, 9 | Add1, 10 | Add1, 11 | Add1, 12 | Add1, 13 | Add1 14 | end 15 | 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /benchmarks/examples/use_case/multiply_with/interactor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MultiplyWith 4 | class Interactor 5 | include ::Interactor 6 | 7 | def call 8 | a = context.a 9 | b = context.b 10 | 11 | if a.is_a?(Numeric) && b.is_a?(Numeric) 12 | context.number = a * b 13 | else 14 | context.fail!(type: :invalid_data) 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/support/todoing/app/models/users/validate_password.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Users 4 | class ValidatePassword < Micro::Case::Strict 5 | attributes :user, :password 6 | 7 | def call! 8 | return Failure(:user_must_be_persisted) if user.new_record? 9 | return Failure(:wrong_password) if user.wrong_password?(password) 10 | 11 | Success result: attributes(:user) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/micro/case/safe.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Micro 4 | class Case 5 | class Safe < ::Micro::Case 6 | def self.__flow_builder__ 7 | Cases::Safe::Flow 8 | end 9 | 10 | def __call__ 11 | __call_the_use_case_or_its_flow 12 | rescue => exception 13 | raise exception if Error.by_wrong_usage?(exception) 14 | 15 | Failure(result: exception) 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/support/todoing/app/models/user_todo_list/mark_item_as_done.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module UserTodoList 4 | class MarkItemAsDone < Micro::Case 5 | attribute :todo 6 | 7 | flow Users::Authenticate, 8 | Todos::FindWithUser, 9 | self.call! 10 | 11 | def call! 12 | if todo.pending? 13 | todo.done = true 14 | todo.save 15 | end 16 | 17 | Success result: attributes(:todo) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/micro/cases.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'micro/cases/utils' 4 | require 'micro/cases/error' 5 | require 'micro/cases/flow' 6 | require 'micro/cases/safe/flow' 7 | require 'micro/cases/map' 8 | 9 | module Micro 10 | module Cases 11 | def self.flow(args) 12 | Flow.build(args) 13 | end 14 | 15 | def self.safe_flow(args) 16 | Safe::Flow.build(args) 17 | end 18 | 19 | def self.map(args) 20 | Map.build(args) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /benchmarks/examples/flow/add_five_with/u-case/flow/using_result_pipes.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AddFiveWith 4 | module MicroCase 5 | module Flow 6 | 7 | module UsingResultPipes 8 | def self.call(params) 9 | ConvertTextToNumber 10 | .call(params) \ 11 | | Add1 \ 12 | | Add1 \ 13 | | Add1 \ 14 | | Add1 \ 15 | | Add1 16 | end 17 | end 18 | 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | group :test do 6 | gem 'minitest' 7 | 8 | gem 'simplecov', require: false 9 | 10 | gem 'sqlite3' 11 | gem 'activerecord', require: 'active_record' 12 | end 13 | 14 | group :development, :test do 15 | gem 'awesome_print' 16 | 17 | gem 'byebug' 18 | 19 | gem 'pry' 20 | gem 'pry-byebug' 21 | end 22 | 23 | # Specify your gem's dependencies in u-case.gemspec 24 | gemspec 25 | -------------------------------------------------------------------------------- /test/support/todoing/app/models/users/create.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Users 4 | class Create < Micro::Case::Safe 5 | attributes :email, :password, :password_confirmation 6 | 7 | def call! 8 | return Failure(:invalid_password) if password != password_confirmation 9 | 10 | user = User.new(attributes(:email, :password)) 11 | 12 | return Failure(:invalid_attributes) unless user.save 13 | 14 | Success result: { user: user } 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /benchmarks/examples/flow/add_five_with/u-case/flow/using_result_thens.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AddFiveWith 4 | module MicroCase 5 | module Flow 6 | 7 | module UsingResultThens 8 | def self.call(params) 9 | ConvertTextToNumber 10 | .call(params) 11 | .then(Add1) 12 | .then(Add1) 13 | .then(Add1) 14 | .then(Add1) 15 | .then(Add1) 16 | end 17 | end 18 | 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/calculator/calc/transform_into_numbers.rb: -------------------------------------------------------------------------------- 1 | class TransformIntoNumbers < Micro::Case 2 | attributes :a, :b 3 | 4 | def call! 5 | number_a, number_b = number(a), number(b) 6 | 7 | if number_a && number_b 8 | Success result: { a: number(a), b: number(b) } 9 | else 10 | Failure(:not_a_number) 11 | end 12 | end 13 | 14 | private def number(value) 15 | return value.to_i if value =~ /\A[\-,=]?\d+\z/ 16 | return value.to_f if value =~ /\A[\-,=]?\d+\.\d+\z/ 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/micro/cases/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Micro 4 | module Cases 5 | 6 | module Utils 7 | def self.map_use_cases(args) 8 | collection = args.is_a?(Array) && args.size == 1 ? args[0] : args 9 | 10 | Array(collection).each_with_object([]) do |arg, memo| 11 | if arg.is_a?(Flow) 12 | arg.use_cases.each { |use_case| memo << use_case } 13 | else 14 | memo << arg 15 | end 16 | end 17 | end 18 | end 19 | 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /examples/calculator/calc/normalize_args.rb: -------------------------------------------------------------------------------- 1 | module Calc 2 | class NormalizeArgs < Micro::Case 3 | attribute :args 4 | 5 | def call! 6 | a, b = normalize(args[:a]), normalize(args[:b]) 7 | 8 | if a !~ /\s/ && b !~ /\s/ 9 | Success result: { a: a, b: b } 10 | else 11 | Failure :arguments_with_space_chars, result: { 12 | attributes: [a, b].map(&:inspect) 13 | } 14 | end 15 | end 16 | 17 | private def normalize(value) 18 | String(value).strip 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/micro/case/result/transitions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Micro 4 | class Case 5 | class Result 6 | class Transitions 7 | MapEverything = -> (result, use_case_attributes) do 8 | { 9 | use_case: { class: result.use_case.class, attributes: use_case_attributes }, 10 | result.to_sym => { type: result.type, result: result.data }, 11 | accessible_attributes: result.accessible_attributes 12 | } 13 | end 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /gemfiles/rails_5.2/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | group :test do 6 | gem 'minitest' 7 | 8 | gem 'simplecov', require: false 9 | 10 | gem 'sqlite3' 11 | gem 'activerecord', '~> 5.2.0', require: 'active_record' 12 | end 13 | 14 | group :development, :test do 15 | gem 'awesome_print' 16 | 17 | gem 'byebug' 18 | 19 | gem 'pry' 20 | gem 'pry-byebug' 21 | end 22 | 23 | # Specify your gem's dependencies in u-case.gemspec 24 | gemspec path: "../.." 25 | -------------------------------------------------------------------------------- /gemfiles/rails_6.0/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | group :test do 6 | gem 'minitest' 7 | 8 | gem 'simplecov', require: false 9 | 10 | gem 'sqlite3' 11 | gem 'activerecord', '~> 6.0.0', require: 'active_record' 12 | end 13 | 14 | group :development, :test do 15 | gem 'awesome_print' 16 | 17 | gem 'byebug' 18 | 19 | gem 'pry' 20 | gem 'pry-byebug' 21 | end 22 | 23 | # Specify your gem's dependencies in u-case.gemspec 24 | gemspec path: "../.." 25 | -------------------------------------------------------------------------------- /gemfiles/rails_6.1/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | group :test do 6 | gem 'minitest' 7 | 8 | gem 'simplecov', require: false 9 | 10 | gem 'sqlite3' 11 | gem 'activerecord', '~> 6.1.0', require: 'active_record' 12 | end 13 | 14 | group :development, :test do 15 | gem 'awesome_print' 16 | 17 | gem 'byebug' 18 | 19 | gem 'pry' 20 | gem 'pry-byebug' 21 | end 22 | 23 | # Specify your gem's dependencies in u-case.gemspec 24 | gemspec path: "../.." 25 | -------------------------------------------------------------------------------- /gemfiles/rails_7.0/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | group :test do 6 | gem 'minitest' 7 | 8 | gem 'simplecov', require: false 9 | 10 | gem 'sqlite3' 11 | gem 'activerecord', '~> 7.0.0', require: 'active_record' 12 | end 13 | 14 | group :development, :test do 15 | gem 'awesome_print' 16 | 17 | gem 'byebug' 18 | 19 | gem 'pry' 20 | gem 'pry-byebug' 21 | end 22 | 23 | # Specify your gem's dependencies in u-case.gemspec 24 | gemspec path: "../.." 25 | -------------------------------------------------------------------------------- /gemfiles/rails_7.1/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | group :test do 6 | gem 'minitest' 7 | 8 | gem 'simplecov', require: false 9 | 10 | gem 'sqlite3' 11 | gem 'activerecord', '~> 7.1.0', require: 'active_record' 12 | end 13 | 14 | group :development, :test do 15 | gem 'awesome_print' 16 | 17 | gem 'byebug' 18 | 19 | gem 'pry' 20 | gem 'pry-byebug' 21 | end 22 | 23 | # Specify your gem's dependencies in u-case.gemspec 24 | gemspec path: "../.." 25 | -------------------------------------------------------------------------------- /lib/micro/cases/safe/flow.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Micro 4 | module Cases 5 | module Safe 6 | class Flow < Cases::Flow 7 | private def __call_use_case(use_case, result, input) 8 | instance = __build_use_case(use_case, result, input) 9 | instance.__call__ 10 | rescue => exception 11 | raise exception if Case::Error.by_wrong_usage?(exception) 12 | 13 | result.__set__(false, exception, :exception, instance) 14 | end 15 | end 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /benchmarks/memory/flow/success/without_transitions/analyze.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd $(dirname $0) 4 | 5 | echo u_case_v4-1-0.rb 6 | echo '----------------' 7 | ruby u_case_v4-1-0.rb | head -n 3 8 | 9 | echo u_case_v3-1-0.rb 10 | echo '----------------' 11 | ruby u_case_v3-1-0.rb | head -n 3 12 | 13 | echo u_case_v2-6-0.rb 14 | echo '----------------' 15 | ruby u_case_v2-6-0.rb | head -n 3 16 | 17 | echo u_case_v2-0-0.rb 18 | echo '----------------' 19 | ruby u_case_v2-0-0.rb | head -n 3 20 | 21 | echo interactor.rb 22 | echo '-------------' 23 | ruby interactor.rb | head -n 3 24 | -------------------------------------------------------------------------------- /gemfiles/rails_edge/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | group :test do 6 | gem 'minitest' 7 | 8 | gem 'simplecov', require: false 9 | 10 | gem 'sqlite3' 11 | gem 'activerecord', git: "https://github.com/rails/rails", branch: "main", require: 'active_record' 12 | end 13 | 14 | group :development, :test do 15 | gem 'awesome_print' 16 | 17 | gem 'byebug' 18 | 19 | gem 'pry' 20 | gem 'pry-byebug' 21 | end 22 | 23 | # Specify your gem's dependencies in u-case.gemspec 24 | gemspec path: "../.." 25 | -------------------------------------------------------------------------------- /test/support/todoing/boot.rb: -------------------------------------------------------------------------------- 1 | require_relative 'lib/inactive_record' 2 | 3 | require_relative 'app/models/user' 4 | require_relative 'app/models/todo' 5 | 6 | require_relative 'app/models/users/create' 7 | require_relative 'app/models/users/find' 8 | require_relative 'app/models/users/validate_password' 9 | require_relative 'app/models/users/authenticate' 10 | 11 | require_relative 'app/models/todos/create' 12 | require_relative 'app/models/todos/find_with_user' 13 | 14 | require_relative 'app/models/user_todo_list/add_item' 15 | require_relative 'app/models/user_todo_list/mark_item_as_done' 16 | -------------------------------------------------------------------------------- /benchmarks/examples/flow/add_five_with/u-case/flow/including_the_class.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AddFiveWith 4 | module MicroCase 5 | module Flow 6 | 7 | class IncludingTheClass < Micro::Case 8 | flow self, 9 | Add1, 10 | Add1, 11 | Add1, 12 | Add1, 13 | Add1 14 | 15 | attribute :text 16 | 17 | def call! 18 | return Success(result: text.to_i) if text =~ /\d+/ 19 | 20 | Failure result: { text: 'must be an integer value' } 21 | end 22 | end 23 | 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/failure/without_transitions/u_case_v2-0-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 2.0.0' 11 | end 12 | 13 | class Multiply < Micro::Case 14 | attributes :a, :b 15 | 16 | def call! 17 | if a.is_a?(Numeric) && b.is_a?(Numeric) 18 | Success { { number: a * b } } 19 | else 20 | Failure(:invalid_data) 21 | end 22 | end 23 | end 24 | 25 | Multiply.call(a: nil, 'b' => 2) 26 | 27 | report = MemoryProfiler.report { Multiply.call(a: nil, 'b' => 2) } 28 | report.pretty_print 29 | -------------------------------------------------------------------------------- /benchmarks/examples/use_case/multiply_with/dry_transaction.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module MultiplyWith 4 | class DryTransaction 5 | include Dry::Transaction 6 | 7 | step :normalize 8 | step :calculate 9 | 10 | private 11 | 12 | def normalize(input) 13 | data = input.map { |key, value| [key.to_s, value] }.to_h 14 | 15 | Success(data) 16 | end 17 | 18 | def calculate(input) 19 | a = input['a'] 20 | b = input['b'] 21 | 22 | if a.is_a?(Numeric) && b.is_a?(Numeric) 23 | Success(a * b) 24 | else 25 | Failure(:invalid_data) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/success/without_transitions/u_case_v2-0-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 2.0.0' 11 | end 12 | 13 | class Multiply < Micro::Case 14 | attributes :a, :b 15 | 16 | def call! 17 | if a.is_a?(Numeric) && b.is_a?(Numeric) 18 | Success { { number: a * b } } 19 | else 20 | Failure(:invalid_data) 21 | end 22 | end 23 | end 24 | 25 | Multiply.call(a: 2, 'b' => 2) 26 | 27 | report = MemoryProfiler.report do 28 | Multiply.call(a: 2, 'b' => 2) 29 | end 30 | 31 | report.pretty_print 32 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/failure/without_transitions/interactor.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'interactor', '~> 3.1' 11 | end 12 | 13 | class Multiply 14 | include Interactor 15 | 16 | def call 17 | a = context.a 18 | b = context.b 19 | 20 | if a.is_a?(Numeric) && b.is_a?(Numeric) 21 | context.number = a * b 22 | else 23 | context.fail!(type: :invalid_data) 24 | end 25 | end 26 | end 27 | 28 | Multiply.call(a: nil, 'b' => 2) 29 | 30 | report = MemoryProfiler.report { Multiply.call(a: nil, 'b' => 2) } 31 | report.pretty_print 32 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/failure/with_transitions/u_case_v2-6-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 2.6.0' 11 | end 12 | 13 | # Micro::Case::Result.disable_transition_tracking 14 | 15 | class Multiply < Micro::Case 16 | attributes :a, :b 17 | 18 | def call! 19 | if a.is_a?(Numeric) && b.is_a?(Numeric) 20 | Success { { number: a * b } } 21 | else 22 | Failure(:invalid_data) 23 | end 24 | end 25 | end 26 | 27 | Multiply.call(a: nil, 'b' => 2) 28 | 29 | report = MemoryProfiler.report { Multiply.call(a: nil, 'b' => 2) } 30 | report.pretty_print 31 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/failure/without_transitions/analyze.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd $(dirname $0) 4 | 5 | echo u_case_v4-1-0.rb 6 | echo '----------------' 7 | ruby u_case_v4-1-0.rb | head -n 3 8 | 9 | echo u_case_v3-1-0.rb 10 | echo '----------------' 11 | ruby u_case_v3-1-0.rb | head -n 3 12 | 13 | echo u_case_v2-6-0.rb 14 | echo '----------------' 15 | ruby u_case_v2-6-0.rb | head -n 3 16 | 17 | echo u_case_v2-0-0.rb 18 | echo '----------------' 19 | ruby u_case_v2-0-0.rb | head -n 3 20 | 21 | echo interactor.rb 22 | echo '-------------' 23 | ruby interactor.rb | head -n 3 24 | 25 | echo trailblazer_operations.rb 26 | echo '-------------------------' 27 | ruby trailblazer_operations.rb | head -n 3 28 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/failure/without_transitions/u_case_v2-6-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 2.6.0' 11 | end 12 | 13 | Micro::Case::Result.disable_transition_tracking 14 | 15 | class Multiply < Micro::Case 16 | attributes :a, :b 17 | 18 | def call! 19 | if a.is_a?(Numeric) && b.is_a?(Numeric) 20 | Success { { number: a * b } } 21 | else 22 | Failure(:invalid_data) 23 | end 24 | end 25 | end 26 | 27 | Multiply.call(a: nil, 'b' => 2) 28 | 29 | report = MemoryProfiler.report { Multiply.call(a: nil, 'b' => 2) } 30 | report.pretty_print 31 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/success/without_transitions/analyze.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd $(dirname $0) 4 | 5 | echo u_case_v4-1-0.rb 6 | echo '----------------' 7 | ruby u_case_v4-1-0.rb | head -n 3 8 | 9 | echo u_case_v3-1-0.rb 10 | echo '----------------' 11 | ruby u_case_v3-1-0.rb | head -n 3 12 | 13 | echo u_case_v2-6-0.rb 14 | echo '----------------' 15 | ruby u_case_v2-6-0.rb | head -n 3 16 | 17 | echo u_case_v2-0-0.rb 18 | echo '----------------' 19 | ruby u_case_v2-0-0.rb | head -n 3 20 | 21 | echo interactor.rb 22 | echo '-------------' 23 | ruby interactor.rb | head -n 3 24 | 25 | echo trailblazer_operations.rb 26 | echo '-------------------------' 27 | ruby trailblazer_operations.rb | head -n 3 28 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/success/without_transitions/interactor.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'interactor', '~> 3.1' 11 | end 12 | 13 | class Multiply 14 | include Interactor 15 | 16 | def call 17 | a = context.a 18 | b = context.b 19 | 20 | if a.is_a?(Numeric) && b.is_a?(Numeric) 21 | context.number = a * b 22 | else 23 | context.fail!(type: :invalid_data) 24 | end 25 | end 26 | end 27 | 28 | Multiply.call(a: 2, 'b' => 2) 29 | 30 | report = MemoryProfiler.report do 31 | Multiply.call(a: 2, 'b' => 2) 32 | end 33 | 34 | report.pretty_print 35 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/success/with_transitions/u_case_v2-6-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 2.6.0' 11 | end 12 | 13 | # Micro::Case::Result.disable_transition_tracking 14 | 15 | class Multiply < Micro::Case 16 | attributes :a, :b 17 | 18 | def call! 19 | if a.is_a?(Numeric) && b.is_a?(Numeric) 20 | Success { { number: a * b } } 21 | else 22 | Failure(:invalid_data) 23 | end 24 | end 25 | end 26 | 27 | Multiply.call(a: 2, 'b' => 2) 28 | 29 | report = MemoryProfiler.report do 30 | Multiply.call(a: 2, 'b' => 2) 31 | end 32 | 33 | report.pretty_print 34 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/success/without_transitions/u_case_v2-6-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 2.6.0' 11 | end 12 | 13 | Micro::Case::Result.disable_transition_tracking 14 | 15 | class Multiply < Micro::Case 16 | attributes :a, :b 17 | 18 | def call! 19 | if a.is_a?(Numeric) && b.is_a?(Numeric) 20 | Success { { number: a * b } } 21 | else 22 | Failure(:invalid_data) 23 | end 24 | end 25 | end 26 | 27 | Multiply.call(a: 2, 'b' => 2) 28 | 29 | report = MemoryProfiler.report do 30 | Multiply.call(a: 2, 'b' => 2) 31 | end 32 | 33 | report.pretty_print 34 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/failure/with_transitions/u_case_v3-1-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 3.1.0' 11 | end 12 | 13 | Micro::Case.config do |config| 14 | config.enable_transitions = true 15 | end 16 | 17 | class Multiply < Micro::Case 18 | attributes :a, :b 19 | 20 | def call! 21 | if a.is_a?(Numeric) && b.is_a?(Numeric) 22 | Success(result: { number: a * b }) 23 | else 24 | Failure(:invalid_data) 25 | end 26 | end 27 | end 28 | 29 | Multiply.call(a: nil, 'b' => 2) 30 | 31 | report = MemoryProfiler.report { Multiply.call(a: nil, 'b' => 2) } 32 | report.pretty_print 33 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/failure/with_transitions/u_case_v4-1-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 4.1.0' 11 | end 12 | 13 | Micro::Case.config do |config| 14 | config.enable_transitions = true 15 | end 16 | 17 | class Multiply < Micro::Case 18 | attributes :a, :b 19 | 20 | def call! 21 | if a.is_a?(Numeric) && b.is_a?(Numeric) 22 | Success(result: { number: a * b }) 23 | else 24 | Failure(:invalid_data) 25 | end 26 | end 27 | end 28 | 29 | Multiply.call(a: nil, 'b' => 2) 30 | 31 | report = MemoryProfiler.report { Multiply.call(a: nil, 'b' => 2) } 32 | report.pretty_print 33 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/failure/without_transitions/u_case_v3-1-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 3.1.0' 11 | end 12 | 13 | Micro::Case.config do |config| 14 | config.enable_transitions = false 15 | end 16 | 17 | class Multiply < Micro::Case 18 | attributes :a, :b 19 | 20 | def call! 21 | if a.is_a?(Numeric) && b.is_a?(Numeric) 22 | Success(result: { number: a * b }) 23 | else 24 | Failure(:invalid_data) 25 | end 26 | end 27 | end 28 | 29 | Multiply.call(a: nil, 'b' => 2) 30 | 31 | report = MemoryProfiler.report { Multiply.call(a: nil, 'b' => 2) } 32 | report.pretty_print 33 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/failure/without_transitions/u_case_v4-1-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 4.1.0' 11 | end 12 | 13 | Micro::Case.config do |config| 14 | config.enable_transitions = false 15 | end 16 | 17 | class Multiply < Micro::Case 18 | attributes :a, :b 19 | 20 | def call! 21 | if a.is_a?(Numeric) && b.is_a?(Numeric) 22 | Success(result: { number: a * b }) 23 | else 24 | Failure(:invalid_data) 25 | end 26 | end 27 | end 28 | 29 | Multiply.call(a: nil, 'b' => 2) 30 | 31 | report = MemoryProfiler.report { Multiply.call(a: nil, 'b' => 2) } 32 | report.pretty_print 33 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/success/with_transitions/u_case_v3-1-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 3.1.0' 11 | end 12 | 13 | Micro::Case.config do |config| 14 | config.enable_transitions = true 15 | end 16 | 17 | class Multiply < Micro::Case 18 | attributes :a, :b 19 | 20 | def call! 21 | if a.is_a?(Numeric) && b.is_a?(Numeric) 22 | Success(result: { number: a * b }) 23 | else 24 | Failure(:invalid_data) 25 | end 26 | end 27 | end 28 | 29 | Multiply.call(a: 2, 'b' => 2) 30 | 31 | report = MemoryProfiler.report do 32 | Multiply.call(a: 2, 'b' => 2) 33 | end 34 | 35 | report.pretty_print 36 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/success/with_transitions/u_case_v4-1-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 3.1.0' 11 | end 12 | 13 | Micro::Case.config do |config| 14 | config.enable_transitions = true 15 | end 16 | 17 | class Multiply < Micro::Case 18 | attributes :a, :b 19 | 20 | def call! 21 | if a.is_a?(Numeric) && b.is_a?(Numeric) 22 | Success(result: { number: a * b }) 23 | else 24 | Failure(:invalid_data) 25 | end 26 | end 27 | end 28 | 29 | Multiply.call(a: 2, 'b' => 2) 30 | 31 | report = MemoryProfiler.report do 32 | Multiply.call(a: 2, 'b' => 2) 33 | end 34 | 35 | report.pretty_print 36 | -------------------------------------------------------------------------------- /test/support/todoing/app/models/todo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Todo < InactiveRecord::Base 4 | attr_accessor :description, :done, :user_id 5 | 6 | def self.find_by_id_and_user_id(id, user_id) 7 | all.find { |todo| todo.id == id && todo.user_id && user_id } 8 | end 9 | 10 | def initialize(options = {}) 11 | @user_id = options[:user_id] 12 | @description = options[:description] 13 | end 14 | 15 | def invalid? 16 | description.empty? || user_id.empty? 17 | end 18 | 19 | def save 20 | return false if invalid? 21 | 22 | self.description = description 23 | 24 | save_new_record { @done = done? } 25 | end 26 | 27 | def pending?; !done; end 28 | 29 | def done?; !pending?; end 30 | end 31 | 32 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/success/without_transitions/u_case_v3-1-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 3.1.0' 11 | end 12 | 13 | Micro::Case.config do |config| 14 | config.enable_transitions = false 15 | end 16 | 17 | class Multiply < Micro::Case 18 | attributes :a, :b 19 | 20 | def call! 21 | if a.is_a?(Numeric) && b.is_a?(Numeric) 22 | Success(result: { number: a * b }) 23 | else 24 | Failure(:invalid_data) 25 | end 26 | end 27 | end 28 | 29 | Multiply.call(a: 2, 'b' => 2) 30 | 31 | report = MemoryProfiler.report do 32 | Multiply.call(a: 2, 'b' => 2) 33 | end 34 | 35 | report.pretty_print 36 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/success/without_transitions/u_case_v4-1-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 4.1.0' 11 | end 12 | 13 | Micro::Case.config do |config| 14 | config.enable_transitions = false 15 | end 16 | 17 | class Multiply < Micro::Case 18 | attributes :a, :b 19 | 20 | def call! 21 | if a.is_a?(Numeric) && b.is_a?(Numeric) 22 | Success(result: { number: a * b }) 23 | else 24 | Failure(:invalid_data) 25 | end 26 | end 27 | end 28 | 29 | Multiply.call(a: 2, 'b' => 2) 30 | 31 | report = MemoryProfiler.report do 32 | Multiply.call(a: 2, 'b' => 2) 33 | end 34 | 35 | report.pretty_print 36 | -------------------------------------------------------------------------------- /benchmarks/examples/use_case/multiply_with/dry_monads.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'dry/monads' 4 | require 'dry/monads/do' 5 | 6 | module MultiplyWith 7 | class DryMonads 8 | include Dry::Monads[:result] 9 | include Dry::Monads::Do.for(:call) 10 | 11 | def call(params) 12 | input = yield normalize(params) 13 | 14 | yield calculate(input['a'], input['b']) 15 | end 16 | 17 | private 18 | 19 | def normalize(input) 20 | data = input.map { |key, value| [key.to_s, value] }.to_h 21 | 22 | Success(data) 23 | end 24 | 25 | def calculate(a, b) 26 | if a.is_a?(Numeric) && b.is_a?(Numeric) 27 | Success(a * b) 28 | else 29 | Failure(:invalid_data) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/failure/without_transitions/trailblazer_operations.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | require 'bundler/inline' 4 | 5 | gemfile do 6 | source 'https://rubygems.org' 7 | 8 | ruby '>= 2.4.0' 9 | 10 | gem 'memory_profiler' 11 | 12 | gem 'trailblazer-activity', '~> 0.10.1' 13 | gem 'trailblazer-operation', '~> 0.6.2', require: 'trailblazer/operation' 14 | end 15 | 16 | class Multiply < Trailblazer::Operation 17 | step :calculate 18 | 19 | private 20 | 21 | def calculate(options, a:, b:) 22 | if a.is_a?(Numeric) && b.is_a?(Numeric) 23 | options[:number] = a * b 24 | end 25 | end 26 | end 27 | 28 | Multiply.call(a: nil, 'b' => 2) 29 | 30 | report = MemoryProfiler.report { Multiply.call(a: nil, 'b' => 2) } 31 | report.pretty_print 32 | -------------------------------------------------------------------------------- /test/support/todoing/lib/inactive_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'securerandom' 4 | 5 | module InactiveRecord 6 | class Base 7 | attr_reader :id 8 | 9 | def self.__relation; (@relation ||= []); end 10 | 11 | def self.all; __relation.to_a; end 12 | 13 | def self.count; __relation.size; end 14 | 15 | def self.delete_all; @relation = []; end 16 | 17 | def self.find_by_id(id) 18 | __relation.find { |rec| rec.id == id } 19 | end 20 | 21 | def new_record?; id.nil?; end 22 | 23 | private 24 | 25 | def save_new_record 26 | if new_record? 27 | @id = SecureRandom.uuid 28 | 29 | yield 30 | 31 | self.class.__relation << self 32 | end 33 | 34 | true 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /benchmarks/memory/use_case/success/without_transitions/trailblazer_operations.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | require 'bundler/inline' 4 | 5 | gemfile do 6 | source 'https://rubygems.org' 7 | 8 | ruby '>= 2.4.0' 9 | 10 | gem 'memory_profiler' 11 | 12 | gem 'trailblazer-activity', '~> 0.10.1' 13 | gem 'trailblazer-operation', '~> 0.6.2', require: 'trailblazer/operation' 14 | end 15 | 16 | class Multiply < Trailblazer::Operation 17 | step :calculate 18 | 19 | private 20 | 21 | def calculate(options, a:, b:) 22 | if a.is_a?(Numeric) && b.is_a?(Numeric) 23 | options[:number] = a * b 24 | end 25 | end 26 | end 27 | 28 | Multiply.call(a: 2, 'b' => 2) 29 | 30 | report = MemoryProfiler.report do 31 | Multiply.call(a: 2, 'b' => 2) 32 | end 33 | 34 | report.pretty_print 35 | -------------------------------------------------------------------------------- /test/support/todoing/app/models/user.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'digest' 4 | 5 | class User < InactiveRecord::Base 6 | attr_reader :password_hash 7 | attr_accessor :email 8 | 9 | def self.find_by_email(email) 10 | __relation.find { |rec| rec.email == email } 11 | end 12 | 13 | def initialize(options = {}) 14 | @email = options[:email] 15 | @password = options[:password] 16 | end 17 | 18 | def invalid? 19 | email.empty? || @password.empty? 20 | end 21 | 22 | def save 23 | return false if invalid? 24 | 25 | self.email = email 26 | 27 | save_new_record do 28 | @password_hash = Digest::SHA256.hexdigest(@password) 29 | end 30 | end 31 | 32 | def wrong_password?(value) 33 | password_hash != Digest::SHA256.hexdigest(value) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /benchmarks/memory/flow/success/without_transitions/u_case_v2-0-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 2.0.0' 11 | end 12 | 13 | class ConvertTextToNumber < Micro::Case 14 | attribute :text 15 | 16 | def call! 17 | return Success(number: text.to_i) if text =~ /\d+/ 18 | 19 | Failure { { text: 'must be an integer value' } } 20 | end 21 | end 22 | 23 | class Add1 < Micro::Case 24 | attribute :number 25 | 26 | def call! 27 | Success(number: number + 1) 28 | end 29 | end 30 | 31 | Add5 = Micro::Case::Flow([ 32 | ConvertTextToNumber, 33 | Add1, 34 | Add1, 35 | Add1, 36 | Add1, 37 | Add1 38 | ]) 39 | 40 | Add5.call(text: '0') 41 | 42 | report = MemoryProfiler.report { Add5.call(text: '0') } 43 | report.pretty_print 44 | -------------------------------------------------------------------------------- /benchmarks/examples/flow/add_five_with/interactor.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AddFiveWith 4 | module Interactor 5 | 6 | class ConvertTextToNumber 7 | include ::Interactor 8 | 9 | def call 10 | text = context.text 11 | 12 | if text =~ /\d+/ 13 | context.number = text.to_i 14 | else 15 | context.fail! text: 'must be an integer value' 16 | end 17 | end 18 | end 19 | 20 | class Add1 21 | include ::Interactor 22 | 23 | def call 24 | context.number = context.number + 1 25 | end 26 | end 27 | 28 | class Organizer 29 | include ::Interactor::Organizer 30 | 31 | organize( 32 | ConvertTextToNumber, 33 | Add1, 34 | Add1, 35 | Add1, 36 | Add1, 37 | Add1 38 | ) 39 | end 40 | 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /benchmarks/memory/flow/success/with_transitions/u_case_v2-6-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 2.6.0' 11 | end 12 | 13 | # Micro::Case::Result.disable_transition_tracking 14 | 15 | class ConvertTextToNumber < Micro::Case 16 | attribute :text 17 | 18 | def call! 19 | return Success(number: text.to_i) if text =~ /\d+/ 20 | 21 | Failure { { text: 'must be an integer value' } } 22 | end 23 | end 24 | 25 | class Add1 < Micro::Case 26 | attribute :number 27 | 28 | def call! 29 | Success(number: number + 1) 30 | end 31 | end 32 | 33 | Add5 = Micro::Case::Flow([ 34 | ConvertTextToNumber, 35 | Add1, 36 | Add1, 37 | Add1, 38 | Add1, 39 | Add1 40 | ]) 41 | 42 | Add5.call(text: '0') 43 | 44 | report = MemoryProfiler.report { Add5.call(text: '0') } 45 | report.pretty_print 46 | -------------------------------------------------------------------------------- /benchmarks/memory/flow/success/without_transitions/u_case_v2-6-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 2.6.0' 11 | end 12 | 13 | Micro::Case::Result.disable_transition_tracking 14 | 15 | class ConvertTextToNumber < Micro::Case 16 | attribute :text 17 | 18 | def call! 19 | return Success(number: text.to_i) if text =~ /\d+/ 20 | 21 | Failure { { text: 'must be an integer value' } } 22 | end 23 | end 24 | 25 | class Add1 < Micro::Case 26 | attribute :number 27 | 28 | def call! 29 | Success(number: number + 1) 30 | end 31 | end 32 | 33 | Add5 = Micro::Case::Flow([ 34 | ConvertTextToNumber, 35 | Add1, 36 | Add1, 37 | Add1, 38 | Add1, 39 | Add1 40 | ]) 41 | 42 | Add5.call(text: '0') 43 | 44 | report = MemoryProfiler.report { Add5.call(text: '0') } 45 | report.pretty_print 46 | -------------------------------------------------------------------------------- /examples/calculator/calc/operation.rb: -------------------------------------------------------------------------------- 1 | class Operation < Micro::Case 2 | attributes :a, :b 3 | 4 | private def operation_info(operation_result) 5 | attributes(:a, :operator, :b) 6 | .merge(result: operation_result) 7 | end 8 | 9 | class Add < Operation 10 | attribute :operator, default: '+' 11 | 12 | def call! 13 | Success result: operation_info(a + b) 14 | end 15 | end 16 | 17 | class Subtract < Operation 18 | attribute :operator, default: '-' 19 | 20 | def call! 21 | Success result: operation_info(a - b) 22 | end 23 | end 24 | 25 | class Multiply < Operation 26 | attribute :operator, default: 'x' 27 | 28 | def call! 29 | Success result: operation_info(a * b) 30 | end 31 | end 32 | 33 | class Divide < Operation 34 | attribute :operator, default: '/' 35 | 36 | def call! 37 | Success result: operation_info(a / b) 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/micro/case/utils.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Micro::Case::Utils 4 | 5 | module Hashes 6 | def self.hash_respond_to?(hash, method) 7 | Kind::Hash[hash].respond_to?(method) 8 | end 9 | 10 | def self.symbolize_keys(hash) 11 | return hash.transform_keys { |key| key.to_sym rescue key } if hash_respond_to?(hash, :transform_keys) 12 | 13 | hash.each_with_object({}) do |(k, v), memo| 14 | key = k.to_sym rescue k 15 | memo[key] = v 16 | end 17 | end 18 | 19 | def self.stringify_keys(hash) 20 | return hash.transform_keys(&:to_s) if hash_respond_to?(hash, :transform_keys) 21 | 22 | hash.each_with_object({}) { |(k, v), memo| memo[k.to_s] = v } 23 | end 24 | 25 | def self.slice(hash, keys) 26 | return hash.slice(*keys) if hash_respond_to?(hash, :slice) 27 | 28 | hash.select { |key, _value| keys.include?(key) } 29 | end 30 | end 31 | 32 | end 33 | -------------------------------------------------------------------------------- /benchmarks/memory/flow/success/with_transitions/u_case_v3-1-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 3.1.0' 11 | end 12 | 13 | Micro::Case.config do |config| 14 | config.enable_transitions = true 15 | end 16 | 17 | class ConvertTextToNumber < Micro::Case 18 | attribute :text 19 | 20 | def call! 21 | return Success(result: { number: text.to_i }) if text =~ /\d+/ 22 | 23 | Failure result: { text: 'must be an integer value' } 24 | end 25 | end 26 | 27 | class Add1 < Micro::Case 28 | attribute :number 29 | 30 | def call! 31 | Success result: { number: number + 1 } 32 | end 33 | end 34 | 35 | Add5 = Micro::Cases.flow([ 36 | ConvertTextToNumber, 37 | Add1, 38 | Add1, 39 | Add1, 40 | Add1, 41 | Add1 42 | ]) 43 | 44 | Add5.call(text: '0') 45 | 46 | report = MemoryProfiler.report { Add5.call(text: '0') } 47 | report.pretty_print 48 | -------------------------------------------------------------------------------- /benchmarks/memory/flow/success/with_transitions/u_case_v4-1-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 4.1.0' 11 | end 12 | 13 | Micro::Case.config do |config| 14 | config.enable_transitions = true 15 | end 16 | 17 | class ConvertTextToNumber < Micro::Case 18 | attribute :text 19 | 20 | def call! 21 | return Success(result: { number: text.to_i }) if text =~ /\d+/ 22 | 23 | Failure result: { text: 'must be an integer value' } 24 | end 25 | end 26 | 27 | class Add1 < Micro::Case 28 | attribute :number 29 | 30 | def call! 31 | Success result: { number: number + 1 } 32 | end 33 | end 34 | 35 | Add5 = Micro::Cases.flow([ 36 | ConvertTextToNumber, 37 | Add1, 38 | Add1, 39 | Add1, 40 | Add1, 41 | Add1 42 | ]) 43 | 44 | Add5.call(text: '0') 45 | 46 | report = MemoryProfiler.report { Add5.call(text: '0') } 47 | report.pretty_print 48 | -------------------------------------------------------------------------------- /benchmarks/memory/flow/success/without_transitions/interactor.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'interactor', '~> 3.1' 11 | end 12 | 13 | class ConvertTextToNumber 14 | include ::Interactor 15 | 16 | def call 17 | text = context.text 18 | 19 | if text =~ /\d+/ 20 | context.number = text.to_i 21 | else 22 | context.fail! text: 'must be an integer value' 23 | end 24 | end 25 | end 26 | 27 | class Add1 28 | include ::Interactor 29 | 30 | def call 31 | context.number = context.number + 1 32 | end 33 | end 34 | 35 | class Add5 36 | include ::Interactor::Organizer 37 | 38 | organize( 39 | ConvertTextToNumber, 40 | Add1, 41 | Add1, 42 | Add1, 43 | Add1, 44 | Add1 45 | ) 46 | end 47 | 48 | Add5.call(text: '0') 49 | 50 | report = MemoryProfiler.report { Add5.call(text: '0') } 51 | report.pretty_print 52 | -------------------------------------------------------------------------------- /test/micro/case/then_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Micro::Case::ThenTest < Minitest::Test 4 | class Add1 < Micro::Case 5 | attribute :number 6 | 7 | def call! 8 | Success result: { number: number + 1 } 9 | end 10 | end 11 | 12 | if RUBY_VERSION < '2.5.0' 13 | def test_the_not_implemented_error 14 | assert_raises(NotImplementedError) { Add1.then { 0 } } 15 | end 16 | end 17 | 18 | def test_the_invalid_invocation_error 19 | assert_raises_with_message( 20 | Micro::Case::Error::InvalidInvocationOfTheThenMethod, 21 | 'Invalid invocation of the Micro::Case.then method' 22 | ) { Add1.then(1) } 23 | 24 | if RUBY_VERSION >= '2.5.0' 25 | assert_raises_with_message( 26 | Micro::Case::Error::InvalidInvocationOfTheThenMethod, 27 | 'Invalid invocation of the Micro::Case.then method' 28 | ) { Add1.then(1) { 0 } } 29 | 30 | Add1.then { |arg| assert_same(Add1, arg) } 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /benchmarks/memory/flow/success/without_transitions/u_case_v3-1-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 3.1.0' 11 | end 12 | 13 | Micro::Case.config do |config| 14 | config.enable_transitions = false 15 | end 16 | 17 | class ConvertTextToNumber < Micro::Case 18 | attribute :text 19 | 20 | def call! 21 | return Success(result: { number: text.to_i }) if text =~ /\d+/ 22 | 23 | Failure result: { text: 'must be an integer value' } 24 | end 25 | end 26 | 27 | class Add1 < Micro::Case 28 | attribute :number 29 | 30 | def call! 31 | Success result: { number: number + 1 } 32 | end 33 | end 34 | 35 | Add5 = Micro::Cases.flow([ 36 | ConvertTextToNumber, 37 | Add1, 38 | Add1, 39 | Add1, 40 | Add1, 41 | Add1 42 | ]) 43 | 44 | Add5.call(text: '0') 45 | 46 | report = MemoryProfiler.report { Add5.call(text: '0') } 47 | report.pretty_print 48 | 49 | -------------------------------------------------------------------------------- /benchmarks/memory/flow/success/without_transitions/u_case_v4-1-0.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | ruby '>= 2.4.0' 7 | 8 | gem 'memory_profiler' 9 | 10 | gem 'u-case', '~> 4.1.0' 11 | end 12 | 13 | Micro::Case.config do |config| 14 | config.enable_transitions = false 15 | end 16 | 17 | class ConvertTextToNumber < Micro::Case 18 | attribute :text 19 | 20 | def call! 21 | return Success(result: { number: text.to_i }) if text =~ /\d+/ 22 | 23 | Failure result: { text: 'must be an integer value' } 24 | end 25 | end 26 | 27 | class Add1 < Micro::Case 28 | attribute :number 29 | 30 | def call! 31 | Success result: { number: number + 1 } 32 | end 33 | end 34 | 35 | Add5 = Micro::Cases.flow([ 36 | ConvertTextToNumber, 37 | Add1, 38 | Add1, 39 | Add1, 40 | Add1, 41 | Add1 42 | ]) 43 | 44 | Add5.call(text: '0') 45 | 46 | report = MemoryProfiler.report { Add5.call(text: '0') } 47 | report.pretty_print 48 | 49 | -------------------------------------------------------------------------------- /lib/micro/case/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'singleton' 4 | 5 | module Micro 6 | class Case 7 | class Config 8 | include Singleton 9 | 10 | def enable_transitions=(value) 11 | Micro::Case::Result.class_variable_set( 12 | :@@transitions_enabled, Kind::Boolean[value] 13 | ) 14 | end 15 | 16 | def enable_activemodel_validation=(value) 17 | return unless Kind::Boolean[value] 18 | 19 | require 'micro/case/with_activemodel_validation' 20 | end 21 | 22 | def set_activemodel_validation_errors_failure=(value) 23 | return unless value 24 | 25 | @activemodel_validation_errors_failure = Kind::Symbol[value] 26 | end 27 | 28 | def activemodel_validation_errors_failure 29 | return @activemodel_validation_errors_failure if defined?(@activemodel_validation_errors_failure) 30 | 31 | @activemodel_validation_errors_failure = :invalid_attributes 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/support/steps.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Steps 4 | class ConvertToNumbers < Micro::Case 5 | attribute :numbers 6 | 7 | def call! 8 | if !numbers.nil? && numbers.all? { |value| String(value) =~ /\d+/ } 9 | Success result: { numbers: numbers.map(&:to_i) } 10 | else 11 | Failure result: { message: 'numbers must contain only numeric types' } 12 | end 13 | end 14 | end 15 | 16 | class Add2 < Micro::Case::Strict 17 | attribute :numbers 18 | 19 | def call! 20 | Success result: { numbers: numbers.map { |number| number + 2 } } 21 | end 22 | end 23 | 24 | class Double < Micro::Case::Strict 25 | attribute :numbers 26 | 27 | def call! 28 | Success result: { numbers: numbers.map { |number| number * 2 } } 29 | end 30 | end 31 | 32 | class Square < Micro::Case::Strict 33 | attribute :numbers 34 | 35 | def call! 36 | Success result: { numbers: numbers.map { |number| number * number } } 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /test/micro/case/MWRF/shared_assertions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Micro::Case::MWRF 4 | module SharedAssertions 5 | UUID_FORMAT = /\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b/ 6 | 7 | def use_case 8 | raise NotImplementedError 9 | end 10 | 11 | def test_the_use_case_result 12 | result = use_case.call({ 13 | "name" => " Rodrigo \n Serradura ", 14 | "email" => " RoDRIGo.SERRAdura@gmail.com " 15 | }) 16 | 17 | assert result.success? 18 | 19 | user, crm_id = result.values_at(:user, :crm_id) 20 | 21 | assert_match(UUID_FORMAT, crm_id) 22 | 23 | assert_match(UUID_FORMAT, user.id) 24 | assert_equal('Rodrigo Serradura', user.name) 25 | assert_equal('rodrigo.serradura@gmail.com', user.email) 26 | 27 | # -- 28 | 29 | [ 30 | use_case.call(name: 'A', email: ''), 31 | use_case.call(name: '', email: 'a@a.com') 32 | ].each do |use_case_result| 33 | assert_predicate(use_case_result, :failure?) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /benchmarks/perfomance/use_case/call_use_cases.rb: -------------------------------------------------------------------------------- 1 | CallUseCases = -> (params:) do 2 | -> (x) do 3 | x.config(time: 5, warmup: 2) 4 | x.time = 5 5 | x.warmup = 2 6 | 7 | [ 8 | interactor = -> { MultiplyWith::Interactor.call(params) }, 9 | trailblazer = -> { MultiplyWith::Trailblazer.call(params) }, 10 | dry_monads = -> { MultiplyWith::DryMonads.new.call(params) }, 11 | dry_transaction = -> { MultiplyWith::DryTransaction.new.call(params) }, 12 | u_case = -> { MultiplyWith::MicroCase.call(params) }, 13 | u_case_safe = -> { MultiplyWith::MicroCaseSafe.call(params) }, 14 | u_case_strict = -> { MultiplyWith::MicroCaseStrict.call(params) } 15 | ].each(&:call) 16 | 17 | x.report('Interactor', &interactor) 18 | x.report('Trailblazer::Operation', &trailblazer) 19 | x.report('Dry::Monads', &dry_monads) 20 | x.report('Dry::Transaction', &dry_transaction) 21 | x.report('Micro::Case', &u_case) 22 | x.report('Micro::Case::Safe', &u_case_safe) 23 | x.report('Micro::Case::Strict', &u_case_strict) 24 | 25 | x.compare! 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/micro/case/with_activemodel_validation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'kind/validator' 4 | 5 | require 'micro/case' 6 | 7 | module Micro 8 | class Case 9 | include Micro::Attributes::Features::ActiveModelValidations 10 | 11 | def self.auto_validation_disabled? 12 | return @disable_auto_validation if defined?(@disable_auto_validation) 13 | end 14 | 15 | def self.disable_auto_validation 16 | @disable_auto_validation = true 17 | end 18 | 19 | private 20 | 21 | def __call_use_case 22 | return failure_by_validation_error(self) if !self.class.auto_validation_disabled? && errors.present? 23 | 24 | result = call! 25 | 26 | return result if result.is_a?(Result) 27 | 28 | raise Error::UnexpectedResult.new("#{self.class.name}#call!") 29 | end 30 | 31 | def failure_by_validation_error(object) 32 | errors = object.respond_to?(:errors) ? object.errors : object 33 | 34 | Failure Micro::Case::Config.instance.activemodel_validation_errors_failure, result: { 35 | errors: errors 36 | } 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Rodrigo Serradura 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/MWRF/shared_assertions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Micro::Case::MWRF::WithValidation 4 | module SharedAssertions 5 | UUID_FORMAT = /\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b/ 6 | 7 | def use_case 8 | raise NotImplementedError 9 | end 10 | 11 | def test_the_use_case_result 12 | result = use_case.call({ 13 | "name" => " Rodrigo \n Serradura ", 14 | "email" => " RoDRIGo.SERRAdura@gmail.com " 15 | }) 16 | 17 | assert result.success? 18 | 19 | user, crm_id = result.values_at(:user, :crm_id) 20 | 21 | assert_match(UUID_FORMAT, crm_id) 22 | 23 | assert_match(UUID_FORMAT, user.id) 24 | assert_equal('Rodrigo Serradura', user.name) 25 | assert_equal('rodrigo.serradura@gmail.com', user.email) 26 | 27 | # -- 28 | 29 | [ 30 | use_case.call(name: 'A', email: ''), 31 | use_case.call(name: '', email: 'a@a.com') 32 | ].each do |use_case_result| 33 | assert_predicate(use_case_result, :failure?) 34 | assert_equal(:invalid_attributes, use_case_result.type) 35 | end 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/safe/disable_auto_validation_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | 5 | module Micro::Case::WithActivemodelValidation::Safe 6 | class DisableAutoValidationTest < Minitest::Test 7 | class Multiply < Micro::Case::Safe 8 | disable_auto_validation 9 | 10 | attribute :a 11 | attribute :b 12 | validates :a, :b, presence: true, numericality: true 13 | 14 | def call! 15 | Success(result: { number: a * b }) 16 | end 17 | end 18 | 19 | class Add < Micro::Case::Safe 20 | attribute :a 21 | attribute :b 22 | validates :a, :b, presence: true, numericality: true 23 | 24 | def call! 25 | Success result: { number: a + b } 26 | end 27 | end 28 | 29 | def test_the_disable_auto_validation_macro 30 | result1 = Add.call(a: 'a', b: 2) 31 | 32 | assert_failure_result(result1, type: :invalid_attributes) 33 | 34 | # --- 35 | 36 | result2 = Multiply.call(a: 2, b: 'a') 37 | 38 | assert_exception_result(result2, value: { exception: TypeError }) 39 | end 40 | end 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /lib/micro/cases/map.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Micro 4 | module Cases 5 | class Map 6 | IsAUseCaseOrFlowWithDefaults = -> arg { arg.is_a?(Array) && Micro.case_or_flow?(arg[0]) && arg[1].is_a?(Hash) } 7 | IsAUseCaseOrFlow = -> arg { Micro.case_or_flow?(arg) || IsAUseCaseOrFlowWithDefaults[arg] } 8 | HasValidArgs = -> (args) { Kind::Array[args].all?(&IsAUseCaseOrFlow) } 9 | 10 | attr_reader :use_cases 11 | 12 | def self.build(args) 13 | raise Error::InvalidUseCases unless HasValidArgs[args] 14 | 15 | new(args) 16 | end 17 | 18 | def initialize(use_cases) 19 | @use_cases = use_cases 20 | end 21 | 22 | GetUseCaseResult = -> (hash) do 23 | -> (use_case) do 24 | if use_case.is_a?(Array) 25 | use_case[0].call(hash.merge(use_case[1])) 26 | else 27 | use_case.call(hash) 28 | end 29 | end 30 | end 31 | 32 | def call(arg = {}) 33 | hash = Kind::Hash[arg] 34 | 35 | use_cases.map(&GetUseCaseResult[hash]) 36 | end 37 | 38 | private_constant :HasValidArgs, :IsAUseCaseOrFlow, :IsAUseCaseOrFlowWithDefaults, :GetUseCaseResult 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/disable_auto_validation_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | 5 | module Micro::Case::WithActivemodelValidation 6 | class DisableAutoValidationTest < Minitest::Test 7 | class Multiply < Micro::Case 8 | disable_auto_validation 9 | 10 | attribute :a 11 | attribute :b 12 | validates :a, :b, presence: true, numericality: true 13 | 14 | def call! 15 | Success result: { number: a * b } 16 | end 17 | end 18 | 19 | class Add < Micro::Case 20 | attribute :a 21 | attribute :b 22 | validates :a, :b, presence: true, numericality: true 23 | 24 | def call! 25 | Success result: { number: a + b } 26 | end 27 | end 28 | 29 | def test_the_disable_auto_validation_macro 30 | result1 = Add.call(a: 'a', b: 2) 31 | 32 | assert_failure_result(result1, type: :invalid_attributes) 33 | 34 | # --- 35 | 36 | assert_raises_with_message(TypeError, /String can't be coerced into (Integer|Fixnum)/) do 37 | Multiply.call(a: 2, b: 'a') 38 | end 39 | end 40 | end 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /lib/micro/case/result/wrapper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Micro 4 | class Case 5 | class Result 6 | class Wrapper 7 | attr_reader :output 8 | 9 | def initialize(result) 10 | @result = result 11 | @output = ::Kind::Undefined 12 | 13 | @__is_unknown = true 14 | end 15 | 16 | def failure(type = nil) 17 | return if @result.success? || !undefined_output? 18 | 19 | set_output(yield(@result)) if result_type?(type) 20 | end 21 | 22 | def success(type = nil) 23 | return if @result.failure? || !undefined_output? 24 | 25 | set_output(yield(@result)) if result_type?(type) 26 | end 27 | 28 | def unknown 29 | @output = yield(@result) if @__is_unknown && undefined_output? 30 | end 31 | 32 | private 33 | 34 | def set_output(value) 35 | @__is_unknown = false 36 | 37 | @output = value 38 | end 39 | 40 | def undefined_output? 41 | ::Kind::Undefined == @output 42 | end 43 | 44 | def result_type?(type) 45 | type.nil? || @result.type == type 46 | end 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /benchmarks/perfomance/flow/call_flows.rb: -------------------------------------------------------------------------------- 1 | CallFlows = -> (params:) do 2 | -> (x) do 3 | x.config(time: 5, warmup: 2) 4 | x.time = 5 5 | x.warmup = 2 6 | 7 | [ 8 | interactor = -> { AddFiveWith::Interactor::Organizer.call(params) }, 9 | u_case_flow_collection = -> { AddFiveWith::MicroCase::Flow::Collection.call(params) }, 10 | u_case_flow_collection_in_a_class = -> { AddFiveWith::MicroCase::Flow::CollectionInAClass.call(params) }, 11 | u_case_flow_including_the_class = -> { AddFiveWith::MicroCase::Flow::IncludingTheClass.call(params) }, 12 | u_case_flow_using_result_pipes = -> { AddFiveWith::MicroCase::Flow::UsingResultPipes.call(params) }, 13 | u_case_flow_using_result_thens = -> { AddFiveWith::MicroCase::Flow::UsingResultThens.call(params) } 14 | ].each(&:call) 15 | 16 | x.report('Interactor::Organizer', &interactor) 17 | x.report('Micro::Cases.flow([])', &u_case_flow_collection) 18 | x.report('Micro::Case flow in a class', &u_case_flow_collection_in_a_class) 19 | x.report('Micro::Case including the class', &u_case_flow_including_the_class) 20 | x.report('Micro::Case::Result#|', &u_case_flow_using_result_pipes) 21 | x.report('Micro::Case::Result#then', &u_case_flow_using_result_thens) 22 | 23 | x.compare! 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /u-case.gemspec: -------------------------------------------------------------------------------- 1 | 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'micro/case/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'u-case' 8 | spec.version = Micro::Case::VERSION 9 | spec.authors = ['Rodrigo Serradura'] 10 | spec.email = ['rodrigo.serradura@gmail.com'] 11 | 12 | spec.summary = %q{Represent use cases in a simple and powerful way while writing modular, expressive and sequentially logical code.} 13 | spec.description = %q{Represent use cases in a simple and powerful way while writing modular, expressive and sequentially logical code.} 14 | spec.homepage = 'https://github.com/serradura/u-case' 15 | spec.license = 'MIT' 16 | 17 | raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.' unless spec.respond_to?(:metadata) 18 | 19 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 20 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features|assets|benchmarks|comparisons|examples)/}) } 21 | end 22 | spec.bindir = 'exe' 23 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 24 | spec.require_paths = ['lib'] 25 | 26 | spec.required_ruby_version = '>= 2.2.0' 27 | 28 | spec.add_runtime_dependency 'kind', '>= 5.6', '< 6.0' 29 | spec.add_runtime_dependency 'u-attributes', '>= 2.7', '< 3.0' 30 | 31 | spec.add_development_dependency 'bundler' 32 | spec.add_development_dependency 'rake', '~> 13.0' 33 | end 34 | -------------------------------------------------------------------------------- /test/micro/case/MWRF/steps/01_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | require_relative '../users_entity' 4 | require_relative '../shared_assertions' 5 | 6 | class Micro::Case::MWRF 7 | class Step01Test < Minitest::Test 8 | include SharedAssertions 9 | 10 | module Users::Creation1 11 | require 'uri' 12 | require 'securerandom' 13 | 14 | class Process < Micro::Case 15 | attributes :name, :email 16 | 17 | def call! 18 | normalized_name = String(name).strip.gsub(/\s+/, ' ') 19 | normalized_email = String(email).downcase.strip 20 | 21 | validation_errors = [] 22 | validation_errors << "Name can't be blank" if normalized_name.empty? 23 | validation_errors << "Email is invalid" if normalized_email !~ URI::MailTo::EMAIL_REGEXP 24 | 25 | if !validation_errors.empty? 26 | return Failure :invalid_attributes, result: { 27 | errors: OpenStruct.new(full_messages: validation_errors) 28 | } 29 | end 30 | 31 | user = Users::Entity.new( 32 | id: SecureRandom.uuid, 33 | name: normalized_name, 34 | email: normalized_email 35 | ) 36 | 37 | Success result: { user: user, crm_id: sync_with_crm } 38 | end 39 | 40 | private def sync_with_crm 41 | # Do some integration stuff... 42 | SecureRandom.uuid 43 | end 44 | end 45 | end 46 | 47 | def use_case 48 | Users::Creation1::Process 49 | end 50 | 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /comparisons/u-case.rb: -------------------------------------------------------------------------------- 1 | class CreateResponse < Micro::Case 2 | attributes :responder, :answers, :survey 3 | 4 | def call! 5 | survey_response = responder.survey_responses.build( 6 | response_text: answers[:text], 7 | rating: answers[:rating], 8 | survey: survey 9 | ) 10 | 11 | return Success result: attributes(:responder, :survey) if survey_response.save 12 | 13 | Failure :survey_response_errors, result: survey_response.errors 14 | end 15 | end 16 | 17 | class AddRewardPoints < Micro::Case 18 | attributes :responder, :survey 19 | 20 | def call! 21 | reward_account = responder.reward_account 22 | reward_account.balance += survey.reward_points 23 | 24 | return Success, result: attributes if reward_account.save 25 | 26 | Failure :reward_account_errors, result: reward_account.errors 27 | end 28 | end 29 | 30 | class SendNotifications < Micro::Case 31 | attributes :responder, :survey 32 | 33 | def call! 34 | sender = survey.sender 35 | 36 | SurveyMailer.delay.notify_responder(responder.id) 37 | SurveyMailer.delay.notify_sender(sender.id) 38 | 39 | if sender.add_survey_response_notification 40 | Success, result: attributes(:survey) 41 | else 42 | Failure :sender_errors, result: sender.errors 43 | end 44 | end 45 | end 46 | 47 | class ReplyToSurvey < Micro::Case 48 | flow CreateResponse, 49 | AddRewardPoints, 50 | SendNotifications 51 | end 52 | 53 | # or 54 | 55 | ReplyToSurvey = Micro::Cases.flow([ 56 | CreateResponse, 57 | AddRewardPoints, 58 | SendNotifications 59 | ]) 60 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/config_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | module Micro::Case::WithActivemodelValidation 4 | class ConfigTest < Minitest::Test 5 | i_suck_and_my_tests_are_order_dependent! 6 | 7 | def test_the_default_activemodel_validation_errors_failure_value 8 | assert_raises_with_message( 9 | Kind::Error, 10 | '"validation_error" expected to be a kind of Symbol' 11 | ) do 12 | Micro::Case.config do |config| 13 | config.set_activemodel_validation_errors_failure = 'validation_error' 14 | end 15 | end 16 | 17 | # -- 18 | 19 | assert_equal( 20 | :invalid_attributes, 21 | Micro::Case::Config.instance.activemodel_validation_errors_failure 22 | ) 23 | end 24 | 25 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 26 | class Multiply < Micro::Case 27 | attribute :a 28 | attribute :b 29 | validates :a, :b, presence: true, numericality: true 30 | 31 | def call! 32 | Success(result: {number: a * b}) 33 | end 34 | end 35 | 36 | def test_the_activemodel_validation_errors_failure_config 37 | Micro::Case.config do |config| 38 | config.set_activemodel_validation_errors_failure = :validation_error 39 | end 40 | 41 | assert_failure_result(Multiply.call(a: 2, b: 'a'), type: :validation_error) 42 | 43 | Micro::Case.config do |config| 44 | config.set_activemodel_validation_errors_failure = :invalid_attributes 45 | end 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /comparisons/interactor.rb: -------------------------------------------------------------------------------- 1 | class CreateResponse 2 | include Interactor 3 | 4 | def call 5 | responder = context.responder 6 | 7 | survey_response = responder.survey_responses.build( 8 | response_text: context.answers[:text], 9 | rating: answers[:rating], 10 | survey: context.survey 11 | ) 12 | 13 | if survey_response.save 14 | context.survey_response = survey_response 15 | else 16 | context.fail!(errors: survey_response.errors) 17 | end 18 | end 19 | end 20 | 21 | class AddRewardPoints 22 | include Interactor 23 | 24 | def call 25 | reward_account = context.responder.reward_account 26 | 27 | reward_account.balance += context.survey.reward_points 28 | 29 | unless reward_account.save 30 | context.fail!(errors: reward_account.errors) 31 | end 32 | end 33 | end 34 | 35 | class SendNotifications 36 | include Interactor 37 | 38 | def call 39 | sender = context.survey.sender 40 | 41 | SurveyMailer.delay.notify_responder(context.responder.id) 42 | SurveyMailer.delay.notify_sender(sender.id) 43 | 44 | unless sender.add_survey_response_notification 45 | context.fail!(errors: sender.errors) 46 | end 47 | end 48 | end 49 | 50 | class ReplyToSurvey 51 | include Interactor::Organizer 52 | 53 | organize CreateResponse, AddRewardPoints, SendNotifications 54 | end 55 | 56 | # https://gist.githubusercontent.com/raderj89/cbb84b1f75e67087388bc4cdbe617138/raw/a39c3ba6b416ac3919cc7d32bfa58e82211f24ef/interactor_example.rb 57 | # https://medium.com/reflektive-engineering/from-service-objects-to-interactors-db7d2bb7dfd9 58 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/base_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | 5 | module Micro::Case::WithActivemodelValidation 6 | class BaseTest < Minitest::Test 7 | class Multiply < Micro::Case 8 | attribute :a 9 | attribute :b 10 | validates :a, :b, presence: true, numericality: true 11 | 12 | def call! 13 | Success(result: {number: a * b}) 14 | end 15 | end 16 | 17 | class NumberToString < Micro::Case 18 | attribute :number 19 | validates :number, presence: true, numericality: true 20 | 21 | def call! 22 | Success result: { string: number.to_s } 23 | end 24 | end 25 | 26 | def test_success 27 | calculation = Multiply.call(a: 2, b: 2) 28 | 29 | assert_success_result(calculation, value: { number: 4 }) 30 | 31 | # --- 32 | 33 | flow = Micro::Cases.flow([Multiply, NumberToString]) 34 | 35 | assert_success_result(flow.call(a: 2, b: 2), value: { string: '4' }) 36 | end 37 | 38 | def test_failure 39 | result = Multiply.call(a: 1, b: nil) 40 | 41 | assert_failure_result(result, type: :invalid_attributes) 42 | assert_equal(["can't be blank", 'is not a number'], result.value[:errors][:b]) 43 | 44 | # --- 45 | 46 | result = Multiply.call(a: 1, b: 'a') 47 | 48 | assert_failure_result(result, type: :invalid_attributes) 49 | assert_equal(['is not a number'], result.value[:errors][:b]) 50 | end 51 | end 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/safe/base_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | 5 | module Micro::Case::WithActivemodelValidation::Safe 6 | class BaseTest < Minitest::Test 7 | class Multiply < Micro::Case::Safe 8 | attribute :a 9 | attribute :b 10 | validates :a, :b, presence: true, numericality: true 11 | 12 | def call! 13 | Success(result: { number: a * b }) 14 | end 15 | end 16 | 17 | class NumberToString < Micro::Case::Safe 18 | attribute :number 19 | validates :number, presence: true, numericality: true 20 | 21 | def call! 22 | Success(result: { string: number.to_s }) 23 | end 24 | end 25 | 26 | def test_success 27 | calculation = Multiply.call(a: 2, b: 2) 28 | 29 | assert_success_result(calculation, value: { number: 4 }) 30 | 31 | # --- 32 | 33 | flow = Micro::Cases.flow([Multiply, NumberToString]) 34 | 35 | assert_success_result(flow.call(a: 2, b: 2), value: { string: '4' }) 36 | end 37 | 38 | def test_failure 39 | result = Multiply.call(a: 1, b: nil) 40 | 41 | assert_failure_result(result, type: :invalid_attributes) 42 | assert_equal(["can't be blank", 'is not a number'], result.value[:errors][:b]) 43 | 44 | # --- 45 | 46 | result = Multiply.call(a: 1, b: 'a') 47 | 48 | assert_failure_result(result, type: :invalid_attributes) 49 | assert_equal(['is not a number'], result.value[:errors][:b]) 50 | end 51 | end 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /test/support/jobs/base.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | 3 | module Jobs 4 | class Entity 5 | include Micro::Attributes.with(:diff, initialize: :strict) 6 | 7 | attributes :id, :state 8 | 9 | def sleeping? 10 | state == 'sleeping' 11 | end 12 | 13 | def running? 14 | state == 'running' 15 | end 16 | end 17 | 18 | class SetID < Micro::Case::Strict 19 | attributes :job 20 | 21 | def call! 22 | return Success result: { job: job } if !job.id.nil? 23 | 24 | new_job = job.with_attribute(:id, SecureRandom.uuid) 25 | 26 | Success result: { job: new_job } 27 | end 28 | end 29 | 30 | class ValidateID < Micro::Case::Strict 31 | ACCEPTABLE_UUID = %r{\A(\{)?([a-fA-F0-9]{4}-?){8}(?(1)\}|)\z} 32 | 33 | attributes :job 34 | 35 | def call! 36 | return Success result: { job: job } if job.id =~ ACCEPTABLE_UUID 37 | 38 | Failure :invalid_uuid, result: { job: job } 39 | end 40 | end 41 | 42 | class SetStateToRunning < Micro::Case::Strict 43 | attribute :job 44 | 45 | def call! 46 | return Failure(:invalid_state_transition) unless job.sleeping? 47 | 48 | job_running = job.with_attribute(:state, 'running') 49 | 50 | Success :state_updated, result: { 51 | job: job_running, changes: job.diff_attributes(job_running) 52 | } 53 | end 54 | end 55 | 56 | class Build < Micro::Case 57 | flow self, SetID 58 | 59 | def call! 60 | job = Entity.new(id: nil, state: 'sleeping') 61 | 62 | Success result: { job: job } 63 | end 64 | end 65 | 66 | Run = Micro::Cases.flow([ 67 | ValidateID, 68 | SetStateToRunning 69 | ]) 70 | end 71 | -------------------------------------------------------------------------------- /test/micro/cases/flow/then_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Micro::Cases::Flow::ThenTest < Minitest::Test 4 | class Add1 < Micro::Case 5 | attribute :number 6 | 7 | def call! 8 | Success result: { number: number + 1 } 9 | end 10 | end 11 | 12 | Add2 = Micro::Cases.flow([ 13 | Add1, Add1 14 | ]) 15 | 16 | class Add3 < Micro::Case 17 | flow([Add1, Add1, Add1]) 18 | end 19 | 20 | if RUBY_VERSION < '2.5.0' 21 | def test_the_not_implemented_error 22 | assert_raises(NotImplementedError) { Add2.then { 0 } } 23 | assert_raises(NotImplementedError) { Add3.then { 0 } } 24 | end 25 | end 26 | 27 | def test_the_invalid_invocation_error 28 | assert_raises_with_message( 29 | Micro::Case::Error::InvalidInvocationOfTheThenMethod, 30 | 'Invalid invocation of the Micro::Cases::Flow#then method' 31 | ) { Add2.then(1) } 32 | 33 | assert_raises_with_message( 34 | Micro::Case::Error::InvalidInvocationOfTheThenMethod, 35 | 'Invalid invocation of the Micro::Case.then method' 36 | ) { Add3.then(1) } 37 | 38 | if RUBY_VERSION >= '2.5.0' 39 | assert_raises_with_message( 40 | Micro::Case::Error::InvalidInvocationOfTheThenMethod, 41 | 'Invalid invocation of the Micro::Cases::Flow#then method' 42 | ) { Add2.then(1) { 0 } } 43 | 44 | assert_raises_with_message( 45 | Micro::Case::Error::InvalidInvocationOfTheThenMethod, 46 | 'Invalid invocation of the Micro::Case.then method' 47 | ) { Add3.then(1) { 0 } } 48 | 49 | Add2.then { |arg| assert_same(Add2, arg) } 50 | Add3.then { |arg| assert_same(Add3, arg) } 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /examples/calculator/README.md: -------------------------------------------------------------------------------- 1 | # μ-case - Calculator example 2 | 3 | This example uses [rake](http://rubygems.org/gems/rake) to expose a CLI calculator. 4 | 5 | ## Installation instructions 6 | ```sh 7 | gem install rake 8 | gem install u-case -v 4ß.1.0 9 | ``` 10 | 11 | *Note:* 12 | 13 | If zsh is your shell, use: [`unsetopt nomatch`](https://thoughtbot.com/blog/how-to-use-arguments-in-a-rake-task) to avoid errors when invoking rake tasks with arguments. 14 | 15 | ### Usage 16 | 17 | ![gif](https://github.com/serradura/u-case/blob/main/examples/calculator/assets/usage.gif?raw=true) 18 | 19 | #### Listing the available rake tasks 20 | ```sh 21 | rake -T 22 | 23 | # rake calc:add[a,b] # adds two numbers 24 | # rake calc:divide[a,b] # divides two numbers 25 | # rake calc:multiply[a,b] # multiplies two numbers 26 | # rake calc:subtract[a,b] # subtracts two numbers 27 | ``` 28 | 29 | #### Calculating integer numbers 30 | ```sh 31 | bundle exec rake calc:add[3,2] 32 | # 3 + 2 = 5 33 | 34 | bundle exec rake calc:subtract[3,2] 35 | # 3 - 2 = 1 36 | 37 | bundle exec rake calc:multiply[3,2] 38 | # 3 x 2 = 6 39 | 40 | bundle exec rake calc:divide[3,2] 41 | # 3 / 2 = 1 42 | ``` 43 | 44 | #### Calculating float numbers 45 | ```sh 46 | bundle exec rake calc:divide[3.0,2.0] 47 | # 3.0 / 2.0 = 1.5 48 | 49 | bundle exec rake calc:divide[-3.0,2.0] 50 | # -3.0 / 2.0 = -1.5 51 | ``` 52 | 53 | #### Calculation errors 54 | ```sh 55 | bundle exec rake calc:divide[4,0] 56 | # ERROR: divided by 0 57 | 58 | bundle exec rake calc:add[1,a] 59 | # ERROR: The arguments must contain only numeric values 60 | 61 | bundle exec rake calc:add[-\ 1,2] 62 | # ERROR: Arguments can't have spaces: a: "- 1", b: "2" 63 | ``` 64 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/MWRF/steps/01_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | require_relative '../users_entity' 5 | require_relative '../shared_assertions' 6 | 7 | class Micro::Case::MWRF::WithValidation 8 | class Step01Test < Minitest::Test 9 | include SharedAssertions 10 | 11 | module Users::Creation1 12 | require 'uri' 13 | require 'securerandom' 14 | 15 | class Process < Micro::Case 16 | attributes :name, :email 17 | 18 | def call! 19 | normalized_name = String(name).strip.gsub(/\s+/, ' ') 20 | normalized_email = String(email).downcase.strip 21 | 22 | validation_errors = [] 23 | validation_errors << "Name can't be blank" if normalized_name.empty? 24 | validation_errors << "Email is invalid" if normalized_email !~ URI::MailTo::EMAIL_REGEXP 25 | 26 | if !validation_errors.empty? 27 | return Failure :invalid_attributes, result: { 28 | errors: OpenStruct.new(full_messages: validation_errors) 29 | } 30 | end 31 | 32 | user = Users::Entity.new( 33 | id: SecureRandom.uuid, 34 | name: normalized_name, 35 | email: normalized_email 36 | ) 37 | 38 | Success result: { user: user, crm_id: sync_with_crm } 39 | end 40 | 41 | private def sync_with_crm 42 | # Do some integration stuff... 43 | SecureRandom.uuid 44 | end 45 | end 46 | end 47 | 48 | def use_case 49 | Users::Creation1::Process 50 | end 51 | 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /test/micro/case/utils/hashes_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Micro::Case 4 | class Utils::HashesTest < Minitest::Test 5 | def test_symbolize_hash_keys 6 | assert_raises_with_message( 7 | Kind::Error, 8 | '[] expected to be a kind of Hash' 9 | ) { Utils::Hashes.symbolize_keys([]) } 10 | 11 | # -- 12 | 13 | hash = { 'a' => 1 } 14 | 15 | new_hash = Utils::Hashes.symbolize_keys(hash) 16 | 17 | refute_same(hash, new_hash) 18 | assert_equal({ a: 1 }, new_hash) 19 | 20 | if hash.respond_to?(:transform_keys) 21 | def hash.respond_to?(method) 22 | method == :transform_keys ? false : super 23 | end 24 | 25 | new_hash = Utils::Hashes.symbolize_keys(hash) 26 | 27 | refute_same(hash, new_hash) 28 | assert_equal({ a: 1 }, new_hash) 29 | end 30 | end 31 | 32 | def test_slice_hash 33 | assert_raises_with_message( 34 | Kind::Error, 35 | '[] expected to be a kind of Hash' 36 | ) { Utils::Hashes.slice([], []) } 37 | 38 | # -- 39 | 40 | hash = { 'a' => 1, 'b' => 2, c: 3 } 41 | 42 | new_hash = Utils::Hashes.slice(hash, ['a', 'b', 'c']) 43 | 44 | refute_same(hash, new_hash) 45 | 46 | assert_equal({ 'a' => 1, 'b' => 2 }, new_hash) 47 | 48 | assert_equal({}, Utils::Hashes.slice(hash, ['d'])) 49 | 50 | if hash.respond_to?(:transform_keys) 51 | def hash.respond_to?(method) 52 | method == :slice ? false : super 53 | end 54 | 55 | new_hash = Utils::Hashes.slice(hash, ['a', :c]) 56 | 57 | refute_same(hash, new_hash) 58 | 59 | assert_equal({ 'a' => 1, c: 3 }, new_hash) 60 | 61 | assert_equal({}, Utils::Hashes.slice(hash, ['d', 'e'])) 62 | end 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /test/micro/case/safe/with_inner_flow_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Micro::Case::Safe::WithInnerFlowTest < Minitest::Test 4 | class ConvertTextToNumber < Micro::Case::Safe 5 | attribute :text 6 | 7 | def call! 8 | Success result: { number: text.to_i } 9 | end 10 | end 11 | 12 | class ConvertNumberToText < Micro::Case::Safe 13 | attribute :number 14 | 15 | def call! 16 | Success result: { text: number.to_s } 17 | end 18 | end 19 | 20 | class Double < Micro::Case::Safe 21 | flow ConvertTextToNumber, 22 | self.call!, 23 | ConvertNumberToText 24 | 25 | attribute :number 26 | 27 | def call! 28 | Success result: { number: number * 2 } 29 | end 30 | end 31 | 32 | def test_the_use_case_result 33 | result = Double.call(text: '4') 34 | 35 | assert_success_result(result, value: { text: '8' }) 36 | 37 | assert_equal( 38 | [ConvertTextToNumber, Double::Self, ConvertNumberToText], 39 | Double.use_cases 40 | ) 41 | end 42 | 43 | begin 44 | class DoubleFoo < Double 45 | end 46 | rescue RuntimeError => e 47 | @@__inheritance_violation_message = e.message 48 | end 49 | 50 | def test_the_inheritance_violation 51 | expected_message = 52 | "Wooo, you can't do this! Inherits from a use case which has an inner flow violates "\ 53 | "one of the project principles: Solve complex business logic, by allowing the composition of use cases. "\ 54 | "Instead of doing this, declare a new class/constant with the steps needed.\n\n"\ 55 | "Related issue: https://github.com/serradura/u-case/issues/19\n" 56 | 57 | assert_equal(expected_message, @@__inheritance_violation_message) 58 | 59 | assert_raises_with_message(RuntimeError, expected_message) do 60 | Class.new(Double) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/safe/classes_with_flows/last_step_with_validation_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | 5 | module Micro::Case::WithActivemodelValidation 6 | module ClassesWithFlows 7 | class LastStepWithValidationTest < Minitest::Test 8 | class ConvertTextToNumber < Micro::Case 9 | attribute :text 10 | 11 | def call! 12 | number = text.include?('.') ? text.to_f : text.to_i 13 | 14 | Success result: { number: number } 15 | end 16 | end 17 | 18 | class ConvertNumberToText < Micro::Case 19 | attribute :number 20 | 21 | validates :number, presence: true 22 | 23 | def call! 24 | Success result: { text: number.to_s } 25 | end 26 | end 27 | 28 | class Double < Micro::Case 29 | flow ConvertTextToNumber, 30 | self.call!, 31 | ConvertNumberToText 32 | 33 | attribute :number 34 | 35 | validates :number, numericality: { only_integer: true } 36 | 37 | def call! 38 | Success result: { number: number * 2 } 39 | end 40 | end 41 | 42 | def test_the_use_case_result 43 | result = Double.call(text: '4') 44 | 45 | assert_success_result(result, value: { text: '8' }) 46 | 47 | assert_equal( 48 | [ConvertTextToNumber, Double::Self, ConvertNumberToText], 49 | Double.use_cases 50 | ) 51 | 52 | # --- 53 | 54 | result = Double.call(text: '4.0') 55 | 56 | assert_failure_result(result, type: :invalid_attributes) 57 | 58 | assert_equal(['must be an integer'], result.value[:errors][:number]) 59 | end 60 | end 61 | end 62 | end 63 | 64 | end 65 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/classes_with_flows/last_step_with_validation_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | 5 | module Micro::Case::WithActivemodelValidation::Safe 6 | module ClassesWithFlows 7 | class LastStepWithValidationTest < Minitest::Test 8 | class ConvertTextToNumber < Micro::Case::Safe 9 | attribute :text 10 | 11 | def call! 12 | number = text.include?('.') ? text.to_f : text.to_i 13 | 14 | Success result: { number: number } 15 | end 16 | end 17 | 18 | class ConvertNumberToText < Micro::Case::Safe 19 | attribute :number 20 | 21 | validates :number, presence: true 22 | 23 | def call! 24 | Success result: { text: number.to_s } 25 | end 26 | end 27 | 28 | class Double < Micro::Case::Safe 29 | flow ConvertTextToNumber, 30 | self.call!, 31 | ConvertNumberToText 32 | 33 | attribute :number 34 | 35 | validates :number, kind: Integer 36 | 37 | def call! 38 | Success result: { number: number * 2 } 39 | end 40 | end 41 | 42 | def test_the_use_case_result 43 | result = Double.call(text: '4') 44 | 45 | assert_success_result(result, value: { text: '8' }) 46 | 47 | assert_equal( 48 | [ConvertTextToNumber, Double::Self, ConvertNumberToText], 49 | Double.use_cases 50 | ) 51 | 52 | # --- 53 | 54 | result = Double.call(text: '4.0') 55 | 56 | assert_failure_result(result, type: :invalid_attributes) 57 | 58 | assert_equal('must be a kind of Integer', result.value[:errors][:number].join) 59 | end 60 | end 61 | end 62 | end 63 | 64 | end 65 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/strict_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | 5 | module Micro::Case::WithActivemodelValidation 6 | class StrictTest < Minitest::Test 7 | class Multiply < Micro::Case::Strict 8 | attribute :a 9 | attribute :b 10 | validates :a, :b, presence: true, numericality: true 11 | 12 | def call! 13 | Success result: { number: a * b } 14 | end 15 | end 16 | 17 | class NumberToString < Micro::Case::Strict 18 | attribute :number 19 | validates :number, presence: true, numericality: true 20 | 21 | def call! 22 | Success result: { string: number.to_s } 23 | end 24 | end 25 | 26 | def test_success 27 | calculation = Multiply.call(a: 2, b: 2) 28 | 29 | assert_success_result(calculation, value: { number: 4 }) 30 | 31 | # --- 32 | 33 | flow = Micro::Cases.flow([Multiply, NumberToString]) 34 | 35 | assert_success_result(flow.call(a: 2, b: 2), value: { string: '4' }) 36 | end 37 | 38 | def test_failure 39 | assert_raises_with_message(ArgumentError, 'missing keywords: :a, :b') { Multiply.call({}) } 40 | 41 | assert_raises_with_message(ArgumentError, 'missing keyword: :b') { Multiply.call({a: 1}) } 42 | 43 | # --- 44 | 45 | result = Multiply.call(a: 1, b: nil) 46 | 47 | assert_failure_result(result, type: :invalid_attributes) 48 | assert_equal(["can't be blank", 'is not a number'], result.value[:errors][:b]) 49 | 50 | # --- 51 | 52 | result = Multiply.call(a: 1, b: 'a') 53 | 54 | assert_failure_result(result, type: :invalid_attributes) 55 | assert_equal(['is not a number'], result.value[:errors][:b]) 56 | end 57 | end 58 | end 59 | 60 | end 61 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/safe/strict_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | 5 | module Micro::Case::WithActivemodelValidation::Safe 6 | class StrictTest < Minitest::Test 7 | class Multiply < Micro::Case::Strict::Safe 8 | attribute :a 9 | attribute :b 10 | validates :a, :b, presence: true, numericality: true 11 | 12 | def call! 13 | Success result: { number: a * b } 14 | end 15 | end 16 | 17 | class NumberToString < Micro::Case::Strict::Safe 18 | attribute :number 19 | validates :number, presence: true, numericality: true 20 | 21 | def call! 22 | Success result: { string: number.to_s } 23 | end 24 | end 25 | 26 | def test_success 27 | calculation = Multiply.call(a: 2, b: 2) 28 | 29 | assert_success_result(calculation, value: { number: 4 }) 30 | 31 | # --- 32 | 33 | flow = Micro::Cases.flow([Multiply, NumberToString]) 34 | 35 | assert_success_result(flow.call(a: 2, b: 2), value: { string: '4' } ) 36 | end 37 | 38 | def test_failure 39 | assert_raises_with_message(ArgumentError, 'missing keywords: :a, :b') { Multiply.call({}) } 40 | 41 | assert_raises_with_message(ArgumentError, 'missing keyword: :b') { Multiply.call({a: 1}) } 42 | 43 | # --- 44 | 45 | result = Multiply.call(a: 1, b: nil) 46 | 47 | assert_failure_result(result, type: :invalid_attributes) 48 | assert_equal(["can't be blank", 'is not a number'], result.value[:errors][:b]) 49 | 50 | # --- 51 | 52 | result = Multiply.call(a: 1, b: 'a') 53 | 54 | assert_failure_result(result, type: :invalid_attributes) 55 | assert_equal(['is not a number'], result.value[:errors][:b]) 56 | end 57 | end 58 | end 59 | 60 | end 61 | -------------------------------------------------------------------------------- /test/support/jobs/safe.rb: -------------------------------------------------------------------------------- 1 | require 'securerandom' 2 | 3 | module Safe 4 | module Jobs 5 | class Entity 6 | include Micro::Attributes.with(:diff, initialize: :strict) 7 | 8 | attributes :id, :state 9 | 10 | def sleeping? 11 | state == 'sleeping' 12 | end 13 | 14 | def running? 15 | state == 'running' 16 | end 17 | end 18 | 19 | module State 20 | class FetchSleeping < Micro::Case::Safe 21 | def call! 22 | job = Entity.new(id: nil, state: 'sleeping') 23 | 24 | Success result: { job: job } 25 | end 26 | end 27 | end 28 | 29 | class SetID < Micro::Case::Strict::Safe 30 | attributes :job 31 | 32 | def call! 33 | return Success result: { job: job } if !job.id.nil? 34 | 35 | new_job = job.with_attribute(:id, SecureRandom.uuid) 36 | 37 | Success result: { job: new_job } 38 | end 39 | end 40 | 41 | class ValidateID < Micro::Case::Strict::Safe 42 | ACCEPTABLE_UUID = %r{\A(\{)?([a-fA-F0-9]{4}-?){8}(?(1)\}|)\z} 43 | 44 | attributes :job 45 | 46 | def call! 47 | return Success result: { job: job } if job.id =~ ACCEPTABLE_UUID 48 | 49 | Failure :invalid_uuid, result: { job: job } 50 | end 51 | end 52 | 53 | class SetStateToRunning < Micro::Case::Strict::Safe 54 | attribute :job 55 | 56 | def call! 57 | return Failure(:invalid_state_transition) unless job.sleeping? 58 | 59 | job_running = job.with_attribute(:state, 'running') 60 | 61 | Success :state_updated, result: { 62 | job: job_running, changes: job.diff_attributes(job_running) 63 | } 64 | end 65 | end 66 | 67 | Build = Micro::Cases.safe_flow([ 68 | State::FetchSleeping, 69 | SetID 70 | ]) 71 | 72 | Run = Micro::Cases.safe_flow([ 73 | ValidateID, 74 | SetStateToRunning 75 | ]) 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/safe/classes_with_flows/first_step_with_validation_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | 5 | module Micro::Case::WithActivemodelValidation 6 | module ClassesWithFlows 7 | class FirstStepWithValidationTest < Minitest::Test 8 | class ConvertTextToNumber < Micro::Case 9 | attribute :text 10 | 11 | validates :text, presence: true 12 | 13 | def call! 14 | number = text.include?('.') ? text.to_f : text.to_i 15 | Success result: { number: number } 16 | end 17 | end 18 | 19 | class ConvertNumberToText < Micro::Case 20 | attribute :number 21 | 22 | def call! 23 | Success result: { text: number.to_s } 24 | end 25 | end 26 | 27 | class Double < Micro::Case 28 | flow ConvertTextToNumber, 29 | self.call!, 30 | ConvertNumberToText 31 | 32 | attribute :number 33 | 34 | validates :number, numericality: { only_integer: true } 35 | 36 | def call! 37 | Success result: { number: number * 2 } 38 | end 39 | end 40 | 41 | def test_the_use_case_result 42 | assert_failure_result(Double.call(text: ''), type: :invalid_attributes) 43 | 44 | # --- 45 | 46 | result = Double.call(text: '4') 47 | 48 | assert_success_result(result, value: { text: '8' }) 49 | 50 | assert_equal( 51 | [ConvertTextToNumber, Double::Self, ConvertNumberToText], 52 | Double.use_cases 53 | ) 54 | 55 | # --- 56 | 57 | result = Double.call(text: '4.0') 58 | 59 | assert_failure_result(result, type: :invalid_attributes) 60 | 61 | assert_equal(['must be an integer'], result.value[:errors][:number]) 62 | end 63 | end 64 | end 65 | end 66 | 67 | end 68 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/classes_with_flows/first_step_with_validation_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | 5 | module Micro::Case::WithActivemodelValidation::Safe 6 | module ClassesWithFlows 7 | class FirstStepWithValidationTest < Minitest::Test 8 | class ConvertTextToNumber < Micro::Case::Safe 9 | attribute :text 10 | 11 | validates :text, presence: true 12 | 13 | def call! 14 | number = text.include?('.') ? text.to_f : text.to_i 15 | 16 | Success result: { number: number } 17 | end 18 | end 19 | 20 | class ConvertNumberToText < Micro::Case::Safe 21 | attribute :number 22 | 23 | def call! 24 | Success result: { text: number.to_s } 25 | end 26 | end 27 | 28 | class Double < Micro::Case::Safe 29 | flow ConvertTextToNumber, 30 | self.call!, 31 | ConvertNumberToText 32 | 33 | attribute :number 34 | 35 | validates :number, kind: Integer 36 | 37 | def call! 38 | Success result: { number: number * 2 } 39 | end 40 | end 41 | 42 | def test_the_use_case_result 43 | assert_failure_result(Double.call(text: ''), type: :invalid_attributes) 44 | 45 | # --- 46 | 47 | result = Double.call(text: '4') 48 | 49 | assert_success_result(result, value: { text: '8' }) 50 | 51 | assert_equal( 52 | [ConvertTextToNumber, Double::Self, ConvertNumberToText], 53 | Double.use_cases 54 | ) 55 | 56 | # --- 57 | 58 | result = Double.call(text: '4.0') 59 | 60 | assert_failure_result(result, type: :invalid_attributes) 61 | 62 | assert_equal('must be a kind of Integer', result.value[:errors][:number].join) 63 | end 64 | end 65 | end 66 | end 67 | 68 | end 69 | -------------------------------------------------------------------------------- /test/micro/cases/flow/di_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Micro::Cases::Flow::DITest < Minitest::Test 4 | class Add2 < Micro::Case 5 | attribute :number 6 | 7 | def call! 8 | number.is_a?(Numeric) ? Success(result: { number: number + 2 }) : Failure() 9 | end 10 | end 11 | 12 | class Add3 < Micro::Case 13 | attribute :number 14 | 15 | def call! 16 | number.is_a?(Numeric) ? Success(result: { number: number + 3 }) : Failure() 17 | end 18 | end 19 | 20 | class Sum < Micro::Case 21 | attributes :number, :adder 22 | 23 | def call! 24 | call(adder) 25 | end 26 | end 27 | 28 | Add10 = Micro::Cases.flow([ 29 | Add2, 30 | [Sum, adder: Add3], 31 | [Sum, adder: Add2], 32 | Add3 33 | ]) 34 | 35 | Add20 = Micro::Cases.flow([ 36 | Add10, 37 | Add10 38 | ]) 39 | 40 | def test_dependency_injection_using_a_collection_of_use_cases 41 | result = Add10.call(number: 1) 42 | 43 | assert_predicate(result, :success?) 44 | 45 | assert_equal(11, result[:number]) 46 | 47 | # -- 48 | 49 | result = Add20.call(number: 1) 50 | 51 | assert_predicate(result, :success?) 52 | 53 | assert_equal(21, result[:number]) 54 | end 55 | 56 | class Add11 < Micro::Case 57 | flow([ 58 | Add2, 59 | [Sum, adder: Add3], 60 | [Sum, adder: Add2], 61 | self 62 | ]) 63 | 64 | attribute :number 65 | 66 | def call! 67 | number.is_a?(Numeric) ? Success(result: { number: number + 4 }) : Failure() 68 | end 69 | end 70 | 71 | class Add22 < Micro::Case 72 | flow(Add11, Add11) 73 | end 74 | 75 | def test_dependency_injection_using_an_inner_flow 76 | result = Add11.call(number: 1) 77 | 78 | assert_predicate(result, :success?) 79 | 80 | assert_equal(12, result[:number]) 81 | 82 | # -- 83 | 84 | result = Add22.call(number: 1) 85 | 86 | assert_predicate(result, :success?) 87 | 88 | assert_equal(23, result[:number]) 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/safe/classes_with_flows/all_steps_with_validation_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | 5 | module Micro::Case::WithActivemodelValidation 6 | module ClassesWithFlows 7 | class AllStepsWithValidationTest < Minitest::Test 8 | class ConvertTextToNumber < Micro::Case 9 | attribute :text 10 | 11 | validates :text, presence: true 12 | 13 | def call! 14 | number = text.include?('.') ? text.to_f : text.to_i 15 | 16 | Success result: { number: number } 17 | end 18 | end 19 | 20 | class ConvertNumberToText < Micro::Case 21 | attribute :number 22 | 23 | validates :number, presence: true 24 | 25 | def call! 26 | Success result: { text: number.to_s } 27 | end 28 | end 29 | 30 | class Double < Micro::Case 31 | flow ConvertTextToNumber, 32 | self.call!, 33 | ConvertNumberToText 34 | 35 | attribute :number 36 | 37 | validates :number, numericality: { only_integer: true } 38 | 39 | def call! 40 | Success result: { number: number * 2 } 41 | end 42 | end 43 | 44 | def test_the_use_case_result 45 | assert_failure_result(Double.call(text: ''), type: :invalid_attributes) 46 | 47 | # --- 48 | 49 | result = Double.call(text: '4') 50 | 51 | assert_success_result(result, value: { text: '8' }) 52 | 53 | assert_equal( 54 | [ConvertTextToNumber, Double::Self, ConvertNumberToText], 55 | Double.use_cases 56 | ) 57 | 58 | # --- 59 | 60 | result = Double.call(text: '4.0') 61 | 62 | assert_failure_result(result, type: :invalid_attributes) 63 | 64 | assert_equal(['must be an integer'], result.value[:errors][:number]) 65 | end 66 | end 67 | end 68 | end 69 | 70 | end 71 | -------------------------------------------------------------------------------- /test/micro/cases/safe/flow/di_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Micro::Cases::Safe::Flow::DITest < Minitest::Test 4 | class Add2 < Micro::Case 5 | attribute :number 6 | 7 | def call! 8 | number.is_a?(Numeric) ? Success(result: { number: number + 2 }) : Failure() 9 | end 10 | end 11 | 12 | class Add3 < Micro::Case 13 | attribute :number 14 | 15 | def call! 16 | number.is_a?(Numeric) ? Success(result: { number: number + 3 }) : Failure() 17 | end 18 | end 19 | 20 | class Sum < Micro::Case 21 | attributes :number, :adder 22 | 23 | def call! 24 | call(adder) 25 | end 26 | end 27 | 28 | Add10 = Micro::Cases.safe_flow([ 29 | Add2, 30 | [Sum, adder: Add3], 31 | [Sum, adder: Add2], 32 | Add3 33 | ]) 34 | 35 | Add20 = Micro::Cases.safe_flow([ 36 | Add10, 37 | Add10 38 | ]) 39 | 40 | def test_dependency_injection_using_a_collection_of_use_cases 41 | result = Add10.call(number: 1) 42 | 43 | assert_predicate(result, :success?) 44 | 45 | assert_equal(11, result[:number]) 46 | 47 | # -- 48 | 49 | result = Add20.call(number: 1) 50 | 51 | assert_predicate(result, :success?) 52 | 53 | assert_equal(21, result[:number]) 54 | end 55 | 56 | 57 | class Add11 < Micro::Case::Safe 58 | flow([ 59 | Add2, 60 | [Sum, adder: Add3], 61 | [Sum, adder: Add2], 62 | self 63 | ]) 64 | 65 | attribute :number 66 | 67 | def call! 68 | number.is_a?(Numeric) ? Success(result: { number: number + 4 }) : Failure() 69 | end 70 | end 71 | 72 | class Add22 < Micro::Case::Safe 73 | flow(Add11, Add11) 74 | end 75 | 76 | def test_dependency_injection_using_an_inner_flow 77 | result = Add11.call(number: 1) 78 | 79 | assert_predicate(result, :success?) 80 | 81 | assert_equal(12, result[:number]) 82 | 83 | # -- 84 | 85 | result = Add22.call(number: 1) 86 | 87 | assert_predicate(result, :success?) 88 | 89 | assert_equal(23, result[:number]) 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/classes_with_flows/all_steps_with_validation_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | 5 | module Micro::Case::WithActivemodelValidation::Safe 6 | module ClassesWithFlows 7 | class AllStepsWithValidationTest < Minitest::Test 8 | class ConvertTextToNumber < Micro::Case::Safe 9 | attribute :text 10 | 11 | validates :text, presence: true 12 | 13 | def call! 14 | number = text.include?('.') ? text.to_f : text.to_i 15 | 16 | Success result: { number: number } 17 | end 18 | end 19 | 20 | class ConvertNumberToText < Micro::Case::Safe 21 | attribute :number 22 | 23 | validates :number, presence: true 24 | 25 | def call! 26 | Success result: { text: number.to_s } 27 | end 28 | end 29 | 30 | class Double < Micro::Case::Safe 31 | flow ConvertTextToNumber, 32 | self.call!, 33 | ConvertNumberToText 34 | 35 | attribute :number 36 | 37 | validates :number, kind: Integer 38 | 39 | def call! 40 | Success result: { number: number * 2 } 41 | end 42 | end 43 | 44 | def test_the_use_case_result 45 | assert_failure_result(Double.call(text: ''), type: :invalid_attributes) 46 | 47 | # --- 48 | 49 | result = Double.call(text: '4') 50 | 51 | assert_success_result(result, value: { text: '8' }) 52 | 53 | assert_equal( 54 | [ConvertTextToNumber, Double::Self, ConvertNumberToText], 55 | Double.use_cases 56 | ) 57 | 58 | # --- 59 | 60 | result = Double.call(text: '4.0') 61 | 62 | assert_failure_result(result, type: :invalid_attributes) 63 | 64 | assert_equal('must be a kind of Integer', result.value[:errors][:number].join) 65 | end 66 | end 67 | end 68 | end 69 | 70 | end 71 | -------------------------------------------------------------------------------- /lib/micro/case/error.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Micro 4 | class Case 5 | module Error 6 | class UnexpectedResult < TypeError 7 | MESSAGE = 'must return an instance of Micro::Case::Result'.freeze 8 | 9 | def initialize(context) 10 | super("#{context} #{MESSAGE}") 11 | end 12 | end 13 | 14 | class ResultIsAlreadyDefined < ArgumentError 15 | def initialize; super('result is already defined'.freeze); end 16 | end 17 | 18 | class InvalidResultType < TypeError 19 | def initialize; super('type must be a Symbol'.freeze); end 20 | end 21 | 22 | class InvalidResult < TypeError 23 | def initialize(is_success, type, use_case) 24 | base = 25 | "The result returned from #{use_case.class.name}#call! must be a Hash." 26 | 27 | result = is_success ? 'Success'.freeze : 'Failure'.freeze 28 | 29 | example = 30 | if type === :ok || type === :error || type === :exception 31 | "#{result}(result: { key: 'value' })" 32 | else 33 | "#{result}(:#{type}, result: { key: 'value' })" 34 | end 35 | 36 | super("#{base}\n\nExample:\n #{example}") 37 | end 38 | end 39 | 40 | class InvalidResultInstance < ArgumentError 41 | def initialize; super('argument must be an instance of Micro::Case::Result'.freeze); end 42 | end 43 | 44 | class InvalidUseCase < TypeError 45 | def initialize; super('use case must be a kind or an instance of Micro::Case'.freeze); end 46 | end 47 | 48 | class InvalidInvocationOfTheThenMethod < StandardError 49 | def initialize(class_name) 50 | super("Invalid invocation of the #{class_name}then method") 51 | end 52 | end 53 | 54 | def self.by_wrong_usage?(exception) 55 | case exception 56 | when Kind::Error, ArgumentError, InvalidResult, UnexpectedResult then true 57 | else false 58 | end 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /benchmarks/perfomance/flow/failure_results.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | require 'forwardable' 4 | 5 | gemfile do 6 | source 'https://rubygems.org' 7 | 8 | ruby '>= 2.4.0' 9 | 10 | gem 'benchmark-ips', '~> 2.7', '>= 2.7.2' 11 | 12 | gem 'interactor', '~> 3.1', '>= 3.1.1' 13 | 14 | gem 'u-case', '~> 4.1.0' 15 | end 16 | 17 | require_relative '../../examples/flow/add_five_with/all' 18 | require_relative 'call_flows' 19 | 20 | Micro::Case.config do |config| 21 | config.enable_transitions = false 22 | end 23 | 24 | require 'benchmark/ips' 25 | 26 | Benchmark.ips(&CallFlows.( 27 | params: { text: 'b' } 28 | )) 29 | 30 | # Warming up -------------------------------------- 31 | # Interactor::Organizer 1.734k i/100ms 32 | # Micro::Cases.flow([]) 7.515k i/100ms 33 | # Micro::Case flow in a class 4.636k i/100ms 34 | # Micro::Case including the class 4.114k i/100ms 35 | # Micro::Case::Result#| 7.588k i/100ms 36 | # Micro::Case::Result#then 6.681k i/100ms 37 | 38 | # Calculating ------------------------------------- 39 | # Interactor::Organizer 24.280k (±24.5%) i/s - 112.710k in 5.013334s 40 | # Micro::Cases.flow([]) 74.999k (± 9.8%) i/s - 375.750k in 5.055777s 41 | # Micro::Case flow in a class 46.681k (± 9.3%) i/s - 236.436k in 5.105105s 42 | # Micro::Case including the class 41.921k (± 8.9%) i/s - 209.814k in 5.043622s 43 | # Micro::Case::Result#| 78.280k (±12.6%) i/s - 386.988k in 5.022146s 44 | # Micro::Case::Result#then 68.898k (± 8.8%) i/s - 347.412k in 5.080116s 45 | 46 | # Comparison: 47 | # Micro::Case::Result#|: 78280.4 i/s 48 | # Micro::Cases.flow([]): 74999.4 i/s - same-ish: difference falls within error 49 | # Micro::Case::Result#then: 68898.4 i/s - same-ish: difference falls within error 50 | # Micro::Case flow in a class: 46681.0 i/s - 1.68x (± 0.00) slower 51 | # Micro::Case including the class: 41920.8 i/s - 1.87x (± 0.00) slower 52 | # Interactor::Organizer: 24280.0 i/s - 3.22x (± 0.00) slower 53 | -------------------------------------------------------------------------------- /benchmarks/perfomance/flow/success_results.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | require 'forwardable' 4 | 5 | gemfile do 6 | source 'https://rubygems.org' 7 | 8 | ruby '>= 2.4.0' 9 | 10 | gem 'benchmark-ips', '~> 2.7', '>= 2.7.2' 11 | 12 | gem 'interactor', '~> 3.1', '>= 3.1.1' 13 | 14 | gem 'u-case', '~> 4.1.0' 15 | end 16 | 17 | require_relative '../../examples/flow/add_five_with/all' 18 | require_relative 'call_flows' 19 | 20 | Micro::Case.config do |config| 21 | config.enable_transitions = false 22 | end 23 | 24 | require 'benchmark/ips' 25 | 26 | Benchmark.ips(&CallFlows.( 27 | params: { text: 0 } 28 | )) 29 | 30 | # Warming up -------------------------------------- 31 | # Interactor::Organizer 1.809k i/100ms 32 | # Micro::Cases.flow([]) 7.808k i/100ms 33 | # Micro::Case flow in a class 4.816k i/100ms 34 | # Micro::Case including the class 4.094k i/100ms 35 | # Micro::Case::Result#| 7.656k i/100ms 36 | # Micro::Case::Result#then 7.138k i/100ms 37 | 38 | # Calculating ------------------------------------- 39 | # Interactor::Organizer 24.290k (±24.0%) i/s - 113.967k in 5.032825s 40 | # Micro::Cases.flow([]) 74.790k (±11.1%) i/s - 374.784k in 5.071740s 41 | # Micro::Case flow in a class 47.043k (± 8.0%) i/s - 235.984k in 5.047477s 42 | # Micro::Case including the class 42.030k (± 8.5%) i/s - 208.794k in 5.002138s 43 | # Micro::Case::Result#| 80.936k (±15.9%) i/s - 398.112k in 5.052531s 44 | # Micro::Case::Result#then 71.459k (± 8.8%) i/s - 356.900k in 5.030526s 45 | 46 | # Comparison: 47 | # Micro::Case::Result#|: 80936.2 i/s 48 | # Micro::Cases.flow([]): 74790.1 i/s - same-ish: difference falls within error 49 | # Micro::Case::Result#then: 71459.5 i/s - same-ish: difference falls within error 50 | # Micro::Case flow in a class: 47042.6 i/s - 1.72x (± 0.00) slower 51 | # Micro::Case including the class: 42030.2 i/s - 1.93x (± 0.00) slower 52 | # Interactor::Organizer: 24290.3 i/s - 3.33x (± 0.00) slower 53 | -------------------------------------------------------------------------------- /examples/calculator/Rakefile: -------------------------------------------------------------------------------- 1 | require 'u-case' 2 | 3 | require_relative 'calc/operation' 4 | require_relative 'calc/normalize_args' 5 | require_relative 'calc/transform_into_numbers' 6 | 7 | module Calc 8 | TransformArgsIntoNumbers = 9 | Micro::Cases.safe_flow([NormalizeArgs, TransformIntoNumbers]) 10 | 11 | module With 12 | Add = Micro::Cases.safe_flow([TransformArgsIntoNumbers, Operation::Add]) 13 | Divide = Micro::Cases.safe_flow([TransformArgsIntoNumbers, Operation::Divide]) 14 | Subtract = Micro::Cases.safe_flow([TransformArgsIntoNumbers, Operation::Subtract]) 15 | Multiply = Micro::Cases.safe_flow([TransformArgsIntoNumbers, Operation::Multiply]) 16 | end 17 | end 18 | 19 | class PerformTask < Micro::Case 20 | attributes :args, :operation 21 | 22 | def call! 23 | operation 24 | .call(args: args) 25 | .on_success { |result| print_operation(**result.data) } 26 | .on_exception { |result| puts "ERROR: #{result[:exception].message}" } 27 | .on_failure(:not_a_number) { puts "ERROR: The arguments must contain only numeric values" } 28 | .on_failure(:arguments_with_space_chars) do |result| 29 | a, b = result[:attributes] 30 | 31 | puts "ERROR: Arguments can't have spaces: a: #{a}, b: #{b}" 32 | end 33 | end 34 | 35 | private def print_operation(a:, operator:, b:, result:) 36 | puts "#{a} #{operator} #{b} = #{result}" 37 | end 38 | end 39 | 40 | namespace :calc do 41 | desc 'adds two numbers' 42 | task :add, [:a, :b] do |_task, args| 43 | PerformTask.call(args: args, operation: Calc::With::Add) 44 | end 45 | 46 | desc 'divides two numbers' 47 | task :divide, [:a, :b] do |_task, args| 48 | PerformTask.call(args: args, operation: Calc::With::Divide) 49 | end 50 | 51 | desc 'subtracts two numbers' 52 | task :subtract, [:a, :b] do |_task, args| 53 | PerformTask.call(args: args, operation: Calc::With::Subtract) 54 | end 55 | 56 | desc 'multiplies two numbers' 57 | task :multiply, [:a, :b] do |_task, args| 58 | PerformTask.call(args: args, operation: Calc::With::Multiply) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /examples/rescuing_exceptions.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | gem 'u-case', '~> 4.1.0' 7 | end 8 | 9 | class DivideV1 < Micro::Case 10 | attributes :a, :b 11 | 12 | def call! 13 | return Success result: { division: a / b } if a > 0 && b > 0 14 | 15 | Failure result: { message: 'numbers must be greater than 0' } 16 | rescue => e 17 | Failure(e) 18 | end 19 | end 20 | 21 | class DivideV2 < Micro::Case::Safe 22 | attributes :a, :b 23 | 24 | def call! 25 | return Success result: { division: a / b } if a > 0 && b > 0 26 | 27 | Failure result: { message: 'numbers must be greater than 0' } 28 | end 29 | end 30 | 31 | #-------------------------# 32 | puts "\n== DivideV1 ==\n" 33 | #-------------------------# 34 | 35 | #---------------------------------# 36 | puts "\n-- Success scenario --\n\n" 37 | #---------------------------------# 38 | 39 | result = DivideV1.call(a: 4, b: 2) 40 | 41 | p result.data if result.success? 42 | 43 | #----------------------------------# 44 | puts "\n-- Failure scenarios --\n\n" 45 | #----------------------------------# 46 | 47 | result = DivideV1.call(a: 4, b: 0) 48 | 49 | p result.data if result.failure? 50 | 51 | puts '' 52 | 53 | result = DivideV1.call(a: -4, b: 2) 54 | 55 | p result.data if result.failure? 56 | 57 | # 58 | # --- 59 | # 60 | 61 | #-------------------------# 62 | puts "\n== DivideV2 ==\n" 63 | #-------------------------# 64 | 65 | #---------------------------------# 66 | puts "\n-- Success scenario --\n\n" 67 | #---------------------------------# 68 | 69 | result = DivideV2.call(a: 4, b: 2) 70 | 71 | puts result.value if result.success? 72 | 73 | #----------------------------------# 74 | puts "\n-- Failure scenarios --\n\n" 75 | #----------------------------------# 76 | 77 | result = DivideV2.call(a: 4, b: 0) 78 | 79 | p result.value if result.failure? 80 | 81 | puts '' 82 | 83 | result = DivideV2.call(a: -4, b: 2) 84 | 85 | p result.value if result.failure? 86 | 87 | # :: example of the outputs :: 88 | 89 | # -- Success scenario -- 90 | # 91 | # 2 92 | # 93 | # -- Failure scenarios -- 94 | # 95 | # # 96 | # 97 | # numbers must be greater than 0 98 | -------------------------------------------------------------------------------- /test/micro/case/MWRF/steps/02_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | require_relative '../users_entity' 4 | require_relative '../shared_assertions' 5 | 6 | class Micro::Case::MWRF 7 | class Step02Test < Minitest::Test 8 | include SharedAssertions 9 | 10 | module Users::Creation2 11 | require 'uri' 12 | require 'securerandom' 13 | 14 | class Process < Micro::Case 15 | attributes :name, :email 16 | 17 | def call! 18 | normalize_params 19 | .then(method(:validate_params)) 20 | .then(method(:persist)) 21 | .then(method(:sync_with_crm)) 22 | end 23 | 24 | private 25 | 26 | def normalize_params 27 | Success :normalize_params, result: { 28 | name: String(name).strip.gsub(/\s+/, ' '), 29 | email: String(email).downcase.strip 30 | } 31 | end 32 | 33 | def validate_params(name:, email:) 34 | validation_errors = [] 35 | validation_errors << "Name can't be blank" if name.empty? 36 | validation_errors << "Email is invalid" if email !~ URI::MailTo::EMAIL_REGEXP 37 | 38 | return Success(:validate_params) if validation_errors.empty? 39 | 40 | Failure :invalid_attributes, result: { 41 | errors: OpenStruct.new(full_messages: validation_errors) 42 | } 43 | end 44 | 45 | def persist(name:, email:, **) 46 | user_data = { name: name, email: email, id: SecureRandom.uuid } 47 | 48 | user = Users::Entity.new(user_data) 49 | 50 | Success :persist, result: { user: user } 51 | end 52 | 53 | def sync_with_crm(user:, **) 54 | if user.persisted? 55 | # Do some integration stuff... 56 | crm_id = SecureRandom.uuid 57 | 58 | Success :sync_with_crm, result: { user: user, crm_id: crm_id } 59 | else 60 | Failure :sync_failed, result: { message: "User can't be sent to the CRM" } 61 | end 62 | end 63 | end 64 | end 65 | 66 | def use_case 67 | Users::Creation2::Process 68 | end 69 | 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /test/micro/case/with_inner_flow_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Micro::Case::WithInnerFlowTest < Minitest::Test 4 | class ConvertTextToNumber < Micro::Case 5 | attribute :text 6 | 7 | def call! 8 | Success result: { number: text.to_i } 9 | end 10 | end 11 | 12 | class ConvertNumberToText < Micro::Case 13 | attribute :number 14 | 15 | def call! 16 | Success result: { text: number.to_s } 17 | end 18 | end 19 | 20 | class Double < Micro::Case 21 | flow([ 22 | ConvertTextToNumber, 23 | self.call!, 24 | ConvertNumberToText 25 | ]) 26 | 27 | attribute :number 28 | 29 | def call! 30 | Success result: { number: number * 2 } 31 | end 32 | end 33 | 34 | def test_the_use_case_result 35 | result = Double.call(text: '4') 36 | 37 | assert_success_result(result, value: { text: '8' }) 38 | 39 | assert_equal( 40 | [ConvertTextToNumber, Double::Self, ConvertNumberToText], 41 | Double.use_cases 42 | ) 43 | end 44 | 45 | begin 46 | class DoubleFoo < Double 47 | end 48 | rescue RuntimeError => e 49 | @@__inheritance_violation_message = e.message 50 | end 51 | 52 | def test_the_inheritance_violation 53 | expected_message = 54 | "Wooo, you can't do this! Inherits from a use case which has an inner flow violates "\ 55 | "one of the project principles: Solve complex business logic, by allowing the composition of use cases. "\ 56 | "Instead of doing this, declare a new class/constant with the steps needed.\n\n"\ 57 | "Related issue: https://github.com/serradura/u-case/issues/19\n" 58 | 59 | assert_equal(expected_message, @@__inheritance_violation_message) 60 | 61 | assert_raises_with_message(RuntimeError, expected_message) do 62 | Class.new(Double) 63 | end 64 | end 65 | 66 | def test_the_inspect 67 | assert_equal( 68 | ', #, ]>', 69 | Double.inspect 70 | ) 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /test/micro/case/MWRF/steps/03_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | require_relative '../users_entity' 4 | require_relative '../shared_assertions' 5 | 6 | class Micro::Case::MWRF 7 | class Step03Test < Minitest::Test 8 | include SharedAssertions 9 | 10 | module Users::Creation3 11 | class Persist < Micro::Case 12 | attributes :name, :email 13 | 14 | def call! 15 | user_data = attributes.merge(id: SecureRandom.uuid) 16 | 17 | Success :persist, result: { user: Users::Entity.new(user_data) } 18 | end 19 | end 20 | end 21 | 22 | module Users::Creation3 23 | require 'uri' 24 | require 'securerandom' 25 | 26 | class Process < Micro::Case 27 | attributes :name, :email 28 | 29 | def call! 30 | normalize_params 31 | .then(apply(:validate_params)) 32 | .then(Persist) 33 | .then(apply(:sync_with_crm)) 34 | end 35 | 36 | private 37 | 38 | def normalize_params 39 | Success :normalize_params, result: { 40 | name: String(name).strip.gsub(/\s+/, ' '), 41 | email: String(email).downcase.strip 42 | } 43 | end 44 | 45 | def validate_params(name:, email:) 46 | validation_errors = [] 47 | validation_errors << "Name can't be blank" if name.empty? 48 | validation_errors << "Email is invalid" if email !~ URI::MailTo::EMAIL_REGEXP 49 | 50 | return Success(:validate_params) if validation_errors.empty? 51 | 52 | Failure :invalid_attributes, result: { 53 | errors: OpenStruct.new(full_messages: validation_errors) 54 | } 55 | end 56 | 57 | def sync_with_crm(user:, **) 58 | if user.persisted? 59 | # Do some integration stuff... 60 | crm_id = SecureRandom.uuid 61 | 62 | Success :sync_with_crm, result: { user: user, crm_id: crm_id } 63 | else 64 | Failure :sync_failed, result: { message: "User can't be sent to the CRM" } 65 | end 66 | end 67 | end 68 | end 69 | 70 | def use_case 71 | Users::Creation3::Process 72 | end 73 | 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /benchmarks/perfomance/use_case/failure_results.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | require 'forwardable' 4 | 5 | gemfile do 6 | source 'https://rubygems.org' 7 | 8 | ruby '>= 2.4.0' 9 | 10 | gem 'benchmark-ips', '~> 2.7', '>= 2.7.2' 11 | 12 | gem 'dry-monads', '~> 1.3', '>= 1.3.1' 13 | gem 'dry-transaction', '~> 0.13.0' 14 | 15 | gem 'interactor', '~> 3.1', '>= 3.1.1' 16 | 17 | gem 'trailblazer-activity', '~> 0.10.1' 18 | gem 'trailblazer-operation', '~> 0.6.2', require: 'trailblazer/operation' 19 | 20 | gem 'u-case', '~> 4.1.0' 21 | end 22 | 23 | require_relative '../../examples/use_case/multiply_with/all' 24 | require_relative 'call_use_cases' 25 | 26 | Micro::Case.config do |config| 27 | config.enable_transitions = false 28 | end 29 | 30 | require 'benchmark/ips' 31 | 32 | Benchmark.ips(&CallUseCases.( 33 | params: { a: nil, 'b' => 2 } 34 | )) 35 | 36 | # Warming up -------------------------------------- 37 | # Interactor 2.626k i/100ms 38 | # Trailblazer::Operation 2.343k i/100ms 39 | # Dry::Monads 13.386k i/100ms 40 | # Dry::Transaction 868.000 i/100ms 41 | # Micro::Case 7.603k i/100ms 42 | # Micro::Case::Safe 7.598k i/100ms 43 | # Micro::Case::Strict 6.178k i/100ms 44 | 45 | # Calculating ------------------------------------- 46 | # Interactor 27.037k (±24.9%) i/s - 128.674k in 5.102133s 47 | # Trailblazer::Operation 29.016k (±12.4%) i/s - 145.266k in 5.074991s 48 | # Dry::Monads 135.387k (±15.1%) i/s - 669.300k in 5.055356s 49 | # Dry::Transaction 8.989k (± 9.2%) i/s - 45.136k in 5.084820s 50 | # Micro::Case 73.247k (± 9.9%) i/s - 364.944k in 5.030449s 51 | # Micro::Case::Safe 73.489k (± 9.6%) i/s - 364.704k in 5.007282s 52 | # Micro::Case::Strict 61.980k (± 8.0%) i/s - 308.900k in 5.014821s 53 | 54 | # Comparison: 55 | # Dry::Monads: 135386.9 i/s 56 | # Micro::Case::Safe: 73489.3 i/s - 1.84x (± 0.00) slower 57 | # Micro::Case: 73246.6 i/s - 1.85x (± 0.00) slower 58 | # Micro::Case::Strict: 61979.7 i/s - 2.18x (± 0.00) slower 59 | # Trailblazer::Operation: 29016.4 i/s - 4.67x (± 0.00) slower 60 | # Interactor: 27037.0 i/s - 5.01x (± 0.00) slower 61 | # Dry::Transaction: 8988.6 i/s - 15.06x (± 0.00) slower 62 | -------------------------------------------------------------------------------- /test/micro/cases/flow/collection_test.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'test_helper' 3 | require 'support/steps' 4 | 5 | class Micro::Cases::Flow::CollectionTest < Minitest::Test 6 | Add2ToAllNumbers = Micro::Cases.flow([ 7 | Steps::ConvertToNumbers, 8 | Steps::Add2 9 | ]) 10 | 11 | DoubleAllNumbers = Micro::Cases.flow([ 12 | Steps::ConvertToNumbers, 13 | Steps::Double 14 | ]) 15 | 16 | SquareAllNumbers = Micro::Cases.flow([ 17 | Steps::ConvertToNumbers, 18 | Steps::Square 19 | ]) 20 | 21 | DoubleAllNumbersAndAdd2 = Micro::Cases.flow([ 22 | DoubleAllNumbers, 23 | Steps::Add2 24 | ]) 25 | 26 | SquareAllNumbersAndAdd2 = Micro::Cases.flow([ 27 | SquareAllNumbers, 28 | Steps::Add2 29 | ]) 30 | 31 | SquareAllNumbersAndDouble = 32 | Micro::Cases.flow([SquareAllNumbersAndAdd2, DoubleAllNumbers]) 33 | 34 | DoubleAllNumbersAndSquareAndAdd2 = 35 | Micro::Cases.flow([DoubleAllNumbers, SquareAllNumbersAndAdd2]) 36 | 37 | EXAMPLES = [ 38 | { flow: Add2ToAllNumbers, result: [3, 3, 4, 4, 5, 6] }, 39 | { flow: DoubleAllNumbers, result: [2, 2, 4, 4, 6, 8] }, 40 | { flow: SquareAllNumbers, result: [1, 1, 4, 4, 9, 16] }, 41 | { flow: DoubleAllNumbersAndAdd2, result: [4, 4, 6, 6, 8, 10] }, 42 | { flow: SquareAllNumbersAndAdd2, result: [3, 3, 6, 6, 11, 18] }, 43 | { flow: SquareAllNumbersAndDouble, result: [6, 6, 12, 12, 22, 36] }, 44 | { flow: DoubleAllNumbersAndSquareAndAdd2, result: [6, 6, 18, 18, 38, 66] } 45 | ].map(&OpenStruct.method(:new)) 46 | 47 | def test_the_data_validation_error_when_calling_with_the_wrong_king_of_data 48 | [nil, 1, true, '', []].each do |arg| 49 | EXAMPLES.map(&:flow).each do |flow| 50 | assert_raises_with_message(Kind::Error, 'expected to be a kind of Hash') { flow.call(arg) } 51 | end 52 | end 53 | end 54 | 55 | def test_result_must_be_success 56 | EXAMPLES.each do |example| 57 | result = example.flow.call(numbers: %w[1 1 2 2 3 4]) 58 | 59 | assert_success_result(result, value: { numbers: example.result }) 60 | end 61 | end 62 | 63 | def test_result_must_be_a_failure 64 | EXAMPLES.map(&:flow).each do |flow| 65 | result = flow.call(numbers: %w[1 1 2 a 3 4]) 66 | 67 | assert_failure_result(result, value: { message: 'numbers must contain only numeric types' }) 68 | end 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Test 3 | on: [push, pull_request] 4 | jobs: 5 | test: 6 | name: "Ruby ${{ matrix.ruby }} - Rails ${{ matrix.rails }} - Transitions: ${{ matrix.transitions }}" 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | ruby: [2.7, 3.0, 3.1, 3.2, head] 11 | rails: ["6.1", "7.0", "7.1", "edge"] 12 | transitions: ["true", "false"] 13 | include: 14 | - ruby: 2.5 15 | rails: "5.2" 16 | transitions: true 17 | - ruby: 2.5 18 | rails: "6.0" 19 | transitions: true 20 | - ruby: 2.5 21 | rails: "6.1" 22 | transitions: true 23 | - ruby: 2.6 24 | rails: "5.2" 25 | transitions: true 26 | - ruby: 2.6 27 | rails: "6.0" 28 | transitions: true 29 | - ruby: 2.6 30 | rails: "6.1" 31 | transitions: true 32 | - ruby: 2.5 33 | rails: "5.2" 34 | transitions: false 35 | - ruby: 2.5 36 | rails: "6.0" 37 | transitions: false 38 | - ruby: 2.5 39 | rails: "6.1" 40 | transitions: false 41 | - ruby: 2.6 42 | rails: "5.2" 43 | transitions: false 44 | - ruby: 2.6 45 | rails: "6.0" 46 | transitions: false 47 | - ruby: 2.6 48 | rails: "6.1" 49 | transitions: false 50 | - ruby: 2.7 51 | rails: "6.0" 52 | transitions: true 53 | - ruby: 2.7 54 | rails: "6.0" 55 | transitions: false 56 | env: 57 | BUNDLE_GEMFILE: "gemfiles/rails_${{ matrix.rails }}/Gemfile" 58 | ENABLE_TRANSITIONS: ${{ matrix.transitions }} 59 | ACTIVERECORD_VERSION: ${{ matrix.rails }} 60 | CC_TEST_REPORTER_ID: 0377ece62be9c7042557d76e4e38b867e51c51b2a42d10ef5102b613ac077eab 61 | steps: 62 | - uses: actions/checkout@v4 63 | - uses: ruby/setup-ruby@v1 64 | with: 65 | ruby-version: ${{ matrix.ruby }} 66 | bundler-cache: true 67 | - name: Test and generate coverage 68 | run: bundle exec rake test 69 | - uses: paambaati/codeclimate-action@v5 70 | if: ${{ matrix.ruby == 3.2 && matrix.rails == '7.1' && matrix.transitions == 'true' }} 71 | -------------------------------------------------------------------------------- /benchmarks/perfomance/use_case/success_results.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | require 'forwardable' 4 | 5 | gemfile do 6 | source 'https://rubygems.org' 7 | 8 | ruby '>= 2.4.0' 9 | 10 | gem 'benchmark-ips', '~> 2.7', '>= 2.7.2' 11 | 12 | gem 'dry-monads', '~> 1.3', '>= 1.3.1' 13 | gem 'dry-transaction', '~> 0.13.0' 14 | 15 | gem 'interactor', '~> 3.1', '>= 3.1.1' 16 | 17 | gem 'trailblazer-activity', '~> 0.10.1' 18 | gem 'trailblazer-operation', '~> 0.6.2', require: 'trailblazer/operation' 19 | 20 | gem 'u-case', '~> 4.1.0' 21 | end 22 | 23 | require_relative '../../examples/use_case/multiply_with/all' 24 | require_relative 'call_use_cases' 25 | 26 | Micro::Case.config do |config| 27 | config.enable_transitions = false 28 | end 29 | 30 | require 'benchmark/ips' 31 | 32 | Benchmark.ips(&CallUseCases.( 33 | params: { a: 2, 'b' => 2 } 34 | )) 35 | 36 | # Warming up -------------------------------------- 37 | # Interactor 5.711k i/100ms 38 | # Trailblazer::Operation 39 | # 2.283k i/100ms 40 | # Dry::Monads 31.130k i/100ms 41 | # Dry::Transaction 994.000 i/100ms 42 | # Micro::Case 7.911k i/100ms 43 | # Micro::Case::Safe 7.911k i/100ms 44 | # Micro::Case::Strict 6.248k i/100ms 45 | 46 | # Calculating ------------------------------------- 47 | # Interactor 59.746k (±29.9%) i/s - 274.128k in 5.049901s 48 | # Trailblazer::Operation 49 | # 28.424k (±15.8%) i/s - 141.546k in 5.087882s 50 | # Dry::Monads 315.635k (± 6.1%) i/s - 1.588M in 5.048914s 51 | # Dry::Transaction 10.131k (± 6.4%) i/s - 50.694k in 5.025150s 52 | # Micro::Case 75.838k (± 9.7%) i/s - 379.728k in 5.052573s 53 | # Micro::Case::Safe 75.461k (±10.1%) i/s - 379.728k in 5.079238s 54 | # Micro::Case::Strict 64.235k (± 9.0%) i/s - 324.896k in 5.097028s 55 | 56 | # Comparison: 57 | # Dry::Monads: 315635.1 i/s 58 | # Micro::Case: 75837.7 i/s - 4.16x (± 0.00) slower 59 | # Micro::Case::Safe: 75461.3 i/s - 4.18x (± 0.00) slower 60 | # Micro::Case::Strict: 64234.9 i/s - 4.91x (± 0.00) slower 61 | # Interactor: 59745.5 i/s - 5.28x (± 0.00) slower 62 | # Trailblazer::Operation: 28423.9 i/s - 11.10x (± 0.00) slower 63 | # Dry::Transaction: 10130.9 i/s - 31.16x (± 0.00) slower 64 | -------------------------------------------------------------------------------- /test/micro/case/MWRF/steps/04_using_static_composition_via_inner_flow_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | require_relative '../users_entity' 4 | require_relative '../shared_assertions' 5 | 6 | class Micro::Case::MWRF 7 | class Step04UsingStaticCompositionViaInnerFlowTest < Minitest::Test 8 | include SharedAssertions 9 | 10 | module Users::Creation4b 11 | class NormalizeParams < Micro::Case 12 | attributes :name, :email 13 | 14 | def call! 15 | normalized_name = String(name).strip.gsub(/\s+/, ' ') 16 | normalized_email = String(email).downcase.strip 17 | 18 | Success result: { name: normalized_name, email: normalized_email } 19 | end 20 | end 21 | end 22 | 23 | module Users::Creation4b 24 | require 'uri' 25 | 26 | class ValidateParams < Micro::Case 27 | attributes :name, :email 28 | 29 | def call! 30 | validation_errors = [] 31 | validation_errors << "Name can't be blank" if name.empty? 32 | validation_errors << "Email is invalid" if email !~ URI::MailTo::EMAIL_REGEXP 33 | 34 | return Success() if validation_errors.empty? 35 | 36 | Failure :invalid_attributes, result: { 37 | errors: OpenStruct.new(full_messages: validation_errors) 38 | } 39 | end 40 | end 41 | end 42 | 43 | require 'securerandom' 44 | 45 | module Users::Creation4b 46 | class Persist < Micro::Case 47 | attributes :name, :email 48 | 49 | def call! 50 | user_data = attributes.merge(id: SecureRandom.uuid) 51 | 52 | Success result: { user: Users::Entity.new(user_data) } 53 | end 54 | end 55 | end 56 | 57 | module Users::Creation4b 58 | class Process < Micro::Case 59 | flow(NormalizeParams, ValidateParams, Persist, self) 60 | 61 | attribute :user 62 | 63 | def call! 64 | if user.persisted? 65 | Success result: { user: user, crm_id: sync_with_crm } 66 | else 67 | Failure :sync_failed, result: { message: "User can't be sent to the CRM" } 68 | end 69 | end 70 | 71 | private def sync_with_crm 72 | # Do some integration stuff... 73 | SecureRandom.uuid 74 | end 75 | end 76 | end 77 | 78 | def use_case 79 | Users::Creation4b::Process 80 | end 81 | 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/MWRF/steps/02_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | require_relative '../users_entity' 5 | require_relative '../shared_assertions' 6 | 7 | class Micro::Case::MWRF::WithValidation 8 | class Step02Test < Minitest::Test 9 | include SharedAssertions 10 | 11 | module Users::Creation2 12 | require 'uri' 13 | require 'securerandom' 14 | 15 | class Process < Micro::Case 16 | attributes :name, :email 17 | 18 | def call! 19 | normalize_params 20 | .then(method(:validate_params)) 21 | .then(method(:persist)) 22 | .then(method(:sync_with_crm)) 23 | end 24 | 25 | private 26 | 27 | def normalize_params 28 | Success :normalize_params, result: { 29 | name: String(name).strip.gsub(/\s+/, ' '), 30 | email: String(email).downcase.strip 31 | } 32 | end 33 | 34 | def validate_params(name:, email:) 35 | validation_errors = [] 36 | validation_errors << "Name can't be blank" if name.empty? 37 | validation_errors << "Email is invalid" if email !~ URI::MailTo::EMAIL_REGEXP 38 | 39 | return Success(:validate_params) if validation_errors.empty? 40 | 41 | Failure :invalid_attributes, result: { 42 | errors: OpenStruct.new(full_messages: validation_errors) 43 | } 44 | end 45 | 46 | def persist(name:, email:, **) 47 | user_data = { name: name, email: email, id: SecureRandom.uuid } 48 | 49 | user = Users::Entity.new(user_data) 50 | 51 | Success :persist, result: { user: user } 52 | end 53 | 54 | def sync_with_crm(user:, **) 55 | if user.persisted? 56 | # Do some integration stuff... 57 | crm_id = SecureRandom.uuid 58 | 59 | Success :sync_with_crm, result: { user: user, crm_id: crm_id } 60 | else 61 | Failure :sync_failed, result: { message: "User can't be sent to the CRM" } 62 | end 63 | end 64 | end 65 | end 66 | 67 | def use_case 68 | Users::Creation2::Process 69 | end 70 | 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/micro/cases/safe/flow/collection_test.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'test_helper' 3 | require 'support/steps' 4 | 5 | class Micro::Cases::Safe::Flow::CollectionTest < Minitest::Test 6 | Add2ToAllNumbers = Micro::Cases.safe_flow([ 7 | Steps::ConvertToNumbers, 8 | Steps::Add2 9 | ]) 10 | 11 | DoubleAllNumbers = Micro::Cases.safe_flow([ 12 | Steps::ConvertToNumbers, 13 | Steps::Double 14 | ]) 15 | 16 | SquareAllNumbers = Micro::Cases.safe_flow([ 17 | Steps::ConvertToNumbers, 18 | Steps::Square 19 | ]) 20 | 21 | DoubleAllNumbersAndAdd2 = Micro::Cases.safe_flow([ 22 | DoubleAllNumbers, 23 | Steps::Add2 24 | ]) 25 | 26 | SquareAllNumbersAndAdd2 = Micro::Cases.safe_flow([ 27 | SquareAllNumbers, 28 | Steps::Add2 29 | ]) 30 | 31 | SquareAllNumbersAndDouble = 32 | Micro::Cases.safe_flow([SquareAllNumbersAndAdd2, DoubleAllNumbers]) 33 | 34 | DoubleAllNumbersAndSquareAndAdd2 = 35 | Micro::Cases.safe_flow([DoubleAllNumbers, SquareAllNumbersAndAdd2]) 36 | 37 | 38 | EXAMPLES = [ 39 | { flow: Add2ToAllNumbers, result: [3, 3, 4, 4, 5, 6] }, 40 | { flow: DoubleAllNumbers, result: [2, 2, 4, 4, 6, 8] }, 41 | { flow: SquareAllNumbers, result: [1, 1, 4, 4, 9, 16] }, 42 | { flow: DoubleAllNumbersAndAdd2, result: [4, 4, 6, 6, 8, 10] }, 43 | { flow: SquareAllNumbersAndAdd2, result: [3, 3, 6, 6, 11, 18] }, 44 | { flow: SquareAllNumbersAndDouble, result: [6, 6, 12, 12, 22, 36] }, 45 | { flow: DoubleAllNumbersAndSquareAndAdd2, result: [6, 6, 18, 18, 38, 66] } 46 | ].map(&OpenStruct.method(:new)) 47 | 48 | def test_the_data_validation_error_when_calling_with_the_wrong_king_of_data 49 | [nil, 1, true, '', []].each do |arg| 50 | EXAMPLES.map(&:flow).each do |flow| 51 | assert_raises_with_message(Kind::Error, 'expected to be a kind of Hash') { 52 | flow.call(arg) 53 | } 54 | end 55 | end 56 | end 57 | 58 | def test_result_must_be_success 59 | EXAMPLES.each do |example| 60 | result = example.flow.call(numbers: %w[1 1 2 2 3 4]) 61 | 62 | assert_success_result(result, value: { numbers: example.result }) 63 | end 64 | end 65 | 66 | def test_result_must_be_a_failure 67 | EXAMPLES.map(&:flow).each do |flow| 68 | result = flow.call(numbers: %w[1 1 2 a 3 4]) 69 | 70 | assert_failure_result(result, value: { message: 'numbers must contain only numeric types' }) 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/MWRF/steps/04_using_static_composition_via_inner_flow_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | require_relative '../users_entity' 5 | require_relative '../shared_assertions' 6 | 7 | class Micro::Case::MWRF::WithValidation 8 | class Step04UsingStaticCompositionViaInnerFlowTest < Minitest::Test 9 | include SharedAssertions 10 | 11 | module Users::Creation4b 12 | class NormalizeParams < Micro::Case 13 | attributes :name, :email 14 | 15 | def call! 16 | normalized_name = String(name).strip.gsub(/\s+/, ' ') 17 | normalized_email = String(email).downcase.strip 18 | 19 | Success result: { name: normalized_name, email: normalized_email } 20 | end 21 | end 22 | end 23 | 24 | module Users::Creation4b 25 | require 'uri' 26 | 27 | class ValidateParams < Micro::Case 28 | attribute :name, validates: { presence: true } 29 | attribute :email, validates: { format: URI::MailTo::EMAIL_REGEXP } 30 | 31 | def call! 32 | Success result: attributes(:name, :email) 33 | end 34 | end 35 | end 36 | 37 | require 'securerandom' 38 | 39 | module Users::Creation4b 40 | class Persist < Micro::Case 41 | attributes :name, :email, validates: { kind: String } 42 | 43 | def call! 44 | user_data = attributes.merge(id: SecureRandom.uuid) 45 | 46 | Success result: { user: Users::Entity.new(user_data) } 47 | end 48 | end 49 | end 50 | 51 | module Users::Creation4b 52 | class Process < Micro::Case 53 | flow(NormalizeParams, ValidateParams, Persist, self) 54 | 55 | attribute :user, validates: { kind: Users::Entity } 56 | 57 | def call! 58 | if user.persisted? 59 | Success result: { user: user, crm_id: sync_with_crm } 60 | else 61 | Failure :sync_failed, result: { message: "User can't be sent to the CRM" } 62 | end 63 | end 64 | 65 | private def sync_with_crm 66 | # Do some integration stuff... 67 | SecureRandom.uuid 68 | end 69 | end 70 | end 71 | 72 | def use_case 73 | Users::Creation4b::Process 74 | end 75 | 76 | end 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/MWRF/steps/03_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | require_relative '../users_entity' 5 | require_relative '../shared_assertions' 6 | 7 | class Micro::Case::MWRF::WithValidation 8 | class Step03Test < Minitest::Test 9 | include SharedAssertions 10 | 11 | module Users::Creation3 12 | class Persist < Micro::Case 13 | attributes :name, :email, validates: { kind: String } 14 | 15 | def call! 16 | user_data = attributes.merge(id: SecureRandom.uuid) 17 | 18 | Success :persist, result: { user: Users::Entity.new(user_data) } 19 | end 20 | end 21 | end 22 | 23 | module Users::Creation3 24 | require 'uri' 25 | require 'securerandom' 26 | 27 | class Process < Micro::Case 28 | attributes :name, :email 29 | 30 | def call! 31 | normalize_params 32 | .then(apply(:validate_params)) 33 | .then(Persist) 34 | .then(apply(:sync_with_crm)) 35 | end 36 | 37 | private 38 | 39 | def normalize_params 40 | Success :normalize_params, result: { 41 | name: String(name).strip.gsub(/\s+/, ' '), 42 | email: String(email).downcase.strip 43 | } 44 | end 45 | 46 | def validate_params(name:, email:) 47 | validation_errors = [] 48 | validation_errors << "Name can't be blank" if name.empty? 49 | validation_errors << "Email is invalid" if email !~ URI::MailTo::EMAIL_REGEXP 50 | 51 | return Success(:validate_params) if validation_errors.empty? 52 | 53 | Failure :invalid_attributes, result: { 54 | errors: OpenStruct.new(full_messages: validation_errors) 55 | } 56 | end 57 | 58 | def sync_with_crm(user:, **) 59 | if user.persisted? 60 | # Do some integration stuff... 61 | crm_id = SecureRandom.uuid 62 | 63 | Success :sync_with_crm, result: { user: user, crm_id: crm_id } 64 | else 65 | Failure :sync_failed, result: { message: "User can't be sent to the CRM" } 66 | end 67 | end 68 | end 69 | end 70 | 71 | def use_case 72 | Users::Creation3::Process 73 | end 74 | 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /test/micro/case/MWRF/steps/04_using_static_composition_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | require_relative '../users_entity' 4 | require_relative '../shared_assertions' 5 | 6 | class Micro::Case::MWRF 7 | class Step04UsingStaticCompositionTest < Minitest::Test 8 | include SharedAssertions 9 | 10 | module Users::Creation4a 11 | class NormalizeParams < Micro::Case 12 | attributes :name, :email 13 | 14 | def call! 15 | normalized_name = String(name).strip.gsub(/\s+/, ' ') 16 | normalized_email = String(email).downcase.strip 17 | 18 | Success result: { name: normalized_name, email: normalized_email } 19 | end 20 | end 21 | end 22 | 23 | module Users::Creation4a 24 | require 'uri' 25 | 26 | class ValidateParams < Micro::Case 27 | attributes :name, :email 28 | 29 | def call! 30 | validation_errors = [] 31 | validation_errors << "Name can't be blank" if name.empty? 32 | validation_errors << "Email is invalid" if email !~ URI::MailTo::EMAIL_REGEXP 33 | 34 | return Success() if validation_errors.empty? 35 | 36 | Failure :invalid_attributes, result: { 37 | errors: OpenStruct.new(full_messages: validation_errors) 38 | } 39 | end 40 | end 41 | end 42 | 43 | require 'securerandom' 44 | 45 | module Users::Creation4a 46 | class Persist < Micro::Case 47 | attributes :name, :email 48 | 49 | def call! 50 | user_data = attributes.merge(id: SecureRandom.uuid) 51 | 52 | Success result: { user: Users::Entity.new(user_data) } 53 | end 54 | end 55 | end 56 | 57 | module Users::Creation4a 58 | class SyncWithCRM < Micro::Case 59 | attribute :user 60 | 61 | def call! 62 | if user.persisted? 63 | Success result: { user: user, crm_id: sync_with_crm } 64 | else 65 | Failure :sync_failed, result: { message: "User can't be sent to the CRM" } 66 | end 67 | end 68 | 69 | private def sync_with_crm 70 | # Do some integration stuff... 71 | SecureRandom.uuid 72 | end 73 | end 74 | end 75 | 76 | module Users::Creation4a 77 | Process = Micro::Cases.flow([ 78 | NormalizeParams, 79 | ValidateParams, 80 | Persist, 81 | SyncWithCRM 82 | ]) 83 | end 84 | 85 | def use_case 86 | Users::Creation4a::Process 87 | end 88 | 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/MWRF/steps/04_using_static_composition_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | require_relative '../users_entity' 5 | require_relative '../shared_assertions' 6 | 7 | class Micro::Case::MWRF::WithValidation 8 | class Step04UsingStaticCompositionTest < Minitest::Test 9 | include SharedAssertions 10 | 11 | module Users::Creation4a 12 | class NormalizeParams < Micro::Case 13 | attributes :name, :email 14 | 15 | def call! 16 | normalized_name = String(name).strip.gsub(/\s+/, ' ') 17 | normalized_email = String(email).downcase.strip 18 | 19 | Success result: { name: normalized_name, email: normalized_email } 20 | end 21 | end 22 | end 23 | 24 | module Users::Creation4a 25 | require 'uri' 26 | 27 | class ValidateParams < Micro::Case 28 | attribute :name, validates: { presence: true } 29 | attribute :email, validates: { format: URI::MailTo::EMAIL_REGEXP } 30 | 31 | def call! 32 | Success result: attributes(:name, :email) 33 | end 34 | end 35 | end 36 | 37 | require 'securerandom' 38 | 39 | module Users::Creation4a 40 | class Persist < Micro::Case 41 | attributes :name, :email, validates: { kind: String } 42 | 43 | def call! 44 | user_data = attributes.merge(id: SecureRandom.uuid) 45 | 46 | Success result: { user: Users::Entity.new(user_data) } 47 | end 48 | end 49 | end 50 | 51 | module Users::Creation4a 52 | class SyncWithCRM < Micro::Case 53 | attribute :user, validates: { kind: Users::Entity } 54 | 55 | def call! 56 | if user.persisted? 57 | Success result: { user: user, crm_id: sync_with_crm } 58 | else 59 | Failure :sync_failed, result: { message: "User can't be sent to the CRM" } 60 | end 61 | end 62 | 63 | private def sync_with_crm 64 | # Do some integration stuff... 65 | SecureRandom.uuid 66 | end 67 | end 68 | end 69 | 70 | module Users::Creation4a 71 | Process = Micro::Cases.flow([ 72 | NormalizeParams, 73 | ValidateParams, 74 | Persist, 75 | SyncWithCRM 76 | ]) 77 | end 78 | 79 | def use_case 80 | Users::Creation4a::Process 81 | end 82 | 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /test/micro/case/wrong_usage_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Micro::Case::WrongUsageTest < Minitest::Test 4 | class WrongSuccessResult < Micro::Case 5 | attributes :a, :b 6 | 7 | def call! 8 | if b < 0 9 | Success :divided_by_negative, result: a / b 10 | else 11 | Success result: a / b 12 | end 13 | rescue ZeroDivisionError => exception 14 | Failure result: exception 15 | end 16 | end 17 | 18 | def test_the_wrong_usage_error_by_set_an_invalid_success_result 19 | err1 = assert_raises( 20 | Micro::Case::Error::InvalidResult, 21 | ) { WrongSuccessResult.call(a: 4, b: 2) } 22 | 23 | assert_equal( 24 | "The result returned from Micro::Case::WrongUsageTest::WrongSuccessResult#call! must be a Hash.\n" \ 25 | "\n" \ 26 | "Example:\n" \ 27 | " Success(result: { key: 'value' })", 28 | err1.message) 29 | 30 | # --- 31 | 32 | err2 = assert_raises( 33 | Micro::Case::Error::InvalidResult, 34 | ) { WrongSuccessResult.call(a: 4, b: -2) } 35 | 36 | assert_equal( 37 | "The result returned from Micro::Case::WrongUsageTest::WrongSuccessResult#call! must be a Hash.\n" \ 38 | "\n" \ 39 | "Example:\n" \ 40 | " Success(:divided_by_negative, result: { key: 'value' })", 41 | err2.message) 42 | end 43 | 44 | class WrongFailureResult < Micro::Case 45 | attributes :a, :b 46 | 47 | def call! 48 | if b < 0 49 | Failure :divided_by_negative, result: a / b 50 | else 51 | Success result: { division: a / b } 52 | end 53 | rescue ZeroDivisionError 54 | Failure result: 0 55 | end 56 | end 57 | 58 | def test_the_wrong_usage_error_by_set_an_invalid_failure_result 59 | err1 = assert_raises( 60 | Micro::Case::Error::InvalidResult, 61 | ) { WrongFailureResult.call(a: 4, b: 0) } 62 | 63 | assert_equal( 64 | "The result returned from Micro::Case::WrongUsageTest::WrongFailureResult#call! must be a Hash.\n" \ 65 | "\n" \ 66 | "Example:\n" \ 67 | " Failure(result: { key: 'value' })", 68 | err1.message 69 | ) 70 | 71 | # --- 72 | 73 | err2 = assert_raises( 74 | Micro::Case::Error::InvalidResult, 75 | ) { WrongFailureResult.call(a: 4, b: -2) } 76 | 77 | assert_equal( 78 | "The result returned from Micro::Case::WrongUsageTest::WrongFailureResult#call! must be a Hash.\n" \ 79 | "\n" \ 80 | "Example:\n" \ 81 | " Failure(:divided_by_negative, result: { key: 'value' })", 82 | err2.message 83 | ) 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /test/micro/case/MWRF/steps/04_using_variable_composition_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | require_relative '../users_entity' 4 | require_relative '../shared_assertions' 5 | 6 | class Micro::Case::MWRF 7 | class Step04UsingVariableCompositionTest < Minitest::Test 8 | include SharedAssertions 9 | 10 | module Users::Creation4c 11 | class NormalizeParams < Micro::Case 12 | attributes :name, :email 13 | 14 | def call! 15 | normalized_name = String(name).strip.gsub(/\s+/, ' ') 16 | normalized_email = String(email).downcase.strip 17 | 18 | Success result: { name: normalized_name, email: normalized_email } 19 | end 20 | end 21 | end 22 | 23 | module Users::Creation4c 24 | require 'uri' 25 | 26 | class ValidateParams < Micro::Case 27 | attributes :name, :email 28 | 29 | def call! 30 | validation_errors = [] 31 | validation_errors << "Name can't be blank" if name.empty? 32 | validation_errors << "Email is invalid" if email !~ URI::MailTo::EMAIL_REGEXP 33 | 34 | return Success() if validation_errors.empty? 35 | 36 | Failure :invalid_attributes, result: { 37 | errors: OpenStruct.new(full_messages: validation_errors) 38 | } 39 | end 40 | end 41 | end 42 | 43 | require 'securerandom' 44 | 45 | module Users::Creation4c 46 | class Persist < Micro::Case 47 | attributes :name, :email 48 | 49 | def call! 50 | user_data = attributes.merge(id: SecureRandom.uuid) 51 | 52 | Success result: { user: Users::Entity.new(user_data) } 53 | end 54 | end 55 | end 56 | 57 | module Users::Creation4c 58 | class SyncWithCRM < Micro::Case 59 | attribute :user 60 | 61 | def call! 62 | if user.persisted? 63 | Success result: { user: user, crm_id: sync_with_crm } 64 | else 65 | Failure :sync_failed, result: { message: "User can't be sent to the CRM" } 66 | end 67 | end 68 | 69 | private def sync_with_crm 70 | # Do some integration stuff... 71 | SecureRandom.uuid 72 | end 73 | end 74 | end 75 | 76 | module Users::Creation4c 77 | class Process < Micro::Case 78 | def call! 79 | call(NormalizeParams) 80 | .then(ValidateParams) 81 | .then(Persist) 82 | .then(SyncWithCRM) 83 | end 84 | end 85 | end 86 | 87 | def use_case 88 | Users::Creation4c::Process 89 | end 90 | 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /test/micro/cases/flow/blend_test.rb: -------------------------------------------------------------------------------- 1 | require 'ostruct' 2 | require 'test_helper' 3 | require 'support/steps' 4 | 5 | class Micro::Cases::Flow::BlendTest < Minitest::Test 6 | Add2ToAllNumbers = Micro::Cases.flow([ 7 | Steps::ConvertToNumbers, Steps::Add2 8 | ]) 9 | 10 | DoubleAllNumbers = Micro::Cases.flow([ 11 | Steps::ConvertToNumbers, 12 | Steps::Double 13 | ]) 14 | 15 | class SquareAllNumbers < Micro::Case 16 | flow Steps::ConvertToNumbers, 17 | Steps::Square 18 | end 19 | 20 | DoubleAllNumbersAndAdd2 = Micro::Cases.flow([ 21 | DoubleAllNumbers, Steps::Add2 22 | ]) 23 | 24 | SquareAllNumbersAndAdd2 = Micro::Cases.flow([ 25 | SquareAllNumbers, Steps::Add2 26 | ]) 27 | 28 | SquareAllNumbersAndDouble = Micro::Cases.flow([ 29 | SquareAllNumbersAndAdd2, DoubleAllNumbers 30 | ]) 31 | 32 | class DoubleAllNumbersAndSquareAndAdd2 < Micro::Case 33 | flow DoubleAllNumbers, 34 | SquareAllNumbersAndAdd2 35 | end 36 | 37 | EXAMPLES = [ 38 | { flow: Add2ToAllNumbers, result: [3, 3, 4, 4, 5, 6] }, 39 | { flow: DoubleAllNumbers, result: [2, 2, 4, 4, 6, 8] }, 40 | { flow: SquareAllNumbers, result: [1, 1, 4, 4, 9, 16] }, 41 | { flow: DoubleAllNumbersAndAdd2, result: [4, 4, 6, 6, 8, 10] }, 42 | { flow: SquareAllNumbersAndAdd2, result: [3, 3, 6, 6, 11, 18] }, 43 | { flow: SquareAllNumbersAndDouble, result: [6, 6, 12, 12, 22, 36] }, 44 | { flow: DoubleAllNumbersAndSquareAndAdd2, result: [6, 6, 18, 18, 38, 66] } 45 | ].map(&OpenStruct.method(:new)) 46 | 47 | def test_result_must_be_success 48 | EXAMPLES.each do |example| 49 | result = example.flow.call(numbers: %w[1 1 2 2 3 4]) 50 | 51 | assert_success_result(result, value: { numbers: example.result }) 52 | end 53 | end 54 | 55 | def test_result_must_be_a_failure 56 | EXAMPLES.map(&:flow).each do |flow| 57 | result = flow.call(numbers: %w[1 1 2 a 3 4]) 58 | 59 | assert_failure_result(result, value: { message: 'numbers must contain only numeric types' }) 60 | end 61 | end 62 | 63 | def test_inspect 64 | assert_equal( 65 | '#<(Micro::Cases::Flow) use_cases=[, ]>', 66 | Add2ToAllNumbers.inspect 67 | ) 68 | 69 | # -- 70 | 71 | assert_equal( 72 | ', ]>', 73 | SquareAllNumbers.inspect 74 | ) 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /test/micro/case/with_activemodel_validation/MWRF/steps/04_using_variable_composition_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | require_relative '../users_entity' 5 | require_relative '../shared_assertions' 6 | 7 | class Micro::Case::MWRF::WithValidation 8 | class Step04UsingVariableCompositionTest < Minitest::Test 9 | include SharedAssertions 10 | 11 | module Users::Creation4c 12 | class NormalizeParams < Micro::Case 13 | attributes :name, :email 14 | 15 | def call! 16 | normalized_name = String(name).strip.gsub(/\s+/, ' ') 17 | normalized_email = String(email).downcase.strip 18 | 19 | Success result: { name: normalized_name, email: normalized_email } 20 | end 21 | end 22 | end 23 | 24 | module Users::Creation4c 25 | require 'uri' 26 | 27 | class ValidateParams < Micro::Case 28 | attribute :name, validates: { presence: true } 29 | attribute :email, validates: { format: URI::MailTo::EMAIL_REGEXP } 30 | 31 | def call! 32 | Success result: attributes(:name, :email) 33 | end 34 | end 35 | end 36 | 37 | require 'securerandom' 38 | 39 | module Users::Creation4c 40 | class Persist < Micro::Case 41 | attributes :name, :email, validates: { kind: String } 42 | 43 | def call! 44 | user_data = attributes.merge(id: SecureRandom.uuid) 45 | 46 | Success result: { user: Users::Entity.new(user_data) } 47 | end 48 | end 49 | end 50 | 51 | module Users::Creation4c 52 | class SyncWithCRM < Micro::Case 53 | attribute :user, validates: { kind: Users::Entity } 54 | 55 | def call! 56 | if user.persisted? 57 | Success result: { user: user, crm_id: sync_with_crm } 58 | else 59 | Failure :sync_failed, result: { message: "User can't be sent to the CRM" } 60 | end 61 | end 62 | 63 | private def sync_with_crm 64 | # Do some integration stuff... 65 | SecureRandom.uuid 66 | end 67 | end 68 | end 69 | 70 | module Users::Creation4c 71 | class Process < Micro::Case 72 | def call! 73 | call(NormalizeParams) 74 | .then(ValidateParams) 75 | .then(Persist) 76 | .then(SyncWithCRM) 77 | end 78 | end 79 | end 80 | 81 | def use_case 82 | Users::Creation4c::Process 83 | end 84 | 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/micro/cases/flow.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Micro 4 | module Cases 5 | class Flow 6 | IsAUseCaseWithDefaults = -> arg { arg.is_a?(Array) && Micro.case?(arg[0]) && arg[1].is_a?(Hash) } 7 | IsAValidUseCase = -> use_case { Micro.case?(use_case) || IsAUseCaseWithDefaults[use_case] } 8 | 9 | attr_reader :use_cases 10 | 11 | def self.build(args) 12 | use_cases = Utils.map_use_cases(args) 13 | 14 | raise Error::InvalidUseCases if use_cases.none?(&IsAValidUseCase) 15 | 16 | new(use_cases) 17 | end 18 | 19 | def initialize(use_cases) 20 | @use_cases = use_cases.dup.freeze 21 | @next_ones = use_cases.dup 22 | @first = @next_ones.shift 23 | end 24 | 25 | def inspect 26 | '#<(%s) use_cases=%s>' % [self.class, @use_cases] 27 | end 28 | 29 | def call!(input:, result:) 30 | first_result = __call_use_case(@first, result, input) 31 | 32 | return first_result if @next_ones.empty? 33 | 34 | __call_next_use_cases(first_result) 35 | end 36 | 37 | def call(input = Kind::Empty::HASH) 38 | result = call!(input: input, result: Case::Result.new) 39 | 40 | return result unless block_given? 41 | 42 | result_wrapper = ::Micro::Case::Result::Wrapper.new(result) 43 | 44 | yield(result_wrapper) 45 | 46 | result_wrapper.output 47 | end 48 | 49 | alias __call__ call 50 | 51 | def to_proc 52 | Proc.new { |arg| call(arg) } 53 | end 54 | 55 | def then(use_case = nil, &block) 56 | can_yield_self = respond_to?(:yield_self) 57 | 58 | if block 59 | raise_invalid_invocation_of_the_then_method if use_case 60 | raise NotImplementedError if !can_yield_self 61 | 62 | yield_self(&block) 63 | else 64 | return yield_self if !use_case && can_yield_self 65 | 66 | raise_invalid_invocation_of_the_then_method unless ::Micro.case_or_flow?(use_case) 67 | 68 | self.call.then(use_case) 69 | end 70 | end 71 | 72 | private 73 | 74 | def raise_invalid_invocation_of_the_then_method 75 | raise Case::Error::InvalidInvocationOfTheThenMethod.new("#{self.class.name}#") 76 | end 77 | 78 | def __call_use_case(use_case, result, input) 79 | __build_use_case(use_case, result, input).__call__ 80 | end 81 | 82 | def __call_next_use_cases(first_result) 83 | @next_ones.reduce(first_result) do |result, use_case| 84 | break result if result.failure? 85 | 86 | __call_use_case(use_case, result, result.data) 87 | end 88 | end 89 | 90 | def __build_use_case(use_case, result, input) 91 | return use_case.__new__(result, input) unless use_case.is_a?(Array) 92 | 93 | use_case[0].__new__(result, input.merge(use_case[1])) 94 | end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /test/micro/cases/map_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Micro::Cases::MapTest < Minitest::Test 4 | def test_invalid_types_should_raise_an_exception 5 | [nil, 1, true, '', {}].each do |arg| 6 | assert_raises_with_message(Kind::Error, "#{arg} expected to be a kind of Array") do 7 | Micro::Cases::Map.build(arg) 8 | end 9 | end 10 | end 11 | 12 | def test_invalid_array_should_raise_an_exception 13 | assert_raises_with_message( 14 | Micro::Cases::Error::InvalidUseCases, 15 | 'argument must be a collection of `Micro::Case` classes' 16 | ) { Micro::Cases.map(%w[wrong params]).call(foo: 'foo') } 17 | 18 | # -- 19 | 20 | assert_raises_with_message( 21 | Micro::Cases::Error::InvalidUseCases, 22 | 'argument must be a collection of `Micro::Case` classes' 23 | ) { Micro::Cases.map([String, Integer]).call(foo: 'foo') } 24 | end 25 | 26 | class Foo < Micro::Case 27 | attribute :foo 28 | 29 | def call! 30 | return Success(:filled_foo) if foo 31 | 32 | Failure(:missing_foo) 33 | end 34 | end 35 | 36 | class Bar < Micro::Case 37 | attribute :bar 38 | 39 | def call! 40 | return Success(:filled_bar) if bar 41 | 42 | Failure(:missing_bar) 43 | end 44 | end 45 | 46 | class FooOrBar < Micro::Case 47 | attributes :foo, :bar 48 | 49 | def call! 50 | return Success(:filled_foo_or_bar) if foo || bar 51 | 52 | Failure(:missing_foo_and_bar) 53 | end 54 | end 55 | 56 | class FooAndBar < Micro::Case 57 | attributes :foo, :bar 58 | 59 | def call! 60 | return Success(:filled_foo_and_bar) if foo && bar 61 | 62 | Failure(:missing_foo_or_bar) 63 | end 64 | end 65 | 66 | def test_the_calling_of_use_cases_and_flows 67 | map_use_cases1 = Micro::Cases.map([ 68 | Foo, Bar, FooOrBar, FooAndBar 69 | ]) 70 | 71 | results1 = map_use_cases1.call(foo: 'foo') 72 | 73 | assert_equal(results1.select(&:success?).map { |result| result.use_case.class }, [Foo, FooOrBar]) 74 | assert_equal(results1.select(&:failure?).map { |result| result.use_case.class }, [Bar, FooAndBar]) 75 | 76 | # -- 77 | 78 | flow1 = Micro::Cases.flow([Foo, Foo]) 79 | flow2 = Micro::Cases.flow([Foo, Bar]) 80 | 81 | map_use_cases2 = Micro::Cases.map([ 82 | flow1, flow2, FooOrBar, FooAndBar 83 | ]) 84 | 85 | results2 = map_use_cases2.call(foo: 'foo') 86 | 87 | assert_equal(results2.select(&:success?).map { |result| result.use_case.class }, [Foo, FooOrBar]) 88 | assert_equal(results2.select(&:failure?).map { |result| result.use_case.class }, [Bar, FooAndBar]) 89 | end 90 | 91 | def test_the_calling_with_dependency_injection 92 | map_use_cases = Micro::Cases.map([ 93 | Foo, FooOrBar, [FooAndBar, bar: 'bar'] 94 | ]) 95 | 96 | results = map_use_cases.call(foo: 'foo') 97 | 98 | assert(results.all?(&:success?)) 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /test/micro/case/strict/safe_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Micro::Case::Strict::SafeTest < Minitest::Test 4 | class Multiply < Micro::Case::Strict::Safe 5 | attributes :a, :b 6 | 7 | def call! 8 | if a.is_a?(Numeric) && b.is_a?(Numeric) 9 | Success result: { number: a * b } 10 | else 11 | Failure(:invalid_data) 12 | end 13 | end 14 | end 15 | 16 | class Double < Micro::Case::Strict::Safe 17 | attributes :number 18 | 19 | def call! 20 | return Multiply.call(a: number, b: number) if number > 0 21 | 22 | Failure result: { message: 'number must be greater than 0' } 23 | end 24 | end 25 | 26 | def test_class_call_method 27 | result = Double.call(number: 2) 28 | 29 | assert_success_result(result, value: { number: 4 }) 30 | 31 | result = Double.call(number: 0) 32 | 33 | assert_failure_result(result, type: :error, value: { message: 'number must be greater than 0' }) 34 | end 35 | 36 | class Foo < Micro::Case::Strict::Safe 37 | end 38 | 39 | def test_template_method 40 | assert_raises(NotImplementedError) { Micro::Case::Strict::Safe.call } 41 | 42 | assert_raises(NotImplementedError) { Foo.call } 43 | end 44 | 45 | class LoremIpsum < Micro::Case::Strict::Safe 46 | attributes :text 47 | 48 | def call! 49 | text 50 | end 51 | end 52 | 53 | def test_result_error 54 | assert_raises_with_message( 55 | Micro::Case::Error::UnexpectedResult, 56 | /LoremIpsum#call! must return an instance of Micro::Case::Result/ 57 | ) { LoremIpsum.call(text: 'lorem ipsum') } 58 | end 59 | 60 | def test_keywords_validation 61 | assert_raises_with_message(ArgumentError, 'missing keywords: :a, :b') { Multiply.call({}) } 62 | 63 | assert_raises_with_message(ArgumentError, 'missing keyword: :b') { Multiply.call({a: 1}) } 64 | 65 | assert_raises_with_message(ArgumentError, 'missing keyword: :number') { Double.call(a: 1) } 66 | end 67 | 68 | class Divide < Micro::Case::Strict::Safe 69 | attributes :a, :b 70 | 71 | def call! 72 | if a.is_a?(Integer) && b.is_a?(Integer) 73 | Success(result: a / b) 74 | else 75 | Failure(:not_an_integer) 76 | end 77 | end 78 | end 79 | 80 | def test_that_exceptions_generate_a_failure 81 | result_2 = Divide.call(a: 2, b: 0) 82 | 83 | assert_exception_result(result_2, value: { exception: ZeroDivisionError }) 84 | end 85 | 86 | def test_to_proc 87 | results = [ 88 | {a: 1, b: 2}, 89 | {a: 2, b: 2}, 90 | {a: 3, b: 2}, 91 | {a: 4, b: 2} 92 | ].map(&Multiply) 93 | 94 | values = results.map(&:value) 95 | 96 | assert_equal( 97 | [{number: 2}, {number: 4}, {number: 6}, {number: 8}], 98 | values 99 | ) 100 | end 101 | 102 | def test_inspect 103 | assert_equal( 104 | '', 105 | Multiply.inspect 106 | ) 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /examples/users_creation/01.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | gem 'activemodel', '~> 6.0' 7 | 8 | gem 'u-case', '~> 4.1.0' 9 | end 10 | 11 | Micro::Case.config do |config| 12 | # Use ActiveModel to auto-validate your use cases' attributes. 13 | config.enable_activemodel_validation = true 14 | 15 | # Use to enable/disable the `Micro::Case::Results#transitions` tracking. 16 | config.enable_transitions = true 17 | end 18 | 19 | module Users 20 | class Entity 21 | include Micro::Attributes.with(:initialize) 22 | 23 | attributes :id, :name, :email 24 | 25 | def persisted? 26 | !id.nil? 27 | end 28 | end 29 | end 30 | 31 | module Users::Creation 32 | require 'uri' 33 | require 'securerandom' 34 | 35 | class Process < Micro::Case 36 | attributes :name, :email 37 | 38 | def call! 39 | normalized_name = String(name).strip.gsub(/\s+/, ' ') 40 | normalized_email = String(email).downcase.strip 41 | 42 | validation_errors = [] 43 | validation_errors << "Name can't be blank" if normalized_name.blank? 44 | validation_errors << "Email is invalid" unless normalized_email.match?(URI::MailTo::EMAIL_REGEXP) 45 | 46 | if validation_errors.present? 47 | return Failure :invalid_attributes, result: { 48 | errors: OpenStruct.new(full_messages: validation_errors) 49 | } 50 | end 51 | 52 | user = Users::Entity.new( 53 | id: SecureRandom.uuid, 54 | name: normalized_name, 55 | email: normalized_email 56 | ) 57 | 58 | Success result: { user_id: user.id, crm_id: sync_with_crm } 59 | end 60 | 61 | private def sync_with_crm 62 | # Do some integration stuff... 63 | SecureRandom.uuid 64 | end 65 | end 66 | end 67 | 68 | params = { 69 | "name" => " Rodrigo \n Serradura ", 70 | "email" => " RoDRIGo.SERRAdura@gmail.com " 71 | } 72 | 73 | #---------------------------------# 74 | puts "\n-- Success scenario --\n\n" 75 | #---------------------------------# 76 | 77 | Users::Creation::Process 78 | .call(params) 79 | .on_success do |result| 80 | user_id, crm_id = result.values_at(:user_id, :crm_id) 81 | 82 | puts " CRM ID: #{crm_id}" 83 | puts "USER ID: #{user_id}" 84 | end 85 | 86 | #---------------------------------# 87 | puts "\n-- Failure scenario --\n\n" 88 | #---------------------------------# 89 | 90 | Users::Creation::Process 91 | .call(name: '', email: '') 92 | .on_failure { |(data, _type)| p data[:errors].full_messages } 93 | .on_failure do |_result, use_case| 94 | puts "#{use_case.class.name} was the use case responsible for the failure" 95 | end 96 | 97 | # :: example of the output: :: 98 | # 99 | # -- Success scenario -- 100 | # 101 | # CRM ID: f3f189f6-ba6a-40ab-998c-c86773c41c83 102 | # USER ID: c140ffc3-6a7c-4554-972a-c2a0d59f8cb1 103 | # 104 | # -- Failure scenarios -- 105 | # 106 | # ["Name can't be blank", "Email is invalid"] 107 | # Users::Creation::ValidateParams was the use case responsible for the failure 108 | -------------------------------------------------------------------------------- /test/micro/case/strict_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Micro::Case::StrictTest < Minitest::Test 4 | class Multiply < Micro::Case::Strict 5 | attributes :a, :b 6 | 7 | def call! 8 | if a.is_a?(Numeric) && b.is_a?(Numeric) 9 | Success result: { number: a * b } 10 | else 11 | Failure(:invalid_data) 12 | end 13 | end 14 | end 15 | 16 | class Double < Micro::Case::Strict 17 | attributes :number 18 | 19 | def call! 20 | return Multiply.call(a: number, b: number) if number > 0 21 | 22 | Failure result: { message: 'number must be greater than 0' } 23 | end 24 | end 25 | 26 | def test_class_call_method 27 | result = Double.call(number: 2) 28 | 29 | assert_success_result(result, value: { number: 4 }) 30 | 31 | result = Double.call(number: 0) 32 | 33 | assert_failure_result(result, type: :error, value: { message: 'number must be greater than 0' }) 34 | end 35 | 36 | class Foo < Micro::Case::Strict 37 | end 38 | 39 | def test_template_method 40 | assert_raises(NotImplementedError) { Micro::Case::Strict.call } 41 | 42 | assert_raises(NotImplementedError) { Foo.call } 43 | end 44 | 45 | class LoremIpsum < Micro::Case::Strict 46 | attributes :text 47 | 48 | def call! 49 | text 50 | end 51 | end 52 | 53 | def test_result_error 54 | assert_raises_with_message( 55 | Micro::Case::Error::UnexpectedResult, 56 | /LoremIpsum#call! must return an instance of Micro::Case::Result/ 57 | ) { LoremIpsum.call(text: 'lorem ipsum') } 58 | end 59 | 60 | def test_keywords_validation 61 | assert_raises_with_message(ArgumentError, 'missing keyword: :b') { Multiply.call(a: 1) } 62 | assert_raises_with_message(ArgumentError, 'missing keywords: :a, :b') { Multiply.call({}) } 63 | assert_raises_with_message(ArgumentError, 'missing keyword: :number') { Double.call({}) } 64 | end 65 | 66 | class Divide < Micro::Case::Strict 67 | attributes :a, :b 68 | 69 | def call! 70 | if a.is_a?(Integer) && b.is_a?(Integer) 71 | Success result: { number: a / b } 72 | else 73 | Failure(:not_an_integer) 74 | end 75 | rescue => e 76 | Failure result: e 77 | end 78 | end 79 | 80 | def test_the_exception_result_type 81 | result = Divide.call(a: 2, b: 0) 82 | 83 | assert_exception_result(result, value: { exception: ZeroDivisionError }) 84 | end 85 | 86 | def test_that_when_a_failure_result_is_a_symbol_both_type_and_value_will_be_the_same 87 | result = Divide.call(a: 2, b: 'a') 88 | 89 | assert_failure_result(result, type: :not_an_integer, value: { not_an_integer: true }) 90 | end 91 | 92 | def test_to_proc 93 | results = [ 94 | {a: 1, b: 2}, 95 | {a: 2, b: 2}, 96 | {a: 3, b: 2}, 97 | {a: 4, b: 2} 98 | ].map(&Multiply) 99 | 100 | values = results.map(&:value) 101 | 102 | assert_equal( 103 | [{number: 2}, {number: 4}, {number: 6}, {number: 8}], 104 | values 105 | ) 106 | end 107 | 108 | def test_inspect 109 | assert_equal( 110 | '', 111 | Multiply.inspect 112 | ) 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at rodrigo.serradura@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /test/micro/case/transaction/activerecord_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | if ENV.fetch('ACTIVERECORD_VERSION', '7') <= '6.1.0' 4 | require 'active_record' 5 | require 'sqlite3' 6 | 7 | ActiveRecord::Base.establish_connection( 8 | host: 'localhost', 9 | adapter: 'sqlite3', 10 | database: ':memory:' 11 | ) 12 | 13 | ActiveRecord::Schema.define do 14 | create_table :users do |t| 15 | t.column :name, :string 16 | end 17 | 18 | create_table :user_profiles do |t| 19 | t.column :info, :string 20 | 21 | t.integer :user_id, null: false, 22 | index: { name: 'index_user_profiles_on_user_id' }, 23 | foreign_key: { 24 | references: 'users', 25 | name: 'fk_test_9ad9d5a760', 26 | on_update: :no_action, 27 | on_delete: :no_action 28 | } 29 | end 30 | end 31 | 32 | class Micro::Case::TransactionActiverecordTest < Minitest::Test 33 | class User < ActiveRecord::Base 34 | has_one :user_profile 35 | end 36 | 37 | class UserProfile < ActiveRecord::Base 38 | belongs_to :user 39 | end 40 | 41 | class CreateUser < Micro::Case 42 | attribute :name, validates: { presence: true } 43 | 44 | def call! 45 | user = User.create(name: name) 46 | 47 | Success result: { user: user } 48 | end 49 | end 50 | 51 | class CreateUserProfile < Micro::Case 52 | attribute :user, validates: { kind: User } 53 | attribute :info, validates: { presence: true } 54 | 55 | def call! 56 | profile = UserProfile.create(info: info, user_id: user.id) 57 | 58 | Success result: { user: user, profile: profile } 59 | end 60 | end 61 | 62 | class CreateUserWithAProfile1 < Micro::Case 63 | def call! 64 | transaction { 65 | call(CreateUser) 66 | .then(CreateUserProfile) 67 | } 68 | end 69 | end 70 | 71 | class CreateUserWithAProfile2 < Micro::Case 72 | def call! 73 | transaction { 74 | call(CreateUser) 75 | }.then(CreateUserProfile) 76 | end 77 | end 78 | 79 | def teardown 80 | [UserProfile, User].each(&:delete_all) 81 | end 82 | 83 | def test_a_successful_result_after_a_db_transaction 84 | [CreateUserWithAProfile1, CreateUserWithAProfile2].each do |use_case| 85 | result = use_case.call(name: 'Serradura', info: 'Foo Bar...') 86 | 87 | assert_predicate(result, :success?) 88 | 89 | user, profile = result.values_at(:user, :profile) 90 | 91 | assert_predicate(user, :persisted?) 92 | assert_predicate(profile, :persisted?) 93 | 94 | assert_equal(user.id, profile.user_id) 95 | 96 | assert_equal('Serradura', user.name) 97 | assert_equal('Foo Bar...', profile.info) 98 | end 99 | end 100 | 101 | def test_a_failure_result_after_a_db_transaction 102 | result1 = CreateUserWithAProfile1.call(name: 'Serradura', info: '') 103 | 104 | assert_predicate(result1, :failure?) 105 | 106 | assert_equal(0, User.count) 107 | assert_equal(0, UserProfile.count) 108 | 109 | # -- 110 | 111 | result2 = CreateUserWithAProfile2.call(name: 'Rodrigo', info: '') 112 | 113 | assert_predicate(result2, :failure?) 114 | 115 | assert_equal(1, User.count) 116 | assert_equal(0, UserProfile.count) 117 | 118 | assert_equal('Rodrigo', User.first.name) 119 | end 120 | end 121 | 122 | end 123 | -------------------------------------------------------------------------------- /examples/users_creation/02b.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | gem 'activemodel', '~> 6.0' 7 | 8 | gem 'u-case', '~> 4.1.0' 9 | end 10 | 11 | Micro::Case.config do |config| 12 | # Use ActiveModel to auto-validate your use cases' attributes. 13 | config.enable_activemodel_validation = true 14 | 15 | # Use to enable/disable the `Micro::Case::Results#transitions` tracking. 16 | config.enable_transitions = true 17 | end 18 | 19 | module Users 20 | class Entity 21 | include Micro::Attributes.with(:initialize) 22 | 23 | attributes :id, :name, :email 24 | 25 | def persisted? 26 | !id.nil? 27 | end 28 | end 29 | end 30 | 31 | module Users::Creation 32 | require 'uri' 33 | require 'securerandom' 34 | 35 | class Process < Micro::Case 36 | attributes :name, :email 37 | 38 | def call! 39 | normalize_params 40 | .then(method(:validate_params)) 41 | .then(method(:persist)) 42 | .then(method(:sync_with_crm)) 43 | end 44 | 45 | private 46 | 47 | def normalize_params 48 | Success result: { 49 | name: String(name).strip.gsub(/\s+/, ' '), 50 | email: String(email).downcase.strip 51 | } 52 | end 53 | 54 | def validate_params(name:, email:) 55 | validation_errors = [] 56 | validation_errors << "Name can't be blank" if name.blank? 57 | validation_errors << "Email is invalid" unless email.match?(URI::MailTo::EMAIL_REGEXP) 58 | 59 | return Success() if validation_errors.blank? 60 | 61 | Failure :invalid_attributes, result: { 62 | errors: OpenStruct.new(full_messages: validation_errors) 63 | } 64 | end 65 | 66 | def persist(name:, email:, **) 67 | user_data = { name: name, email: email, id: SecureRandom.uuid } 68 | 69 | user = Users::Entity.new(user_data) 70 | 71 | Success result: { user: user } 72 | end 73 | 74 | def sync_with_crm(user:, **) 75 | if user.persisted? 76 | # Do some integration stuff... 77 | crm_id = SecureRandom.uuid 78 | 79 | Success result: { user_id: user.id, crm_id: crm_id } 80 | else 81 | Failure :crm_error, result: { message: "User can't be sent to the CRM" } 82 | end 83 | end 84 | end 85 | end 86 | 87 | params = { 88 | "name" => " Rodrigo \n Serradura ", 89 | "email" => " RoDRIGo.SERRAdura@gmail.com " 90 | } 91 | 92 | #---------------------------------# 93 | puts "\n-- Success scenario --\n\n" 94 | #---------------------------------# 95 | 96 | Users::Creation::Process 97 | .call(params) 98 | .on_success do |result| 99 | user_id, crm_id = result.values_at(:user_id, :crm_id) 100 | 101 | puts " CRM ID: #{crm_id}" 102 | puts "USER ID: #{user_id}" 103 | end 104 | 105 | #---------------------------------# 106 | puts "\n-- Failure scenario --\n\n" 107 | #---------------------------------# 108 | 109 | Users::Creation::Process 110 | .call(name: '', email: '') 111 | .on_failure { |(data, _type)| p data[:errors].full_messages } 112 | .on_failure do |_result, use_case| 113 | puts "#{use_case.class.name} was the use case responsible for the failure" 114 | end 115 | 116 | # :: example of the output: :: 117 | # 118 | # -- Success scenario -- 119 | # 120 | # CRM ID: f3f189f6-ba6a-40ab-998c-c86773c41c83 121 | # USER ID: c140ffc3-6a7c-4554-972a-c2a0d59f8cb1 122 | # 123 | # -- Failure scenarios -- 124 | # 125 | # ["Name can't be blank", "Email is invalid"] 126 | # Users::Creation::ValidateParams was the use case responsible for the failure 127 | -------------------------------------------------------------------------------- /examples/users_creation/03.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | gem 'activemodel', '~> 6.0' 7 | 8 | gem 'u-case', '~> 4.1.0' 9 | end 10 | 11 | Micro::Case.config do |config| 12 | # Use ActiveModel to auto-validate your use cases' attributes. 13 | config.enable_activemodel_validation = true 14 | 15 | # Use to enable/disable the `Micro::Case::Results#transitions` tracking. 16 | config.enable_transitions = true 17 | end 18 | 19 | module Users 20 | class Entity 21 | include Micro::Attributes.with(:initialize) 22 | 23 | attributes :id, :name, :email 24 | 25 | def persisted? 26 | !id.nil? 27 | end 28 | end 29 | end 30 | 31 | module Users::Creation 32 | class Persist < Micro::Case 33 | attributes :name, :email 34 | 35 | validates :name, :email, kind: String 36 | 37 | def call! 38 | user_data = attributes.merge(id: SecureRandom.uuid) 39 | 40 | Success result: { user: Users::Entity.new(user_data) } 41 | end 42 | end 43 | end 44 | 45 | module Users::Creation 46 | require 'uri' 47 | require 'securerandom' 48 | 49 | class Process < Micro::Case 50 | attributes :name, :email 51 | 52 | def call! 53 | normalize_params 54 | .then(method(:validate_params)) 55 | .then(Persist) 56 | .then(method(:sync_with_crm)) 57 | end 58 | 59 | private 60 | 61 | def normalize_params 62 | Success result: { 63 | name: String(name).strip.gsub(/\s+/, ' '), 64 | email: String(email).downcase.strip 65 | } 66 | end 67 | 68 | def validate_params(name:, email:) 69 | validation_errors = [] 70 | validation_errors << "Name can't be blank" if name.blank? 71 | validation_errors << "Email is invalid" unless email.match?(URI::MailTo::EMAIL_REGEXP) 72 | 73 | return Success() if validation_errors.blank? 74 | 75 | Failure :invalid_attributes, result: { 76 | errors: OpenStruct.new(full_messages: validation_errors) 77 | } 78 | end 79 | 80 | def sync_with_crm(user:, **) 81 | if user.persisted? 82 | # Do some integration stuff... 83 | crm_id = SecureRandom.uuid 84 | 85 | Success result: { user_id: user.id, crm_id: crm_id } 86 | else 87 | Failure :crm_error, result: { message: "User can't be sent to the CRM" } 88 | end 89 | end 90 | end 91 | end 92 | 93 | params = { 94 | "name" => " Rodrigo \n Serradura ", 95 | "email" => " RoDRIGo.SERRAdura@gmail.com " 96 | } 97 | 98 | #---------------------------------# 99 | puts "\n-- Success scenario --\n\n" 100 | #---------------------------------# 101 | 102 | Users::Creation::Process 103 | .call(params) 104 | .on_success do |result| 105 | user_id, crm_id = result.values_at(:user_id, :crm_id) 106 | 107 | puts " CRM ID: #{crm_id}" 108 | puts "USER ID: #{user_id}" 109 | end 110 | 111 | #---------------------------------# 112 | puts "\n-- Failure scenario --\n\n" 113 | #---------------------------------# 114 | 115 | Users::Creation::Process 116 | .call(name: '', email: '') 117 | .on_failure { |(data, _type)| p data[:errors].full_messages } 118 | .on_failure do |_result, use_case| 119 | puts "#{use_case.class.name} was the use case responsible for the failure" 120 | end 121 | 122 | # :: example of the output: :: 123 | # 124 | # -- Success scenario -- 125 | # 126 | # CRM ID: f3f189f6-ba6a-40ab-998c-c86773c41c83 127 | # USER ID: c140ffc3-6a7c-4554-972a-c2a0d59f8cb1 128 | # 129 | # -- Failure scenarios -- 130 | # 131 | # ["Name can't be blank", "Email is invalid"] 132 | # Users::Creation::ValidateParams was the use case responsible for the failure 133 | -------------------------------------------------------------------------------- /examples/users_creation/02a.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | gem 'activemodel', '~> 6.0' 7 | 8 | gem 'u-case', '~> 4.1.0' 9 | end 10 | 11 | Micro::Case.config do |config| 12 | # Use ActiveModel to auto-validate your use cases' attributes. 13 | config.enable_activemodel_validation = true 14 | 15 | # Use to enable/disable the `Micro::Case::Results#transitions` tracking. 16 | config.enable_transitions = true 17 | end 18 | 19 | module Users 20 | class Entity 21 | include Micro::Attributes.with(:initialize) 22 | 23 | attributes :id, :name, :email 24 | 25 | def persisted? 26 | !id.nil? 27 | end 28 | end 29 | end 30 | 31 | module Users::Creation 32 | require 'uri' 33 | require 'securerandom' 34 | 35 | class Process < Micro::Case 36 | attributes :name, :email 37 | 38 | def call! 39 | normalize_params 40 | .then(-> data { validate_params(data) }) 41 | .then(-> data { persist(data) }) 42 | .then(-> data { sync_with_crm(data) }) 43 | end 44 | 45 | private 46 | 47 | def normalize_params 48 | Success result: { 49 | name: String(name).strip.gsub(/\s+/, ' '), 50 | email: String(email).downcase.strip 51 | } 52 | end 53 | 54 | def validate_params(data) 55 | name, email = data.values_at(:name, :email) 56 | 57 | validation_errors = [] 58 | validation_errors << "Name can't be blank" if name.blank? 59 | validation_errors << "Email is invalid" unless email.match?(URI::MailTo::EMAIL_REGEXP) 60 | 61 | return Success() if validation_errors.blank? 62 | 63 | Failure :invalid_attributes, result: { 64 | errors: OpenStruct.new(full_messages: validation_errors) 65 | } 66 | end 67 | 68 | def persist(data) 69 | user_data = data.slice(:name, :email).merge(id: SecureRandom.uuid) 70 | 71 | user = Users::Entity.new(user_data) 72 | 73 | Success result: { user: user } 74 | end 75 | 76 | def sync_with_crm(data) 77 | user = data.fetch(:user) 78 | 79 | if user.persisted? 80 | # Do some integration stuff... 81 | crm_id = SecureRandom.uuid 82 | 83 | Success result: { user_id: user.id, crm_id: crm_id } 84 | else 85 | Failure :crm_error, result: { message: "User can't be sent to the CRM" } 86 | end 87 | end 88 | end 89 | end 90 | 91 | params = { 92 | "name" => " Rodrigo \n Serradura ", 93 | "email" => " RoDRIGo.SERRAdura@gmail.com " 94 | } 95 | 96 | #---------------------------------# 97 | puts "\n-- Success scenario --\n\n" 98 | #---------------------------------# 99 | 100 | Users::Creation::Process 101 | .call(params) 102 | .on_success do |result| 103 | user_id, crm_id = result.values_at(:user_id, :crm_id) 104 | 105 | puts " CRM ID: #{crm_id}" 106 | puts "USER ID: #{user_id}" 107 | end 108 | 109 | #---------------------------------# 110 | puts "\n-- Failure scenario --\n\n" 111 | #---------------------------------# 112 | 113 | Users::Creation::Process 114 | .call(name: '', email: '') 115 | .on_failure { |(data, _type)| p data[:errors].full_messages } 116 | .on_failure do |_result, use_case| 117 | puts "#{use_case.class.name} was the use case responsible for the failure" 118 | end 119 | 120 | # :: example of the output: :: 121 | # 122 | # -- Success scenario -- 123 | # 124 | # CRM ID: f3f189f6-ba6a-40ab-998c-c86773c41c83 125 | # USER ID: c140ffc3-6a7c-4554-972a-c2a0d59f8cb1 126 | # 127 | # -- Failure scenarios -- 128 | # 129 | # ["Name can't be blank", "Email is invalid"] 130 | # Users::Creation::ValidateParams was the use case responsible for the failure 131 | -------------------------------------------------------------------------------- /test/micro/case/safe_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class Micro::Case::SafeTest < Minitest::Test 4 | class Divide < Micro::Case::Safe 5 | attributes :a, :b 6 | 7 | def call! 8 | if a.is_a?(Integer) && b.is_a?(Integer) 9 | Success(result: { number: a / b }) 10 | else 11 | Failure(:not_an_integer) 12 | end 13 | end 14 | end 15 | 16 | def test_the_case_flow_builder 17 | assert_same(Micro::Cases::Safe::Flow, Divide.__flow_builder__) 18 | end 19 | 20 | def test_class_call_method 21 | result = Divide.call(a: 4, b: 2) 22 | 23 | assert_success_result(result, { number: 2 }) 24 | 25 | # --- 26 | 27 | result = Divide.call(a: 2.0, b: 2) 28 | 29 | assert_failure_result(result, type: :not_an_integer, value: { not_an_integer: true }) 30 | end 31 | 32 | class Foo < Micro::Case::Safe 33 | end 34 | 35 | def test_template_method 36 | assert_raises(NotImplementedError) { Micro::Case::Safe.call } 37 | 38 | assert_raises(NotImplementedError) { Foo.call } 39 | end 40 | 41 | class LoremIpsum < Micro::Case::Safe 42 | attributes :text 43 | 44 | def call! 45 | text 46 | end 47 | end 48 | 49 | def test_result_error 50 | assert_raises_with_message( 51 | Micro::Case::Error::UnexpectedResult, 52 | /LoremIpsum#call! must return an instance of Micro::Case::Result/ 53 | ) { LoremIpsum.call(text: 'lorem ipsum') } 54 | end 55 | 56 | def test_that_exceptions_generate_a_failure 57 | result = Divide.call(a: 2, b: 0) 58 | 59 | assert_exception_result(result, value: { exception: ZeroDivisionError }) 60 | end 61 | 62 | class Divide2ByArgV1 < Micro::Case::Safe 63 | attribute :arg 64 | 65 | def call! 66 | Success result: 2 / arg 67 | rescue => e 68 | Failure result: e 69 | end 70 | end 71 | 72 | class Divide2ByArgV2 < Micro::Case::Safe 73 | attribute :arg 74 | 75 | def call! 76 | Success(result: 2 / arg) 77 | rescue => e 78 | Failure result: e 79 | end 80 | end 81 | 82 | class Divide2ByArgV3 < Micro::Case::Safe 83 | attribute :arg 84 | 85 | def call! 86 | Success(result: 2 / arg) 87 | rescue => e 88 | Failure :foo, result: e 89 | end 90 | end 91 | 92 | class GenerateZeroDivisionError < Micro::Case::Safe 93 | attribute :arg 94 | 95 | def call! 96 | Failure(result: arg / 0) 97 | rescue => e 98 | Success(result: e) 99 | end 100 | end 101 | 102 | def test_the_rescue_of_an_exception_inside_of_a_safe_use_case 103 | [ 104 | Divide2ByArgV1.call(arg: 0), 105 | Divide2ByArgV2.call(arg: 0) 106 | ].each do |result| 107 | assert_exception_result(result, value: { exception: ZeroDivisionError }) 108 | end 109 | 110 | # --- 111 | 112 | result = Divide2ByArgV3.call(arg: 0) 113 | 114 | assert_exception_result(result, type: :foo, value: { exception: ZeroDivisionError }) 115 | 116 | # --- 117 | 118 | result = GenerateZeroDivisionError.call(arg: 2) 119 | assert_success_result(result) 120 | 121 | assert_kind_of(ZeroDivisionError, result.value[:exception]) 122 | end 123 | 124 | def test_that_when_a_failure_result_is_a_symbol_both_type_and_value_will_be_the_same 125 | result = Divide.call(a: 2, b: 'a') 126 | 127 | assert_failure_result(result, value: { not_an_integer: true }) 128 | end 129 | 130 | def test_to_proc 131 | results = [ 132 | {a: 2, b: 2}, 133 | {a: 4, b: 2}, 134 | {a: 6, b: 2}, 135 | {a: 8, b: 2} 136 | ].map(&Divide) 137 | 138 | values = results.map(&:value) 139 | 140 | assert_equal( 141 | [{number: 1}, {number: 2}, {number: 3}, {number: 4}], 142 | values 143 | ) 144 | end 145 | 146 | def test_inspect 147 | assert_equal( 148 | '', 149 | Divide.inspect 150 | ) 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /examples/users_creation/04a.rb: -------------------------------------------------------------------------------- 1 | require 'bundler/inline' 2 | 3 | gemfile do 4 | source 'https://rubygems.org' 5 | 6 | gem 'activemodel', '~> 6.0' 7 | 8 | gem 'u-case', '~> 4.1.0' 9 | end 10 | 11 | Micro::Case.config do |config| 12 | # Use ActiveModel to auto-validate your use cases' attributes. 13 | config.enable_activemodel_validation = true 14 | 15 | # Use to enable/disable the `Micro::Case::Results#transitions` tracking. 16 | config.enable_transitions = false 17 | end 18 | 19 | module Users 20 | class Entity 21 | include Micro::Attributes.with(:initialize) 22 | 23 | attributes :id, :name, :email 24 | 25 | def persisted? 26 | !id.nil? 27 | end 28 | end 29 | end 30 | 31 | module Users::Creation 32 | class NormalizeParams < Micro::Case 33 | attributes :name, :email 34 | 35 | def call! 36 | normalized_name = String(name).strip.gsub(/\s+/, ' ') 37 | normalized_email = String(email).downcase.strip 38 | 39 | Success result: { name: normalized_name, email: normalized_email } 40 | end 41 | end 42 | end 43 | 44 | module Users::Creation 45 | require 'uri' 46 | 47 | class ValidateParams < Micro::Case 48 | attributes :name, :email 49 | 50 | validates :name, presence: true 51 | validates :email, format: { with: URI::MailTo::EMAIL_REGEXP } 52 | 53 | def call! 54 | Success result: attributes(:name, :email) 55 | end 56 | end 57 | end 58 | 59 | require 'securerandom' 60 | 61 | module Users::Creation 62 | class Persist < Micro::Case 63 | attributes :name, :email 64 | 65 | validates :name, :email, kind: String 66 | 67 | def call! 68 | user_data = attributes.merge(id: SecureRandom.uuid) 69 | 70 | Success result: { user: Users::Entity.new(user_data) } 71 | end 72 | end 73 | end 74 | 75 | module Users::Creation 76 | class SyncWithCRM < Micro::Case 77 | attribute :user 78 | 79 | validates :user, kind: Users::Entity 80 | 81 | def call! 82 | if user.persisted? 83 | Success result: { user_id: user.id, crm_id: sync_with_crm } 84 | else 85 | Failure :crm_error, result: { message: "User can't be sent to the CRM" } 86 | end 87 | end 88 | 89 | private def sync_with_crm 90 | # Do some integration stuff... 91 | SecureRandom.uuid 92 | end 93 | end 94 | end 95 | 96 | module Users::Creation 97 | Process = Micro::Cases.flow([ 98 | NormalizeParams, 99 | ValidateParams, 100 | Persist, 101 | SyncWithCRM 102 | ]) 103 | end 104 | 105 | params = { 106 | "name" => " Rodrigo \n Serradura ", 107 | "email" => " RoDRIGo.SERRAdura@gmail.com " 108 | } 109 | 110 | #--------------------------------------# 111 | puts "\n-- Parameters processing --\n\n" 112 | #--------------------------------------# 113 | 114 | print 'Before: ' 115 | p params 116 | 117 | print ' After: ' 118 | 119 | Users::Creation::NormalizeParams 120 | .call(params) 121 | .on_success { |result| p result.data } 122 | 123 | #---------------------------------# 124 | puts "\n-- Success scenario --\n\n" 125 | #---------------------------------# 126 | 127 | Users::Creation::Process 128 | .call(params) 129 | .on_success do |result| 130 | user_id, crm_id = result.values_at(:user_id, :crm_id) 131 | 132 | puts " CRM ID: #{crm_id}" 133 | puts "USER ID: #{user_id}" 134 | end 135 | 136 | #---------------------------------# 137 | puts "\n-- Failure scenario --\n\n" 138 | #---------------------------------# 139 | 140 | Users::Creation::Process 141 | .call(name: '', email: '') 142 | .on_failure { |(data, _type)| p data[:errors].full_messages } 143 | .on_failure do |_result, use_case| 144 | puts "#{use_case.class.name} was the use case responsible for the failure" 145 | end 146 | 147 | # :: example of the output: :: 148 | # -- Parameters processing -- 149 | # 150 | # Before: {"name"=>" Rodrigo \n Serradura ", "email"=>" RoDRIGo.SERRAdura@gmail.com "} 151 | # After: {:name=>"Rodrigo Serradura", :email=>"rodrigo.serradura@gmail.com"} 152 | # 153 | # -- Success scenario -- 154 | # 155 | # CRM ID: f3f189f6-ba6a-40ab-998c-c86773c41c83 156 | # USER ID: c140ffc3-6a7c-4554-972a-c2a0d59f8cb1 157 | # 158 | # -- Failure scenarios -- 159 | # 160 | # ["Name can't be blank", "Email is invalid"] 161 | # Users::Creation::ValidateParams was the use case responsible for the failure 162 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'pry-byebug' 2 | 3 | if RUBY_VERSION >= '2.4.0' 4 | require 'simplecov' 5 | 6 | SimpleCov.start do 7 | add_filter '/test/' 8 | 9 | enable_coverage :branch if RUBY_VERSION >= '2.5.0' 10 | end 11 | end 12 | 13 | if ENV.fetch('ACTIVERECORD_VERSION', '7') < '4.1' 14 | require 'minitest/unit' 15 | 16 | module Minitest 17 | Test = MiniTest::Unit::TestCase 18 | end 19 | end 20 | 21 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 22 | 23 | require 'u-case' 24 | 25 | Micro::Case.config do |config| 26 | enable_activemodel = ENV.fetch('ACTIVERECORD_VERSION', '7') < '6.1.0' 27 | 28 | config.enable_activemodel_validation = enable_activemodel 29 | 30 | enable_transitions = ENV.fetch('ENABLE_TRANSITIONS', 'true') == 'true' 31 | 32 | config.enable_transitions = enable_transitions 33 | end 34 | 35 | require 'minitest/pride' 36 | require 'minitest/autorun' 37 | 38 | module MicroCaseAssertions 39 | def assert_raises_with_message(exception, msg, &block) 40 | block.call 41 | rescue exception => e 42 | assert_match(msg, e.message) 43 | else 44 | raise "Expected to raise #{exception} w/ message #{msg}, none raised" 45 | end 46 | 47 | def assert_kind_of_result(result) 48 | assert_kind_of(Micro::Case::Result, result) 49 | end 50 | 51 | # assert*result 52 | 53 | def assert_result(result, options) 54 | type = options[:type] || :____skip____ 55 | value = options[:value] || :____skip____ 56 | 57 | assert_kind_of_result(result) 58 | assert_predicate(result.data, :frozen?) 59 | assert_equal(type, result.type) if type != :____skip____ 60 | assert_equal(value, result.value) if value != :____skip____ 61 | end 62 | 63 | def assert_success_result(result, options = { type: :ok }) 64 | value = (block_given? ? yield : options[:value]) 65 | 66 | assert_result(result, options.merge(value: value)) if value 67 | 68 | assert_predicate(result, :success?) 69 | 70 | # assert the on_success hook 71 | count = 0 72 | result 73 | .on_failure { raise } # should never be called, because is a successful result. 74 | .on_success { count += 1 } 75 | .on_success(options[:type]) { count += 1 } 76 | 77 | assert_equal(2, count) 78 | end 79 | 80 | def assert_failure_result(result, options = {}) 81 | value = (block_given? ? yield : options[:value]) 82 | 83 | assert_result(result, options.merge(value: value)) 84 | 85 | assert_predicate(result, :failure?) 86 | 87 | # assert the on_failure hook 88 | 89 | count = 0 90 | result 91 | .on_success { raise } # should never be called, because is a failure result. 92 | .on_failure { count += 1 } 93 | .on_failure(options[:type]) { count += 1 } 94 | 95 | assert_equal(2, count) 96 | end 97 | 98 | def assert_exception_result(result, value: :____skip____, type: :exception) 99 | assert_kind_of_result(result) 100 | assert_equal(type, result.type) 101 | assert_kind_of(value[:exception], result.value[:exception]) if value != :____skip____ 102 | assert_predicate(result, :failure?) 103 | 104 | # assert the on_failure hook 105 | 106 | count = 0 107 | result 108 | .on_success { raise } # should never be called, because is a failure result. 109 | .on_failure { count += 1 } 110 | .on_failure(type) { count += 1 } 111 | .on_failure(:error) { raise } # will be avoided 112 | 113 | assert_equal(2, count) 114 | end 115 | 116 | # refute*result 117 | 118 | def refute_result(result, options) 119 | type = options[:type] || :____skip____ 120 | value = options[:value] || :____skip____ 121 | 122 | assert_kind_of_result(result) 123 | refure_equal(type, result.type) if type != :____skip____ 124 | refute_equal(value, result.value) if value != :____skip____ 125 | end 126 | 127 | def refute_success_result(result, options = {}) 128 | value = (block_given? ? yield : options[:value]) 129 | 130 | refute_result(result, options.merge(value: value)) 131 | refute_predicate(result, :success?) 132 | end 133 | 134 | def refute_failure_result(result, options = {}) 135 | value = (block_given? ? yield : options[:value]) 136 | 137 | refute_result(result, options.merge(value: value)) 138 | refute_predicate(result, :failure?) 139 | end 140 | end 141 | 142 | Minitest::Test.send(:include, MicroCaseAssertions) 143 | --------------------------------------------------------------------------------