flow 0.9.3 → 0.10.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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +177 -245
  3. data/lib/flow.rb +2 -0
  4. data/lib/flow/concerns/transaction_wrapper.rb +10 -17
  5. data/lib/flow/custom_matchers.rb +5 -3
  6. data/lib/flow/custom_matchers/define_failure.rb +21 -0
  7. data/lib/flow/custom_matchers/define_output.rb +34 -0
  8. data/lib/flow/custom_matchers/handle_error.rb +32 -0
  9. data/lib/flow/custom_matchers/have_on_state.rb +54 -0
  10. data/lib/flow/custom_matchers/use_operations.rb +13 -11
  11. data/lib/flow/custom_matchers/wrap_in_transaction.rb +60 -0
  12. data/lib/flow/flow/callbacks.rb +1 -1
  13. data/lib/flow/flow/core.rb +1 -0
  14. data/lib/flow/flow/flux.rb +0 -2
  15. data/lib/flow/flow/status.rb +0 -4
  16. data/lib/flow/flow/transactions.rb +2 -2
  17. data/lib/flow/flow/trigger.rb +1 -1
  18. data/lib/flow/flow_base.rb +13 -15
  19. data/lib/flow/operation/accessors.rb +66 -0
  20. data/lib/flow/operation/callbacks.rb +12 -10
  21. data/lib/flow/operation/core.rb +10 -8
  22. data/lib/flow/operation/error_handler.rb +18 -12
  23. data/lib/flow/operation/errors/already_executed.rb +5 -3
  24. data/lib/flow/operation/errors/already_rewound.rb +5 -3
  25. data/lib/flow/operation/execute.rb +28 -26
  26. data/lib/flow/operation/failures.rb +48 -42
  27. data/lib/flow/operation/status.rb +18 -21
  28. data/lib/flow/operation/transactions.rb +8 -6
  29. data/lib/flow/operation_base.rb +15 -13
  30. data/lib/flow/rspec_configuration.rb +5 -0
  31. data/lib/flow/spec_helper.rb +3 -0
  32. data/lib/flow/state/errors/not_validated.rb +9 -0
  33. data/lib/flow/state/output.rb +59 -0
  34. data/lib/flow/state/status.rb +22 -0
  35. data/lib/flow/state_base.rb +9 -16
  36. data/lib/flow/version.rb +1 -1
  37. data/lib/generators/flow/application_flow/templates/application_flow.rb +1 -1
  38. data/lib/generators/flow/application_operation/templates/application_operation.rb +1 -1
  39. data/lib/generators/flow/application_state/templates/application_state.rb +1 -1
  40. data/lib/generators/flow/operation/USAGE +1 -1
  41. data/lib/generators/flow/operation/templates/operation.rb.erb +0 -5
  42. data/lib/generators/flow/state/USAGE +1 -1
  43. data/lib/generators/flow/state/templates/state.rb.erb +2 -1
  44. data/lib/generators/rspec/application_flow/templates/application_flow_spec.rb +1 -1
  45. data/lib/generators/rspec/application_operation/templates/application_operation_spec.rb +1 -9
  46. data/lib/generators/rspec/application_state/templates/application_state_spec.rb +1 -1
  47. data/lib/generators/rspec/flow/templates/flow_spec.rb.erb +0 -8
  48. data/lib/generators/rspec/operation/templates/operation_spec.rb.erb +5 -11
  49. data/lib/generators/rspec/state/templates/state_spec.rb.erb +8 -10
  50. metadata +39 -52
  51. data/lib/flow/custom_matchers/define_argument.rb +0 -19
  52. data/lib/flow/custom_matchers/define_attribute.rb +0 -19
  53. data/lib/flow/custom_matchers/define_option.rb +0 -26
  54. data/lib/flow/flow/ebb.rb +0 -28
  55. data/lib/flow/flow/revert.rb +0 -18
  56. data/lib/flow/operation/rewind.rb +0 -25
  57. data/lib/flow/state/arguments.rb +0 -30
  58. data/lib/flow/state/attributes.rb +0 -31
  59. data/lib/flow/state/callbacks.rb +0 -13
  60. data/lib/flow/state/core.rb +0 -14
  61. data/lib/flow/state/options.rb +0 -45
  62. data/lib/flow/state/string.rb +0 -34
