business_flow 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d9c94efd239befaaff61cf57d19cb6b275435d02
4
- data.tar.gz: 944f1a4075af4781ea1ee873366c4900e5e26241
3
+ metadata.gz: 1a7e45240693906221c1e55e093c0588274746f0
4
+ data.tar.gz: 4b2191aaf8e84752f4183f9ea89ce5fe1299ddda
5
5
  SHA512:
6
- metadata.gz: 2fc9b094b2677ed5e13ed1b17c58d07de5340bef3608540f6171783656d238a54346988361dd96bf94665b9730941edffd247d06c9fd9695355cb6822df9e01a
7
- data.tar.gz: a3a9e27a570b2e14669521de739982abb05558723c7b820a22c1d60ccbbd76b6e4a5e8fb589cf08e6f224b5ce04d9d5322b2f1fa1d10cc9689c410b58cf4fd5c
6
+ metadata.gz: 2907e0e9d8139972e810c667ce65a6dedbc4e95a5a069e16f40a59ba12dd1c808cfb77fe572089375b92a3c22c94bebe42f9dc8dfadea480938410049c092c22
7
+ data.tar.gz: aea9b30bd764f0ae977381ab77cf1b5d1c1d91e873ff8d6fe7ed57657d566d2bc5f94e68641fbcc84a09a1c5bf90cab447bca74e85950af147a7b99eb577732e
data/Gemfile.lock CHANGED
@@ -3,6 +3,7 @@ PATH
3
3
  specs:
4
4
  business_flow (0.3.0)
5
5
  activemodel (>= 3.0)
6
+ activesupport (>= 3.0)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
@@ -27,10 +28,12 @@ GEM
27
28
  descendants_tracker (0.0.4)
28
29
  thread_safe (~> 0.3, >= 0.3.1)
29
30
  diff-lcs (1.3)
31
+ docile (1.1.5)
30
32
  equalizer (0.0.11)
31
33
  i18n (0.9.5)
32
34
  concurrent-ruby (~> 1.0)
33
35
  ice_nine (0.11.2)
36
+ json (2.1.0)
34
37
  minitest (5.11.3)
35
38
  parallel (1.12.1)
36
39
  parser (2.5.0.3)
@@ -62,7 +65,14 @@ GEM
62
65
  rainbow (>= 2.2.2, < 4.0)
63
66
  ruby-progressbar (~> 1.7)
64
67
  unicode-display_width (~> 1.0, >= 1.0.1)
68
+ rubocop-rspec (1.24.0)
69
+ rubocop (>= 0.53.0)
65
70
  ruby-progressbar (1.9.0)
71
+ simplecov (0.15.1)
72
+ docile (~> 1.1.0)
73
+ json (>= 1.8, < 3)
74
+ simplecov-html (~> 0.10.0)
75
+ simplecov-html (0.10.2)
66
76
  thread_safe (0.3.6)
67
77
  tzinfo (1.2.5)
68
78
  thread_safe (~> 0.1)
@@ -83,6 +93,8 @@ DEPENDENCIES
83
93
  reek (~> 4.8)
84
94
  rspec (~> 3.0)
85
95
  rubocop (~> 0.53)
96
+ rubocop-rspec (~> 1.24.0)
97
+ simplecov (~> 0.15.1)
86
98
 
87
99
  BUNDLED WITH
88
100
  1.16.1
@@ -21,10 +21,13 @@ Gem::Specification.new do |spec|
21
21
  spec.require_paths = ['lib']
22
22
 
23
23
  spec.add_dependency 'activemodel', '>= 3.0'
24
+ spec.add_dependency 'activesupport', '>= 3.0'
24
25
 
25
26
  spec.add_development_dependency 'bundler', '~> 1.16'
26
27
  spec.add_development_dependency 'rake', '~> 10.0'
27
28
  spec.add_development_dependency 'reek', '~> 4.8'
28
29
  spec.add_development_dependency 'rspec', '~> 3.0'
29
30
  spec.add_development_dependency 'rubocop', '~> 0.53'
31
+ spec.add_development_dependency 'rubocop-rspec', '~> 1.24.0'
32
+ spec.add_development_dependency 'simplecov', '~> 0.15.1'
30
33
  end
@@ -1,104 +1,40 @@
1
1
  # Magic!
2
2
  module BusinessFlow
3
3
  # More magic!
4
- # Look at use of class attrs
5
4
  # call! to raise errors
