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.
- checksums.yaml +4 -4
- data/lib/flow.rb +3 -3
- data/lib/flow/concerns/transaction_wrapper.rb +27 -0
- data/lib/flow/custom_matchers.rb +5 -0
- data/lib/flow/custom_matchers/define_argument.rb +19 -0
- data/lib/flow/custom_matchers/define_option.rb +26 -0
- data/lib/flow/custom_matchers/use_operations.rb +23 -0
- data/lib/flow/flow/callbacks.rb +13 -0
- data/lib/flow/flow/core.rb +24 -0
- data/lib/flow/flow/ebb.rb +28 -0
- data/lib/flow/flow/errors/state_invalid.rb +7 -0
- data/lib/flow/flow/flux.rb +55 -0
- data/lib/flow/flow/operations.rb +27 -0
- data/lib/flow/flow/revert.rb +18 -0
- data/lib/flow/flow/status.rb +28 -0
- data/lib/flow/flow/transactions.rb +14 -0
- data/lib/flow/flow/trigger.rb +38 -0
- data/lib/flow/flow_base.rb +23 -37
- data/lib/flow/operation/callbacks.rb +19 -0
- data/lib/flow/operation/error_handler.rb +24 -0
- data/lib/flow/operation/execute.rb +25 -12
- data/lib/flow/operation/failures.rb +65 -0
- data/lib/flow/operation/rewind.rb +24 -0
- data/lib/flow/operation/status.rb +28 -0
- data/lib/flow/operation/transactions.rb +14 -0
- data/lib/flow/operation_base.rb +17 -0
- data/lib/flow/shoulda_matcher_helper.rb +7 -0
- data/lib/flow/spec_helper.rb +4 -0
- data/lib/flow/state/arguments.rb +1 -1
- data/lib/flow/state/attributes.rb +1 -2
- data/lib/flow/state/callbacks.rb +1 -1
- data/lib/flow/state/core.rb +1 -1
- data/lib/flow/state/options.rb +4 -2
- data/lib/flow/state/string.rb +2 -6
- data/lib/flow/state_base.rb +2 -0
- data/lib/flow/version.rb +1 -1
- data/lib/generators/flow/USAGE +11 -0
- data/lib/generators/flow/application_flow/USAGE +9 -0
- data/lib/generators/flow/application_flow/application_flow_generator.rb +15 -0
- data/lib/generators/flow/application_flow/templates/application_flow.rb +3 -0
- data/lib/generators/flow/application_operation/USAGE +9 -0
- data/lib/generators/flow/application_operation/application_operation_generator.rb +15 -0
- data/lib/generators/flow/application_operation/templates/application_operation.rb +3 -0
- data/lib/generators/flow/application_state/USAGE +9 -0
- data/lib/generators/flow/application_state/application_state_generator.rb +15 -0
- data/lib/generators/flow/application_state/templates/application_state.rb +3 -0
- data/lib/generators/flow/flow_generator.rb +18 -0
- data/lib/generators/flow/install/USAGE +13 -0
- data/lib/generators/flow/install/install_generator.rb +13 -0
- data/lib/generators/flow/operation/USAGE +9 -0
- data/lib/generators/flow/operation/operation_generator.rb +15 -0
- data/lib/generators/flow/operation/templates/operation.rb.erb +7 -0
- data/lib/generators/flow/state/USAGE +9 -0
- data/lib/generators/flow/state/state_generator.rb +15 -0
- data/lib/generators/flow/state/templates/state.rb.erb +8 -0
- data/lib/generators/flow/templates/flow.rb.erb +5 -0
- data/lib/generators/rspec/application_flow/USAGE +8 -0
- data/lib/generators/rspec/application_flow/application_flow_generator.rb +13 -0
- data/lib/generators/rspec/application_flow/templates/application_flow_spec.rb +7 -0
- data/lib/generators/rspec/application_operation/USAGE +8 -0
- data/lib/generators/rspec/application_operation/application_operation_generator.rb +13 -0
- data/lib/generators/rspec/application_operation/templates/application_operation_spec.rb +11 -0
- data/lib/generators/rspec/application_state/USAGE +8 -0
- data/lib/generators/rspec/application_state/application_state_generator.rb +13 -0
- data/lib/generators/rspec/application_state/templates/application_state_spec.rb +7 -0
- data/lib/generators/rspec/flow/USAGE +8 -0
- data/lib/generators/rspec/flow/flow_generator.rb +13 -0
- data/lib/generators/rspec/flow/templates/flow_spec.rb.erb +14 -0
- data/lib/generators/rspec/operation/USAGE +8 -0
- data/lib/generators/rspec/operation/operation_generator.rb +13 -0
- data/lib/generators/rspec/operation/templates/operation_spec.rb.erb +24 -0
- data/lib/generators/rspec/state/USAGE +8 -0
- data/lib/generators/rspec/state/state_generator.rb +13 -0
- data/lib/generators/rspec/state/templates/state_spec.rb.erb +18 -0
- metadata +93 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 06cd9cd8135b5b9c52b88e812c87250ada48cb408a2bff65d32b72440c89994d
|
4
|
+
data.tar.gz: f876e5506c3753696e4ea2f11c4f8f9f903648d290040d8099a7ebfac68ff0e2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 92dc6d0367748c85ddff07a3d9b0c2ed0eb76090a1fa5707f1d2305ea876e2184e279b5278d6d94c8a812e4f045e06c0a46c2945278789f9d346b95c5984f4c2
|
7
|
+
data.tar.gz: 377228fbebeac2e36d6ff0ce56f6f7b50383f62ae434f75e661803e71663e90ef246d328985fb481dd827818ed9fc67483a9a7fa860459bed9839c834fc07eea
|
data/lib/flow.rb
CHANGED
@@ -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,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,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
|
data/lib/flow/flow_base.rb
CHANGED
@@ -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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|