next_station 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.
- checksums.yaml +7 -0
- data/.aiignore +36 -0
- data/.idea/.gitignore +10 -0
- data/.idea/inspectionProfiles/Project_Default.xml +8 -0
- data/.idea/junie.xml +6 -0
- data/.idea/modules.xml +8 -0
- data/.idea/next_station.iml +54 -0
- data/.idea/vcs.xml +6 -0
- data/AGENTS.md +157 -0
- data/Gemfile +11 -0
- data/PLUGIN_SYSTEM_GUIDE.md +521 -0
- data/README.md +790 -0
- data/TODO.txt +6 -0
- data/examples/plugin_http_example.rb +102 -0
- data/lib/next_station/config/errors.yml +149 -0
- data/lib/next_station/config.rb +49 -0
- data/lib/next_station/environment.rb +42 -0
- data/lib/next_station/errors.rb +21 -0
- data/lib/next_station/logging/formatters/console.rb +38 -0
- data/lib/next_station/logging/formatters/json.rb +80 -0
- data/lib/next_station/logging/subscribers/base.rb +70 -0
- data/lib/next_station/logging/subscribers/custom.rb +25 -0
- data/lib/next_station/logging/subscribers/operation.rb +41 -0
- data/lib/next_station/logging/subscribers/step.rb +54 -0
- data/lib/next_station/logging.rb +35 -0
- data/lib/next_station/operation/class_methods.rb +299 -0
- data/lib/next_station/operation/errors.rb +97 -0
- data/lib/next_station/operation/node.rb +49 -0
- data/lib/next_station/operation.rb +393 -0
- data/lib/next_station/plugins.rb +23 -0
- data/lib/next_station/result.rb +124 -0
- data/lib/next_station/state.rb +64 -0
- data/lib/next_station/types.rb +11 -0
- data/lib/next_station/version.rb +5 -0
- data/lib/next_station.rb +36 -0
- metadata +203 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'logging/subscribers/operation'
|
|
4
|
+
require_relative 'logging/subscribers/step'
|
|
5
|
+
require_relative 'logging/subscribers/custom'
|
|
6
|
+
|
|
7
|
+
module NextStation
|
|
8
|
+
# Entry point for logging configuration and setup.
|
|
9
|
+
module Logging
|
|
10
|
+
# Initializes the default logging subscribers.
|
|
11
|
+
# @param monitor [Dry::Monitor::Notifications] The monitor to subscribe to.
|
|
12
|
+
# @return [void]
|
|
13
|
+
def self.setup!(monitor = NextStation.config.monitor)
|
|
14
|
+
setup_formatter!
|
|
15
|
+
Subscribers::Operation.subscribe(monitor)
|
|
16
|
+
Subscribers::Step.subscribe(monitor)
|
|
17
|
+
Subscribers::Custom.subscribe(monitor)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Selects the log formatter based on the current environment.
|
|
21
|
+
# It uses the Console formatter for development and the JSON formatter otherwise.
|
|
22
|
+
# @return [void]
|
|
23
|
+
def self.setup_formatter!
|
|
24
|
+
formatter = if NextStation.config.environment.development?
|
|
25
|
+
Formatter::Console.new
|
|
26
|
+
else
|
|
27
|
+
Formatter::Json.new
|
|
28
|
+
end
|
|
29
|
+
NextStation.config.logger.formatter = formatter
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private_class_method :setup_formatter!
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NextStation
|
|
4
|
+
class Operation
|
|
5
|
+
|
|
6
|
+
# The `ClassMethods` module provides a set of class-level methods for
|
|
7
|
+
# defining the structure, validation, dependencies, and processing logic
|
|
8
|
+
# of an operation. It introduces a DSL for custom error handling, step
|
|
9
|
+
# management, schema enforcement, and parameter validation.
|
|
10
|
+
#
|
|
11
|
+
# === Error Handling
|
|
12
|
+
# - Allows defining custom error types for the operation using a DSL.
|
|
13
|
+
#
|
|
14
|
+
# === Result Management
|
|
15
|
+
# - Supports setting and retrieving the key where the operation result
|
|
16
|
+
# is stored.
|
|
17
|
+
# - Enables schema enforcement for result values using a Dry::Struct schema.
|
|
18
|
+
#
|
|
19
|
+
# === Steps and Branches
|
|
20
|
+
# - Facilitates defining execution steps and branches within an operation.
|
|
21
|
+
# - Allows querying the presence of specific steps within the operation.
|
|
22
|
+
#
|
|
23
|
+
# === Validation
|
|
24
|
+
# - Provides integration with Dry::Validation for validating operation parameters.
|
|
25
|
+
# - Enables enforcing or skipping validation on demand.
|
|
26
|
+
#
|
|
27
|
+
# === Dependencies
|
|
28
|
+
# - Supports defining and managing dependencies for the operation.
|
|
29
|
+
#
|
|
30
|
+
# === Methods
|
|
31
|
+
# - {#errors}: Defines custom error types using an error DSL.
|
|
32
|
+
# - {#error_definitions}: Returns all defined error mappings.
|
|
33
|
+
# - {#result_at}: Defines the key in the state for the operation result.
|
|
34
|
+
# - {#result_key}: Retrieves the key where the operation result is stored.
|
|
35
|
+
# - {#result_schema}: Defines a schema for the result using Dry::Struct.
|
|
36
|
+
# - {#result_class}: Retrieves the Dry::Struct class for the result schema.
|
|
37
|
+
# - {#enforce_result_schema}: Enables schema enforcement for the result.
|
|
38
|
+
# - {#disable_result_schema}: Disables schema enforcement for the result.
|
|
39
|
+
# - {#schema_enforced?}: Checks if schema enforcement is enabled.
|
|
40
|
+
# - {#process}: Defines the root execution block for the operation.
|
|
41
|
+
# - {#step}: Adds a single execution step to the operation.
|
|
42
|
+
# - {#branch}: Adds a conditional branch to the operation's execution path.
|
|
43
|
+
# - {#steps}: Returns the defined execution steps for the operation.
|
|
44
|
+
# - {#validate_with}: Defines validation rules using Dry::Validation.
|
|
45
|
+
# - {#validation_contract_class}: Retrieves the validation contract class.
|
|
46
|
+
# - {#validation_contract_instance}: Returns an instance of the
|
|
47
|
+
# validation contract.
|
|
48
|
+
# - {#force_validation!}: Forces validation to be applied, even if not part
|
|
49
|
+
# of the operation steps.
|
|
50
|
+
# - {#skip_validation!}: Skips validation for the operation.
|
|
51
|
+
# - {#validation_enforced?}: Checks if validation is enforced.
|
|
52
|
+
# - {#has_step?}: Checks the presence of a specific step in the operation.
|
|
53
|
+
# - {#depends}: Defines dependencies required by the operation.
|
|
54
|
+
# - {#dependencies}: Retrieves the defined dependencies.
|
|
55
|
+
module ClassMethods
|
|
56
|
+
# Defines error types for the operation.
|
|
57
|
+
# @param external_source [Class, Hash, nil] An external error collection class or a hash of definitions.
|
|
58
|
+
# @yield The block defining errors via ErrorsDSL.
|
|
59
|
+
def errors(external_source = nil, &block)
|
|
60
|
+
@error_definitions ||= {}
|
|
61
|
+
|
|
62
|
+
# 1. Handle external source (e.g., SharedErrors < NextStation::Errors)
|
|
63
|
+
if external_source.respond_to?(:definitions)
|
|
64
|
+
@error_definitions.merge!(external_source.definitions)
|
|
65
|
+
elsif external_source.is_a?(Hash)
|
|
66
|
+
external_source.each do |type, config|
|
|
67
|
+
definition = ErrorDefinition.new(type)
|
|
68
|
+
definition.message(config[:message]) if config[:message]
|
|
69
|
+
definition.help_url(config[:help_url]) if config[:help_url]
|
|
70
|
+
definition.validate!
|
|
71
|
+
@error_definitions[type] = definition
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# 2. Handle inline block
|
|
76
|
+
if block_given?
|
|
77
|
+
dsl = ErrorsDSL.new
|
|
78
|
+
dsl.instance_eval(&block)
|
|
79
|
+
@error_definitions.merge!(dsl.definitions)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @return [Hash] The registered error definitions.
|
|
84
|
+
def error_definitions
|
|
85
|
+
parent_defs = if superclass.respond_to?(:error_definitions)
|
|
86
|
+
superclass.error_definitions
|
|
87
|
+
else
|
|
88
|
+
{}
|
|
89
|
+
end
|
|
90
|
+
parent_defs.merge(@error_definitions || {})
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Defines the key in the state where the final result is stored.
|
|
94
|
+
# @param key [Symbol]
|
|
95
|
+
def result_at(key)
|
|
96
|
+
@result_key = key
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# @return [Symbol, nil] The key where the result is stored.
|
|
100
|
+
def result_key
|
|
101
|
+
@result_key || (superclass.result_key if superclass.respond_to?(:result_key))
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Defines a Dry::Struct schema for the result value.
|
|
105
|
+
# @param struct_class [Class, nil] A Dry::Struct class or nil if a block is provided.
|
|
106
|
+
# @yield The block defining the schema.
|
|
107
|
+
def result_schema(struct_class = nil, &block)
|
|
108
|
+
require 'dry-struct'
|
|
109
|
+
|
|
110
|
+
if @result_class
|
|
111
|
+
raise NextStation::DoubleResultSchemaError, 'result_schema has already been defined'
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
if struct_class && block_given?
|
|
115
|
+
raise NextStation::DoubleResultSchemaError, 'result_schema accepts either a Dry::Struct class OR a block, but not both.'
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
if struct_class
|
|
119
|
+
if struct_class.is_a?(Class) && struct_class < Dry::Struct
|
|
120
|
+
@result_class = struct_class
|
|
121
|
+
else
|
|
122
|
+
raise ArgumentError, 'result_schema requires a subclass of Dry::Struct'
|
|
123
|
+
end
|
|
124
|
+
elsif block_given?
|
|
125
|
+
@result_class = Class.new(Dry::Struct, &block)
|
|
126
|
+
const_set(:ResultSchema, @result_class) unless const_defined?(:ResultSchema, false)
|
|
127
|
+
else
|
|
128
|
+
raise ArgumentError, 'result_schema requires either a Dry::Struct class or a block'
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
@schema_enforced = true
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @return [Class, nil] The Dry::Struct class for the result.
|
|
135
|
+
def result_class
|
|
136
|
+
@result_class || (superclass.result_class if superclass.respond_to?(:result_class))
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Enables result schema enforcement.
|
|
140
|
+
def enforce_result_schema
|
|
141
|
+
@schema_enforced = true
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Disables result schema enforcement.
|
|
145
|
+
def disable_result_schema
|
|
146
|
+
@schema_enforced = false
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @return [Boolean] Whether schema enforcement is enabled.
|
|
150
|
+
def schema_enforced?
|
|
151
|
+
return @schema_enforced unless @schema_enforced.nil?
|
|
152
|
+
return superclass.schema_enforced? if superclass.respond_to?(:schema_enforced?)
|
|
153
|
+
|
|
154
|
+
false
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Defines the root execution block for the operation.
|
|
158
|
+
# @yield The block defining steps and branches.
|
|
159
|
+
def process(&block)
|
|
160
|
+
@root = Node.new(:root, &block)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Adds a step to the operation.
|
|
164
|
+
# @param method_name [Symbol]
|
|
165
|
+
# @param options [Hash]
|
|
166
|
+
def step(method_name, options = {})
|
|
167
|
+
@root ||= Node.new(:root)
|
|
168
|
+
@root.step(method_name, options)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Adds a branch to the operation.
|
|
172
|
+
# @param condition [Proc]
|
|
173
|
+
# @yield
|
|
174
|
+
def branch(condition, &block)
|
|
175
|
+
@root ||= Node.new(:root)
|
|
176
|
+
@root.branch(condition, &block)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# @return [Array<Node>] The steps defined for the operation.
|
|
180
|
+
def steps
|
|
181
|
+
@root&.children || (superclass.steps if superclass.respond_to?(:steps)) || []
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Defines a Dry::Validation::Contract to validate the params.
|
|
185
|
+
#
|
|
186
|
+
# @param contract_or_block [Class, nil] A Contract class or nil if a block is provided.
|
|
187
|
+
# @yield The block defining the validation rules.
|
|
188
|
+
def validate_with(contract_or_block = nil, &block)
|
|
189
|
+
require 'dry-validation'
|
|
190
|
+
@validation_contract_class = if block_given?
|
|
191
|
+
Class.new(Dry::Validation::Contract) do
|
|
192
|
+
config.messages.backend = :yaml
|
|
193
|
+
config.messages.top_namespace = 'next_station_validations'
|
|
194
|
+
config.messages.load_paths << File.expand_path('../../config/errors.yml', __FILE__)
|
|
195
|
+
instance_eval(&block)
|
|
196
|
+
end
|
|
197
|
+
elsif contract_or_block.is_a?(Class) && contract_or_block < Dry::Validation::Contract
|
|
198
|
+
contract_or_block
|
|
199
|
+
else
|
|
200
|
+
raise ValidationError,
|
|
201
|
+
'validate_with requires a block or a Dry::Validation::Contract class'
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
@validation_enforced = true
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# @return [Class, nil] The validation contract class.
|
|
208
|
+
def validation_contract_class
|
|
209
|
+
@validation_contract_class || (if superclass.respond_to?(:validation_contract_class)
|
|
210
|
+
superclass.validation_contract_class
|
|
211
|
+
end)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# @return [Dry::Validation::Contract, nil] An instance of the validation contract.
|
|
215
|
+
def validation_contract_instance
|
|
216
|
+
@validation_contract_instance ||= validation_contract_class&.new
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Forces validation even if not explicitly defined in steps.
|
|
220
|
+
def force_validation!
|
|
221
|
+
@validation_enforced = true
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Skips validation even if defined.
|
|
225
|
+
def skip_validation!
|
|
226
|
+
@validation_enforced = false
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# @return [Boolean] Whether validation is enforced.
|
|
230
|
+
def validation_enforced?
|
|
231
|
+
return @validation_enforced unless @validation_enforced.nil?
|
|
232
|
+
|
|
233
|
+
superclass.respond_to?(:validation_enforced?) ? superclass.validation_enforced? : false
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Checks if a step exists in the operation.
|
|
237
|
+
# @param name [Symbol]
|
|
238
|
+
# @param nodes [Array<Node>]
|
|
239
|
+
# @return [Boolean]
|
|
240
|
+
def has_step?(name, nodes = steps)
|
|
241
|
+
nodes.any? do |node|
|
|
242
|
+
node.name == name || (node.type == :branch && has_step?(name, node.children))
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Defines dependencies for the operation.
|
|
247
|
+
# @param deps [Hash] A mapping of dependency names to values or Procs.
|
|
248
|
+
# @example depends mailer: -> { Mailer.new }
|
|
249
|
+
# @example depends repository: UserRepository.new
|
|
250
|
+
# @example Usage inside a step:
|
|
251
|
+
# def send_email
|
|
252
|
+
# # Access dependencies using the dependency() method
|
|
253
|
+
# dependency(:mailer).send_welcome(state.params[:email])
|
|
254
|
+
# # rest of the step
|
|
255
|
+
# end
|
|
256
|
+
#
|
|
257
|
+
# @example You can override the dependencies when instantiating the operation by passing the deps: argument:
|
|
258
|
+
# mock_mailer = double("Mailer")
|
|
259
|
+
# operation = CreateUser.new(deps: { mailer: mock_mailer })
|
|
260
|
+
# operation.call(email: "test@example.com")
|
|
261
|
+
def depends(deps)
|
|
262
|
+
@dependencies = dependencies.merge(deps)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# @return [Hash] The defined dependencies.
|
|
266
|
+
def dependencies
|
|
267
|
+
@dependencies || (superclass.respond_to?(:dependencies) ? superclass.dependencies : {})
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Convenience method to instantiate and call the operation.
|
|
271
|
+
def call(params = {}, context = {}, deps: {})
|
|
272
|
+
new(deps: deps).call(params, context)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Enables a plugin for the operation.
|
|
276
|
+
# @param name [Symbol] The registered plugin name.
|
|
277
|
+
def plugin(name)
|
|
278
|
+
require 'dry-configurable'
|
|
279
|
+
mod = NextStation::Plugins.load_plugin(name)
|
|
280
|
+
loaded_plugins << mod
|
|
281
|
+
|
|
282
|
+
extend mod::ClassMethods if mod.const_defined?(:ClassMethods)
|
|
283
|
+
include mod::InstanceMethods if mod.const_defined?(:InstanceMethods)
|
|
284
|
+
NextStation::Operation::Node.include mod::DSL if mod.const_defined?(:DSL)
|
|
285
|
+
|
|
286
|
+
if mod.const_defined?(:Errors) && mod::Errors.respond_to?(:definitions)
|
|
287
|
+
errors(mod::Errors.definitions)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
mod.configure(self) if mod.respond_to?(:configure)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# @return [Array<Module>] The plugins loaded into this operation.
|
|
294
|
+
def loaded_plugins
|
|
295
|
+
@loaded_plugins ||= (superclass.respond_to?(:loaded_plugins) ? superclass.loaded_plugins.dup : [])
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NextStation
|
|
4
|
+
class Operation
|
|
5
|
+
# Raised internally to stop the operation flow and return a failure.
|
|
6
|
+
class Halt < StandardError
|
|
7
|
+
# @return [Symbol] The error type.
|
|
8
|
+
attr_reader :type
|
|
9
|
+
# @return [Hash] Keys for message interpolation.
|
|
10
|
+
attr_reader :msg_keys
|
|
11
|
+
# @return [Hash] Additional error details.
|
|
12
|
+
attr_reader :details
|
|
13
|
+
# @return [NextStation::Result::Error] An existing error object.
|
|
14
|
+
attr_reader :error
|
|
15
|
+
|
|
16
|
+
# @param type [Symbol] The error type.
|
|
17
|
+
# @param msg_keys [Hash] Keys for message interpolation.
|
|
18
|
+
# @param details [Hash] Additional error details.
|
|
19
|
+
# @param error [NextStation::Result::Error] An existing error object.
|
|
20
|
+
def initialize(type: nil, msg_keys: {}, details: {}, error: nil)
|
|
21
|
+
@type = type
|
|
22
|
+
@msg_keys = msg_keys
|
|
23
|
+
@details = details
|
|
24
|
+
@error = error
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Defines an error with its messages and optional help URL.
|
|
29
|
+
class ErrorDefinition
|
|
30
|
+
# @return [Symbol] The error type.
|
|
31
|
+
attr_reader :type
|
|
32
|
+
# @return [Hash] Map of locales to message templates.
|
|
33
|
+
attr_reader :messages
|
|
34
|
+
# @return [String, nil] The help URL for this error.
|
|
35
|
+
attr_reader :help_url
|
|
36
|
+
|
|
37
|
+
# @param type [Symbol] The error type.
|
|
38
|
+
def initialize(type)
|
|
39
|
+
@type = type
|
|
40
|
+
@messages = {}
|
|
41
|
+
@help_url = nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Adds localized messages for the error.
|
|
45
|
+
# @param hashes [Hash] A hash mapping locale symbols to message templates.
|
|
46
|
+
def message(hashes)
|
|
47
|
+
@messages.merge!(hashes)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Sets or returns the help URL for the error.
|
|
51
|
+
# @param url [String, nil] The URL to set.
|
|
52
|
+
# @return [String, nil] The current help URL.
|
|
53
|
+
def help_url(url = nil)
|
|
54
|
+
return @help_url if url.nil?
|
|
55
|
+
raise 'Only one help_url is allowed' if @help_url
|
|
56
|
+
|
|
57
|
+
@help_url = url
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Validates whether the error definition is complete.
|
|
61
|
+
# @raise [RuntimeError] if the English message is missing.
|
|
62
|
+
def validate!
|
|
63
|
+
raise "English message is required for error type: #{@type}" unless @messages[:en]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Resolves the error message for a given language.
|
|
67
|
+
# @param lang [Symbol, String]
|
|
68
|
+
# @param msg_keys [Hash]
|
|
69
|
+
# @return [String]
|
|
70
|
+
def resolve_message(lang, msg_keys)
|
|
71
|
+
template = @messages[lang.to_sym] || @messages[:en]
|
|
72
|
+
template % msg_keys
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# DSL for defining multiple errors.
|
|
77
|
+
class ErrorsDSL
|
|
78
|
+
# @return [Hash<Symbol, ErrorDefinition>]
|
|
79
|
+
attr_reader :definitions
|
|
80
|
+
|
|
81
|
+
# Initializes a new ErrorsDSL.
|
|
82
|
+
def initialize
|
|
83
|
+
@definitions = {}
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Defines a new error type.
|
|
87
|
+
# @param type [Symbol] The error type.
|
|
88
|
+
# @yield [ErrorDefinition] The block to configure the error.
|
|
89
|
+
def error_type(type, &block)
|
|
90
|
+
definition = ErrorDefinition.new(type)
|
|
91
|
+
definition.instance_eval(&block) if block_given?
|
|
92
|
+
definition.validate!
|
|
93
|
+
@definitions[type] = definition
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NextStation
|
|
4
|
+
class Operation
|
|
5
|
+
# Represents a node in the operation's execution graph (step or branch).
|
|
6
|
+
class Node
|
|
7
|
+
# @return [Symbol] The node type (:step, :branch, or :root).
|
|
8
|
+
attr_reader :type
|
|
9
|
+
# @return [Symbol, nil] The name of the step.
|
|
10
|
+
attr_reader :name
|
|
11
|
+
# @return [Hash] Execution options.
|
|
12
|
+
attr_reader :options
|
|
13
|
+
# @return [Array<Node>] Child nodes (for branches or root).
|
|
14
|
+
attr_reader :children
|
|
15
|
+
|
|
16
|
+
# @param type [Symbol] :step, :branch, or :root.
|
|
17
|
+
# @param name [Symbol, nil] The name of the step.
|
|
18
|
+
# @param options [Hash] Execution options.
|
|
19
|
+
# @yield The block for branch nodes.
|
|
20
|
+
def initialize(type, name = nil, options = {}, &block)
|
|
21
|
+
@type = type
|
|
22
|
+
@name = name
|
|
23
|
+
@options = options
|
|
24
|
+
@children = []
|
|
25
|
+
instance_eval(&block) if block_given?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Adds a child node.
|
|
29
|
+
# @param node [Node]
|
|
30
|
+
def add_child(node)
|
|
31
|
+
@children << node
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Adds a step to the node.
|
|
35
|
+
# @param name [Symbol] The method name to execute.
|
|
36
|
+
# @param options [Hash] Execution options like :skip_if, :retry_if, :attempts, :delay.
|
|
37
|
+
def step(name, options = {})
|
|
38
|
+
@children << Node.new(:step, name, options)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Adds a branch to the node.
|
|
42
|
+
# @param condition [Proc] A proc that receives the state and returns a boolean.
|
|
43
|
+
# @yield The block defining steps inside the branch.
|
|
44
|
+
def branch(condition, &block)
|
|
45
|
+
@children << Node.new(:branch, nil, { condition: condition }, &block)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|