flow 0.3.0 → 0.9.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/lib/flow.rb +3 -3
  3. data/lib/flow/concerns/transaction_wrapper.rb +27 -0
  4. data/lib/flow/custom_matchers.rb +5 -0
  5. data/lib/flow/custom_matchers/define_argument.rb +19 -0
  6. data/lib/flow/custom_matchers/define_option.rb +26 -0
  7. data/lib/flow/custom_matchers/use_operations.rb +23 -0
  8. data/lib/flow/flow/callbacks.rb +13 -0
  9. data/lib/flow/flow/core.rb +24 -0
  10. data/lib/flow/flow/ebb.rb +28 -0
  11. data/lib/flow/flow/errors/state_invalid.rb +7 -0
  12. data/lib/flow/flow/flux.rb +55 -0
  13. data/lib/flow/flow/operations.rb +27 -0
  14. data/lib/flow/flow/revert.rb +18 -0
  15. data/lib/flow/flow/status.rb +28 -0
  16. data/lib/flow/flow/transactions.rb +14 -0
  17. data/lib/flow/flow/trigger.rb +38 -0
  18. data/lib/flow/flow_base.rb +23 -37
  19. data/lib/flow/operation/callbacks.rb +19 -0
  20. data/lib/flow/operation/error_handler.rb +24 -0
  21. data/lib/flow/operation/execute.rb +25 -12
  22. data/lib/flow/operation/failures.rb +65 -0
  23. data/lib/flow/operation/rewind.rb +24 -0
  24. data/lib/flow/operation/status.rb +28 -0
  25. data/lib/flow/operation/transactions.rb +14 -0
  26. data/lib/flow/operation_base.rb +17 -0
  27. data/lib/flow/shoulda_matcher_helper.rb +7 -0
  28. data/lib/flow/spec_helper.rb +4 -0
  29. data/lib/flow/state/arguments.rb +1 -1
  30. data/lib/flow/state/attributes.rb +1 -2
  31. data/lib/flow/state/callbacks.rb +1 -1
  32. data/lib/flow/state/core.rb +1 -1
  33. data/lib/flow/state/options.rb +4 -2
  34. data/lib/flow/state/string.rb +2 -6
  35. data/lib/flow/state_base.rb +2 -0
  36. data/lib/flow/version.rb +1 -1
  37. data/lib/generators/flow/USAGE +11 -0
  38. data/lib/generators/flow/application_flow/USAGE +9 -0
  39. data/lib/generators/flow/application_flow/application_flow_generator.rb +15 -0
  40. data/lib/generators/flow/application_flow/templates/application_flow.rb +3 -0
  41. data/lib/generators/flow/application_operation/USAGE +9 -0
  42. data/lib/generators/flow/application_operation/application_operation_generator.rb +15 -0
  43. data/lib/generators/flow/application_operation/templates/application_operation.rb +3 -0
  44. data/lib/generators/flow/application_state/USAGE +9 -0
  45. data/lib/generators/flow/application_state/application_state_generator.rb +15 -0
  46. data/lib/generators/flow/application_state/templates/application_state.rb +3 -0
  47. data/lib/generators/flow/flow_generator.rb +18 -0
  48. data/lib/generators/flow/install/USAGE +13 -0
  49. data/lib/generators/flow/install/install_generator.rb +13 -0
  50. data/lib/generators/flow/operation/USAGE +9 -0
  51. data/lib/generators/flow/operation/operation_generator.rb +15 -0
  52. data/lib/generators/flow/operation/templates/operation.rb.erb +7 -0
  53. data/lib/generators/flow/state/USAGE +9 -0
  54. data/lib/generators/flow/state/state_generator.rb +15 -0
  55. data/lib/generators/flow/state/templates/state.rb.erb +8 -0
  56. data/lib/generators/flow/templates/flow.rb.erb +5 -0
  57. data/lib/generators/rspec/application_flow/USAGE +8 -0
  58. data/lib/generators/rspec/application_flow/application_flow_generator.rb +13 -0
  59. data/lib/generators/rspec/application_flow/templates/application_flow_spec.rb +7 -0
  60. data/lib/generators/rspec/application_operation/USAGE +8 -0
  61. data/lib/generators/rspec/application_operation/application_operation_generator.rb +13 -0
  62. data/lib/generators/rspec/application_operation/templates/application_operation_spec.rb +11 -0
  63. data/lib/generators/rspec/application_state/USAGE +8 -0
  64. data/lib/generators/rspec/application_state/application_state_generator.rb +13 -0
  65. data/lib/generators/rspec/application_state/templates/application_state_spec.rb +7 -0
  66. data/lib/generators/rspec/flow/USAGE +8 -0
  67. data/lib/generators/rspec/flow/flow_generator.rb +13 -0
  68. data/lib/generators/rspec/flow/templates/flow_spec.rb.erb +14 -0
  69. data/lib/generators/rspec/operation/USAGE +8 -0
  70. data/lib/generators/rspec/operation/operation_generator.rb +13 -0
  71. data/lib/generators/rspec/operation/templates/operation_spec.rb.erb +24 -0
  72. data/lib/generators/rspec/state/USAGE +8 -0
  73. data/lib/generators/rspec/state/state_generator.rb +13 -0
  74. data/lib/generators/rspec/state/templates/state_spec.rb.erb +18 -0
  75. metadata +93 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76a25492b68978e0c6dd95fe793c915181ec5943da1009c95e3411529aed8947