6
5
  # Hooks for cross cutting concerns
7
6
  # Figure out how much we can freeze
8
- # Check output slots even if they're not going through our defined setter
9
- # Conditional steps?
10
- # ActiveSupport notifiers
11
7
  module Base
12
8
  def self.included(klass)
13
9
  klass.include(DSL)
14
10
  klass.prepend(InstanceMethods)
15
11
  end
16
12
 
13
+ # "Helper" methods which we put on an object to provide some default
14
+ # behavior.
17
15
  module InstanceMethods
18
16
  attr_reader :parameter_object
19
17
  private :parameter_object
20
18
 
21
19
  def initialize(parameter_object)
22
20
  @parameter_object = parameter_object
23
- super()
21
+ catch(:halt_step) do
22
+ super()
23
+ end
24
24
  end
25
25
 
26
26
  def call
27
- return if invalid?
27
+ # If our initialization process set any errors, return
28
+ return if errors.any? || invalid?
28
29
  if defined?(super)
29
- super
30
- else
31
- process_steps
32
- end
33
- end
34
-
35
- private
36
-
37
- def process_steps
38
- steps.each do |step_name|
39
30
  catch(:halt_step) do
40
- process_step(step_name)
31
+ super
41
32
  end
42
- break if errors.any?
43
- end
44
- end
45
-
46
- def process_step(step)
47
- input_object = marshall_input_object(step.inputs)
48
- result = step.dispatch(self, input_object)
49
- marshall_outputs(result, step.outputs) if result.present?
50
- end
51
-
52
- def marshall_input_object(input_object)
53
- return self if input_object.blank?
54
- Hash[input_object.map do |input_name, input_value|
55
- [
56
- input_name,
57
- process_input(input_value)
58
- ]
59
- end
60
- ]
61
- end
62
-
63
- def marshall_outputs(result, output_object)
64
- merge_other_errors(result.errors, result.class.name.underscore)
65
- output_object.each do |(output_name, output_setter)|
66
- break if errors.any?
67
- output = result.public_send(output_name)
68
- process_output(output, output_setter)
69
- end
70
- end
71
-
72
- def merge_other_errors(other_errors, base_name)
73
- other_errors.each do |attribute, message|
74
- attribute = "#{base_name}.#{attribute}"
75
- (errors[attribute] << message).uniq!
76
- end
77
- end
78
-
79
- def process_input(input_value)
80
- case input_value
81
- when Symbol
82
- send(input_value)
83
- when Proc
84
- instance_exec(&input_value)
85
33
  else
86
- input_value
87
- end
88
- end
89
-
90
- def process_output(output, output_setter)
91
- case output_setter
92
- when Symbol
93
- send("#{output_setter}=", output)
94
- when Proc
95
- instance_exec(output, &output_setter)
34
+ ::BusinessFlow::DefaultStepExecutor.new(self.class.step_queue, self)
35
+ .call
96
36
  end
97
37
  end
98
-
99
- def steps
100
- self.class.step_queue || []
101
- end
102
38
  end
103
39
  end
104
40
  end
@@ -13,14 +13,16 @@ module BusinessFlow
13
13
  cached_proc.call(instance, inputs)
14
14
  end
15
15
 
16
+ def to_s
17
+ @callable.to_s
18
+ end
19
+
16
20
  private
17
21
 
18
22
  # :reek:ManualDispatch Look reek I don't know what you want me to do.
19
23
  def check_callable
20
24
  if @callable.is_a?(Proc)
21
- @cached_proc = proc do |instance, inputs|
22
- instance.instance_exec(inputs, &@callable)
23
- end
25
+ @cached_proc = proc_callable
24
26
  elsif @callable.respond_to?(:call)
25
27
  @cached_proc = proc { |_, inputs| @callable.call(inputs) }
26
28
  elsif !@callable.is_a?(Symbol)
@@ -32,23 +34,34 @@ module BusinessFlow
32
34
  @cached_proc ||=
33
35
  if @metaclass.method_defined?(@callable) ||
34
36
  @metaclass.private_method_defined?(@callable)
35
- proc { |instance, _| instance.send(@callable) && nil }
37
+ proc { |instance, _| instance.send(@callable) }
36
38
  else
37
39
  @callable = lookup_callable ||
