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
@@ -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