├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── usecasing.rb └── usecasing │ ├── base.rb │ ├── context.rb │ ├── execution_order.rb │ └── version.rb ├── spec ├── context_spec.rb ├── execution_order_spec.rb ├── spec_helper.rb └── usecase_base_spec.rb └── usecasing.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 2.1.0 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in usecasing.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Thiago Dantas 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UseCase your code 2 | 3 | 4 | ## Installation 5 | 6 | [![Build Status](https://secure.travis-ci.org/tdantas/usecasing.png)](http://travis-ci.org/tdantas/usecasing) 7 | [![Dependency Status](https://gemnasium.com/tdantas/usecasing.svg)](https://gemnasium.com/tdantas/usecasing) 8 | [![Coverage Status](https://coveralls.io/repos/tdantas/usecasing/badge.png)](https://coveralls.io/r/tdantas/usecasing) 9 | [![Gem Version](https://badge.fury.io/rb/usecasing.svg)](http://badge.fury.io/rb/usecasing) 10 | 11 | Add this line to your application's Gemfile: 12 | 13 | gem 'usecasing' 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | ### Usage 20 | 21 | Let's build a Invoice System, right ? 22 | So the product owner will create some usecases/stories to YOU. 23 | 24 | Imagine this usecase/story: 25 | 26 | ```` 27 | As a user I want to finalize an Invoice and an email should be delivered to the customer. 28 | ```` 29 | 30 | Let's build a controller 31 | 32 | ```` 33 | class InvoicesController < ApplicationController 34 | 35 | def finalize 36 | 37 | params[:current_user] = current_user 38 | # params = { invoice_id: 123 , current_user: # } 39 | context = FinalizeInvoiceUseCase.perform(params) 40 | 41 | if context.success? 42 | redirect_to invoices_path(context.invoice) 43 | else 44 | @errors = context.errors 45 | redirect_to invoices_path 46 | end 47 | 48 | end 49 | 50 | end 51 | ```` 52 | 53 | Ok, What is FinalizeInvoiceUseCase ? 54 | 55 | FinalizeInvoiceUseCase will be responsible for perform the Use Case/Story. 56 | Each usecase should satisfy the [Single Responsibility Principle](http://en.wikipedia.org/wiki/Single_responsibility_principle) and to achieve this principle, one usecase depends of others usecases building a Chain of Resposibility. 57 | 58 | 59 | ```` 60 | 61 | class FinalizeInvoiceUseCase < UseCase::Base 62 | depends FindInvoice, ValidateToFinalize, FinalizeInvoice, SendEmail 63 | end 64 | 65 | ```` 66 | 67 | IMHO, when I read this Chain I really know what this class will do. 68 | astute readers will ask: How FindInvoice pass values to ValidateToFinalize ? 69 | 70 | When we call in the Controller *FinalizeInvoiceUseCase.perform* we pass a parameter (Hash) to the usecase. 71 | 72 | This is what we call context, the usecase context will be shared between all chain. 73 | 74 | ```` 75 | class FindInvoice < UseCase::Base 76 | 77 | def before 78 | @user = context.current_user 79 | end 80 | 81 | def perform 82 | 83 | # we could do that in one before_filter 84 | invoice = @user.invoices.find(context.invoice_id) 85 | 86 | # assign to the context make available to all chain 87 | context.invoice = invoice 88 | 89 | end 90 | 91 | end 92 | ```` 93 | 94 | Is the invoice valid to be finalized ? 95 | 96 | ```` 97 | class ValidateToFinalize < UseCase::Base 98 | 99 | def perform 100 | #failure will stop the chain flow and mark the context as error. 101 | 102 | failure(:validate, "#{context.invoice.id} not ready to be finalized") unless valid? 103 | end 104 | 105 | private 106 | def valid? 107 | #contextual validation to finalize an invoice 108 | end 109 | end 110 | 111 | ```` 112 | 113 | So, after validate, we already know that the invoice exists and it is ready to be finalized. 114 | 115 | ```` 116 | class FinalizeInvoice < UseCase::Base 117 | 118 | def before 119 | @invoice = context.invoice 120 | end 121 | 122 | def perform 123 | @invoice.finalize! #update database with finalize state 124 | context.customer = invoice.customer 125 | end 126 | 127 | end 128 | ```` 129 | 130 | Oww, yeah, let's notify the customer 131 | 132 | ```` 133 | class SendEmail < UseCase::Base 134 | 135 | def perform 136 | to = context.customer.email 137 | 138 | # Call whatever service 139 | EmailService.send('customer_invoice_template', to, locals: { invoice: context.invoice } ) 140 | end 141 | 142 | end 143 | ```` 144 | 145 | #### Stopping the UseCase dependencies Flow 146 | 147 | There are 2 ways to stop the dependency flow. 148 | - stop! ( stop the flow without marking the usecase with error ) 149 | - failure ( stop the flow but mark the usecase with errors ) 150 | 151 | 152 | Imagine a Read Through Cache Strategy. 153 | How can we stop the usecase flow without marking as failure ? 154 | 155 | ```` 156 | class ReadThrough < UseCase::Base 157 | depends MemCacheReader, DataBaseReader, MemCacheWriter 158 | end 159 | 160 | class MemCacheReader < UseCase::Base 161 | def perform 162 | context.data = CacheAdapter.read('key') 163 | stop! if context.data 164 | end 165 | end 166 | 167 | class DataBaseReader < UseCase::Base 168 | def perform 169 | context.data = DataBase.find('key') 170 | end 171 | end 172 | 173 | class MemCacheWriter < UseCase::Base 174 | def perform 175 | CacheAdapter.write('key', context.data); 176 | end 177 | end 178 | 179 | ```` 180 | 181 | 182 | 183 | 184 | Let me know what do you think about it. 185 | 186 | 187 | #### UseCase::Base contract 188 | 189 | ```` 190 | # None of those methods are required. 191 | 192 | 193 | class BusinessRule < UseCase::Base 194 | 195 | def before 196 | # executed before perform 197 | end 198 | 199 | def perform 200 | # execute the responsibility that you want 201 | end 202 | 203 | def rollback 204 | # Will be called only on failure 205 | end 206 | 207 | end 208 | 209 | 210 | ```` 211 | 212 | 213 | 214 | 215 | #### TODO 216 | 217 | Create real case examples (40%) 218 | 219 | 220 | 221 | ## Contributing 222 | 223 | 1. Fork it 224 | 2. Create your feature branch (`git checkout -b my-new-feature`) 225 | 3. Commit your changes (`git commit -am 'Add some feature'`) 226 | 4. Push to the branch (`git push origin my-new-feature`) 227 | 5. Create new Pull Request 228 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | require 'rspec/core/rake_task' 4 | 5 | task :default => :test 6 | RSpec::Core::RakeTask.new(:test) 7 | -------------------------------------------------------------------------------- /lib/usecasing.rb: -------------------------------------------------------------------------------- 1 | require "usecasing/version" 2 | 3 | module UseCase 4 | autoload :Context, 'usecasing/context' 5 | autoload :Base, 'usecasing/base' 6 | autoload :ExecutionOrder, 'usecasing/execution_order' 7 | end 8 | -------------------------------------------------------------------------------- /lib/usecasing/base.rb: -------------------------------------------------------------------------------- 1 | module UseCase 2 | 3 | module BaseClassMethod 4 | 5 | def self.included(base) 6 | base.extend ClassMethods 7 | end 8 | 9 | module ClassMethods 10 | 11 | def depends(*deps) 12 | @dependencies ||= [] 13 | @dependencies.push(*deps) 14 | end 15 | 16 | def dependencies 17 | return [] unless superclass.ancestors.include? UseCase::Base 18 | value = (@dependencies && @dependencies.dup || []).concat(superclass.dependencies) 19 | value 20 | end 21 | 22 | def perform(ctx = { }) 23 | tx(ExecutionOrder.run(self), ctx) do |usecase, context| 24 | instance = usecase.new(context) 25 | instance.tap do | me | 26 | me.before 27 | me.perform 28 | end 29 | end 30 | end 31 | 32 | private 33 | 34 | def tx(execution_order, context) 35 | ctx = (context.is_a?(Context) ? context : Context.new(context)) 36 | executed = [] 37 | execution_order.each do |usecase| 38 | break if !ctx.success? || ctx.stopped? 39 | executed.push(usecase) 40 | yield usecase, ctx 41 | end 42 | rollback(executed, ctx) unless ctx.success? 43 | ctx 44 | end 45 | 46 | def rollback(execution_order, context) 47 | execution_order.each do |usecase| 48 | usecase.new(context).rollback 49 | end 50 | context 51 | end 52 | 53 | end #ClassMethods 54 | end #BaseClassMethod 55 | 56 | class Base 57 | 58 | include BaseClassMethod 59 | 60 | attr_reader :context 61 | def initialize(context) 62 | @context = context 63 | end 64 | 65 | def before; end 66 | def perform; end 67 | def rollback; end 68 | 69 | def stop! 70 | context.stop! 71 | end 72 | 73 | def failure(key, value) 74 | @context.failure(key, value) 75 | end 76 | 77 | end 78 | end -------------------------------------------------------------------------------- /lib/usecasing/context.rb: -------------------------------------------------------------------------------- 1 | module UseCase 2 | 3 | class Context 4 | 5 | class Errors 6 | 7 | def initialize 8 | @errors = Hash.new 9 | end 10 | 11 | def all(delimiter= ", ", &block) 12 | values = @errors.map {|key, value| value }.flatten 13 | if block_given? 14 | values.each &block 15 | else 16 | values.join(delimiter) 17 | end 18 | end 19 | 20 | def [](index) 21 | @errors[index.to_sym] 22 | end 23 | 24 | def push(key, value) 25 | @errors[key.to_sym] = [] unless @errors[key.to_sym] 26 | @errors[key.to_sym].push(value) 27 | end 28 | 29 | def empty? 30 | @errors.keys.empty? 31 | end 32 | 33 | def each(&block) 34 | @errors.each(&block) 35 | end 36 | 37 | end 38 | 39 | attr_accessor :errors 40 | 41 | def initialize(param = {}) 42 | raise ArgumentError.new('Must be a Hash or other Context') unless (param.is_a? ::Hash) || (param.is_a? Context) 43 | @values = symbolyze_keys(param.to_hash) 44 | @errors = Errors.new 45 | end 46 | 47 | def to_hash 48 | @values 49 | end 50 | 51 | def method_missing(method, *args, &block) 52 | return @values[extract_key_from(method)] = args.first if setter? method 53 | @values[method] 54 | end 55 | 56 | def respond_to?(method, include_all = false) 57 | @values.keys.include?(method.to_sym) 58 | end 59 | 60 | def success? 61 | @errors.empty? 62 | end 63 | 64 | def stop! 65 | @stopped = true 66 | end 67 | 68 | def stopped? 69 | !!@stopped 70 | end 71 | 72 | def failure(key, value) 73 | @errors.push(key, value) 74 | end 75 | 76 | private 77 | def setter?(method) 78 | !! ((method.to_s) =~ /=$/) 79 | end 80 | 81 | def extract_key_from(method) 82 | method.to_s[0..-2].to_sym 83 | end 84 | 85 | def symbolyze_keys(hash) 86 | hash.keys.reduce({ }) do |acc, key| 87 | acc[key.to_sym] = hash[key] 88 | acc 89 | end 90 | end 91 | 92 | end 93 | 94 | end -------------------------------------------------------------------------------- /lib/usecasing/execution_order.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | require 'tsort' 3 | 4 | module UseCase 5 | 6 | class ExecutionOrder 7 | 8 | def self.run(start_node) 9 | any_ciclic, ciclic = CyclicFinder.cyclic?(start_node) 10 | raise StandardError.new("cyclic detected: #{ciclic}") if any_ciclic 11 | post_order(start_node, []) 12 | end 13 | 14 | private 15 | def self.post_order(node, result) 16 | return result.push(node) if node.dependencies.empty? 17 | 18 | node.dependencies.each do |item| 19 | post_order(item, result) 20 | end 21 | 22 | result.push(node) 23 | end 24 | end 25 | 26 | class CyclicFinder 27 | include TSort 28 | 29 | def self.cyclic?(start_point) 30 | new(start_point).cyclic? 31 | end 32 | 33 | def initialize(start_point) 34 | @start_point = start_point 35 | @nodes = discover_nodes 36 | end 37 | 38 | 39 | def cyclic? 40 | components = strongly_connected_components 41 | result = components.any?{ |component| component.size != 1 } 42 | [ result, components.select{|component| component.size != 1 } ] 43 | end 44 | 45 | private 46 | 47 | def tsort_each_node(&block) 48 | @nodes.each &block 49 | end 50 | 51 | def tsort_each_child(node, &block) 52 | node.dependencies.each &block 53 | end 54 | 55 | def discover_nodes 56 | visited = {} 57 | stack = [@start_point] 58 | result = Set.new 59 | until stack.empty? 60 | node = stack.pop 61 | result.add node 62 | stack.push(*(node.dependencies)) if not visited[node] 63 | visited[node] = true 64 | end 65 | return result 66 | end 67 | 68 | end 69 | end -------------------------------------------------------------------------------- /lib/usecasing/version.rb: -------------------------------------------------------------------------------- 1 | module Usecasing 2 | VERSION = "0.1.10" 3 | end 4 | -------------------------------------------------------------------------------- /spec/context_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe UseCase::Context do 4 | 5 | 6 | it 'receives a hash and generate setters from key' do 7 | hash = {name: 'thiago', last: 'dantas', github: 'tdantas'} 8 | context = described_class.new(hash) 9 | expect(context.name).to eql(hash[:name]) 10 | expect(context.last).to eql(hash[:last]) 11 | expect(context.github).to eql(hash[:github]) 12 | end 13 | 14 | it 'initializes without parameters' do 15 | expect(described_class.new).to be_an(described_class) 16 | end 17 | 18 | it 'raises exception when argument is not a hash' do 19 | expect {described_class.new(Object.new)}.to raise_error(ArgumentError) 20 | end 21 | 22 | it 'assign new values' do 23 | context = described_class.new 24 | context.dog_name = 'mali' 25 | context.country = 'lisbon' 26 | context.age = 1 27 | 28 | expect(context.dog_name).to eql('mali') 29 | expect(context.country).to eql('lisbon') 30 | expect(context.age).to eql(1) 31 | end 32 | 33 | it 'handle hash with indifference' do 34 | hash = { "name" => 'thiago', last: 'dantas'} 35 | context = described_class.new(hash) 36 | expect(context.name).to eql('thiago') 37 | expect(context.last).to eql('dantas') 38 | end 39 | 40 | it 'is success when there is no error' do 41 | context = described_class.new({}) 42 | expect(context.success?).to eql(true) 43 | end 44 | 45 | it 'adds error messages to errors' do 46 | context = described_class.new({}) 47 | context.failure(:email, 'email already exist') 48 | expect(context.errors[:email]).to eql(['email already exist']) 49 | end 50 | 51 | it 'fails when exist errors' do 52 | context = described_class.new({}) 53 | context.failure(:email, 'email already exist') 54 | expect(context.success?).to eql(false) 55 | end 56 | 57 | it 'returns all messages indexed by key' do 58 | context = described_class.new({}) 59 | context.failure(:email, 'first') 60 | context.failure(:email, 'second') 61 | expect(context.errors[:email]).to include('first') 62 | expect(context.errors[:email]).to include('second') 63 | expect(context.errors[:email].join(",")).to eql("first,second") 64 | end 65 | 66 | it 'returns all messages indexed by key' do 67 | context = described_class.new({}) 68 | context.failure(:email, 'email') 69 | context.failure(:base, 'base') 70 | expect(context.errors.all("
")).to eql('email
base') 71 | end 72 | 73 | it 'returns all iterate over messages' do 74 | context = described_class.new({}) 75 | context.failure(:email, 'email') 76 | context.failure(:base, 'base') 77 | @expected = "" 78 | context.errors.all { |message| @expected.concat"
  • #{message}
  • " } 79 | expect(@expected).to eql('
  • email
  • base
  • ') 80 | end 81 | 82 | it 'returns a hash' do 83 | context = described_class.new({}) 84 | context.name = 'thiago' 85 | context.last_name = 'dantas' 86 | expect(context.to_hash).to eql({ name: 'thiago' , last_name: 'dantas'}) 87 | end 88 | 89 | it "iterates successfully over errors" do 90 | context = described_class.new({}) 91 | context.failure(:error_1, "this is the first error") 92 | context.failure(:error_2, "this is a second error") 93 | 94 | errors_keys = []; 95 | errors_values = []; 96 | context.errors.each do |k, v| 97 | errors_keys << k 98 | errors_values << v; 99 | end 100 | 101 | expect(errors_keys).to eql([:error_1, :error_2]) 102 | expect(errors_values).to eql([["this is the first error"], ["this is a second error"]]) 103 | end 104 | 105 | # https://github.com/tdantas/usecasing/issues/4 106 | it "does not mark failure when access key that does not exist" do 107 | ctx = described_class.new 108 | expect(ctx.success?).to be_true 109 | ctx[:key] 110 | expect(ctx.success?).to be_true 111 | end 112 | 113 | end 114 | 115 | -------------------------------------------------------------------------------- /spec/execution_order_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe UseCase::ExecutionOrder do 4 | 5 | context "with only one node" do 6 | it 'returns post order' do 7 | EOFirst = Class.new(UseCase::Base) 8 | expect(UseCase::ExecutionOrder.run(EOFirst)).to eql([EOFirst]) 9 | end 10 | end 11 | 12 | context "with two nodes" do 13 | it 'returns the dependency first' do 14 | EODepdency = Class.new(UseCase::Base) 15 | EODependent = Class.new(UseCase::Base) do 16 | depends EODepdency 17 | end 18 | 19 | expect(UseCase::ExecutionOrder.run(EODependent)).to eql([EODepdency, EODependent]) 20 | 21 | end 22 | end 23 | 24 | context 'with repeated nodes' do 25 | it 'returns duplicated nodes' do 26 | EORepeatedSMS = Class.new(UseCase::Base) 27 | 28 | EOAlert = Class.new(UseCase::Base) do 29 | depends EORepeatedSMS 30 | end 31 | 32 | EOCreate = Class.new(UseCase::Base) do 33 | depends EOAlert, EORepeatedSMS 34 | end 35 | 36 | expect(UseCase::ExecutionOrder.run(EOCreate)).to eql([EORepeatedSMS, EOAlert, EORepeatedSMS, EOCreate]) 37 | end 38 | end 39 | 40 | context 'context sharing' do 41 | it 'reads inner context values' do 42 | FirstUseCase = Class.new(UseCase::Base) do 43 | def perform 44 | SecondUseCase.perform(context) 45 | end 46 | end 47 | 48 | SecondUseCase = Class.new(UseCase::Base) do 49 | def perform 50 | context.second = 'The quick brown fox jumps over the lazy dog' 51 | end 52 | end 53 | 54 | expect(FirstUseCase.perform.second).to eq ( 55 | 'The quick brown fox jumps over the lazy dog') 56 | 57 | end 58 | end 59 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'usecasing' 2 | 3 | if ENV["TRAVIS"] 4 | require "coveralls" 5 | Coveralls.wear! 6 | end 7 | 8 | 9 | RSpec.configure do |config| 10 | config.treat_symbols_as_metadata_keys_with_true_values = true 11 | config.run_all_when_everything_filtered = true 12 | config.filter_run :focus 13 | config.mock_framework = :mocha 14 | 15 | config.order = 'random' 16 | end 17 | -------------------------------------------------------------------------------- /spec/usecase_base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe UseCase::Base do 4 | 5 | context "depends" do 6 | 7 | it 'initialize without any dependency' do 8 | AppUseCaseInitialize = Class.new(UseCase::Base) 9 | expect(AppUseCaseInitialize.dependencies).to be_empty 10 | end 11 | 12 | it "adds usecase dependency" do 13 | AppOtherUseCase = Class.new 14 | AppUseCase = Class.new(UseCase::Base) do 15 | depends AppOtherUseCase 16 | end 17 | 18 | expect(AppUseCase.dependencies).to eql([AppOtherUseCase]) 19 | end 20 | 21 | 22 | it 'subclass adds dependency from subclass to superclass' do 23 | 24 | SuperClassDependency = Class.new(UseCase::Base) 25 | UseCaseSuperClass = Class.new(UseCase::Base) do 26 | depends SuperClassDependency 27 | end 28 | 29 | SubClassDependency = Class.new(UseCase::Base) 30 | UseCaseSubClass = Class.new(UseCaseSuperClass) do 31 | depends SubClassDependency 32 | end 33 | 34 | expect(UseCaseSubClass.dependencies).to eql([SubClassDependency, SuperClassDependency]) 35 | #idempotent operation 36 | expect(UseCaseSubClass.dependencies).to eql([SubClassDependency, SuperClassDependency]) 37 | 38 | end 39 | 40 | 41 | end 42 | 43 | 44 | context '##perform' do 45 | 46 | it 'call instance #perform method' do 47 | AppUseCaseInstance = Class.new(UseCase::Base) do 48 | def perform 49 | #some business rule here 50 | end 51 | end 52 | AppUseCaseInstance.any_instance.expects(:perform).once 53 | AppUseCaseInstance.perform 54 | end 55 | 56 | it 'receives a hash and create a execution context' do 57 | 58 | SendEmailUseCase = Class.new(UseCase::Base) do 59 | def perform 60 | context.sent = 'sent' 61 | end 62 | end 63 | 64 | ctx = SendEmailUseCase.perform({email: 'thiago.teixeira.dantas@gmail.com' }) 65 | expect(ctx.sent).to eql('sent') 66 | expect(ctx.email).to eql('thiago.teixeira.dantas@gmail.com') 67 | end 68 | 69 | it 'raises exception when params is neither context or a hash' do 70 | UseCaseArgumentException = Class.new(UseCase::Base) 71 | expect{ UseCaseArgumentException.perform(Object.new) }.to raise_error(ArgumentError) 72 | end 73 | 74 | it 'accepts a hash' do 75 | UseCaseArgumentHash = Class.new(UseCase::Base) 76 | expect{ UseCaseArgumentHash.perform({}) }.not_to raise_error 77 | end 78 | 79 | it 'accepts other context' do 80 | UseCaseArgumentContext = Class.new(UseCase::Base) 81 | expect{ UseCaseArgumentContext.perform(UseCase::Context.new) }.not_to raise_error 82 | end 83 | 84 | it 'with success when usecase do not register failure' do 85 | pending 86 | end 87 | 88 | it 'fail when usecase register failure' do 89 | pending 90 | end 91 | 92 | 93 | it 'detects cyclic' do 94 | 95 | CyclicFirst = Class.new(UseCase::Base) 96 | CyclicSecond = Class.new(UseCase::Base) do 97 | depends CyclicFirst 98 | end 99 | 100 | CyclicFirst.instance_eval do 101 | depends CyclicSecond 102 | end 103 | 104 | FinalUseCase = Class.new(UseCase::Base) do 105 | depends CyclicSecond 106 | end 107 | 108 | expect { FinalUseCase.perform }.to raise_error(StandardError, /cyclic detected/) 109 | 110 | end 111 | 112 | end 113 | 114 | context '##perfoms execution chain' do 115 | it 'executes in lexical order cascading context among usecases' do 116 | 117 | FirstUseCase = Class.new(UseCase::Base) do 118 | def perform 119 | context.first = context.first_arg 120 | end 121 | end 122 | 123 | SecondUseCase = Class.new(UseCase::Base) do 124 | def perform 125 | context.second = context.second_arg 126 | end 127 | end 128 | 129 | UseCaseChain = Class.new(UseCase::Base) do 130 | depends FirstUseCase, SecondUseCase 131 | end 132 | 133 | ctx = UseCaseChain.perform({:first_arg => 1, :second_arg => 2}) 134 | expect(ctx.first).to eql(1) 135 | expect(ctx.second).to eql(2) 136 | end 137 | 138 | 139 | it 'stops the flow when failure happen' do 140 | 141 | FirstUseCaseFailure = Class.new(UseCase::Base) do 142 | def perform 143 | context.first = context.first_arg 144 | end 145 | end 146 | 147 | SecondUseCaseFailure = Class.new(UseCase::Base) do 148 | 149 | def perform 150 | context.second = context.second_arg 151 | failure(:second, 'next will not be called') 152 | end 153 | 154 | end 155 | 156 | ThirdUseCaseFailure = Class.new(UseCase::Base) do 157 | def perform 158 | context.third = true 159 | end 160 | end 161 | 162 | UseCaseFailure = Class.new(UseCase::Base) do 163 | depends FirstUseCaseFailure, SecondUseCaseFailure, ThirdUseCaseFailure 164 | end 165 | 166 | ThirdUseCaseFailure.any_instance.expects(:perform).never 167 | UseCaseFailure.perform 168 | 169 | end 170 | end 171 | 172 | context '#perform' do 173 | it 'receive an Context instance' do 174 | InstanceUseCase = Class.new(UseCase::Base) do 175 | def perform 176 | context.executed = true 177 | end 178 | end 179 | ctx = UseCase::Context.new 180 | InstanceUseCase.new(ctx).perform 181 | expect(ctx.executed).to be_true 182 | end 183 | end 184 | 185 | 186 | context 'rolling back the flow' do 187 | 188 | it 'rollback without dependencies' do 189 | 190 | UseCaseWithRollback = Class.new(UseCase::Base) do 191 | 192 | def perform 193 | failure(:rollback, 'must be called') 194 | end 195 | 196 | def rollback 197 | context.rollback_executed = 'true' 198 | end 199 | 200 | end 201 | 202 | context = UseCaseWithRollback.perform 203 | expect(context.rollback_executed).to eql('true') 204 | 205 | end 206 | 207 | it 'in reverse order of execution' do 208 | order = 0 209 | 210 | UseCaseWithRollbackOrderDepdens = Class.new(UseCase::Base) do 211 | define_method :rollback do 212 | order += 1 213 | context.first_rollback = order 214 | end 215 | 216 | end 217 | 218 | UseCaseWithRollbackOrder = Class.new(UseCase::Base) do 219 | depends UseCaseWithRollbackOrderDepdens 220 | 221 | def perform 222 | failure(:rollback, 'error') 223 | end 224 | 225 | define_method :rollback do 226 | order += 1 227 | context.second_rollback = order 228 | end 229 | end 230 | 231 | context = UseCaseWithRollbackOrder.perform 232 | expect(context.first_rollback).to eql(1) 233 | expect(context.second_rollback).to eql(2) 234 | end 235 | 236 | 237 | it 'only rollbacks usecase that ran' do 238 | 239 | UseCaseFailRanThird = Class.new(UseCase::Base) do 240 | 241 | def rollback 242 | context.rollback_third = 'true' 243 | end 244 | 245 | end 246 | 247 | UseCaseFailRanSecond = Class.new(UseCase::Base) do 248 | 249 | def rollback 250 | context.rollback_second = 'true_2' 251 | end 252 | 253 | end 254 | 255 | UseCaseFailRanFirst = Class.new(UseCase::Base) do 256 | depends UseCaseFailRanSecond 257 | 258 | def perform 259 | failure(:rollback, 'error') 260 | end 261 | 262 | def rollback 263 | context.rollback_first = 'true_1' 264 | end 265 | 266 | end 267 | 268 | UseCaseFailRan = Class.new(UseCase::Base) do 269 | depends UseCaseFailRanFirst, UseCaseFailRanThird 270 | end 271 | 272 | context = UseCaseFailRan.perform 273 | expect(context.rollback_third).to_not be 274 | expect(context.rollback_second).to be 275 | expect(context.rollback_first).to be 276 | 277 | end 278 | 279 | end 280 | 281 | context 'stopping the flow' do 282 | 283 | FirstCase = Class.new(UseCase::Base) do 284 | def perform 285 | context.wizzard_name = "Gandalf" 286 | end 287 | end 288 | 289 | StopCase = Class.new(UseCase::Base) do 290 | def perform 291 | context.result = "YOUUUU SHHHAAALLLL NOOOTTTT PASSSSSS!" 292 | stop! 293 | end 294 | end 295 | 296 | UnachievableCase = Class.new(UseCase::Base) do 297 | def perform 298 | context.result = "Still here! Muahaha!" 299 | end 300 | end 301 | 302 | Base = Class.new(UseCase::Base) do 303 | depends FirstCase, StopCase, UnachievableCase 304 | end 305 | 306 | let(:subject) { Base.perform } 307 | 308 | it 'returns variables inserted by first dependency' do 309 | expect(subject.wizzard_name).to eq("Gandalf") 310 | end 311 | 312 | it 'does not have variables inserted by unachievable case' do 313 | expect(subject.result).to eq("YOUUUU SHHHAAALLLL NOOOTTTT PASSSSSS!") 314 | end 315 | 316 | it 'is successfull' do 317 | expect(subject.success?).to be_true 318 | end 319 | end 320 | 321 | 322 | describe '#before' do 323 | 324 | it 'calls "before" method before perform' do 325 | BeforeUsecasing = Class.new(UseCase::Base) do 326 | 327 | def before 328 | context.before = true 329 | end 330 | 331 | def perform 332 | raise 'Should be called' unless context.before 333 | end 334 | end 335 | 336 | expected = BeforeUsecasing.perform 337 | expect(expected.before).to eql(true) 338 | 339 | end 340 | 341 | end 342 | 343 | end -------------------------------------------------------------------------------- /usecasing.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'usecasing/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "usecasing" 8 | gem.version = Usecasing::VERSION 9 | gem.authors = ["Thiago Dantas"] 10 | gem.email = ["thiago.teixeira.dantas@gmail.com"] 11 | gem.description = %q{UseCase Driven approach to your code} 12 | gem.summary = %q{UseCase Driven Approach} 13 | gem.homepage = "" 14 | 15 | gem.files = `git ls-files`.split($/) 16 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 17 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 18 | gem.require_paths = ["lib"] 19 | 20 | 21 | #development dependecy 22 | gem.add_development_dependency "rspec",'~> 2.14.1' 23 | gem.add_development_dependency "rake", '~> 10.1' 24 | gem.add_development_dependency "mocha",'~> 1.0.0' 25 | gem.add_development_dependency "coveralls", '~> 0.7' 26 | 27 | end 28 | --------------------------------------------------------------------------------