@@ -4,6 +4,8 @@ require "active_model"
4
4
  require "active_record"
5
5
  require "active_support"
6
6
 
7
+ require "instructor"
8
+ require "short_circu_it"
7
9
  require "technologic"
8
10
 
9
11
  require "flow/version"
@@ -1,26 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # A callback driven approach to wrap business logic within database transaction.
4
- module TransactionWrapper
5
- extend ActiveSupport::Concern
4
+ module Flow
5
+ module TransactionWrapper
6
+ extend ActiveSupport::Concern
6
7
 
7
- class_methods do
8
- def transaction_provider
9
- ActiveRecord::Base
10
- end
11
-
12
- private
13
-
14
- def wrap_in_transaction(only: nil, except: nil)
15
- whitelist = Array.wrap(only).map(&:to_sym)
16
- blacklist = Array.wrap(except).map(&:to_sym)
8
+ class_methods do
9
+ def transaction_provider
10
+ ActiveRecord::Base
11
+ end
17
12
 
18
- callbacks_to_wrap = callbacks_for_transaction
19
- callbacks_to_wrap &= whitelist if whitelist.present?
20
- callbacks_to_wrap -= blacklist if blacklist.present?
13
+ private
21
14
 
22
- callbacks_to_wrap.each do |method_name|
23
- set_callback method_name, :around, ->(_, block) { self.class.transaction_provider.transaction { block.call } }
15
+ def wrap_in_transaction
16
+ set_callback callback_name, :around, ->(_, block) { self.class.transaction_provider.transaction { block.call } }
24
17
  end
25
18
  end
26
19
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "custom_matchers/define_argument"
4
- require_relative "custom_matchers/define_attribute"
5
- require_relative "custom_matchers/define_option"
3
+ require_relative "custom_matchers/define_failure"
4
+ require_relative "custom_matchers/define_output"
5
+ require_relative "custom_matchers/handle_error"
6
6
  require_relative "custom_matchers/use_operations"
