business_flow 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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