ruby_reactor 0.1.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 +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +98 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/README.md +570 -0
  6. data/Rakefile +12 -0
  7. data/documentation/DAG.md +457 -0
  8. data/documentation/README.md +123 -0
  9. data/documentation/async_reactors.md +369 -0
  10. data/documentation/composition.md +199 -0
  11. data/documentation/core_concepts.md +662 -0
  12. data/documentation/data_pipelines.md +224 -0
  13. data/documentation/examples/inventory_management.md +749 -0
  14. data/documentation/examples/order_processing.md +365 -0
  15. data/documentation/examples/payment_processing.md +654 -0
  16. data/documentation/getting_started.md +224 -0
  17. data/documentation/retry_configuration.md +357 -0
  18. data/lib/ruby_reactor/async_router.rb +91 -0
  19. data/lib/ruby_reactor/configuration.rb +41 -0
  20. data/lib/ruby_reactor/context.rb +169 -0
  21. data/lib/ruby_reactor/context_serializer.rb +164 -0
  22. data/lib/ruby_reactor/dependency_graph.rb +126 -0
  23. data/lib/ruby_reactor/dsl/compose_builder.rb +86 -0
  24. data/lib/ruby_reactor/dsl/map_builder.rb +112 -0
  25. data/lib/ruby_reactor/dsl/reactor.rb +151 -0
  26. data/lib/ruby_reactor/dsl/step_builder.rb +177 -0
  27. data/lib/ruby_reactor/dsl/template_helpers.rb +36 -0
  28. data/lib/ruby_reactor/dsl/validation_helpers.rb +35 -0
  29. data/lib/ruby_reactor/error/base.rb +16 -0
  30. data/lib/ruby_reactor/error/compensation_error.rb +8 -0
  31. data/lib/ruby_reactor/error/context_too_large_error.rb +11 -0
  32. data/lib/ruby_reactor/error/dependency_error.rb +8 -0
  33. data/lib/ruby_reactor/error/deserialization_error.rb +11 -0
  34. data/lib/ruby_reactor/error/input_validation_error.rb +29 -0
  35. data/lib/ruby_reactor/error/schema_version_error.rb +11 -0
  36. data/lib/ruby_reactor/error/step_failure_error.rb +18 -0
  37. data/lib/ruby_reactor/error/undo_error.rb +8 -0
  38. data/lib/ruby_reactor/error/validation_error.rb +8 -0
  39. data/lib/ruby_reactor/executor/compensation_manager.rb +79 -0
  40. data/lib/ruby_reactor/executor/graph_manager.rb +41 -0
  41. data/lib/ruby_reactor/executor/input_validator.rb +39 -0
  42. data/lib/ruby_reactor/executor/result_handler.rb +103 -0
  43. data/lib/ruby_reactor/executor/retry_manager.rb +156 -0
  44. data/lib/ruby_reactor/executor/step_executor.rb +319 -0
  45. data/lib/ruby_reactor/executor.rb +123 -0
  46. data/lib/ruby_reactor/map/collector.rb +65 -0
  47. data/lib/ruby_reactor/map/element_executor.rb +154 -0
  48. data/lib/ruby_reactor/map/execution.rb +60 -0
  49. data/lib/ruby_reactor/map/helpers.rb +67 -0
  50. data/lib/ruby_reactor/max_retries_exhausted_failure.rb +19 -0
  51. data/lib/ruby_reactor/reactor.rb +75 -0
  52. data/lib/ruby_reactor/retry_context.rb +92 -0
  53. data/lib/ruby_reactor/retry_queued_result.rb +26 -0
  54. data/lib/ruby_reactor/sidekiq_workers/map_collector_worker.rb +13 -0
  55. data/lib/ruby_reactor/sidekiq_workers/map_element_worker.rb +13 -0
  56. data/lib/ruby_reactor/sidekiq_workers/map_execution_worker.rb +15 -0
  57. data/lib/ruby_reactor/sidekiq_workers/worker.rb +55 -0
  58. data/lib/ruby_reactor/step/compose_step.rb +107 -0
  59. data/lib/ruby_reactor/step/map_step.rb +234 -0
  60. data/lib/ruby_reactor/step.rb +33 -0
  61. data/lib/ruby_reactor/storage/adapter.rb +51 -0
  62. data/lib/ruby_reactor/storage/configuration.rb +15 -0
  63. data/lib/ruby_reactor/storage/redis_adapter.rb +140 -0
  64. data/lib/ruby_reactor/template/base.rb +15 -0
  65. data/lib/ruby_reactor/template/element.rb +25 -0
  66. data/lib/ruby_reactor/template/input.rb +48 -0
  67. data/lib/ruby_reactor/template/result.rb +48 -0
  68. data/lib/ruby_reactor/template/value.rb +22 -0
  69. data/lib/ruby_reactor/validation/base.rb +26 -0
  70. data/lib/ruby_reactor/validation/input_validator.rb +62 -0
  71. data/lib/ruby_reactor/validation/schema_builder.rb +17 -0
  72. data/lib/ruby_reactor/version.rb +5 -0
  73. data/lib/ruby_reactor.rb +159 -0
  74. data/sig/ruby_reactor.rbs +4 -0
  75. metadata +178 -0
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReactor
4
+ module Dsl
5
+ module Reactor
6
+ def self.included(base)
7
+ base.extend(ClassMethods)
8
+ base.instance_variable_set(:@inputs, {})
9
+ base.instance_variable_set(:@steps, {})
10
+ base.instance_variable_set(:@return_step, nil)
11
+ base.instance_variable_set(:@middlewares, [])
12
+ base.instance_variable_set(:@input_validations, {})
13
+ base.instance_variable_set(:@async, false)
14
+ base.instance_variable_set(:@retry_defaults, { max_attempts: 3, backoff: :exponential, base_delay: 1 })
15
+ end
16
+
17
+ module ClassMethods
18
+ include RubyReactor::Dsl::TemplateHelpers
19
+ include RubyReactor::Dsl::ValidationHelpers
20
+
21
+ def inputs
22
+ @inputs ||= {}
23
+ end
24
+
25
+ def steps
26
+ @steps ||= {}
27
+ end
28
+
29
+ def return_step
30
+ @return_step
31
+ end
32
+
33
+ def middlewares
34
+ @middlewares ||= []
35
+ end
36
+
37
+ def input_validations
38
+ @input_validations ||= {}
39
+ end
40
+
41
+ def async(async = true)
42
+ @async = async
43
+ end
44
+
45
+ def async?
46
+ @async ||= false
47
+ end
48
+
49
+ def retry_defaults(**kwargs)
50
+ if kwargs.empty?
51
+ @retry_defaults ||= { max_attempts: 1, backoff: :exponential, base_delay: 1 }
52
+ else
53
+ @retry_defaults = {
54
+ max_attempts: kwargs[:max_attempts] || 1,
55
+ backoff: kwargs[:backoff] || :exponential,
56
+ base_delay: kwargs[:base_delay] || 1
57
+ }
58
+ end
59
+ end
60
+
61
+ # rubocop:disable Metrics/ParameterLists
62
+ def input(name, transform: nil, description: nil, validate: nil, optional: false, redact: false,
63
+ &validation_block)
64
+ # rubocop:enable Metrics/ParameterLists
65
+ inputs[name] = {
66
+ transform: transform,
67
+ description: description,
68
+ optional: optional,
69
+ redact: redact
70
+ }
71
+
72
+ # Handle validation
73
+ return unless validate || validation_block
74
+
75
+ validator = create_input_validator(validation_block || validate)
76
+ input_validations[name] = validator
77
+ end
78
+
79
+ def step(name, impl = nil, &block)
80
+ builder = RubyReactor::Dsl::StepBuilder.new(name, impl, self)
81
+
82
+ builder.instance_eval(&block) if block_given?
83
+
84
+ step_config = builder.build
85
+ steps[name] = step_config
86
+ step_config
87
+ end
88
+
89
+ def compose(name, composed_reactor_class = nil, &block)
90
+ builder = RubyReactor::Dsl::ComposeBuilder.new(name, composed_reactor_class, self, &block)
91
+
92
+ builder.instance_eval(&block) if block_given?
93
+
94
+ step_config = builder.build
95
+ steps[name] = step_config
96
+ step_config
97
+ end
98
+
99
+ def map(name, reactor_class = nil, &block)
100
+ builder = RubyReactor::Dsl::MapBuilder.new(name, reactor_class, self, &block)
101
+
102
+ builder.instance_eval(&block) if block_given?
103
+
104
+ step_config = builder.build
105
+ steps[name] = step_config
106
+ step_config
107
+ end
108
+
109
+ def returns(step_name)
110
+ @return_step = step_name
111
+ end
112
+
113
+ def middleware(middleware_class)
114
+ middlewares << middleware_class
115
+ end
116
+
117
+ def validate_inputs(inputs_hash)
118
+ errors = {}
119
+
120
+ input_validations.each do |input_name, validator|
121
+ # Skip validation if input is optional and not provided
122
+ next if inputs[input_name][:optional] && !inputs_hash.key?(input_name)
123
+
124
+ input_data = inputs_hash[input_name]
125
+ # Validate by wrapping the individual input in a hash with its name
126
+ result = validator.call({ input_name => input_data })
127
+
128
+ errors.merge!(result.error.field_errors) if result.failure? && result.error.respond_to?(:field_errors)
129
+ end
130
+
131
+ if errors.empty?
132
+ RubyReactor.Success(inputs_hash)
133
+ else
134
+ error = RubyReactor::Error::InputValidationError.new(errors)
135
+ RubyReactor.Failure(error)
136
+ end
137
+ end
138
+
139
+ # Entry point for running the reactor
140
+ def run(inputs = {})
141
+ reactor = new
142
+ reactor.run(inputs)
143
+ end
144
+
145
+ def call(inputs = {})
146
+ run(inputs)
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,177 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReactor
4
+ module Dsl
5
+ class StepBuilder
6
+ include RubyReactor::Dsl::TemplateHelpers
7
+ include RubyReactor::Dsl::ValidationHelpers
8
+
9
+ attr_accessor :name, :impl, :arguments, :run_block, :compensate_block, :undo_block, :conditions, :guards,
10
+ :dependencies, :args_validator, :output_validator, :retry_config
11
+
12
+ def initialize(name, impl = nil, reactor = nil)
13
+ @name = name
14
+ @impl = impl
15
+ @reactor = reactor
16
+ @arguments = {}
17
+ @run_block = nil
18
+ @compensate_block = nil
19
+ @undo_block = nil
20
+ @conditions = []
21
+ @guards = []
22
+ @dependencies = []
23
+ @args_validator = nil
24
+ @output_validator = nil
25
+ @async = false
26
+ @retry_config = {}
27
+ end
28
+
29
+ def argument(name, source, transform: nil)
30
+ @arguments[name] = {
31
+ source: source,
32
+ transform: transform
33
+ }
34
+ end
35
+
36
+ def run(&block)
37
+ @run_block = block
38
+ end
39
+
40
+ def compensate(&block)
41
+ @compensate_block = block
42
+ end
43
+
44
+ def undo(&block)
45
+ @undo_block = block
46
+ end
47
+
48
+ def where(&predicate)
49
+ @conditions << predicate
50
+ end
51
+
52
+ def guard(&guard_fn)
53
+ @guards << guard_fn
54
+ end
55
+
56
+ def wait_for(*step_names)
57
+ @dependencies.concat(step_names)
58
+ end
59
+
60
+ def validate_args(schema_or_validator = nil, &block)
61
+ if block_given?
62
+ @args_validator = build_input_validator(block)
63
+ elsif schema_or_validator
64
+ @args_validator = build_input_validator(schema_or_validator)
65
+ end
66
+ end
67
+
68
+ def validate_output(schema_or_validator = nil, &block)
69
+ if block_given?
70
+ @output_validator = build_input_validator(block)
71
+ elsif schema_or_validator
72
+ @output_validator = build_input_validator(schema_or_validator)
73
+ end
74
+ end
75
+
76
+ def async(async = true)
77
+ @async = async
78
+ end
79
+
80
+ def retries(max_attempts: 3, backoff: :exponential, base_delay: 1)
81
+ @retry_config = {
82
+ max_attempts: max_attempts,
83
+ backoff: backoff,
84
+ base_delay: base_delay
85
+ }
86
+ end
87
+
88
+ def build
89
+ step_config = {
90
+ name: @name,
91
+ impl: @impl,
92
+ arguments: @arguments,
93
+ run_block: @run_block,
94
+ compensate_block: @compensate_block,
95
+ undo_block: @undo_block,
96
+ conditions: @conditions,
97
+ guards: @guards,
98
+ dependencies: @dependencies,
99
+ args_validator: @args_validator,
100
+ output_validator: @output_validator,
101
+ async: @async,
102
+ retry_config: @retry_config.empty? ? (@reactor&.retry_defaults || {}) : @retry_config
103
+ }
104
+
105
+ RubyReactor::Dsl::StepConfig.new(step_config)
106
+ end
107
+
108
+ private
109
+
110
+ def build_input_validator(schema_or_block)
111
+ check_dry_validation_available!
112
+
113
+ schema = case schema_or_block
114
+ when Proc
115
+ build_validation_schema(&schema_or_block)
116
+ else
117
+ schema_or_block
118
+ end
119
+
120
+ RubyReactor::Validation::InputValidator.new(schema)
121
+ end
122
+
123
+ def build_validation_schema(&block)
124
+ RubyReactor::Validation::SchemaBuilder.build_from_block(&block)
125
+ end
126
+
127
+ def check_dry_validation_available!
128
+ return if defined?(Dry::Schema)
129
+
130
+ raise LoadError,
131
+ "dry-validation gem is required for validation features. Add 'gem \"dry-validation\"' to your Gemfile."
132
+ end
133
+ end
134
+
135
+ class StepConfig
136
+ attr_reader :name, :impl, :arguments, :run_block, :compensate_block, :undo_block, :conditions, :guards,
137
+ :dependencies, :args_validator, :output_validator, :async, :retry_config
138
+
139
+ def initialize(config)
140
+ @name = config[:name]
141
+ @impl = config[:impl]
142
+ @arguments = config[:arguments] || {}
143
+ @run_block = config[:run_block]
144
+ @compensate_block = config[:compensate_block]
145
+ @undo_block = config[:undo_block]
146
+ @conditions = config[:conditions] || []
147
+ @guards = config[:guards] || []
148
+ @dependencies = config[:dependencies] || []
149
+ @args_validator = config[:args_validator]
150
+ @output_validator = config[:output_validator]
151
+ @async = config[:async] || false
152
+ @retry_config = { max_attempts: 1 }.merge(config[:retry_config] || {})
153
+ end
154
+
155
+ def has_impl?
156
+ !@impl.nil?
157
+ end
158
+
159
+ def has_run_block?
160
+ !@run_block.nil?
161
+ end
162
+
163
+ def async?
164
+ @async
165
+ end
166
+
167
+ def retryable?
168
+ (retry_config[:max_attempts] || 0) > 1
169
+ end
170
+
171
+ def should_run?(context)
172
+ @conditions.all? { |condition| condition.call(context) } &&
173
+ @guards.all? { |guard| guard.call(context) }
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReactor
4
+ module Dsl
5
+ module TemplateHelpers
6
+ def input(name, path = nil)
7
+ RubyReactor::Template::Input.new(name, path)
8
+ end
9
+
10
+ def result(step_name, path = nil)
11
+ RubyReactor::Template::Result.new(step_name, path)
12
+ end
13
+
14
+ def value(val)
15
+ RubyReactor::Template::Value.new(val)
16
+ end
17
+
18
+ def element(map_name, path = nil)
19
+ RubyReactor::Template::Element.new(map_name, path)
20
+ end
21
+
22
+ # Make Success and Failure available in DSL contexts
23
+ # rubocop:disable Naming/MethodName
24
+ def Success(value = nil)
25
+ # rubocop:enable Naming/MethodName
26
+ RubyReactor.Success(value)
27
+ end
28
+
29
+ # rubocop:disable Naming/MethodName
30
+ def Failure(error)
31
+ # rubocop:enable Naming/MethodName
32
+ RubyReactor.Failure(error)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReactor
4
+ module Dsl
5
+ module ValidationHelpers
6
+ # Validation helper methods
7
+ def build_validation_schema(&block)
8
+ check_dry_validation_available!
9
+ RubyReactor::Validation::SchemaBuilder.build_from_block(&block)
10
+ end
11
+
12
+ def create_input_validator(schema_or_block)
13
+ check_dry_validation_available!
14
+
15
+ schema = case schema_or_block
16
+ when Proc
17
+ build_validation_schema(&schema_or_block)
18
+ else
19
+ schema_or_block
20
+ end
21
+
22
+ RubyReactor::Validation::InputValidator.new(schema)
23
+ end
24
+
25
+ private
26
+
27
+ def check_dry_validation_available!
28
+ return if defined?(Dry::Schema)
29
+
30
+ raise LoadError,
31
+ "dry-validation gem is required for validation features. Add 'gem \"dry-validation\"' to your Gemfile."
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReactor
4
+ module Error
5
+ class Base < StandardError
6
+ attr_reader :step, :context, :original_error
7
+
8
+ def initialize(message, step: nil, context: nil, original_error: nil)
9
+ super(message)
10
+ @step = step
11
+ @context = context
12
+ @original_error = original_error
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReactor
4
+ module Error
5
+ class CompensationError < Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReactor
4
+ module Error
5
+ class ContextTooLargeError < Base
6
+ def initialize(message)
7
+ super("Context size exceeds limits: #{message}")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReactor
4
+ module Error
5
+ class DependencyError < Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReactor
4
+ module Error
5
+ class DeserializationError < Base
6
+ def initialize(message)
7
+ super("Context deserialization failed: #{message}")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReactor
4
+ module Error
5
+ class InputValidationError < Base
6
+ attr_reader :field_errors
7
+
8
+ def initialize(field_errors)
9
+ @field_errors = field_errors
10
+ @message = build_message
11
+ super(@message)
12
+ end
13
+
14
+ def build_message
15
+ return "Input validation failed" if field_errors.empty?
16
+
17
+ error_messages = field_errors.map do |field, errors|
18
+ "#{field} #{errors}"
19
+ end
20
+
21
+ "Input validation failed: #{error_messages.join(", ")}"
22
+ end
23
+
24
+ def to_s
25
+ @message || build_message
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReactor
4
+ module Error
5
+ class SchemaVersionError < Base
6
+ def initialize(message)
7
+ super("Schema version mismatch: #{message}")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReactor
4
+ module Error
5
+ class StepFailureError < Base
6
+ attr_reader :step_arguments
7
+
8
+ def initialize(message, step: nil, context: nil, original_error: nil, step_arguments: {})
9
+ super(message, step: step, context: context, original_error: original_error)
10
+ @step_arguments = step_arguments
11
+ end
12
+
13
+ def retryable?
14
+ true
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReactor
4
+ module Error
5
+ class UndoError < Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReactor
4
+ module Error
5
+ class ValidationError < Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReactor
4
+ class Executor
5
+ class CompensationManager
6
+ def initialize(context)
7
+ @context = context
8
+ @undo_stack = []
9
+ @undo_trace = []
10
+ end
11
+
12
+ attr_reader :undo_stack, :undo_trace
13
+
14
+ def add_to_undo_stack(step_info)
15
+ @undo_stack << step_info
16
+ end
17
+
18
+ def handle_step_failure(step_config, error, arguments)
19
+ # Try compensation
20
+ compensation_result = compensate_step(step_config, error, arguments)
21
+ case compensation_result
22
+ when RubyReactor::Success
23
+ # Compensation succeeded, continue with rollback
24
+ rollback_completed_steps
25
+ RubyReactor.Failure("Step '#{step_config.name}' failed: #{error}")
26
+ when RubyReactor::Failure
27
+ # Compensation failed, this is more serious
28
+ rollback_completed_steps
29
+ raise Error::CompensationError.new(
30
+ "Compensation for step '#{step_config.name}' failed: #{compensation_result.error}",
31
+ step: step_config.name,
32
+ context: @context,
33
+ original_error: error
34
+ )
35
+ end
36
+ end
37
+
38
+ def rollback_completed_steps
39
+ @undo_stack.reverse_each do |step_info|
40
+ result = undo_step(step_info[:step], step_info[:result], step_info[:arguments])
41
+ @undo_trace << { type: :undo, step: step_info[:step].name, result: result,
42
+ arguments: step_info[:arguments] }
43
+ end
44
+ @undo_stack.clear
45
+ end
46
+
47
+ private
48
+
49
+ def compensate_step(step_config, error, arguments)
50
+ if step_config.compensate_block
51
+ @context.execution_trace << { type: :compensate, step: step_config.name, timestamp: Time.now, error: error,
52
+ arguments: arguments }
53
+ @undo_trace << { type: :compensation, step: step_config.name, error: error, arguments: arguments }
54
+ step_config.compensate_block.call(error, arguments, @context)
55
+ elsif step_config.has_impl?
56
+ @context.execution_trace << { type: :compensate, step: step_config.name, timestamp: Time.now, error: error,
57
+ arguments: arguments }
58
+ @undo_trace << { type: :compensation, step: step_config.name, error: error, arguments: arguments }
59
+ step_config.impl.compensate(error, arguments, @context)
60
+ else
61
+ RubyReactor.Success() # Default compensation
62
+ end
63
+ end
64
+
65
+ def undo_step(step_config, result, arguments)
66
+ @context.execution_trace << { type: :undo, step: step_config.name, timestamp: Time.now, result: result.value,
67
+ arguments: arguments }
68
+ if step_config.undo_block
69
+ step_config.undo_block.call(result.value, arguments, @context)
70
+ elsif step_config.has_impl?
71
+ step_config.impl.undo(result.value, arguments, @context)
72
+ end
73
+ rescue StandardError
74
+ # Log undo failure but don't halt the rollback process
75
+ # In a real implementation, this would use a logger
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyReactor
4
+ class Executor
5
+ class GraphManager
6
+ def initialize(reactor_class, dependency_graph, context)
7
+ @reactor_class = reactor_class
8
+ @dependency_graph = dependency_graph
9
+ @context = context
10
+ end
11
+
12
+ def build_and_validate!
13
+ build_dependency_graph
14
+ validate_graph!
15
+ end
16
+
17
+ def mark_completed_steps_from_context
18
+ @context.intermediate_results.each_key do |step_name|
19
+ @dependency_graph.complete_step(step_name)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def build_dependency_graph
26
+ @reactor_class.steps.each_value do |step_config|
27
+ @dependency_graph.add_step(step_config)
28
+ end
29
+ end
30
+
31
+ def validate_graph!
32
+ return unless @dependency_graph.has_cycles?
33
+
34
+ raise Error::DependencyError.new(
35
+ "Dependency graph contains cycles",
36
+ context: @context
37
+ )
38
+ end
39
+ end
40
+ end
41
+ end