7
+ require_relative "custom_matchers/wrap_in_transaction"
8
+ require_relative "custom_matchers/have_on_state"
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RSpec matcher that tests usage of `ApplicationOperation.failure`
4
+ #
5
+ # class ExampleOperation
6
+ # failure :foo
7
+ # end
8
+ #
9
+ # RSpec.describe ExampleOperation, type: :operation do
10
+ # subject { described_class.new(**input) }
11
+ #
12
+ # let(:input) { {} }
13
+ #
14
+ # it { is_expected.to define_failure :foo }
15
+ # end
16
+
17
+ RSpec::Matchers.define :define_failure do |problem|
18
+ match { |operation| expect(operation._failures).to include problem.to_sym }
19
+ description { "defines failure #{problem}" }
20
+ failure_message { |operation| "expected #{operation.class.name} to define failure #{problem}" }
21
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RSpec matcher that tests usage of `ApplicationState.output`
4
+ #
5
+ # class ExampleState
6
+ # output :foo
7
+ # output :bar, default: :baz
8
+ # output(:gaz) { :haz }
9
+ # end
10
+ #
11
+ # RSpec.describe ExampleState, type: :state do
12
+ # subject { described_class.new(**input) }
13
+ #
14
+ # let(:input) { {} }
15
+ #
16
+ # it { is_expected.to define_output :foo }
17
+ # it { is_expected.to define_output :bar, default: :baz }
18
+ # it { is_expected.to define_output :gaz, default: :haz }
19
+ # end
20
+
21
+ RSpec::Matchers.define :define_output do |output, default: nil|
22
+ match do |state|
23
+ expect(state._defaults[output]&.value).to eq default
24
+ expect(state._outputs).to include output
25
+ end
26
+ description { "define output" }
27
+ failure_message { "expected #{described_class} to define output #{output} #{for_default(default)}" }
28
+
29
+ def for_default(default)
30
+ return "without a default value" if default.nil?
31
+
32
+ "with default value #{default}"
33
+ end
34
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RSpec matcher that tests usage of `ApplicationOperation.handle_error`
4
+ #
5
+ # class ExampleFlow
6
+ # operations OperationOne, OperationTwo
7
+ # end
8
+ #
9
+ # RSpec.describe ExampleFlow, type: :flow do
10
+ # subject { described_class.new(**input) }
11
+ #
12
+ # let(:input) { {} }
13
+ #
14
+ # it { is_expected.to use_operations OperationOne, OperationTwo }
15
+ # end
16
+
17
+ RSpec::Matchers.define :handle_error do |error_class, problem: error_class.name.demodulize.underscore, with: nil|
18
+ match do |operation|
19
+ handlers = operation.rescue_handlers.select { |handler| handler[0] == error_class.to_s }
20
+
21
+ expect(handlers).to be_present
22
+
23
+ if with.present?
24
+ expect(handlers.find { |handler| (with == :a_block) ? handler[1].is_a?(Proc) : handler[1] == with }).to be_present
25
+ end
26
+
27
+ expect(operation).to define_failure problem
28
+ end
29
+
30
+ description { "handles error #{error_class}" }
31
+ failure_message { |flow| "expected #{flow.class.name} to handle error #{error_class}" }
32
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RSpec matcher for making assertions on the state of a Flow or Operation after it has been run.
4
+ #
5
+ # class ExampleOperation
6
+ # def behavior
7
+ # state.foo = "some data"
8
+ # state.bar = "some other data"
9
+ # end
10
+ # end
11
+ #
12
+ # class ExampleFlow
13
+ # operations [ ExampleOperation ]
14
+ # end
15
+ #
16
+ # RSpec.describe ExampleOperation, type: :operation do
17
+ # subject { operation.execute }
18
+ #
19
+ # it { is_expected.to have_on_state foo: "some data" }
20
+ # it { is_expected.to have_on_state foo: instance_of(String) }
21
+ # it { is_expected.to have_on_state foo: "some data", bar: "some other data" }
22
+ # end
23
+ #
24
+ # RSpec.describe ExampleFlow, type: :operation do
25
+ # subject { flow.trigger }
26
+ #
27
+ # it { is_expected.to have_on_state foo: "some data" }
28
+ # it { is_expected.to have_on_state foo: instance_of(String) }
29
+ # it { is_expected.to have_on_state foo: "some data", bar: "some other data" }
30
+ # end
31
+
32
+ module CustomMatchers
33
+ def have_on_state(expectations)
34
+ HaveOnState.new(expectations)
35
+ end
36
+
37
+ class HaveOnState
38
+ include RSpec::Matchers
39
+
40
+ def initialize(state_expectations)
41
+ @state_expectations = state_expectations
42
+ end
43
+
44
+ def matches?(object)
45
+ @state_expectations.all? do |key, value|
46
+ expect(object.state.public_send(key)).to match value
47
+ end
48
+ end
49
+
50
+ def description
51
+ "have the expected data on state"
52
+ end
53
+ end
54
+ end
@@ -1,23 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # RSpec matcher for flow operations.
3
+ # RSpec matcher that tests usage of `ApplicationFlow.operations`
4
4
  #
5
- # Usage:
5
+ # class ExampleFlow
6
+ # operations OperationOne, OperationTwo
7
+ # end
6
8
  #
7
- # RSpec.describe ExampleFlow, type: :flow do
8
- # subject { described_class.new(**input) }
9
+ # RSpec.describe ExampleFlow, type: :flow do
10
+ # subject { described_class.new(**input) }
9
11
  #
10
- # let(:input) { {} }
12
+ # let(:input) { {} }
11
13
  #
