├── .gitignore ├── .rspec ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── business_process.gemspec ├── lib ├── business_process.rb └── business_process │ ├── base.rb │ └── version.rb └── spec ├── business_process └── base_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | # Because this is a gem, ignore Gemfile.lock: 2 | 3 | Gemfile.lock 4 | 5 | # And because this is Ruby, ignore the following 6 | # (source: https://github.com/github/gitignore/blob/master/Ruby.gitignore): 7 | 8 | *.gem 9 | *.rbc 10 | .bundle 11 | .config 12 | coverage 13 | InstalledFiles 14 | lib/bundler/man 15 | pkg 16 | rdoc 17 | spec/reports 18 | test/tmp 19 | test/version_tmp 20 | tmp 21 | 22 | # YARD artifacts 23 | .yardoc 24 | _yardoc 25 | doc/ 26 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT LICENSE 2 | 3 | Copyright (c) stevo 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ⚠️ ATTENTION ⚠️ 2 | 3 | BusinessProcess gem is no longer supported, as it has been replaced by [rails-patterns](https://github.com/Selleo/pattern) gem, which includes more light-weight version of Service Object pattern 4 | 5 | # BusinessProcess 6 | 7 | ServiceObject'a'like pattern 8 | 9 | #### Setup 10 | 11 | ```ruby 12 | # Gemfile 13 | 14 | gem 'business_process' 15 | ``` 16 | 17 | then `bundle` 18 | 19 | #### Usage 20 | 21 | ```ruby 22 | # Define business process 23 | 24 | class DoSomething < BusinessProcess::Base 25 | # Specify requirements 26 | needs :some_method 27 | needs :some_other_method 28 | 29 | # Specify process (action) 30 | def call 31 | some_method + some_other_method 32 | end 33 | end 34 | 35 | # Execute using named parameters 36 | DoSomething.call(some_method: 10, some_other_method: 20) 37 | 38 | # Execute using parameter object 39 | class Something 40 | def some_method 41 | 10 42 | end 43 | 44 | def some_other_method 45 | 20 46 | end 47 | end 48 | 49 | DoSomething.call(Something.new) 50 | 51 | # Read result of execution 52 | service = DoSomething.call(Something.new) 53 | service.result # => 30 54 | service.success? # => true 55 | ``` 56 | 57 | #### Failing steps 58 | 59 | To indicate failure of business process, use #fail method. Whatever you pass into the method, will be available through #error attribute on the business process object. If you pass a class that inherits from `Exception` it will also be raised. 60 | 61 | ```ruby 62 | # Define business process 63 | 64 | class DoSomething < BusinessProcess::Base 65 | # Specify requirements 66 | needs :some_method 67 | needs :some_other_method 68 | 69 | # Specify process (action) 70 | def call 71 | do_something 72 | do_something_else 73 | end 74 | 75 | private 76 | 77 | def do_something 78 | fail(:too_low) if some_method < 10 79 | end 80 | 81 | def do_something_else 82 | some_other_method + 20 83 | end 84 | end 85 | 86 | # Execute using named parameters 87 | DoSomething.call(some_method: 5, some_other_method: 20) 88 | 89 | 90 | # Read result of execution 91 | service = DoSomething.call(Something.new) 92 | service.result # => 25 93 | service.success? # => false 94 | service.error # => :too_low 95 | ``` 96 | 97 | 98 | #### Process definition using .steps 99 | 100 | ```ruby 101 | # Define business process 102 | 103 | class DoSomething < BusinessProcess::Base 104 | # Specify requirements 105 | needs :some_method 106 | needs :some_other_method 107 | 108 | # Specify steps 109 | steps :do_something, 110 | :do_something_else 111 | 112 | private 113 | 114 | def do_something 115 | @some_result = some_method + 10 116 | end 117 | 118 | def do_something_else 119 | some_other_method * 20 + @some_result 120 | end 121 | end 122 | ``` 123 | 124 | #### Process definition using .steps and calling related service object 125 | 126 | This is useful when some business processes can be composed of other business processes. Remember, that caller business process should provide `needs` for the callee business process. 127 | 128 | ```ruby 129 | # Define business process 130 | 131 | class DoSomething < BusinessProcess::Base 132 | # Specify requirements 133 | needs :some_method 134 | needs :some_other_method 135 | 136 | # Specify steps 137 | steps :do_something, 138 | :do_something_else 139 | 140 | private 141 | 142 | def do_something 143 | @some_result = some_method + 10 144 | end 145 | end 146 | 147 | class DoSomethingElse < BusinessProcess::Base 148 | needs :some_other_method 149 | 150 | steps :do_something_fancy 151 | 152 | private 153 | 154 | def do_something_fancy 155 | 100.times { puts "#{some_other_method} is fancy!" } 156 | end 157 | end 158 | ``` 159 | 160 | #### Running specs 161 | 162 | ``` 163 | rspec spec 164 | ``` 165 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | 4 | Rake::TestTask.new do |t| 5 | t.libs << 'lib' 6 | t.pattern = 'test/**/*_test.rb' 7 | t.verbose = false 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /business_process.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | $:.unshift File.expand_path('../lib', __FILE__) 4 | require 'business_process/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "business_process" 8 | gem.version = BusinessProcess::VERSION 9 | gem.authors = ["stevo"] 10 | gem.email = ["blazejek@gmail.com"] 11 | gem.homepage = "https://github.com/Selleo/business_process" 12 | gem.summary = "General purpose service object abstraction" 13 | gem.description = "General purpose service object abstraction" 14 | gem.license = "MIT" 15 | 16 | gem.files = `git ls-files app lib`.split("\n") 17 | gem.platform = Gem::Platform::RUBY 18 | gem.require_paths = ['lib'] 19 | gem.rubyforge_project = '[none]' 20 | 21 | gem.add_development_dependency "rspec" 22 | end 23 | -------------------------------------------------------------------------------- /lib/business_process.rb: -------------------------------------------------------------------------------- 1 | require 'business_process/base' 2 | -------------------------------------------------------------------------------- /lib/business_process/base.rb: -------------------------------------------------------------------------------- 1 | module BusinessProcess 2 | class Base 3 | class_attribute :steps_queue, :requirements 4 | attr_accessor :result 5 | attr_reader :parameter_object, :error 6 | private :result=, :parameter_object 7 | 8 | def self.call(parameter_object) 9 | new(parameter_object).tap do |business_process| 10 | business_process.instance_eval do 11 | self.result = call 12 | @success = !!self.result unless @success == false 13 | end 14 | end 15 | end 16 | 17 | def self.needs(field) 18 | self.requirements ||= [] 19 | self.requirements << field 20 | 21 | define_method field do 22 | if parameter_object.is_a?(Hash) && parameter_object.has_key?(field) 23 | parameter_object[field] 24 | elsif parameter_object.respond_to?(field) 25 | parameter_object.public_send(field) 26 | else 27 | raise NoMethodError, "Missing method: #{field.inspect} for the parameter object called for class: #{self.class.name}" 28 | end 29 | end 30 | end 31 | 32 | def initialize(parameter_object) 33 | @parameter_object = parameter_object 34 | end 35 | 36 | # Defaults to the boolean'ed result of "call" 37 | def success? 38 | @success 39 | end 40 | 41 | def fail(error = nil) 42 | @error = error 43 | @success = false 44 | raise error if error.is_a?(Class) && (error < Exception) 45 | end 46 | 47 | def self.steps(*step_names) 48 | self.steps_queue = step_names 49 | end 50 | 51 | def call 52 | if steps.present? 53 | process_steps 54 | else 55 | raise NoMethodError, "Called undefined #call. You need either define steps or implement the #call method in the class: #{self.class.name}" 56 | end 57 | end 58 | 59 | private 60 | 61 | def process_steps 62 | _result = nil 63 | steps.map(&:to_s).each do |step_name| 64 | _result = process_step(step_name) 65 | return _result if @success == false 66 | end 67 | _result 68 | end 69 | 70 | def process_step(step_name) 71 | if respond_to?(step_name, true) 72 | send(step_name) 73 | else 74 | begin 75 | step_class = step_name.camelize.constantize 76 | step_class.call(self).result 77 | rescue NameError => exc 78 | if step_name.starts_with?('return_') and respond_to?(step_name.sub('return_', ''), true) 79 | send(step_name.sub('return_', '')) 80 | else 81 | raise NoMethodError, "Cannot find step implementation for <#{step_name}>. Step should be either a private instance method of #{self.class.name} or camel_case'd name of another business process class.\n Original exception: #{exc.message}" 82 | end 83 | end 84 | end 85 | end 86 | 87 | def steps 88 | self.class.steps_queue || [] 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/business_process/version.rb: -------------------------------------------------------------------------------- 1 | module BusinessProcess 2 | VERSION = "1.0.4" 3 | end 4 | -------------------------------------------------------------------------------- /spec/business_process/base_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'business_process/base' 3 | 4 | class BusinessProcessSubclassFactory 5 | def self.get(*requirements) 6 | Class.new(BusinessProcess::Base).tap do |klass| 7 | requirements.each { |requirement| klass.needs requirement } 8 | klass.send(:define_method, :call, -> { requirements.map { |requirement| self.send(requirement) } }) 9 | end 10 | end 11 | end 12 | 13 | describe BusinessProcess::Base do 14 | let(:klass) { BusinessProcessSubclassFactory.get(*requirements) } 15 | let(:requirements) { [] } 16 | let(:attribute_object) { double } 17 | 18 | describe '.call' do 19 | let(:perform) { klass.call(attribute_object) } 20 | 21 | it { expect(perform).to be_kind_of BusinessProcess::Base } 22 | 23 | context 'parameter is required' do 24 | let(:requirements) { [:some_method] } 25 | 26 | context 'attribute object does not provide method' do 27 | it { expect { perform }.to raise_error NoMethodError } 28 | end 29 | 30 | context 'attribute object does provide method' do 31 | let(:attribute_object) { double(some_method: 10) } 32 | 33 | it { expect { perform }.not_to raise_error } 34 | end 35 | 36 | context 'attribute object is a hash' do 37 | let(:attribute_object) { {} } 38 | 39 | context 'attribute object does not provide parameter key' do 40 | it { expect { perform }.to raise_error NoMethodError } 41 | end 42 | 43 | context 'attribute object does provide parameter key' do 44 | let(:attribute_object) { {some_method: 10} } 45 | 46 | it { expect { perform }.not_to raise_error } 47 | end 48 | end 49 | end 50 | end 51 | 52 | describe '#result' do 53 | let!(:instance) { klass.call(attribute_object) } 54 | 55 | context 'some attributes are provided' do 56 | let(:requirements) { [:some_method, :some_other_method] } 57 | let(:attribute_object) { double(some_method: 10, some_other_method: 20) } 58 | 59 | it 'stores result of execution of #call method in result accessor' do 60 | expect(instance.result).to eq [10, 20] 61 | end 62 | 63 | context 'attribute object is a hash' do 64 | let(:attribute_object) { {some_method: 10, some_other_method: 20} } 65 | 66 | it 'stores result of execution of #call method in result accessor' do 67 | expect(instance.result).to eq [10, 20] 68 | end 69 | end 70 | end 71 | end 72 | 73 | describe '#success!' do 74 | let(:instance) { klass.call(double) } 75 | 76 | context '#call returns false' do 77 | before { klass.send(:define_method, :call, -> { false }) } 78 | 79 | it { expect(instance.success?).to be_falsey } 80 | end 81 | 82 | context '#call returns true' do 83 | before { klass.send(:define_method, :call, -> { true }) } 84 | 85 | it { expect(instance.success?).to be_truthy } 86 | end 87 | end 88 | 89 | 90 | describe '#valid?' do 91 | let(:instance) { klass.new(attribute_object) } 92 | 93 | context 'parameter is required' do 94 | let(:requirements) { [:some_method] } 95 | 96 | context 'attribute object does not provide method' do 97 | it { expect(instance.valid?).to be_falsey } 98 | end 99 | 100 | context 'attribute object does provide method' do 101 | let(:attribute_object) { double(some_method: 10) } 102 | 103 | it { expect(instance.valid?).to be_truthy } 104 | end 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | end 3 | --------------------------------------------------------------------------------