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