ruby_slm 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.
@@ -0,0 +1,523 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'timeout'
4
+ require 'securerandom'
5
+ require 'jsonpath'
6
+
7
+ module StatesLanguageMachine
8
+ module States
9
+ class Task < Base
10
+ # @return [String] the resource ARN or URI to invoke
11
+ attr_reader :resource
12
+ # @return [Integer, nil] the timeout in seconds for the task
13
+ attr_reader :timeout_seconds
14
+ # @return [Integer, nil] the heartbeat interval in seconds
15
+ attr_reader :heartbeat_seconds
16
+ # @return [Array<Hash>] the retry configuration
17
+ attr_reader :retry
18
+ # @return [Array<Hash>] the catch configuration
19
+ attr_reader :catch
20
+ # @return [Hash] the parameters to pass to the resource
21
+ attr_reader :parameters
22
+ # @return [String, nil] the result path
23
+ attr_reader :result_path
24
+ # @return [Hash, nil] the result selector
25
+ attr_reader :result_selector
26
+ # @return [String, nil] the input path
27
+ attr_reader :input_path
28
+ # @return [String, nil] the output path
29
+ attr_reader :output_path
30
+ # @return [String, nil] the credentials ARN for the task
31
+ attr_reader :credentials
32
+ # @return [String, nil] the comment for the state
33
+ attr_reader :comment
34
+
35
+ # Intrinsic functions supported by AWS Step Functions
36
+ INTRINSIC_FUNCTIONS = %w[
37
+ States.Format States.StringToJson States.JsonToString
38
+ States.Array States.ArrayPartition States.ArrayContains
39
+ States.ArrayRange States.ArrayGetItem States.ArrayLength
40
+ States.ArrayUnique States.Base64Encode States.Base64Decode
41
+ States.Hash States.JsonMerge States.MathRandom
42
+ States.MathAdd States.StringSplit States.UUID
43
+ ].freeze
44
+
45
+ # @param name [String] the name of the state
46
+ # @param definition [Hash] the state definition
47
+ def initialize(name, definition)
48
+ super
49
+ @resource = definition["Resource"]
50
+ @timeout_seconds = definition["TimeoutSeconds"]
51
+ @heartbeat_seconds = definition["HeartbeatSeconds"]
52
+ @parameters = definition["Parameters"] || {}
53
+ @result_path = definition["ResultPath"]
54
+ @result_selector = definition["ResultSelector"]
55
+ @input_path = definition["InputPath"]
56
+ @output_path = definition["OutputPath"]
57
+ @credentials = definition["Credentials"]
58
+ @comment = definition["Comment"]
59
+ @retry = definition["Retry"] || []
60
+ @catch = definition["Catch"] || []
61
+
62
+ # Initialize retry and catch objects
63
+ @retry_objects = @retry.map { |r| RetryPolicy.new(r) }
64
+ @catch_objects = @catch.map { |c| CatchPolicy.new(c) }
65
+
66
+ validate!
67
+ end
68
+
69
+ # @param execution [Execution] the current execution
70
+ # @param input [Hash] the input data for the state
71
+ # @return [Hash] the output data from the state
72
+ def execute(execution, input)
73
+ execution.logger&.info("Executing task state: #{@name}")
74
+ execution.context[:current_state] = @name
75
+ execution.context[:state_entered_time] = Time.now
76
+
77
+ begin
78
+ if @timeout_seconds || @heartbeat_seconds
79
+ execute_with_timeout(execution, input)
80
+ else
81
+ execute_without_timeout(execution, input)
82
+ end
83
+ rescue => error
84
+ handle_execution_error(execution, error, input)
85
+ end
86
+ end
87
+
88
+ # Check if this task supports retry for a specific error
89
+ # @param error [Exception] the error to check
90
+ # @param attempt [Integer] the current attempt number
91
+ # @return [RetryPolicy, nil] the retry policy if applicable
92
+ def retry_policy_for(error, attempt)
93
+ @retry_objects.find do |retry_policy|
94
+ retry_policy.matches?(error, attempt)
95
+ end
96
+ end
97
+
98
+ # Check if this task has a catch handler for a specific error
99
+ # @param error [Exception] the error to check
100
+ #
101
+ # @return [CatchPolicy, nil] the catch policy if applicable
102
+ def catch_policy_for(error)
103
+ @catch_objects.find do |catch_policy|
104
+ catch_policy.matches?(error)
105
+ end
106
+ end
107
+
108
+ private
109
+
110
+ # Execute task with timeout and heartbeat support
111
+ def execute_with_timeout(execution, input)
112
+ timeout = @timeout_seconds || 999999 # Very high default timeout
113
+
114
+ Timeout.timeout(timeout) do
115
+ if @heartbeat_seconds
116
+ execute_with_heartbeat(execution, input)
117
+ else
118
+ execute_task_logic(execution, input)
119
+ end
120
+ end
121
+ rescue Timeout::Error
122
+ execution.logger&.error("Task '#{@name}' timed out after #{timeout} seconds")
123
+ raise TaskTimeoutError.new("Task timed out after #{timeout} seconds")
124
+ end
125
+
126
+ # Execute task with heartbeat monitoring
127
+ def execute_with_heartbeat(execution, input)
128
+ heartbeat_thread = start_heartbeat_monitor(execution)
129
+ result = execute_task_logic(execution, input)
130
+ heartbeat_thread&.kill
131
+ result
132
+ rescue => error
133
+ heartbeat_thread&.kill
134
+ raise error
135
+ end
136
+
137
+ # Start heartbeat monitoring thread
138
+ def start_heartbeat_monitor(execution)
139
+ return unless @heartbeat_seconds
140
+
141
+ Thread.new do
142
+ loop do
143
+ sleep @heartbeat_seconds
144
+ execution.logger&.debug("Heartbeat from task: #{@name}")
145
+ # In a real implementation, you might send actual heartbeat notifications
146
+ end
147
+ end
148
+ end
149
+
150
+ # Execute task without timeout constraints
151
+ def execute_without_timeout(execution, input)
152
+ execute_task_logic(execution, input)
153
+ end
154
+
155
+ # Main task execution logic
156
+ def execute_task_logic(execution, input)
157
+ # Apply input path
158
+ processed_input = apply_input_path(input, @input_path)
159
+
160
+ # Apply parameters with intrinsic function support
161
+ final_input = apply_parameters(processed_input, execution.context)
162
+
163
+ # Execute the actual task
164
+ execution.logger&.debug("Invoking resource: #{@resource}")
165
+ result = execute_task(execution, final_input)
166
+
167
+ # Apply result selector
168
+ selected_result = apply_result_selector(result, execution.context)
169
+
170
+ # Apply result path
171
+ output = apply_result_path(input, selected_result, @result_path)
172
+
173
+ # Apply output path
174
+ final_output = apply_output_path(output, @output_path)
175
+
176
+ process_result(execution, final_output)
177
+ final_output
178
+ end
179
+
180
+ # Execute the task using the configured executor
181
+ def execute_task(execution, input)
182
+ if execution.context[:task_executor]
183
+ execution.context[:task_executor].call(@resource, input, @credentials)
184
+ else
185
+ simulate_task_execution(input)
186
+ end
187
+ end
188
+
189
+ # Simulate task execution for testing/demo
190
+ def simulate_task_execution(input)
191
+ # Simulate some processing time
192
+ sleep(0.1) if ENV['SIMULATE_TASK_DELAY']
193
+
194
+ {
195
+ "task_result" => "completed",
196
+ "resource" => @resource,
197
+ "input_received" => input,
198
+ "timestamp" => Time.now.to_i,
199
+ "execution_id" => SecureRandom.uuid,
200
+ "simulated" => true
201
+ }
202
+ end
203
+
204
+ # Apply parameters with intrinsic function support
205
+ def apply_parameters(parameters_template, context)
206
+ return parameters_template if parameters_template.empty?
207
+
208
+ evaluate_parameters(parameters_template, context)
209
+ end
210
+
211
+ # Recursively evaluate parameters including intrinsic functions
212
+ def evaluate_parameters(value, context)
213
+ case value
214
+ when Hash
215
+ value.transform_values { |v| evaluate_parameters(v, context) }
216
+ when Array
217
+ value.map { |v| evaluate_parameters(v, context) }
218
+ when String
219
+ evaluate_intrinsic_functions(value, context)
220
+ else
221
+ value
222
+ end
223
+ end
224
+
225
+ # Evaluate intrinsic functions and JSONPath references
226
+ def evaluate_intrinsic_functions(value, context)
227
+ # Handle JSONPath references (starts with $.)
228
+ if value.start_with?('$.')
229
+ return get_value_from_path(context[value[2..-1].to_sym] || {}, value)
230
+ end
231
+
232
+ # Handle intrinsic functions
233
+ intrinsic_function = INTRINSIC_FUNCTIONS.find { |func| value.include?(func) }
234
+ return value unless intrinsic_function
235
+
236
+ case intrinsic_function
237
+ when 'States.Format'
238
+ evaluate_format_function(value, context)
239
+ when 'States.StringToJson'
240
+ evaluate_string_to_json(value, context)
241
+ when 'States.JsonToString'
242
+ evaluate_json_to_string(value, context)
243
+ when 'States.Array'
244
+ evaluate_array_function(value, context)
245
+ when 'States.MathRandom'
246
+ evaluate_math_random(value, context)
247
+ when 'States.UUID'
248
+ SecureRandom.uuid
249
+ else
250
+ value # Return as-is for unimplemented functions
251
+ end
252
+ end
253
+
254
+ # Evaluate States.Format intrinsic function
255
+ def evaluate_format_function(value, context)
256
+ # Extract format string and arguments from the intrinsic function
257
+ match = value.match(/States\.Format\('([^']+)',\s*(.+)\)/)
258
+ return value unless match
259
+
260
+ format_string = match[1]
261
+ arguments_json = match[2]
262
+
263
+ begin
264
+ arguments = evaluate_parameters(JSON.parse(arguments_json), context)
265
+ format(format_string, *arguments)
266
+ rescue => e
267
+ value # Return original if parsing fails
268
+ end
269
+ end
270
+
271
+ # Evaluate States.StringToJson intrinsic function
272
+ def evaluate_string_to_json(value, context)
273
+ match = value.match(/States\.StringToJson\((.+)\)/)
274
+ return value unless match
275
+
276
+ string_value = evaluate_parameters(match[1], context)
277
+ JSON.parse(string_value)
278
+ rescue => e
279
+ value
280
+ end
281
+
282
+ # Evaluate States.JsonToString intrinsic function
283
+ def evaluate_json_to_string(value, context)
284
+ match = value.match(/States\.JsonToString\((.+)\)/)
285
+ return value unless match
286
+
287
+ json_value = evaluate_parameters(match[1], context)
288
+ JSON.generate(json_value)
289
+ rescue => e
290
+ value
291
+ end
292
+
293
+ # Evaluate States.Array intrinsic function
294
+ def evaluate_array_function(value, context)
295
+ match = value.match(/States\.Array\((.+)\)/)
296
+ return value unless match
297
+
298
+ elements_json = match[1]
299
+ evaluate_parameters(JSON.parse("[#{elements_json}]"), context)
300
+ rescue => e
301
+ value
302
+ end
303
+
304
+ # Evaluate States.MathRandom intrinsic function
305
+ def evaluate_math_random(value, context)
306
+ match = value.match(/States\.MathRandom\((\d+),\s*(\d+)\)/)
307
+ return value unless match
308
+
309
+ min = match[1].to_i
310
+ max = match[2].to_i
311
+ rand(min..max)
312
+ end
313
+
314
+ # Apply result selector to filter and transform task result
315
+ def apply_result_selector(result, context)
316
+ return result unless @result_selector
317
+
318
+ evaluate_parameters(@result_selector, context.merge(task_result: result))
319
+ end
320
+
321
+ # Handle execution errors with retry and catch logic
322
+ def handle_execution_error(execution, error, input)
323
+ execution.logger&.error("Task execution failed: #{error.class.name} - #{error.message}")
324
+
325
+ # Check if we should retry
326
+ retry_policy = retry_policy_for(error, execution.context[:attempt] || 1)
327
+ if retry_policy
328
+ return handle_retry(execution, error, input, retry_policy)
329
+ end
330
+
331
+ # Check if we have a catch handler
332
+ catch_policy = catch_policy_for(error)
333
+ if catch_policy
334
+ return handle_catch(execution, error, input, catch_policy)
335
+ end
336
+
337
+ # No retry or catch - re-raise the error
338
+ raise error
339
+ end
340
+
341
+ # Handle retry logic
342
+ def handle_retry(execution, error, input, retry_policy)
343
+ execution.context[:attempt] = (execution.context[:attempt] || 1) + 1
344
+ execution.logger&.info("Retrying task (attempt #{execution.context[:attempt]})")
345
+
346
+ # Apply retry interval
347
+ sleep(retry_policy.interval_seconds) if retry_policy.interval_seconds > 0
348
+
349
+ # Retry the execution
350
+ execute(execution, input)
351
+ end
352
+
353
+ # Handle catch logic
354
+ def handle_catch(execution, error, input, catch_policy)
355
+ execution.logger&.info("Handling error with catch policy: #{catch_policy.next}")
356
+
357
+ # Prepare error result
358
+ error_result = {
359
+ "Error" => error.class.name,
360
+ "Cause" => error.message
361
+ }
362
+
363
+ # Apply result path from catch policy or use default
364
+ result_path = catch_policy.result_path || @result_path
365
+ output = apply_result_path(input, error_result, result_path)
366
+
367
+ # Transition to next state specified in catch policy
368
+ execution.context[:next_state] = catch_policy.next
369
+ output
370
+ end
371
+
372
+ # Validate the task state definition
373
+ def validate!
374
+ super
375
+
376
+ raise DefinitionError, "Task state '#{@name}' must have a Resource" unless @resource
377
+
378
+ if @timeout_seconds && @timeout_seconds <= 0
379
+ raise DefinitionError, "TimeoutSeconds must be positive"
380
+ end
381
+
382
+ if @heartbeat_seconds && @heartbeat_seconds <= 0
383
+ raise DefinitionError, "HeartbeatSeconds must be positive"
384
+ end
385
+
386
+ if @heartbeat_seconds && @timeout_seconds && @heartbeat_seconds >= @timeout_seconds
387
+ raise DefinitionError, "HeartbeatSeconds must be less than TimeoutSeconds"
388
+ end
389
+
390
+ validate_retry_policies!
391
+ validate_catch_policies!
392
+ end
393
+
394
+ def validate_retry_policies!
395
+ @retry_objects.each(&:validate!)
396
+ end
397
+
398
+ def validate_catch_policies!
399
+ @catch_objects.each(&:validate!)
400
+ end
401
+
402
+ # Helper method to get value from JSONPath
403
+ def get_value_from_path(data, path)
404
+ JsonPath.new(path).first(data)
405
+ rescue
406
+ nil
407
+ end
408
+
409
+ # Helper method to set value at JSONPath
410
+ def set_value_at_path(data, path, value)
411
+ # Simple implementation - for production use a proper JSONPath setter
412
+ if path == "$"
413
+ value
414
+ else
415
+ deep_merge(data, create_nested_hash(path.gsub('$.', '').split('.'), value))
416
+ end
417
+ end
418
+
419
+ def create_nested_hash(keys, value)
420
+ return value if keys.empty?
421
+ { keys.first => create_nested_hash(keys[1..-1], value) }
422
+ end
423
+
424
+ def deep_merge(hash1, hash2)
425
+ hash1.merge(hash2) do |key, old_val, new_val|
426
+ if old_val.is_a?(Hash) && new_val.is_a?(Hash)
427
+ deep_merge(old_val, new_val)
428
+ else
429
+ new_val
430
+ end
431
+ end
432
+ end
433
+ end
434
+
435
+ # Retry policy class
436
+ class RetryPolicy
437
+ attr_reader :error_equals, :interval_seconds, :max_attempts, :backoff_rate
438
+
439
+ def initialize(definition)
440
+ @error_equals = Array(definition["ErrorEquals"])
441
+ @interval_seconds = definition["IntervalSeconds"] || 1
442
+ @max_attempts = definition["MaxAttempts"] || 3
443
+ @backoff_rate = definition["BackoffRate"] || 2.0
444
+ @max_delay = definition["MaxDelay"] || 3600 # 1 hour default
445
+ end
446
+
447
+ def matches?(error, attempt)
448
+ return false if attempt >= @max_attempts
449
+
450
+ @error_equals.any? do |error_match|
451
+ case error_match
452
+ when "States.ALL"
453
+ true
454
+ when "States.Timeout"
455
+ error.is_a?(TaskTimeoutError)
456
+ when "States.TaskFailed"
457
+ error.is_a?(StandardError) && !error.is_a?(TaskTimeoutError)
458
+ when "States.Permissions"
459
+ error.is_a?(SecurityError) || error.message.include?("permission")
460
+ else
461
+ error.class.name == error_match || error.message.include?(error_match)
462
+ end
463
+ end
464
+ end
465
+
466
+ def validate!
467
+ if @error_equals.empty?
468
+ raise DefinitionError, "Retry policy must specify ErrorEquals"
469
+ end
470
+
471
+ if @interval_seconds < 0
472
+ raise DefinitionError, "IntervalSeconds must be non-negative"
473
+ end
474
+
475
+ if @max_attempts < 0
476
+ raise DefinitionError, "MaxAttempts must be non-negative"
477
+ end
478
+ end
479
+ end
480
+
481
+ # Catch policy class
482
+ class CatchPolicy
483
+ attr_reader :error_equals, :next, :result_path
484
+
485
+ def initialize(definition)
486
+ @error_equals = Array(definition["ErrorEquals"])
487
+ @next = definition["Next"]
488
+ @result_path = definition["ResultPath"]
489
+ end
490
+
491
+ def matches?(error)
492
+ @error_equals.any? do |error_match|
493
+ case error_match
494
+ when "States.ALL"
495
+ true
496
+ when "States.Timeout"
497
+ error.is_a?(TaskTimeoutError)
498
+ when "States.TaskFailed"
499
+ error.is_a?(StandardError) && !error.is_a?(TaskTimeoutError)
500
+ when "States.Permissions"
501
+ error.is_a?(SecurityError) || error.message.include?("permission")
502
+ else
503
+ error.class.name == error_match || error.message.include?(error_match)
504
+ end
505
+ end
506
+ end
507
+
508
+ def validate!
509
+ if @error_equals.empty?
510
+ raise DefinitionError, "Catch policy must specify ErrorEquals"
511
+ end
512
+
513
+ unless @next
514
+ raise DefinitionError, "Catch policy must specify Next state"
515
+ end
516
+ end
517
+ end
518
+
519
+ # Custom error classes
520
+ class TaskTimeoutError < StandardError; end
521
+ class TaskExecutionError < StandardError; end
522
+ end
523
+ end
@@ -0,0 +1,123 @@
1
+ require 'time'
2
+
3
+ module StatesLanguageMachine
4
+ module States
5
+ class Wait
6
+ attr_reader :state_type, :seconds, :timestamp, :seconds_path, :timestamp_path, :next_state, :end_state
7
+
8
+ def initialize(definition, state_name)
9
+ @state_name = state_name
10
+
11
+ # Ensure definition is a Hash and extract values safely
12
+ @state_type = definition.is_a?(Hash) ? definition["Type"] : nil
13
+ @seconds = definition.is_a?(Hash) ? definition["Seconds"] : nil
14
+ @timestamp = definition.is_a?(Hash) ? definition["Timestamp"] : nil
15
+ @seconds_path = definition.is_a?(Hash) ? definition["SecondsPath"] : nil
16
+ @timestamp_path = definition.is_a?(Hash) ? definition["TimestampPath"] : nil
17
+ @next_state = definition.is_a?(Hash) ? definition["Next"] : nil
18
+
19
+ # Safely handle End key - check if it exists and is truthy
20
+ if definition.is_a?(Hash)
21
+ @end_state = definition.key?("End") ? !!definition["End"] : false
22
+ else
23
+ @end_state = false
24
+ end
25
+
26
+ validate
27
+ end
28
+
29
+ def execute(context)
30
+ # Determine how long to wait
31
+ wait_seconds = calculate_wait_seconds(context)
32
+
33
+ # Perform the wait
34
+ sleep(wait_seconds) if wait_seconds > 0
35
+
36
+ # Return execution result
37
+ ExecutionResult.new(
38
+ next_state: @end_state ? nil : @next_state,
39
+ output: context.execution_input,
40
+ end_execution: @end_state
41
+ )
42
+ end
43
+
44
+ private
45
+
46
+ def calculate_wait_seconds(context)
47
+ if @seconds
48
+ @seconds.to_i
49
+ elsif @timestamp
50
+ target_time = Time.parse(@timestamp) # Use :: to specify the class method
51
+ wait_time = target_time - Time.now
52
+ wait_time > 0 ? wait_time : 0
53
+ elsif @seconds_path
54
+ seconds_value = extract_path_value(context.execution_input, @seconds_path)
55
+ validate_seconds_value(seconds_value)
56
+ seconds_value.to_i
57
+ elsif @timestamp_path
58
+ timestamp_value = extract_path_value(context.execution_input, @timestamp_path)
59
+ target_time = Time.parse(timestamp_value) # Use :: to specify the class method
60
+ wait_time = target_time - Time.now
61
+ wait_time > 0 ? wait_time : 0
62
+ else
63
+ 0
64
+ end
65
+ end
66
+
67
+ def extract_path_value(input, path)
68
+ # Simple path extraction - you might want to use a JSONPath library
69
+ if path.start_with?("$.")
70
+ key = path[2..-1]
71
+ input[key]
72
+ else
73
+ input[path]
74
+ end
75
+ end
76
+
77
+ def validate_seconds_value(seconds)
78
+ return if seconds.is_a?(Integer) && seconds >= 0
79
+ return if seconds.is_a?(String) && seconds.match?(/^\d+$/) && seconds.to_i >= 0
80
+
81
+ raise StatesLanguageMachine::Error, "Seconds value must be a positive integer"
82
+ end
83
+
84
+ def validate
85
+ raise StatesLanguageMachine::Error, "State definition must be a Hash" unless @state_type
86
+
87
+ wait_methods = [@seconds, @timestamp, @seconds_path, @timestamp_path].compact
88
+ raise StatesLanguageMachine::Error, "Wait state must specify one of: Seconds, Timestamp, SecondsPath, or TimestampPath" if wait_methods.empty?
89
+
90
+ raise StatesLanguageMachine::Error, "Wait state can only specify one wait method" if wait_methods.size > 1
91
+
92
+ validate_seconds_value(@seconds) if @seconds
93
+
94
+ if @timestamp
95
+ begin
96
+ ::Time.parse(@timestamp) # Use :: to specify the class method
97
+ rescue ArgumentError
98
+ raise StatesLanguageMachine::Error, "Invalid timestamp format: #{@timestamp}"
99
+ end
100
+ end
101
+
102
+ if @end_state && @next_state
103
+ raise StatesLanguageMachine::Error, "Wait state cannot have both 'End' and 'Next'"
104
+ end
105
+
106
+ unless @end_state || @next_state
107
+ raise StatesLanguageMachine::Error, "Wait state must have either 'End' or 'Next'"
108
+ end
109
+ end
110
+ end
111
+
112
+ # Simple result class for execution
113
+ class ExecutionResult
114
+ attr_reader :next_state, :output, :end_execution
115
+
116
+ def initialize(next_state: nil, output: nil, end_execution: false)
117
+ @next_state = next_state
118
+ @output = output
119
+ @end_execution = end_execution
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StatesLanguageMachine
4
+ VERSION = "0.1.0"
5
+ end
data/lib/ruby_slm.rb ADDED
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ruby_slm/version"
4
+ require_relative "ruby_slm/errors"
5
+ require_relative "ruby_slm/state_machine"
6
+ require_relative "ruby_slm/state"
7
+ require_relative "ruby_slm/execution"
8
+
9
+ # State implementations
10
+ require_relative "ruby_slm/states/base"
11
+ require_relative "ruby_slm/states/task"
12
+ require_relative "ruby_slm/states/choice"
13
+ require_relative "ruby_slm/states/wait"
14
+ require_relative "ruby_slm/states/parallel"
15
+ require_relative "ruby_slm/states/pass"
16
+ require_relative "ruby_slm/states/succeed"
17
+ require_relative "ruby_slm/states/fail"
18
+
19
+ module StatesLanguageMachine
20
+ class << self
21
+ # Create a state machine from a YAML string
22
+ # @param yaml_string [String] the YAML definition of the state machine
23
+ # @return [StateMachine] the parsed state machine
24
+ def from_yaml(yaml_string)
25
+ StateMachine.new(yaml_string)
26
+ end
27
+
28
+ # Create a state machine from a YAML file
29
+ # @param file_path [String] the path to the YAML file
30
+ # @return [StateMachine] the parsed state machine
31
+ def from_yaml_file(file_path)
32
+ yaml_content = File.read(file_path)
33
+ StateMachine.new(yaml_content)
34
+ end
35
+
36
+ # Create a state machine from a JSON string
37
+ # @param json_string [String] the JSON definition of the state machine
38
+ # @return [StateMachine] the parsed state machine
39
+ def from_json(json_string)
40
+ StateMachine.new(json_string, format: :json)
41
+ end
42
+
43
+ # Create a state machine from a Hash
44
+ # @param hash [Hash] the Hash definition of the state machine
45
+ # @return [StateMachine] the parsed state machine
46
+ def from_hash(hash)
47
+ StateMachine.new(hash, format: :hash)
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,4 @@
1
+ module StatesLanguageMachine
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end