├── .rspec
├── .travis.yml
├── lib
├── usecasing
│ ├── version.rb
│ ├── execution_order.rb
│ ├── base.rb
│ └── context.rb
└── usecasing.rb
├── Gemfile
├── Rakefile
├── .gitignore
├── spec
├── spec_helper.rb
├── execution_order_spec.rb
├── context_spec.rb
└── usecase_base_spec.rb
├── usecasing.gemspec
├── LICENSE.txt
└── README.md
/.rspec:
--------------------------------------------------------------------------------
1 | --color
2 | --format progress
3 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: ruby
2 | rvm:
3 | - 1.9.3
4 | - 2.1.0
5 |
--------------------------------------------------------------------------------
/lib/usecasing/version.rb:
--------------------------------------------------------------------------------
1 | module Usecasing
2 | VERSION = "0.1.10"
3 | end
4 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in usecasing.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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/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
--------------------------------------------------------------------------------
/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"