12
- # it { is_expected.to use_operations [ OperationOne, OperationTwo ] }
13
- # end
14
+ # it { is_expected.to use_operations OperationOne, OperationTwo }
15
+ # end
14
16
 
15
- RSpec::Matchers.define :use_operations do |operations|
16
- match { |flow| expect(flow._operations).to eq Array.wrap(operations) }
17
+ RSpec::Matchers.define :use_operations do |*operations|
18
+ match { |flow| expect(flow._operations).to eq operations.flatten }
17
19
  description { "uses operations #{display_operations(operations)}" }
18
- failure_message { |flow| "expected #{flow.class.name}# to use operations #{display_operations(operations)}" }
20
+ failure_message { |flow| "expected #{flow.class.name} to use operations #{display_operations(operations)}" }
19
21
 
20
22
  def display_operations(operations)
21
- Array.wrap(operations).join(", ")
23
+ operations.flatten.join(", ")
22
24
  end
23
25
  end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RSpec matcher that tests usage of `.wrap_in_transaction` for `ApplicationFlow` or `ApplicationOperation`
4
+ #
5
+ # class ExampleFlow
6
+ # wrap_in_transaction
7
+ # operations OperationOne, OperationTwo
8
+ # end
9
+ #
10
+ # class ExampleState
11
+ # wrap_in_transaction
12
+ # end
13
+ #
14
+ # RSpec.describe ExampleFlow, type: :flow do
15
+ # subject { described_class.new(**input) }
16
+ #
17
+ # let(:input) { {} }
18
+ #
19
+ # it { is_expected.to wrap_in_transaction }
20
+ # end
21
+ #
22
+ # RSpec.describe Example, type: :operation do
23
+ # subject(:operation) { described_class.new(state) }
24
+ #
25
+ # let(:state) { example_state_class.new(**state_input) }
26
+ # let(:example_state_class) { Class.new(ApplicationState) }
27
+ # let(:state_input) do
28
+ # {}
29
+ # end
30
+ #
31
+ # it { is_expected.to wrap_in_transaction }
32
+ # end
33
+
34
+ # rubocop:disable Metrics/BlockLength
35
+ RSpec::Matchers.define :wrap_in_transaction do
36
+ match do |instance|
37
+ callback_name = instance.class.callback_name
38
+ original_transaction_count = instance.class.transaction_provider.connection.open_transactions
39
+
40
+ allow(instance).to receive(callback_name) do
41
+ expect(instance.class.transaction_provider.connection.open_transactions).to eq original_transaction_count + 1
42
+ end
43
+
44
+ instance.run_callbacks(callback_name) { instance.public_send(callback_name) }
45
+ expect(instance).to have_received(callback_name)
46
+ end
47
+
48
+ description do
49
+ "wrap in a transaction"
50
+ end
51
+
52
+ failure_message do |instance|
53
+ "expected #{instance.class.name} to wrap in a transaction"
54
+ end
55
+
56
+ def pretty_callbacks(callbacks)
57
+ callbacks.join(", ")
58
+ end
59
+ end
60
+ # rubocop:enable Metrics/BlockLength
@@ -7,7 +7,7 @@ module Flow
7
7
 
8
8
  included do
9
9
  include ActiveSupport::Callbacks
10
- define_callbacks :initialize, :trigger, :flux, :revert, :ebb
10
+ define_callbacks :initialize, :trigger, :flux
11
11
  end
12
12
  end
13
13
  end
@@ -13,6 +13,7 @@ module Flow
13
13
 
14
14
  included do
15
15
  delegate :state_class, to: :class
16
+ delegate :outputs, to: :state
16
17
 
17
18
  attr_reader :state
18
19
  end
@@ -41,8 +41,6 @@ module Flow
41
41
  rescue StandardError => exception
42
42
  error :error_executing_operation, state: state, exception: exception
43
43
 
44
- revert
45
-
46
44
  raise exception unless exception.is_a? Flow::Flux::Failure
47
45
  end
48
46
 