4
- data.tar.gz: a652814c8cf408281de94acf9197d040ba458cc07e547592bd485a35097cbc67
3
+ metadata.gz: 06cd9cd8135b5b9c52b88e812c87250ada48cb408a2bff65d32b72440c89994d
4
+ data.tar.gz: f876e5506c3753696e4ea2f11c4f8f9f903648d290040d8099a7ebfac68ff0e2
5
5
  SHA512:
6
- metadata.gz: 047c0d4f98028c99f8929fb8d850b6b35772a1689d28e268caa0944486b1ca8960bebc8335f36bd21a45f68a201076e9640d3558feb6f9c7efedcdef9a44dbe0
7
- data.tar.gz: 983411cc472456dad56653736ec4e8430919fdc0af6fb15f6076b2d684e10279695017061766aa7d8495245ba501ffd4ee93c74ad3b385ac64d4b9cddab7758d
6
+ metadata.gz: 92dc6d0367748c85ddff07a3d9b0c2ed0eb76090a1fa5707f1d2305ea876e2184e279b5278d6d94c8a812e4f045e06c0a46c2945278789f9d346b95c5984f4c2
7
+ data.tar.gz: 377228fbebeac2e36d6ff0ce56f6f7b50383f62ae434f75e661803e71663e90ef246d328985fb481dd827818ed9fc67483a9a7fa860459bed9839c834fc07eea
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_model"
4
-
4
+ require "active_record"
5
5
  require "active_support"
6
- require "active_support/core_ext/class/attribute"
7
- require "active_support/core_ext/module/delegation"
8
6
 
9
7
  require "technologic"
10
8
 
11
9
  require "flow/version"
12
10
 
11
+ require "flow/concerns/transaction_wrapper"
12
+
13
13
  require "flow/flow_base"
14
14
  require "flow/operation_base"