38
- raise(NameError, "undefined constant #{@klass}")
39
- proc { |_, inputs| @callable.call(inputs) }
40
+ raise(NameError, "undefined constant #{@callable}")
41
+ check_callable
42
+ end
43
+ end
44
+
45
+ def proc_callable
46
+ proc do |instance, inputs|
47
+ if @callable.arity.zero?
48
+ instance.instance_exec(&@callable)
49
+ else
50
+ instance.instance_exec(inputs, &@callable)
40
51
  end
52
+ end
41
53
  end
42
54
 
43
55
  def lookup_callable
44
56
  constant_name = @callable.to_s.camelcase
45
57
  @metaclass.parents.each do |parent|
46
58
  begin
47
- break parent.const_get(constant_name)
59
+ return parent.const_get(constant_name)
48
60
  rescue NameError
49
61
  next
50
62
  end
51
63
  end
64
+ nil
52
65
  end
53
66
  end
54
67
  end
@@ -0,0 +1,35 @@
1
+ module BusinessFlow
2
+ # Default behavior for running a step queue -- execute each step in turn
3
+ # halting the moment something goes wrong. Use the same flow as input
4
+ # and output to all steps.
5
+ class DefaultStepExecutor
6
+ def initialize(step_queue, flow)
7
+ @step_queue = step_queue
8
+ @flow = flow
9
+ end
10
+
11
+ def call
12
+ @step_queue.each do |step|
13
+ catch(:halt_step) { process_step(step) }
14
+ break if @flow.errors.any?
15
+ end
16
+ end
17
+
18
+ protected
19
+
20
+ def process_step(step)
21
+ ActiveSupport::Notifications.instrument(
22
+ event_name(step), flow: @flow
23
+ ) do |payload|
24
+ payload[:step_result] = result = step.call(@flow)
25
+ result.merge_into(@flow)
26
+ result
27
+ end
28
+ end
29
+
30
+ def event_name(step)
31
+ "business_flow.step.#{@flow.class.to_s.underscore}. " \
32
+ "#{step.to_s.underscore}"
33
+ end
34
+ end
35
+ end
@@ -1,55 +1,85 @@
1
1
  module BusinessFlow
2
+ # Core DSL for BusinessFlow. The relevant methods are all in
3
+ # ClassMethods.
2
4
  module DSL
3
- def self.included(klass)
4
- klass.include(ActiveModel::Validations)
5
- klass.extend(ClassMethods)
6
- klass.instance_eval do
7
- class << self
8
- alias invariant validates
9
- end
10
- end
11
- end
12
-
5
+ # Contains the DSL for BusinessFlow
6
+ # The class that includes this must implement a parameter_object reader
7
+ # which returns a hash or object representing the parameters the flow
8
+ # was initialized with. The provided .call will instantiate the including
9
+ # class with a parameter_object as the only argument.
13
10
  module ClassMethods
14
- def step_queue
15
- @step_queue ||= []
16
- end
17
-
18
- def add_requirement(fields)
19
- @requirements ||= []
20
- @requirements.concat(fields)
21
- fields.each do |field|
22
- validates_with NotNilValidator, attributes: [field]
23
- end
24
- end
25
-
11
+ # Requires that a field be retrievable from the initialization parameters
12
+ #
13
+ # This will only require that the field is not nil. The field may still
14
+ # be #empty?
15
+ #
16
+ # @param fields The fields required from the initialization parameters
26
17
  def needs(*fields)
27
- add_requirement(fields)
18
+ validates_with NotNilValidator, attributes: fields
28
19
  wants(*fields)
29
20
  end
30
21
 
22
+ # Allows a field to be retrieved from the initialiaztoin parameters
23
+ #
24
+ # Unlike needs, this applies no validation to the field. It may
25
+ # be nil.
26
+ #
27
+ # @param (see #needs)
31
28
  def wants(*fields)
32
- fields.each { |field| add_parameter_field(field) }
29
+ fields.each do |field|
30
+ PrivateHelpers.create_parameter_field(self, field)
31
+ end
33
32
  end
34
33
 
34
+ # Declares that you will expose a field to the outside world.
35
35
  def provides(*fields)
36
- attr_reader(*fields)
37
- attr_writer(*fields)
38
- fields.each { |field| private("#{field}=") }
36
+ fields.each { |field| PrivateHelpers.create_field(self, field) }
39
37
  end
40
38
 
39
+ # Declares that you expect to set this field during the course of
40
+ # processing, and that it should meet the given ActiveModel
41
+ # validations.
41
42
  def expects(field, options = {})
42
- attr_reader field
43
43
  validates field, options.merge(on: field)
