fractor 0.1.4 → 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-https---raw-githubusercontent-com-riboseinc-oss-guides-main-ci-rubocop-yml +552 -0
- data/.rubocop.yml +14 -8
- data/.rubocop_todo.yml +284 -43
- data/README.adoc +111 -950
- 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/auto_detection/auto_detection.rb +9 -9
- data/examples/continuous_chat_common/message_protocol.rb +53 -0
- data/examples/continuous_chat_fractor/README.adoc +217 -0
- data/examples/continuous_chat_fractor/chat_client.rb +303 -0
- data/examples/continuous_chat_fractor/chat_common.rb +83 -0
- data/examples/continuous_chat_fractor/chat_server.rb +167 -0
- data/examples/continuous_chat_fractor/simulate.rb +345 -0
- data/examples/continuous_chat_server/README.adoc +135 -0
- data/examples/continuous_chat_server/chat_client.rb +303 -0
- data/examples/continuous_chat_server/chat_server.rb +359 -0
- data/examples/continuous_chat_server/simulate.rb +343 -0
- 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/hierarchical_hasher/hierarchical_hasher.rb +12 -8
- 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/multi_work_type/multi_work_type.rb +30 -29
- data/examples/performance_monitoring.rb +120 -0
- data/examples/pipeline_processing/README.adoc +740 -26
- data/examples/pipeline_processing/pipeline_processing.rb +16 -16
- data/examples/priority_work_example.rb +155 -0
- data/examples/producer_subscriber/README.adoc +889 -46
- data/examples/producer_subscriber/producer_subscriber.rb +20 -16
- data/examples/scatter_gather/README.adoc +829 -27
- data/examples/scatter_gather/scatter_gather.rb +29 -28
- data/examples/simple/README.adoc +347 -0
- data/examples/simple/sample.rb +5 -5
- data/examples/specialized_workers/README.adoc +622 -26
- data/examples/specialized_workers/specialized_workers.rb +88 -45
- 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 +183 -0
- 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 +33 -1
- data/lib/fractor/shutdown_handler.rb +168 -0
- data/lib/fractor/signal_handler.rb +80 -0
- data/lib/fractor/supervisor.rb +430 -144
- 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 +88 -0
- data/lib/fractor/work_result.rb +181 -9
- data/lib/fractor/worker.rb +75 -1
- 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 -91
- data/lib/fractor/wrapped_ractor3.rb +161 -0
- data/lib/fractor/wrapped_ractor4.rb +242 -0
- data/lib/fractor.rb +93 -3
- metadata +192 -6
- data/tests/sample.rb.bak +0 -309
- data/tests/sample_working.rb.bak +0 -209
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../../lib/fractor"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "uri"
|
|
7
|
+
require "json"
|
|
8
|
+
require "time"
|
|
9
|
+
|
|
10
|
+
# API endpoint configuration
|
|
11
|
+
class APIEndpoint
|
|
12
|
+
attr_reader :name, :url, :timeout, :rate_limit_delay
|
|
13
|
+
|
|
14
|
+
def initialize(name:, url:, timeout: 5, rate_limit_delay: 0.1)
|
|
15
|
+
@name = name
|
|
16
|
+
@url = url
|
|
17
|
+
@timeout = timeout
|
|
18
|
+
@rate_limit_delay = rate_limit_delay
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_s
|
|
22
|
+
"APIEndpoint(#{name}: #{url})"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Mock API responses for demonstration
|
|
27
|
+
module MockAPIResponses
|
|
28
|
+
USERS_API = [
|
|
29
|
+
{ id: 1, name: "Alice Johnson", email: "alice@example.com", role: "admin" },
|
|
30
|
+
{ id: 2, name: "Bob Smith", email: "bob@example.com", role: "user" },
|
|
31
|
+
{ id: 3, name: "Carol Williams", email: "carol@example.com", role: "user" }
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
PRODUCTS_API = [
|
|
35
|
+
{ id: 101, name: "Laptop", price: 999.99, stock: 15 },
|
|
36
|
+
{ id: 102, name: "Mouse", price: 29.99, stock: 50 },
|
|
37
|
+
{ id: 103, name: "Keyboard", price: 79.99, stock: 30 }
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
40
|
+
ORDERS_API = [
|
|
41
|
+
{ id: 1001, user_id: 1, product_id: 101, quantity: 1, status: "shipped" },
|
|
42
|
+
{ id: 1002, user_id: 2, product_id: 102, quantity: 2, status: "pending" },
|
|
43
|
+
{ id: 1003, user_id: 3, product_id: 103, quantity: 1, status: "delivered" }
|
|
44
|
+
].freeze
|
|
45
|
+
|
|
46
|
+
ANALYTICS_API = {
|
|
47
|
+
total_users: 3,
|
|
48
|
+
total_products: 3,
|
|
49
|
+
total_orders: 3,
|
|
50
|
+
revenue: 1139.97,
|
|
51
|
+
timestamp: Time.now.iso8601
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
def self.get_response(endpoint_name, simulate_error: false, simulate_slow: false)
|
|
55
|
+
sleep(2) if simulate_slow
|
|
56
|
+
|
|
57
|
+
raise "Simulated API error" if simulate_error
|
|
58
|
+
|
|
59
|
+
case endpoint_name
|
|
60
|
+
when :users
|
|
61
|
+
{ status: "success", data: USERS_API, timestamp: Time.now.iso8601 }
|
|
62
|
+
when :products
|
|
63
|
+
{ status: "success", data: PRODUCTS_API, timestamp: Time.now.iso8601 }
|
|
64
|
+
when :orders
|
|
65
|
+
{ status: "success", data: ORDERS_API, timestamp: Time.now.iso8601 }
|
|
66
|
+
when :analytics
|
|
67
|
+
{ status: "success", data: ANALYTICS_API, timestamp: Time.now.iso8601 }
|
|
68
|
+
else
|
|
69
|
+
raise "Unknown endpoint: #{endpoint_name}"
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# API aggregator - simplified without workflow for this example
|
|
75
|
+
class APIAggregator
|
|
76
|
+
attr_reader :endpoints, :results, :errors
|
|
77
|
+
|
|
78
|
+
def initialize
|
|
79
|
+
@endpoints = []
|
|
80
|
+
@results = {}
|
|
81
|
+
@errors = {}
|
|
82
|
+
@request_count = 0
|
|
83
|
+
@mutex = Mutex.new
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def add_endpoint(endpoint)
|
|
87
|
+
@endpoints << endpoint
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def fetch_all(simulate_errors: false, simulate_slow: false)
|
|
91
|
+
return {} if @endpoints.empty?
|
|
92
|
+
|
|
93
|
+
puts "Fetching data from #{@endpoints.size} API endpoints..."
|
|
94
|
+
puts "Retry enabled with exponential backoff (max 3 attempts)"
|
|
95
|
+
puts
|
|
96
|
+
|
|
97
|
+
@results = {}
|
|
98
|
+
@errors = {}
|
|
99
|
+
|
|
100
|
+
# Fetch from each endpoint with retry logic
|
|
101
|
+
@endpoints.each do |endpoint|
|
|
102
|
+
max_attempts = 3
|
|
103
|
+
attempt = 0
|
|
104
|
+
|
|
105
|
+
while attempt < max_attempts
|
|
106
|
+
attempt += 1
|
|
107
|
+
|
|
108
|
+
begin
|
|
109
|
+
data = fetch_endpoint(
|
|
110
|
+
endpoint,
|
|
111
|
+
{},
|
|
112
|
+
simulate_error: simulate_errors,
|
|
113
|
+
simulate_slow: simulate_slow
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@results[endpoint.name.to_sym] = data
|
|
117
|
+
break
|
|
118
|
+
rescue StandardError => e
|
|
119
|
+
if attempt < max_attempts
|
|
120
|
+
delay = 0.5 * (2 ** (attempt - 1))
|
|
121
|
+
puts "[#{endpoint.name}] Retrying (attempt #{attempt + 1}/#{max_attempts}) after #{delay}s..."
|
|
122
|
+
sleep(delay)
|
|
123
|
+
else
|
|
124
|
+
puts "[#{endpoint.name}] All retry attempts exhausted"
|
|
125
|
+
@errors[endpoint.name.to_sym] = e.message
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
puts "\n=== Aggregation Complete ==="
|
|
132
|
+
puts "Successful fetches: #{@results.keys.size}"
|
|
133
|
+
puts "Failed fetches: #{@errors.keys.size}"
|
|
134
|
+
puts "Total API requests: #{@request_count}"
|
|
135
|
+
puts
|
|
136
|
+
|
|
137
|
+
aggregate_data(@results)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
def fetch_endpoint(endpoint, context, simulate_error: false, simulate_slow: false)
|
|
143
|
+
@mutex.synchronize { @request_count += 1 }
|
|
144
|
+
|
|
145
|
+
puts "[#{endpoint.name}] Fetching from #{endpoint.url}..."
|
|
146
|
+
|
|
147
|
+
# Simulate rate limiting
|
|
148
|
+
sleep(endpoint.rate_limit_delay)
|
|
149
|
+
|
|
150
|
+
# Use mock API for demonstration
|
|
151
|
+
response = MockAPIResponses.get_response(
|
|
152
|
+
endpoint.name.to_sym,
|
|
153
|
+
simulate_error: simulate_error,
|
|
154
|
+
simulate_slow: simulate_slow
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
puts "[#{endpoint.name}] ✓ Success (#{response[:data].size rescue 'N/A'} items)"
|
|
158
|
+
|
|
159
|
+
response[:data]
|
|
160
|
+
rescue StandardError => e
|
|
161
|
+
puts "[#{endpoint.name}] ✗ Error: #{e.message}"
|
|
162
|
+
raise e
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def aggregate_data(results)
|
|
166
|
+
aggregated = {
|
|
167
|
+
users: results[:users] || [],
|
|
168
|
+
products: results[:products] || [],
|
|
169
|
+
orders: results[:orders] || [],
|
|
170
|
+
analytics: results[:analytics] || {},
|
|
171
|
+
summary: {}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
# Calculate summary statistics
|
|
175
|
+
aggregated[:summary] = {
|
|
176
|
+
total_users: aggregated[:users].size,
|
|
177
|
+
total_products: aggregated[:products].size,
|
|
178
|
+
total_orders: aggregated[:orders].size,
|
|
179
|
+
endpoints_successful: results.keys.size,
|
|
180
|
+
endpoints_failed: @errors.keys.size,
|
|
181
|
+
timestamp: Time.now.iso8601
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
# Enrich orders with user and product information
|
|
185
|
+
if aggregated[:orders].any?
|
|
186
|
+
aggregated[:enriched_orders] = enrich_orders(
|
|
187
|
+
aggregated[:orders],
|
|
188
|
+
aggregated[:users],
|
|
189
|
+
aggregated[:products]
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
aggregated
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def enrich_orders(orders, users, products)
|
|
197
|
+
orders.map do |order|
|
|
198
|
+
user = users.find { |u| u[:id] == order[:user_id] }
|
|
199
|
+
product = products.find { |p| p[:id] == order[:product_id] }
|
|
200
|
+
|
|
201
|
+
order.merge(
|
|
202
|
+
user_name: user&.dig(:name),
|
|
203
|
+
user_email: user&.dig(:email),
|
|
204
|
+
product_name: product&.dig(:name),
|
|
205
|
+
product_price: product&.dig(:price),
|
|
206
|
+
total_price: (product&.dig(:price) || 0) * order[:quantity]
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Report generator for aggregated data
|
|
213
|
+
class AggregationReport
|
|
214
|
+
def self.generate(data, output_file = nil)
|
|
215
|
+
report = build_report(data)
|
|
216
|
+
|
|
217
|
+
if output_file
|
|
218
|
+
File.write(output_file, report)
|
|
219
|
+
puts "Report saved to #{output_file}"
|
|
220
|
+
else
|
|
221
|
+
puts report
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
report
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def self.build_report(data)
|
|
228
|
+
lines = []
|
|
229
|
+
lines << "=" * 80
|
|
230
|
+
lines << "API AGGREGATION REPORT"
|
|
231
|
+
lines << "=" * 80
|
|
232
|
+
lines << ""
|
|
233
|
+
|
|
234
|
+
# Summary
|
|
235
|
+
if data[:summary]
|
|
236
|
+
lines << "SUMMARY"
|
|
237
|
+
lines << "-" * 80
|
|
238
|
+
lines << format("Total Users: %d", data[:summary][:total_users] || 0)
|
|
239
|
+
lines << format("Total Products: %d", data[:summary][:total_products] || 0)
|
|
240
|
+
lines << format("Total Orders: %d", data[:summary][:total_orders] || 0)
|
|
241
|
+
lines << format("Endpoints Successful: %d", data[:summary][:endpoints_successful] || 0)
|
|
242
|
+
lines << format("Endpoints Failed: %d", data[:summary][:endpoints_failed] || 0)
|
|
243
|
+
lines << format("Timestamp: %s", data[:summary][:timestamp] || "N/A")
|
|
244
|
+
lines << ""
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Users
|
|
248
|
+
if data[:users]&.any?
|
|
249
|
+
lines << "USERS (#{data[:users].size})"
|
|
250
|
+
lines << "-" * 80
|
|
251
|
+
data[:users].first(5).each do |user|
|
|
252
|
+
lines << format(" %d. %s <%s> [%s]",
|
|
253
|
+
user[:id], user[:name], user[:email], user[:role])
|
|
254
|
+
end
|
|
255
|
+
lines << " ..." if data[:users].size > 5
|
|
256
|
+
lines << ""
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Products
|
|
260
|
+
if data[:products]&.any?
|
|
261
|
+
lines << "PRODUCTS (#{data[:products].size})"
|
|
262
|
+
lines << "-" * 80
|
|
263
|
+
data[:products].first(5).each do |product|
|
|
264
|
+
lines << format(" %d. %s - $%.2f (Stock: %d)",
|
|
265
|
+
product[:id], product[:name], product[:price], product[:stock])
|
|
266
|
+
end
|
|
267
|
+
lines << " ..." if data[:products].size > 5
|
|
268
|
+
lines << ""
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Enriched Orders
|
|
272
|
+
if data[:enriched_orders]&.any?
|
|
273
|
+
lines << "ENRICHED ORDERS (#{data[:enriched_orders].size})"
|
|
274
|
+
lines << "-" * 80
|
|
275
|
+
data[:enriched_orders].each do |order|
|
|
276
|
+
lines << format(" Order #%d:", order[:id])
|
|
277
|
+
lines << format(" User: %s <%s>", order[:user_name], order[:user_email])
|
|
278
|
+
lines << format(" Product: %s", order[:product_name])
|
|
279
|
+
lines << format(" Quantity: %d × $%.2f = $%.2f",
|
|
280
|
+
order[:quantity], order[:product_price], order[:total_price])
|
|
281
|
+
lines << format(" Status: %s", order[:status])
|
|
282
|
+
lines << ""
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Analytics
|
|
287
|
+
if data[:analytics].is_a?(Hash) && data[:analytics].any?
|
|
288
|
+
lines << "ANALYTICS"
|
|
289
|
+
lines << "-" * 80
|
|
290
|
+
data[:analytics].each do |key, value|
|
|
291
|
+
lines << format(" %s: %s", key.to_s.gsub("_", " ").capitalize, value)
|
|
292
|
+
end
|
|
293
|
+
lines << ""
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
lines << "=" * 80
|
|
297
|
+
|
|
298
|
+
lines.join("\n")
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Run example if executed directly
|
|
303
|
+
if __FILE__ == $PROGRAM_NAME
|
|
304
|
+
require "optparse"
|
|
305
|
+
|
|
306
|
+
options = {
|
|
307
|
+
simulate_errors: false,
|
|
308
|
+
simulate_slow: false,
|
|
309
|
+
output: nil
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
OptionParser.new do |opts|
|
|
313
|
+
opts.banner = "Usage: api_aggregator.rb [options]"
|
|
314
|
+
|
|
315
|
+
opts.on("--simulate-errors", "Simulate API errors to test circuit breaker") do
|
|
316
|
+
options[:simulate_errors] = true
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
opts.on("--simulate-slow", "Simulate slow API responses") do
|
|
320
|
+
options[:simulate_slow] = true
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
opts.on("-o", "--output FILE", "Output report file") do |f|
|
|
324
|
+
options[:output] = f
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
opts.on("-h", "--help", "Show this message") do
|
|
328
|
+
puts opts
|
|
329
|
+
exit
|
|
330
|
+
end
|
|
331
|
+
end.parse!
|
|
332
|
+
|
|
333
|
+
puts "=== API Data Aggregator with Circuit Breaker ==="
|
|
334
|
+
puts
|
|
335
|
+
|
|
336
|
+
# Create aggregator
|
|
337
|
+
aggregator = APIAggregator.new
|
|
338
|
+
|
|
339
|
+
# Add API endpoints
|
|
340
|
+
aggregator.add_endpoint(APIEndpoint.new(
|
|
341
|
+
name: "users",
|
|
342
|
+
url: "https://api.example.com/users",
|
|
343
|
+
timeout: 5,
|
|
344
|
+
rate_limit_delay: 0.1
|
|
345
|
+
))
|
|
346
|
+
|
|
347
|
+
aggregator.add_endpoint(APIEndpoint.new(
|
|
348
|
+
name: "products",
|
|
349
|
+
url: "https://api.example.com/products",
|
|
350
|
+
timeout: 5,
|
|
351
|
+
rate_limit_delay: 0.1
|
|
352
|
+
))
|
|
353
|
+
|
|
354
|
+
aggregator.add_endpoint(APIEndpoint.new(
|
|
355
|
+
name: "orders",
|
|
356
|
+
url: "https://api.example.com/orders",
|
|
357
|
+
timeout: 5,
|
|
358
|
+
rate_limit_delay: 0.1
|
|
359
|
+
))
|
|
360
|
+
|
|
361
|
+
aggregator.add_endpoint(APIEndpoint.new(
|
|
362
|
+
name: "analytics",
|
|
363
|
+
url: "https://api.example.com/analytics",
|
|
364
|
+
timeout: 5,
|
|
365
|
+
rate_limit_delay: 0.1
|
|
366
|
+
))
|
|
367
|
+
|
|
368
|
+
# Fetch all data
|
|
369
|
+
data = aggregator.fetch_all(
|
|
370
|
+
simulate_errors: options[:simulate_errors],
|
|
371
|
+
simulate_slow: options[:simulate_slow]
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
# Generate report
|
|
375
|
+
AggregationReport.generate(data, options[:output])
|
|
376
|
+
end
|