15
15
  require "flow/state_base"
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A callback driven approach to wrap some business logic within a transaction.
4
+ module TransactionWrapper
5
+ extend ActiveSupport::Concern
6
+
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)
17
+
18
+ callbacks_to_wrap = callbacks_for_transaction
19
+ callbacks_to_wrap &= whitelist if whitelist.present?
20
+ callbacks_to_wrap -= blacklist if blacklist.present?
21
+
22
+ callbacks_to_wrap.each do |method_name|
23
+ set_callback method_name, :around, ->(_, block) { self.class.transaction_provider.transaction { block.call } }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "custom_matchers/define_argument"
4
+ require_relative "custom_matchers/define_option"
5
+ require_relative "custom_matchers/use_operations"
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RSpec matcher for flow state arguments.
4
+ #
5
+ # Usage:
6
+ #
7
+ # RSpec.describe ExampleState, type: :state do
8
+ # subject { described_class.new(**input) }
9
+ #
10
+ # let(:input) { {} }
11
+ #
12
+ # it { is_expected.to define_argument :foo }
13
+ # end
14
+
15
+ RSpec::Matchers.define :define_argument do |argument|
16
+ match { |state| expect(state._arguments).to include argument }
17
+ description { "has argument #{argument}" }
18
+ failure_message { |state| "expected #{state.class.name}# to have argument #{argument}" }
19
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RSpec matcher for flow state options.
4
+ #
5
+ # Usage:
6
+ #
7
+ # RSpec.describe ExampleState, type: :state do
8
+ # subject { described_class.new(**input) }
9
+ #
10
+ # let(:input) { {} }
11
+ #
12
+ # it { is_expected.to define_option :foo }
13
+ # it { is_expected.to define_option :foo, default_value }
14
+ # end
15
+
16
+ RSpec::Matchers.define :define_option do |option, default_value = nil|
17
+ match { |state| expect(state._options[option].default_value).to eq default_value }
18
+ description { "has option #{option}" }
19
+ failure_message { |state| "expected #{state.class.name}# to have option #{option}, #{for_default(default_value)}" }
20
+
21
+ def for_default(default_value)
22
+ return "without a default value" if default_value.nil?
23
+
24
+ "with default value #{default_value}"
25
+ end
26
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RSpec matcher for flow operations.
4
+ #
5
+ # Usage:
6
+ #
7
+ # RSpec.describe ExampleFlow, type: :flow do
8
+ # subject { described_class.new(**input) }
9
+ #
10
+ # let(:input) { {} }
11
+ #
12
+ # it { is_expected.to use_operations [ OperationOne, OperationTwo ] }
13
+ # end
14
+
15
+ RSpec::Matchers.define :use_operations do |operations|
16
+ match { |flow| expect(flow._operations).to eq Array.wrap(operations) }
17
+ description { "uses operations #{display_operations(operations)}" }
18
+ failure_message { |flow| "expected #{flow.class.name}# to use operations #{display_operations(operations)}" }
19
+
20
+ def display_operations(operations)
21
+ Array.wrap(operations).join(", ")
22
+ end
23
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Callbacks provide an extensible mechanism for hooking into a Flow.
4
+ module Flow
5
+ module Callbacks
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include ActiveSupport::Callbacks
10
+ define_callbacks :initialize, :trigger, :flux, :revert, :ebb
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Accepts input representing the arguments and options which define the initial state.
4
+ module Flow
5
+ module Core
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def state_class
10
+ "#{name.chomp("Flow")}State".constantize
11
+ end
12
+ end
13
+
14
+ included do
15
+ delegate :state_class, to: :class
16
+
17
+ attr_reader :state
18
+ end
19
+
20
+ def initialize(**input)
21
+ run_callbacks(:initialize) { @state = state_class.new(**input) }
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # When a `#revert` is called on a Flow, `#rewind` is called on Operations in reverse of the order they were executed.
4
+ module Flow
5
+ module Ebb
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ set_callback(:initialize, :after) { @rewound_operations = [] }
10
+
11
+ private
12
+
13
+ attr_reader :rewound_operations
14
+
15
+ def _ebb
16
+ rewindable_operations.reverse_each { |executed_operation| rewound_operations << executed_operation.rewind }
17
+ end
18
+
19
+ def rewindable_operations
20
+ executed_operations - rewound_operations
21
+ end
22
+ end
23
+
24
+ def ebb
25
+ run_callbacks(:ebb) { _ebb }
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flow
4
+ module Errors
5
+ class StateInvalid < StandardError; end
6
+ end
7
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ # When `#trigger` is called on a Flow, `#execute` is called on Operations sequentially in their given order.
4
+ module Flow
5
+ module Flux
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ set_callback(:initialize, :after) { @executed_operations = [] }
10
+
11
+ attr_reader :failed_operation
12
+
13
+ private
14
+
15
+ attr_reader :executed_operations
16
+
17
+ def _flux
18
+ executable_operations.each do |operation|
19
+ operation.execute
20
+ (@failed_operation = operation) and raise Flow::Flux::Failure if operation.failed?
21
+ executed_operations << operation
22
+ end
23
+ end
24
+
25
+ def executable_operations
26
+ operation_instances - executed_operations
27
+ end
28
+
29
+ def operation_instances
30
+ _operations.map { |operation_class| operation_class.new(state) }
31
+ end
32
+ memoize :operation_instances
33
+ end
34
+
35
+ def failed_operation?
36
+ failed_operation.present?
37
+ end
38
+
39
+ def flux
40
+ flux!
41
+ rescue StandardError => exception
42
+ error :error_executing_operation, state: state, exception: exception
43
+
44
+ revert
45
+
46
+ raise exception unless exception.is_a? Flow::Flux::Failure
47
+ end
48
+
49
+ def flux!
50
+ run_callbacks(:flux) { _flux }
51
+ end
52
+
53
+ class Failure < StandardError; end
54
+ end
55
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Operations are an ordered list of the behaviors which should are executed with and possibly change the Flow's state.
4
+ module Flow
5
+ module Operations
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :_operations, instance_writer: false, default: []
10
+
11
+ delegate :_operations, to: :class
12
+ end
13
+
14
+ class_methods do
15
+ def inherited(base)
16
+ base._operations = _operations.dup
17
+ super
18
+ end
19
+
20
+ private
21
+
22
+ def operations(*operations)
23
+ _operations.concat(operations.flatten)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Reverting a Flow rewinds all its executed operations in reverse order (see `Flow::Ebb`).
4
+ module Flow
5
+ module Revert
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ set_callback :revert, :around, ->(_, block) { surveil(:revert) { block.call } }
10
+ end
11
+
12
+ def revert
13
+ run_callbacks(:revert) { ebb }
14
+
15
+ state
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The Flow status is a summary of what has occurred during the runtime, used mainly for analysis and program flow.
4
+ module Flow
5
+ module Status
6
+ extend ActiveSupport::Concern
7
+
8
+ def pending?
9
+ executed_operations.none?
10
+ end
11
+
12
+ def triggered?
13
+ executed_operations.any?
14
+ end
15
+
16
+ def success?
17
+ triggered? && (operation_instances - executed_operations).none?
18
+ end
19
+
20
+ def failed?
21
+ triggered? && !success?
22
+ end
23
+
24
+ def reverted?
25
+ rewound_operations.any?
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # It's best practice to have Flows in which nothing should be done unless everything is successful to use a transaction.
4
+ module Flow
5
+ module Transactions
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def callbacks_for_transaction
10
+ %i[flux ebb].freeze
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Triggering a Flow executes all its operations in sequential order (see `Flow::Flux`) *iff* it has a valid state.
4
+ module Flow
5
+ module Trigger
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def trigger!(*arguments)
10
+ new(*arguments).trigger!
11
+ end
12
+
13
+ def trigger(*arguments)
14
+ new(*arguments).trigger
15
+ end
16
+ end
17
+
18
+ included do
19
+ delegate :valid?, to: :state, prefix: true
20
+
21
+ set_callback :trigger, :around, ->(_, block) { surveil(:trigger) { block.call } }
22
+ end
23
+
24
+ def trigger!
25
+ raise Flow::Errors::StateInvalid unless state_valid?
26
+
27
+ run_callbacks(:trigger) { flux }
28
+
29
+ self
30
+ end
31
+
32
+ def trigger
33
+ trigger!
34
+ rescue Flow::Errors::StateInvalid
35
+ self
36
+ end
37
+ end
38
+ end
@@ -1,43 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "flow/errors/state_invalid"
4
+
5
+ require_relative "flow/callbacks"
6
+ require_relative "flow/core"
7
+ require_relative "flow/ebb"
8
+ require_relative "flow/flux"
9
+ require_relative "flow/operations"
10
+ require_relative "flow/revert"
11
+ require_relative "flow/status"
12
+ require_relative "flow/transactions"
13
+ require_relative "flow/trigger"
14
+
3
15
  # A flow is a collection of procedurally executed operations sharing a common state.
4
16
  class FlowBase
17
+ include ShortCircuIt
5
18
  include Technologic
6
-
7
- class_attribute :_operations, instance_writer: false, default: []
8
-
9
- class << self
10
- def state_class
11
- "#{name.chomp("Flow")}State".constantize
12
- end
13
-
14
- def trigger(*arguments)
15
- new(*arguments).trigger
16
- end
17
-
18
- def operations(*operations)
19
- _operations.concat(operations.flatten)
20
- end
21
-
22
- def inherited(base)
23
- base._operations = _operations.dup
24
- super
25
- end
26
- end
27
-
28
- attr_reader :state
29
-
30
- delegate :state_class, :_operations, to: :class
31
-
32
- def initialize(**input)
33
- @state = state_class.new(**input)
34
- end
35
-
36
- def trigger
37
- surveil(:trigger) do
38
- _operations.each { |operation| operation.execute(state) }
39
- end
40
-
41
- state
42
- end
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
43
29
  end