44
- setter_name = "#{field}=".to_sym
45
- unless method_defined?(setter_name) ||
46
- private_method_defined?(setter_name)
47
- create_setter(field)
44
+ provides field
45
+ end
46
+
47
+ def step(klass, opts = {})
48
+ callable = Callable.new(klass, self)
49
+ opts = opts.merge(
50
+ condition: PrivateHelpers.create_conditional_callable(self, opts)
51
+ )
52
+ step_queue << step = Step.new(callable, opts)
53
+ provides(*step.outputs.values)
54
+ end
55
+
56
+ def call(parameter_object)
57
+ new(parameter_object).tap(&:call)
58
+ end
59
+
60
+ def step_queue
61
+ @step_queue ||= []
62
+ end
63
+ end
64
+
65
+ def self.included(klass)
66
+ # That we include ActiveModel::Validations is considered part of our
67
+ # public API, even though we provide our own aliases.
68
+ klass.include(ActiveModel::Validations)
69
+ klass.extend(ClassMethods)
70
+ klass.instance_eval do
71
+ class << self
72
+ # See above -- that this is an alias is considered public API.
73
+ alias invariant validates
48
74
  end
49
75
  end
76
+ end
50
77
 
51
- def add_parameter_field(field)
52
- define_method field do
78
+ # Keep our internal helpers in a different module to avoid polluting the
79
+ # namespace of whoever includes us.
80
+ module PrivateHelpers
81
+ def self.create_parameter_field(klass, field)
82
+ klass.send(:define_method, field) do
53
83
  if parameter_object.is_a?(Hash) && parameter_object.key?(field)
54
84
  parameter_object[field]
55
85
  else
@@ -58,30 +88,27 @@ module BusinessFlow
58
88
  end
59
89
  end
60
90
 
61
- def step(klass, opts = {})
62
- create_fields_for_step_outputs(opts.fetch(:outputs, {}))
63
- step_queue << Step.new(klass, opts, self)
91
+ def self.create_conditional_callable(klass, opts)
92
+ condition = opts[:if]
93
+ condition && Callable.new(condition, klass)
64
94
  end
65
95
 
66
- def create_fields_for_step_outputs(outputs)
67
- outputs.values.select { |field| field.is_a?(Symbol) }.map do |field|
68
- attr_reader field
69
- create_setter(field)
70
- end
96
+ def self.create_field(klass, field)
97
+ return unless field.is_a?(Symbol)
98
+ klass.send(:attr_reader, field)
99
+ setter_name = "#{field}=".to_sym
100
+ define_setter(klass, setter_name, field)
101
+ klass.send(:private, setter_name)
71
102
  end
72
103
 
73
- def create_setter(field)
74
- setter_name = "#{field}=".to_sym
75
- define_method setter_name do |new_value|
104
+ def self.define_setter(klass, setter_name, field)
105
+ return if klass.method_defined?(setter_name) ||
106
+ klass.private_method_defined?(setter_name)
107
+ klass.send(:define_method, setter_name) do |new_value|
76
108
  instance_variable_set("@#{field}".to_sym, new_value)
77
109
  throw :halt_step unless valid?(field)
78
110
  new_value
79
111
  end
80
- private(setter_name)
81
- end
82
-
83
- def call(parameter_object)
84
- new(parameter_object).tap(&:call)
85
112
  end
86
113
  end
87
114
  end
@@ -1,19 +1,124 @@
1
1
  module BusinessFlow
2
2
  class Step
