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
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Callbacks provide an extensible mechanism for hooking into a Operation.
4
+ module Operation
5
+ module Callbacks
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include ActiveSupport::Callbacks
10
+ define_callbacks :failure, :execute, :behavior, :rewind, :undo
11
+ end
12
+
13
+ class_methods do
14
+ def on_failure(*filters, &block)
15
+ set_callback(:failure, :before, *filters, &block)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # An unhandled error during execution is exceptional, and handlers unable to rescue an error cause a failure instead.
4
+ module Operation
5
+ module ErrorHandler
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ private
10
+
11
+ def handle_error(error_class, problem: error_class.name.demodulize.underscore, with: nil, &block)
12
+ failure problem
13
+
14
+ rescue_from(error_class) { |exception| fail!(problem.to_sym, exception: exception) }
15
+
16
+ if with.present?
17
+ rescue_from(error_class, with: with)
18
+ elsif block_given?
19
+ rescue_from(error_class, &block)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,27 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Execute the operation.
3
+ # Operations *must* define the `#behavior` that occurs when `#execute` is called.
4
4
  module Operation
5
5
  module Execute
6
6
  extend ActiveSupport::Concern
7
7
 
8
- class_methods do
9
- def execute(*arguments)
10
- new(*arguments).execute
8
+ included do
9
+ include ActiveSupport::Rescuable
10
+
11
+ attr_reader :operation_failure
12
+
13
+ set_callback :execute, :around, ->(_, block) { surveil(:execute) { block.call } }
14
+ end
15
+
16
+ def execute!
17
+ run_callbacks(:execute) do
18
+ run_callbacks(:behavior) { behavior }
11
19
  end
20
+
21
+ self
22
+ rescue StandardError => exception
23
+ rescue_with_handler(exception) || raise
24
+
25
+ self
12
26
  end
13
27
 
14
28
  def execute
15
- raise NotImplementedError
16
- end
29
+ execute!
30
+ rescue Operation::Failures::OperationFailure => exception
31
+ @operation_failure = exception
17
32
 
18
- included do
19
- include AroundTheWorld
20
- include Technologic
33
+ self
34
+ end
21
35
 
22
- around_method :execute, prevent_double_wrapping_for: Technologic do
23
- surveil(:execute) { super() }
24
- end
36
+ def behavior
37
+ # abstract method which should be defined by descendants with the functionality of the given operation
25
38
  end
26
39
  end
27
40
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ # When `#execute` is unsuccessful, expected problems are *failures* and unexpected problems are *Exceptions*.
4
+ module Operation
5
+ module Failures
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ class_attribute :_failures, instance_writer: false, default: []
10
+
11
+ delegate :_failures, to: :class
12
+ end
13
+
14
+ class_methods do
15
+ def inherited(base)
16
+ base._failures = _failures.dup
17
+ super
18
+ end
19
+
20
+ private
21
+
22
+ def failure(problem)
23
+ problem = problem.to_s.to_sym
24
+ warn(:problem_already_defined) if _failures.include? problem
25
+
26
+ _failures << problem
27
+ define_callbacks problem
28
+ define_on_failure_for_problem(problem)
29
+ define_fail_method_for_problem(problem)
30
+ end
31
+
32
+ def define_on_failure_for_problem(problem)
33
+ on_failure_name = "on_#{problem}_failure".to_sym
34
+ return if respond_to?(on_failure_name)
35
+
36
+ define_singleton_method(on_failure_name) { |*filters, &block| set_callback(problem, :before, *filters, &block) }
37
+ end
38
+
39
+ def define_fail_method_for_problem(problem)
40
+ problem_failure_name = "#{problem}_failure!".to_sym
41
+ return if method_defined?(problem_failure_name)
42
+
43
+ define_method(problem_failure_name) { |**details| fail!(problem, **details) }
44
+ end
45
+ end
46
+
47
+ def fail!(problem, **details)
48
+ run_callbacks(problem) do
49
+ run_callbacks(:failure) do
50
+ error! OperationFailure.new(problem, **details), **details
51
+ end
52
+ end
53
+ end
54
+
55
+ class OperationFailure < StandardError
56
+ attr_reader :problem, :details
57
+
58
+ def initialize(problem = nil, **details)
59
+ super(problem)
60
+ @problem = problem
61
+ @details = OpenStruct.new(details)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # When something goes wrong in Flow, `#rewind` is called on all executed Operations to `#undo` their behavior.
4
+ module Operation
5
+ module Rewind
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ set_callback :rewind, :around, ->(_, block) { surveil(:rewind) { block.call } }
10
+ end
11
+
12
+ def rewind
13
+ run_callbacks(:rewind) do
14
+ run_callbacks(:undo) { undo }
15
+ end
16
+
17
+ self
18
+ end
19
+
20
+ def undo
21
+ # abstract method which should be defined by descendants to undo the functionality of the `#behavior` method
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The Operation status is used by the Flow calling it to determine what to do next; continue on or stop and rollback.
4
+ module Operation
5
+ module Status
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ set_callback(:execute, :before) { self.was_executed = true }
10
+
11
+ private
12
+
13
+ attr_accessor :was_executed
14
+ end
15
+
16
+ def executed?
17
+ was_executed.present?
18
+ end
19
+
20
+ def failed?
21
+ operation_failure.present?
22
+ end
23
+
24
+ def success?
25
+ executed? && !failed?
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # It's best practice to have Operations which modify several persisted objects to use a transaction.
4
+ module Operation
5
+ module Transactions
6
+ extend ActiveSupport::Concern
7
+
8
+ class_methods do
9
+ def callbacks_for_transaction
10
+ %i[behavior undo].freeze
11
+ end
12
+ end
13
+ end
14
+ end
@@ -1,10 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "flow/errors/state_invalid"
4
+
5
+ require_relative "operation/callbacks"
3
6
  require_relative "operation/core"