@@ -20,9 +20,5 @@ module Flow
20
20
  def failed?
21
21
  triggered? && !success?
22
22
  end
23
-
24
- def reverted?
25
- rewound_operations.any?
26
- end
27
23
  end
28
24
  end
@@ -6,8 +6,8 @@ module Flow
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  class_methods do
9
- def callbacks_for_transaction
10
- %i[flux ebb].freeze
9
+ def callback_name
10
+ :flux
11
11
  end
12
12
  end
13
13
  end
@@ -16,7 +16,7 @@ module Flow
16
16
  end
17
17
 
18
18
  included do
19
- delegate :valid?, to: :state, prefix: true
19
+ delegate :valid?, :validated?, to: :state, prefix: true
20
20
 
21
21
  set_callback :trigger, :around, ->(_, block) { surveil(:trigger) { block.call } }
22
22
  end
@@ -4,26 +4,24 @@ require_relative "flow/errors/state_invalid"
4
4
 
5
5
  require_relative "flow/callbacks"
6
6
  require_relative "flow/core"
7
- require_relative "flow/ebb"
8
7
  require_relative "flow/flux"
9
8
  require_relative "flow/operations"
10
- require_relative "flow/revert"
11
9
  require_relative "flow/status"
12
10
  require_relative "flow/transactions"
13
11
  require_relative "flow/trigger"
14
12
 
15
13
  # A **Flow** is a collection of procedurally executed **Operations** sharing a common **State**.
16
- class FlowBase
17
- include ShortCircuIt
18
- include Technologic
19
- include TransactionWrapper
20
- include Flow::Callbacks
21
- include Flow::Core
22
- include Flow::Ebb
23
- include Flow::Flux
24
- include Flow::Operations
25
- include Flow::Revert
26
- include Flow::Status
27
- include Flow::Transactions
28
- include Flow::Trigger
14
+ module Flow
15
+ class FlowBase
16
+ include ShortCircuIt
17
+ include Technologic
18
+ include Flow::TransactionWrapper
19
+ include Flow::Callbacks
20
+ include Flow::Core
21
+ include Flow::Flux
22
+ include Flow::Operations
23
+ include Flow::Status
24
+ include Flow::Transactions
25
+ include Flow::Trigger
26
+ end
29
27
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flow
4
+ module Operation
5
+ module Accessors
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :_state_readers, instance_writer: false, default: []
10
+ class_attribute :_state_writers, instance_writer: false, default: []
11
+ class_attribute :_state_accessors, instance_writer: false, default: []
12
+ end
13
+
14
+ class_methods do
15
+ protected
16
+
17
+ def state_reader(name)
18
+ return unless _add_state_reader_tracker(name.to_sym)
19
+
20
+ delegate name, to: :state
21
+ end
22
+
23
+ def state_writer(name)
24
+ return unless _add_state_writer_tracker(name.to_sym)
25
+
26
+ delegate("#{name}=", to: :state)
27
+ end
28
+
29
+ def state_accessor(name)
30
+ state_reader name
31
+ state_writer name
32
+ end
33
+
34
+ private
35
+
36
+ def _add_state_reader_tracker(name)
37
+ return false if _state_readers.include?(name)
38
+
39
+ _add_state_accessor_tracker(name) if _state_writers.include?(name)
40
+ _state_readers << name
41
+ end
42
+
43
+ def _add_state_writer_tracker(name)
44
+ return false if _state_writers.include?(name)
45
+
46
+ _add_state_accessor_tracker(name) if _state_readers.include?(name)
47
+ _state_writers << name
48
+ end
49
+
50
+ def _add_state_accessor_tracker(name)
51
+ return if _state_accessors.include?(name)
52
+
53
+ _state_accessors << name
54
+ end
55
+
56
+ def inherited(base)
57
+ base._state_readers = _state_readers.dup
58
+ base._state_writers = _state_writers.dup
59
+ base._state_accessors = _state_accessors.dup
60
+
61
+ super
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end