flow 0.9.3 → 0.10.0

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