fractor 0.1.6 → 0.1.7
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 +4 -4
- data/.rubocop_todo.yml +227 -102
- data/README.adoc +113 -1940
- data/docs/.lycheeignore +16 -0
- data/docs/Gemfile +24 -0
- data/docs/README.md +157 -0
- data/docs/_config.yml +151 -0
- data/docs/_features/error-handling.adoc +1192 -0
- data/docs/_features/index.adoc +80 -0
- data/docs/_features/monitoring.adoc +589 -0
- data/docs/_features/signal-handling.adoc +202 -0
- data/docs/_features/workflows.adoc +1235 -0
- data/docs/_guides/continuous-mode.adoc +736 -0
- data/docs/_guides/cookbook.adoc +1133 -0
- data/docs/_guides/index.adoc +55 -0
- data/docs/_guides/pipeline-mode.adoc +730 -0
- data/docs/_guides/troubleshooting.adoc +358 -0
- data/docs/_pages/architecture.adoc +1390 -0
- data/docs/_pages/core-concepts.adoc +1392 -0
- data/docs/_pages/design-principles.adoc +862 -0
- data/docs/_pages/getting-started.adoc +290 -0
- data/docs/_pages/installation.adoc +143 -0
- data/docs/_reference/api.adoc +1080 -0
- data/docs/_reference/error-reporting.adoc +670 -0
- data/docs/_reference/examples.adoc +181 -0
- data/docs/_reference/index.adoc +96 -0
- data/docs/_reference/troubleshooting.adoc +862 -0
- data/docs/_tutorials/complex-workflows.adoc +1022 -0
- data/docs/_tutorials/data-processing-pipeline.adoc +740 -0
- data/docs/_tutorials/first-application.adoc +384 -0
- data/docs/_tutorials/index.adoc +48 -0
- data/docs/_tutorials/long-running-services.adoc +931 -0
- data/docs/assets/images/favicon-16.png +0 -0
- data/docs/assets/images/favicon-32.png +0 -0
- data/docs/assets/images/favicon-48.png +0 -0
- data/docs/assets/images/favicon.ico +0 -0
- data/docs/assets/images/favicon.png +0 -0
- data/docs/assets/images/favicon.svg +45 -0
- data/docs/assets/images/fractor-icon.svg +49 -0
- data/docs/assets/images/fractor-logo.svg +61 -0
- data/docs/index.adoc +131 -0
- data/docs/lychee.toml +39 -0
- data/examples/api_aggregator/README.adoc +627 -0
- data/examples/api_aggregator/api_aggregator.rb +376 -0
- data/examples/auto_detection/README.adoc +407 -29
- data/examples/continuous_chat_common/message_protocol.rb +1 -1
- data/examples/error_reporting.rb +207 -0
- data/examples/file_processor/README.adoc +170 -0
- data/examples/file_processor/file_processor.rb +615 -0
- data/examples/file_processor/sample_files/invalid.csv +1 -0
- data/examples/file_processor/sample_files/orders.xml +24 -0
- data/examples/file_processor/sample_files/products.json +23 -0
- data/examples/file_processor/sample_files/users.csv +6 -0
- data/examples/hierarchical_hasher/README.adoc +629 -41
- data/examples/image_processor/README.adoc +610 -0
- data/examples/image_processor/image_processor.rb +349 -0
- data/examples/image_processor/processed_images/sample_10_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_1_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_2_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_3_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_4_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_5_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_6_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_7_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_8_processed.jpg.json +12 -0
- data/examples/image_processor/processed_images/sample_9_processed.jpg.json +12 -0
- data/examples/image_processor/test_images/sample_1.png +1 -0
- data/examples/image_processor/test_images/sample_10.png +1 -0
- data/examples/image_processor/test_images/sample_2.png +1 -0
- data/examples/image_processor/test_images/sample_3.png +1 -0
- data/examples/image_processor/test_images/sample_4.png +1 -0
- data/examples/image_processor/test_images/sample_5.png +1 -0
- data/examples/image_processor/test_images/sample_6.png +1 -0
- data/examples/image_processor/test_images/sample_7.png +1 -0
- data/examples/image_processor/test_images/sample_8.png +1 -0
- data/examples/image_processor/test_images/sample_9.png +1 -0
- data/examples/log_analyzer/README.adoc +662 -0
- data/examples/log_analyzer/log_analyzer.rb +579 -0
- data/examples/log_analyzer/sample_logs/apache.log +20 -0
- data/examples/log_analyzer/sample_logs/json.log +15 -0
- data/examples/log_analyzer/sample_logs/nginx.log +15 -0
- data/examples/log_analyzer/sample_logs/rails.log +29 -0
- data/examples/multi_work_type/README.adoc +576 -26
- data/examples/performance_monitoring.rb +120 -0
- data/examples/pipeline_processing/README.adoc +740 -26
- data/examples/pipeline_processing/pipeline_processing.rb +2 -2
- data/examples/priority_work_example.rb +155 -0
- data/examples/producer_subscriber/README.adoc +889 -46
- data/examples/scatter_gather/README.adoc +829 -27
- data/examples/simple/README.adoc +347 -0
- data/examples/specialized_workers/README.adoc +622 -26
- data/examples/specialized_workers/specialized_workers.rb +44 -8
- data/examples/stream_processor/README.adoc +206 -0
- data/examples/stream_processor/stream_processor.rb +284 -0
- data/examples/web_scraper/README.adoc +625 -0
- data/examples/web_scraper/web_scraper.rb +285 -0
- data/examples/workflow/README.adoc +406 -0
- data/examples/workflow/circuit_breaker/README.adoc +360 -0
- data/examples/workflow/circuit_breaker/circuit_breaker_workflow.rb +225 -0
- data/examples/workflow/conditional/README.adoc +483 -0
- data/examples/workflow/conditional/conditional_workflow.rb +215 -0
- data/examples/workflow/dead_letter_queue/README.adoc +374 -0
- data/examples/workflow/dead_letter_queue/dead_letter_queue_workflow.rb +217 -0
- data/examples/workflow/fan_out/README.adoc +381 -0
- data/examples/workflow/fan_out/fan_out_workflow.rb +202 -0
- data/examples/workflow/retry/README.adoc +248 -0
- data/examples/workflow/retry/retry_workflow.rb +195 -0
- data/examples/workflow/simple_linear/README.adoc +267 -0
- data/examples/workflow/simple_linear/simple_linear_workflow.rb +175 -0
- data/examples/workflow/simplified/README.adoc +329 -0
- data/examples/workflow/simplified/simplified_workflow.rb +222 -0
- data/exe/fractor +10 -0
- data/lib/fractor/cli.rb +288 -0
- data/lib/fractor/configuration.rb +307 -0
- data/lib/fractor/continuous_server.rb +60 -65
- data/lib/fractor/error_formatter.rb +72 -0
- data/lib/fractor/error_report_generator.rb +152 -0
- data/lib/fractor/error_reporter.rb +244 -0
- data/lib/fractor/error_statistics.rb +147 -0
- data/lib/fractor/execution_tracer.rb +162 -0
- data/lib/fractor/logger.rb +230 -0
- data/lib/fractor/main_loop_handler.rb +406 -0
- data/lib/fractor/main_loop_handler3.rb +135 -0
- data/lib/fractor/main_loop_handler4.rb +299 -0
- data/lib/fractor/performance_metrics_collector.rb +181 -0
- data/lib/fractor/performance_monitor.rb +215 -0
- data/lib/fractor/performance_report_generator.rb +202 -0
- data/lib/fractor/priority_work.rb +93 -0
- data/lib/fractor/priority_work_queue.rb +189 -0
- data/lib/fractor/result_aggregator.rb +32 -0
- data/lib/fractor/shutdown_handler.rb +168 -0
- data/lib/fractor/signal_handler.rb +80 -0
- data/lib/fractor/supervisor.rb +382 -269
- data/lib/fractor/supervisor_logger.rb +88 -0
- data/lib/fractor/version.rb +1 -1
- data/lib/fractor/work.rb +12 -0
- data/lib/fractor/work_distribution_manager.rb +151 -0
- data/lib/fractor/work_queue.rb +20 -0
- data/lib/fractor/work_result.rb +181 -9
- data/lib/fractor/worker.rb +73 -0
- data/lib/fractor/workflow/builder.rb +210 -0
- data/lib/fractor/workflow/chain_builder.rb +169 -0
- data/lib/fractor/workflow/circuit_breaker.rb +183 -0
- data/lib/fractor/workflow/circuit_breaker_orchestrator.rb +208 -0
- data/lib/fractor/workflow/circuit_breaker_registry.rb +112 -0
- data/lib/fractor/workflow/dead_letter_queue.rb +334 -0
- data/lib/fractor/workflow/execution_hooks.rb +39 -0
- data/lib/fractor/workflow/execution_strategy.rb +225 -0
- data/lib/fractor/workflow/execution_trace.rb +134 -0
- data/lib/fractor/workflow/helpers.rb +191 -0
- data/lib/fractor/workflow/job.rb +290 -0
- data/lib/fractor/workflow/job_dependency_validator.rb +120 -0
- data/lib/fractor/workflow/logger.rb +110 -0
- data/lib/fractor/workflow/pre_execution_context.rb +193 -0
- data/lib/fractor/workflow/retry_config.rb +156 -0
- data/lib/fractor/workflow/retry_orchestrator.rb +184 -0
- data/lib/fractor/workflow/retry_strategy.rb +93 -0
- data/lib/fractor/workflow/structured_logger.rb +30 -0
- data/lib/fractor/workflow/type_compatibility_validator.rb +222 -0
- data/lib/fractor/workflow/visualizer.rb +211 -0
- data/lib/fractor/workflow/workflow_context.rb +132 -0
- data/lib/fractor/workflow/workflow_executor.rb +669 -0
- data/lib/fractor/workflow/workflow_result.rb +55 -0
- data/lib/fractor/workflow/workflow_validator.rb +295 -0
- data/lib/fractor/workflow.rb +333 -0
- data/lib/fractor/wrapped_ractor.rb +66 -101
- data/lib/fractor/wrapped_ractor3.rb +161 -0
- data/lib/fractor/wrapped_ractor4.rb +242 -0
- data/lib/fractor.rb +92 -4
- metadata +179 -6
- data/tests/sample.rb.bak +0 -309
- data/tests/sample_working.rb.bak +0 -209
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fractor
|
|
4
|
+
class Workflow
|
|
5
|
+
# Generates visual representations of workflows.
|
|
6
|
+
# Supports Mermaid, DOT/Graphviz, and ASCII art formats.
|
|
7
|
+
class Visualizer
|
|
8
|
+
def initialize(workflow_class)
|
|
9
|
+
@workflow_class = workflow_class
|
|
10
|
+
@jobs = workflow_class.jobs
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Generate Mermaid flowchart diagram
|
|
14
|
+
#
|
|
15
|
+
# @return [String] Mermaid diagram syntax
|
|
16
|
+
def to_mermaid
|
|
17
|
+
lines = ["flowchart TD"]
|
|
18
|
+
lines << " Start([Start: #{@workflow_class.workflow_name}])"
|
|
19
|
+
|
|
20
|
+
# Add job nodes
|
|
21
|
+
@jobs.each do |name, job|
|
|
22
|
+
label = escape_mermaid(name)
|
|
23
|
+
worker = escape_mermaid(job.worker_class.name.split("::").last)
|
|
24
|
+
|
|
25
|
+
# Different shapes based on job type
|
|
26
|
+
lines << if job.terminates
|
|
27
|
+
" #{node_id(name)}[/#{label}<br/>#{worker}/]"
|
|
28
|
+
else
|
|
29
|
+
" #{node_id(name)}[#{label}<br/>#{worker}]"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
lines << " End([End])"
|
|
34
|
+
|
|
35
|
+
# Add edges
|
|
36
|
+
@jobs.each do |name, job|
|
|
37
|
+
if job.dependencies.empty?
|
|
38
|
+
# Start job
|
|
39
|
+
lines << " Start --> #{node_id(name)}"
|
|
40
|
+
else
|
|
41
|
+
# Dependencies
|
|
42
|
+
job.dependencies.each do |dep|
|
|
43
|
+
edge_label = ""
|
|
44
|
+
if job.condition_proc
|
|
45
|
+
edge_label = "|conditional|"
|
|
46
|
+
end
|
|
47
|
+
lines << " #{node_id(dep)} -->#{edge_label} #{node_id(name)}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Terminating jobs
|
|
52
|
+
if job.terminates
|
|
53
|
+
lines << " #{node_id(name)} --> End"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Add styling
|
|
58
|
+
lines << ""
|
|
59
|
+
lines << " classDef terminating fill:#f9f,stroke:#333,stroke-width:2px"
|
|
60
|
+
@jobs.each do |name, job|
|
|
61
|
+
lines << " class #{node_id(name)} terminating" if job.terminates
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
lines.join("\n")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Generate DOT/Graphviz diagram
|
|
68
|
+
#
|
|
69
|
+
# @return [String] DOT syntax
|
|
70
|
+
def to_dot
|
|
71
|
+
lines = ["digraph #{dot_id(@workflow_class.workflow_name)} {"]
|
|
72
|
+
lines << " rankdir=TD;"
|
|
73
|
+
lines << " node [shape=box, style=rounded];"
|
|
74
|
+
lines << ""
|
|
75
|
+
|
|
76
|
+
# Start node
|
|
77
|
+
lines << ' start [label="Start", shape=ellipse];'
|
|
78
|
+
|
|
79
|
+
# Job nodes
|
|
80
|
+
@jobs.each do |name, job|
|
|
81
|
+
worker = job.worker_class.name
|
|
82
|
+
label = "#{name}\\n(#{worker})"
|
|
83
|
+
|
|
84
|
+
lines << if job.terminates
|
|
85
|
+
" #{dot_id(name)} [label=\"#{label}\", " \
|
|
86
|
+
"style=\"rounded,filled\", fillcolor=lightpink];"
|
|
87
|
+
else
|
|
88
|
+
" #{dot_id(name)} [label=\"#{label}\"];"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# End node
|
|
93
|
+
lines << ' end [label="End", shape=ellipse];'
|
|
94
|
+
lines << ""
|
|
95
|
+
|
|
96
|
+
# Edges
|
|
97
|
+
@jobs.each do |name, job|
|
|
98
|
+
if job.dependencies.empty?
|
|
99
|
+
lines << " start -> #{dot_id(name)};"
|
|
100
|
+
else
|
|
101
|
+
job.dependencies.each do |dep|
|
|
102
|
+
lines << if job.condition_proc
|
|
103
|
+
" #{dot_id(dep)} -> #{dot_id(name)} " \
|
|
104
|
+
"[label=\"conditional\", style=dashed];"
|
|
105
|
+
else
|
|
106
|
+
" #{dot_id(dep)} -> #{dot_id(name)};"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
if job.terminates
|
|
112
|
+
lines << " #{dot_id(name)} -> end;"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
lines << "}"
|
|
117
|
+
lines.join("\n")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Generate ASCII art diagram
|
|
121
|
+
#
|
|
122
|
+
# @return [String] ASCII art representation
|
|
123
|
+
def to_ascii
|
|
124
|
+
lines = []
|
|
125
|
+
lines << "┌─────────────────────────────────────────┐"
|
|
126
|
+
lines << "│ Workflow: #{@workflow_class.workflow_name.ljust(27)} │"
|
|
127
|
+
lines << "└─────────────────────────────────────────┘"
|
|
128
|
+
lines << ""
|
|
129
|
+
|
|
130
|
+
# Compute execution order
|
|
131
|
+
order = compute_execution_order
|
|
132
|
+
|
|
133
|
+
order.each_with_index do |job_group, index|
|
|
134
|
+
if job_group.size == 1
|
|
135
|
+
# Single job
|
|
136
|
+
job = @jobs[job_group.first]
|
|
137
|
+
lines << " ┌─────────────────────────┐"
|
|
138
|
+
lines << " │ #{job_group.first.ljust(23)} │"
|
|
139
|
+
lines << " │ (#{job.worker_class.name.split('::').last.ljust(21)}) │"
|
|
140
|
+
lines << " └─────────────────────────┘"
|
|
141
|
+
else
|
|
142
|
+
# Parallel jobs
|
|
143
|
+
lines << " ╔═════════════════════════╗"
|
|
144
|
+
lines << " ║ PARALLEL EXECUTION ║"
|
|
145
|
+
lines << " ╚═════════════════════════╝"
|
|
146
|
+
job_group.each do |job_name|
|
|
147
|
+
job = @jobs[job_name]
|
|
148
|
+
lines << " ├─ #{job_name}"
|
|
149
|
+
lines << " │ (#{job.worker_class.name.split('::').last})"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Arrow to next group
|
|
154
|
+
if index < order.size - 1
|
|
155
|
+
lines << " │"
|
|
156
|
+
lines << " ▼"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
lines << ""
|
|
161
|
+
lines << "Legend: Regular jobs │ Parallel jobs ╔═══╗"
|
|
162
|
+
|
|
163
|
+
lines.join("\n")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Print ASCII diagram to stdout
|
|
167
|
+
def print
|
|
168
|
+
puts to_ascii
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
private
|
|
172
|
+
|
|
173
|
+
def node_id(name)
|
|
174
|
+
name.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def dot_id(name)
|
|
178
|
+
name.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def escape_mermaid(text)
|
|
182
|
+
text.to_s.gsub(/["\[\]()]/, "")
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def compute_execution_order
|
|
186
|
+
# Topological sort
|
|
187
|
+
jobs = @jobs
|
|
188
|
+
order = []
|
|
189
|
+
remaining = jobs.keys.to_set
|
|
190
|
+
processed = Set.new
|
|
191
|
+
|
|
192
|
+
until remaining.empty?
|
|
193
|
+
ready = remaining.select do |job_name|
|
|
194
|
+
job = jobs[job_name]
|
|
195
|
+
job.dependencies.all? { |dep| processed.include?(dep) }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
break if ready.empty?
|
|
199
|
+
|
|
200
|
+
order << ready
|
|
201
|
+
ready.each do |job_name|
|
|
202
|
+
processed.add(job_name)
|
|
203
|
+
remaining.delete(job_name)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
order
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fractor
|
|
4
|
+
class Workflow
|
|
5
|
+
# Manages data flow and state during workflow execution.
|
|
6
|
+
# Stores workflow inputs, job outputs, and provides resolution of data dependencies.
|
|
7
|
+
class WorkflowContext
|
|
8
|
+
attr_reader :workflow_input, :job_outputs, :correlation_id, :logger
|
|
9
|
+
|
|
10
|
+
def initialize(workflow_input, correlation_id: nil, logger: nil)
|
|
11
|
+
@workflow_input = workflow_input
|
|
12
|
+
@job_outputs = {}
|
|
13
|
+
@correlation_id = correlation_id || generate_correlation_id
|
|
14
|
+
@logger = logger || WorkflowLogger.new(correlation_id: @correlation_id)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Store the output of a completed job.
|
|
18
|
+
#
|
|
19
|
+
# @param job_name [String] The job name
|
|
20
|
+
# @param output [Lutaml::Model::Serializable] The job's output
|
|
21
|
+
def store_job_output(job_name, output)
|
|
22
|
+
@job_outputs[job_name] = output
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Get the output of a completed job.
|
|
26
|
+
#
|
|
27
|
+
# @param job_name [String] The job name
|
|
28
|
+
# @return [Lutaml::Model::Serializable, nil] The job's output
|
|
29
|
+
def job_output(job_name)
|
|
30
|
+
@job_outputs[job_name]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Build input for a job based on its input mappings.
|
|
34
|
+
#
|
|
35
|
+
# @param job [Job] The job to build input for
|
|
36
|
+
# @return [Lutaml::Model::Serializable] The constructed input
|
|
37
|
+
def build_job_input(job)
|
|
38
|
+
return @workflow_input if job.input_mappings[:workflow]
|
|
39
|
+
|
|
40
|
+
input_type = job.input_type
|
|
41
|
+
unless input_type
|
|
42
|
+
raise "Job '#{job.name}' has no input_type defined in its worker"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Collect attributes from all mapped sources
|
|
46
|
+
input_attrs = {}
|
|
47
|
+
|
|
48
|
+
job.input_mappings.each do |source_job_name, attr_mappings|
|
|
49
|
+
source_output = job_output(source_job_name)
|
|
50
|
+
unless source_output
|
|
51
|
+
raise "Job '#{job.name}' depends on '#{source_job_name}' but its output is not available"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if attr_mappings == :all
|
|
55
|
+
# Map all attributes from source to input
|
|
56
|
+
copy_all_attributes(source_output, input_attrs, input_type)
|
|
57
|
+
else
|
|
58
|
+
# Map specific attributes
|
|
59
|
+
attr_mappings.each do |target_attr, source_attr|
|
|
60
|
+
target_attr = target_attr.to_sym
|
|
61
|
+
source_attr = source_attr.to_sym
|
|
62
|
+
|
|
63
|
+
# Get value from source output
|
|
64
|
+
value = if source_output.respond_to?(source_attr)
|
|
65
|
+
source_output.send(source_attr)
|
|
66
|
+
elsif source_output.respond_to?(:[])
|
|
67
|
+
source_output[source_attr]
|
|
68
|
+
else
|
|
69
|
+
raise "Source output from '#{source_job_name}' does not have attribute '#{source_attr}'"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
input_attrs[target_attr] = value
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Create input instance
|
|
78
|
+
input_type.new(**input_attrs)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Check if a job's output is available.
|
|
82
|
+
#
|
|
83
|
+
# @param job_name [String] The job name
|
|
84
|
+
# @return [Boolean] Whether the output is available
|
|
85
|
+
def job_completed?(job_name)
|
|
86
|
+
@job_outputs.key?(job_name)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Convert context to hash for debugging/logging
|
|
90
|
+
def to_h
|
|
91
|
+
{
|
|
92
|
+
correlation_id: @correlation_id,
|
|
93
|
+
workflow_input: @workflow_input.class.name,
|
|
94
|
+
completed_jobs: @job_outputs.keys,
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def generate_correlation_id
|
|
101
|
+
require "securerandom"
|
|
102
|
+
"wf-#{SecureRandom.hex(8)}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def copy_all_attributes(source, target_hash, input_type)
|
|
106
|
+
# Copy all compatible attributes from source to target
|
|
107
|
+
if defined?(Lutaml::Model::Serializable) &&
|
|
108
|
+
source.is_a?(Lutaml::Model::Serializable) &&
|
|
109
|
+
input_type.respond_to?(:attributes)
|
|
110
|
+
# Lutaml::Model path
|
|
111
|
+
source.class.attributes.each_key do |attr_name|
|
|
112
|
+
if input_type.attributes.key?(attr_name)
|
|
113
|
+
target_hash[attr_name] = source.send(attr_name)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
else
|
|
117
|
+
# Fallback for plain Ruby classes
|
|
118
|
+
# Copy all instance variables from source that exist in target
|
|
119
|
+
source.instance_variables.each do |var|
|
|
120
|
+
attr_name = var.to_s.delete("@").to_sym
|
|
121
|
+
|
|
122
|
+
# Check if target class has this attribute (via attr_accessor/reader)
|
|
123
|
+
if input_type.instance_methods.include?(attr_name) ||
|
|
124
|
+
input_type.instance_methods.include?("#{attr_name}=".to_sym)
|
|
125
|
+
target_hash[attr_name] = source.instance_variable_get(var)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|