7
+ require_relative "operation/error_handler"
4
8
  require_relative "operation/execute"
9
+ require_relative "operation/failures"
10
+ require_relative "operation/rewind"
11
+ require_relative "operation/status"
12
+ require_relative "operation/transactions"
5
13
 
6
14
  # Operations are service objects which are executed with a state.
7
15
  class OperationBase
16
+ include ShortCircuIt
17
+ include Technologic
18
+ include TransactionWrapper
19
+ include Operation::Callbacks
8
20
  include Operation::Core
21
+ include Operation::ErrorHandler
9
22
  include Operation::Execute
23
+ include Operation::Failures
24
+ include Operation::Rewind
25
+ include Operation::Status
26
+ include Operation::Transactions
10
27
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(Shoulda::Matchers::ActiveModel)
4
+ RSpec.configure do |config|
5
+ config.include(Shoulda::Matchers::ActiveModel, type: :state)
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "custom_matchers"
4
+ require_relative "shoulda_matcher_helper"
@@ -19,7 +19,7 @@ module State
19
19
  super
20
20
  end
21
21
 
22
- protected
22
+ private
23
23
 
24
24
  def argument(argument)
25
25
  _arguments << argument
@@ -17,14 +17,13 @@ module State
17
17
  super
18
18
  end
19
19
 
20
- protected
20
+ private
21
21
 
22
22
  def define_attribute(attribute)
23
23
  _attributes << attribute
24
24
 
25
25
  attr_accessor attribute
26
26
  define_attribute_methods attribute
27
- protected "#{attribute}=".to_sym
28
27
  end
29
28
  end
30
29
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # State callbacks provide an extensible mechanism for composing functionality for a state object.
3
+ # Callbacks provide an extensible mechanism for hooking into a State.
4
4
  module State
5
5
  module Callbacks
6
6
  extend ActiveSupport::Concern
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Accepts input representing the arguments and input which define the initial state.
3
+ # Accepts input representing the arguments and options which define the initial state.
4
4
  module State
5
5
  module Core
6
6
  extend ActiveSupport::Concern
@@ -9,7 +9,9 @@ module State
9
9
  class_attribute :_options, instance_writer: false, default: {}
10
10
 
11
11
  set_callback :initialize, :after do
12
- _options.each { |option, info| __send__("#{option}=".to_sym, info.default_value) if public_send(option).nil? }
12
+ _options.each do |option, info|
13
+ __send__("#{option}=".to_sym, info.default_value.dup) if public_send(option).nil?
14
+ end
13
15
  end
14
16
  end
15
17
 
@@ -20,7 +22,7 @@ module State
20
22
  super
21
23
  end
22
24
 
23
- protected
25
+ private
24
26
 
25
27
  def option(option, default: nil, &block)
26
28
  _options[option] = Option.new(default: default, &block)
@@ -5,8 +5,6 @@ module State
5
5
  module String
6
6
  extend ActiveSupport::Concern
7
7
 
8
- delegate :name, to: :class, prefix: true
9
-
10
8
  def to_s
11
9
  string_for(__method__)
12
10
  end
@@ -15,16 +13,14 @@ module State
15
13
  string_for(__method__)
16
14
  end
17
15
 
18
- protected
16
+ private
19
17
 
20
18
  def stringable_attributes
21
19
  self.class._attributes
22
20
  end
23
21
 
24
- private
25
-
26
22
  def string_for(method)
27
- "#<#{class_name} #{attribute_string(method)}>"
23
+ "#<#{self.class.name} #{attribute_string(method)}>"
28
24
  end
29
25
 
30
26
  def attribute_string(method)
@@ -9,6 +9,8 @@ require_relative "state/string"
9
9
 
10
10
  # A flow state is the immutable structure of relevant data.
11
11
  class StateBase
12
+ include ShortCircuIt
13
+ include Technologic
12
14
  include ActiveModel::Model
13
15
  include State::Callbacks
14
16
  include State::Attributes
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Flow
4
- VERSION = "0.3.0"
4
+ VERSION = "0.9.0"
5
5
  end
@@ -0,0 +1,11 @@
1
+ Description:
2
+ Stub an Flow and State with test.
3
+
4
+ Example:
5
+ `rails generate flow foo`
6
+
7
+ Generates:
8
+ Flow: app/flows/foo_flow.rb
9
+ State: app/states/foo_state.rb
10
+ Test: spec/flows/foo_flow_spec.rb
11
+ Test: spec/states/foo_state_spec.rb
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Create the ApplicationFlow and test.
3
+
4
+ Example:
5
+ `rails generate flow:application_flow`
6
+
7
+ Generates:
8
+ Flow: app/flows/application_flow.rb
9
+ Test: spec/flows/application_flow_spec.rb
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flow
4
+ module Generators
5
+ class ApplicationFlowGenerator < Rails::Generators::Base
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ hook_for :test_framework
9
+
10
+ def create_application_flow
11
+ template "application_flow.rb", File.join("app/flows/application_flow.rb")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ApplicationFlow < FlowBase; end
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Create the ApplicationOperation and test.
3
+
4
+ Example:
5
+ `rails generate flow:application_operation`
6
+
7
+ Generates:
8
+ Operation: app/operations/application_operation.rb
9
+ Test: spec/operations/application_operation_spec.rb