3
- attr_reader :klass, :inputs, :outputs
4
- def initialize(klass, opts, flow_klass)
5
- @klass = klass
6
- @inputs = opts.fetch(:inputs, {})
7
- @outputs = opts.fetch(:outputs, {})
8
- if_condition = opts[:if]
9
- @condition = Callable.new(if_condition, flow_klass) if if_condition
10
- @callable = Callable.new(@klass, flow_klass)
3
+ class Inputs
4
+ attr_reader :inputs
5
+
6
+ def initialize(inputs)
7
+ @inputs = inputs
8
+ end
9
+
10
+ def parameters_from_source(source)
11
+ return source if inputs.blank?
12
+ Hash[inputs.map do |input_name, input_value|
13
+ [
14
+ input_name,
15
+ Inputs.process_input(source, input_value)
16
+ ]
17
+ end]
18
+ end
19
+
20
+ def self.process_input(source, input_value)
21
+ case input_value
22
+ when Symbol
23
+ source.send(input_value)
24
+ when Proc
25
+ source.instance_exec(&input_value)
26
+ else
27
+ input_value
28
+ end
29
+ end
30
+ end
31
+
32
+ class Result
33
+ def initialize(result, output_map)
34
+ @result = result
35
+ @output_map = output_map
36
+ @result_errors = begin
37
+ result.errors
38
+ rescue NoMethodError
39
+ nil
40
+ end
41
+ end
42
+
43
+ def merge_into(object)
44
+ merge_errors_into(object.errors)
45
+ merge_outputs_into(object)
46
+ end
47
+
48
+ def executed?
49
+ true
50
+ end
51
+
52
+ def errors?
53
+ @result_errors.present?
54
+ end
55
+
56
+ def self.process_output(object, output, output_setter)
57
+ case output_setter
58
+ when Symbol
59
+ object.send("#{output_setter}=", output)
60
+ when Proc
61
+ object.instance_exec(output, &output_setter)
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def merge_errors_into(errors)
68
+ return if @result_errors.blank?
69
+ @result_errors.each do |attribute, message|
70
+ attribute = "#{@result.class.name}.#{attribute}"
71
+ (errors[attribute] << message).uniq!
72
+ end
73
+ throw :halt_step
74
+ end
75
+
76
+ def merge_outputs_into(object)
77
+ @output_map.each do |(output_name, output_setter)|
78
+ output = @result.public_send(output_name)
79
+ Result.process_output(object, output, output_setter)
80
+ end
81
+ end
82
+ end
83
+
84
+ # Returned if our conditional check failed. Does nothing.
85
+ class ConditionFailedResult
86
+ def executed?
87
+ false
88
+ end
89
+
90
+ def errors?
91
+ false
92
+ end
93
+
94
+ def merge_into(_object); end
95
+ end
96
+
97
+ attr_reader :outputs
98
+
99
+ def initialize(callable, opts)
100
+ @callable = callable
101
+ @input_object = Inputs.new(opts[:inputs] || {})
102
+ @outputs = opts[:outputs] || {}
103
+ @condition = opts[:condition] || proc { true }
11
104
  end
12
105
 
13
106
  # @klass can be a symbol or class
14
- def dispatch(flow, inputs)
15
- return if @condition && !@condition.call(flow, inputs)
16
- @callable.call(flow, inputs)
107
+ def call(parameter_source)
108
+ parameters = @input_object.parameters_from_source(parameter_source)
109
+ if @condition.call(parameter_source, parameters)
110
+ Result.new(@callable.call(parameter_source, parameters), outputs)
111
+ else
112
+ ConditionFailedResult.new
113
+ end
114
+ end
115
+
116
+ def inputs
117
+ @input_object.inputs
118
+ end
119
+
120
+ def to_s
121
+ @callable.to_s
17
122
  end
18
123
  end
19
124
  end
@@ -1,3 +1,3 @@
1
1
  module BusinessFlow
2
- VERSION = '0.3.0'.freeze
2
+ VERSION = '0.4.0'.freeze
3
3
  end
data/lib/business_flow.rb CHANGED
@@ -1,8 +1,10 @@
1
1
  require 'active_model'
2
+ require 'active_support/core_ext'
2
3
  require 'business_flow/version'
3
4
  require 'business_flow/not_nil_validator'
4
5
  require 'business_flow/callable'
5
6
  require 'business_flow/step'
7
+ require 'business_flow/default_step_executor'
6
8
  require 'business_flow/dsl'
7
9
  require 'business_flow/base'
8
10
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: business_flow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Scarborough
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-03-10 00:00:00.000000000 Z
11
+ date: 2018-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +108,34 @@ dependencies:
94
108
  - - "~>"
95
109
  - !ruby/object:Gem::Version
96
110
  version: '0.53'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 1.24.0
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 1.24.0
125
+ - !ruby/object:Gem::Dependency
126
+ name: simplecov
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: 0.15.1
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: 0.15.1
97
139
  description:
98
140
  email:
99
141
  - alex@teak.io
@@ -115,6 +157,7 @@ files:
115
157
  - lib/business_flow.rb
116
158
  - lib/business_flow/base.rb
117
159
  - lib/business_flow/callable.rb
160
+ - lib/business_flow/default_step_executor.rb
118
161
  - lib/business_flow/dsl.rb
119
162
  - lib/business_flow/not_nil_validator.rb
120
163
  - lib/business_flow/step.rb