flow 0.3.0 → 0.9.0

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