smart_message 0.0.6 → 0.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eac068dc19706e57d2c66a1dfe36981fbac33e6d7b180fe0429f81d874727cd4
4
- data.tar.gz: 91585bc79637c4feab1256716519c0c3c7aa36c2b313ed09ebee7788b11d3f1a
3
+ metadata.gz: 118cf15fea99493c202bd383a547358a44a3873284cfe656665701213b40b51e
4
+ data.tar.gz: 7e542557b5b88f2963291232a875ba4dab44c682673251d7e976ac913ff41c14
5
5
  SHA512:
6
- metadata.gz: a0c0ead05e3cb104873e5cfa53cfc182ee06c6fd2f2e56fe02c0d64a4313dc4356b84d3b5be251e5438e6e381812848f6f587b005a315282204d8160f785f87b
7
- data.tar.gz: 3a84537cca151b3b8f3efa16b9ad8591811be19ab5547013fe9f33d7df58474e5db2a74e85e669fb95c17e5b375dd2d889b3a3051b6ded765120dd208246c26a
6
+ metadata.gz: 637591235fcdf1feb8708f244d1f2f6b416d7330bca7f06c597581ea9ffcb3eb55e74610bcc045c7839e50304f978fd24092c870528a7f3277cd0d8be2620ae4
7
+ data.tar.gz: 35773299f97565981fc778bd76358c2560eef1d6c4885367ddb66d81448136c8a8a9f402d26477d2c7ac29c885f07906a7e3f0396fb827fdf15e5bbe3e5a43bc
data/CHANGELOG.md CHANGED
@@ -6,6 +6,61 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
+ ## [0.0.7] 2025-08-19
10
+ ### Added
11
+ - **Production-Grade Circuit Breaker Integration**: Comprehensive reliability patterns using BreakerMachines gem
12
+ - Circuit breaker protection for message processing operations with configurable failure thresholds
13
+ - Transport-level circuit breakers for publish/subscribe operations with automatic fallback
14
+ - Integrated circuit breaker DSL throughout SmartMessage components for production reliability
15
+ - Memory and Redis storage backends for circuit breaker state persistence
16
+ - Built-in fallback mechanisms including dead letter queue, retry with exponential backoff, and graceful degradation
17
+ - Circuit breaker statistics and introspection capabilities for monitoring and debugging
18
+
19
+ ### Fixed
20
+ - **Critical: Redis Transport Header Preservation**: Fixed message header information loss during Redis pub/sub transport
21
+ - **Root Cause**: Redis transport was only sending message payload through channels, reconstructing generic headers on receive
22
+ - **Impact**: Message addressing information (`from`, `to`, `reply_to`) was lost, breaking entity-aware filtering and routing
23
+ - **Solution**: Modified Redis transport to combine header and payload into JSON structure during publish/subscribe
24
+ - **Result**: Full header information now preserved across Redis transport with backward compatibility fallback
25
+ - Redis IoT example now displays all expected sensor data, alerts, and real-time device monitoring output
26
+ - **Performance: Dispatcher Shutdown Optimization**: Dramatically improved dispatcher shutdown speed
27
+ - **Previous Issue**: Slow 1-second polling with no timeout mechanism caused delays up to several minutes
28
+ - **Optimization**: Replaced with native `wait_for_termination(3)` using Concurrent::ThreadPool built-in timeout
29
+ - **Performance Gain**: Shutdown now completes in milliseconds instead of seconds
30
+ - Removed unnecessary circuit breaker wrapper - thread pool handles timeout and fallback natively
31
+ - **Code Quality: Transport Method Signature Consistency**: Ensured all transport `do_publish` methods use consistent parameters
32
+ - Standardized `do_publish(message_header, message_payload)` signature across all transport implementations
33
+ - Updated Redis, STDOUT, and Memory transports for consistent interface
34
+
35
+ ### Changed
36
+ - **Dispatcher Architecture**: Simplified shutdown mechanism leveraging Concurrent::ThreadPool native capabilities
37
+ - Removed custom timeout polling logic in favor of built-in `wait_for_termination` method
38
+ - Eliminated redundant circuit breaker for shutdown operations - thread pool provides timeout and fallback
39
+ - **BREAKING**: Removed `router_pool_shutdown` circuit breaker configuration (no longer needed)
40
+ - **Redis Transport Protocol**: Enhanced message format to preserve complete header information
41
+ - Redis messages now contain both header and payload: `{header: {...}, payload: "..."}`
42
+ - Automatic fallback to legacy behavior for malformed or old-format messages
43
+ - Maintains full backward compatibility with existing Redis deployments
44
+
45
+ ### Enhanced
46
+ - **Circuit Breaker Integration**: Strategic application of reliability patterns where most beneficial
47
+ - Message processing operations protected with configurable failure thresholds and fallback behavior
48
+ - Transport publish/subscribe operations wrapped with circuit breakers for external service protection
49
+ - Clean separation between internal operations (thread pools) and external dependencies (Redis, etc.)
50
+ - **Redis Transport Reliability**: Production-ready Redis pub/sub with complete message fidelity
51
+ - Full message header preservation enables proper entity-aware filtering and routing
52
+ - IoT examples now demonstrate complete real-time messaging with sensor data, alerts, and device commands
53
+ - Enhanced error handling with JSON parsing fallbacks for malformed messages
54
+ - **Developer Experience**: Cleaner, more maintainable codebase with proper separation of concerns
55
+ - Circuit breakers applied strategically for external dependencies, not internal thread management
56
+ - Simplified shutdown logic leverages proven Concurrent::ThreadPool patterns
57
+ - Clear distinction between reliability mechanisms and business logic
58
+
59
+ ### Documentation
60
+ - **Circuit Breaker Patterns**: Examples of proper BreakerMachines DSL usage throughout codebase
61
+ - Strategic application for external service protection vs internal thread management
62
+ - Configuration examples for different failure scenarios and recovery patterns
63
+ - Best practices for production-grade messaging reliability
9
64
 
10
65
  ## [0.0.6] 2025-08-19
11
66
 
data/Gemfile.lock CHANGED
@@ -1,8 +1,9 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- smart_message (0.0.6)
4
+ smart_message (0.0.7)
5
5
  activesupport
6
+ breaker_machines
6
7
  concurrent-ruby
7
8
  hashie
8
9
  redis
@@ -27,6 +28,11 @@ GEM
27
28
  base64 (0.3.0)
28
29
  benchmark (0.4.1)
29
30
  bigdecimal (3.2.2)
31
+ breaker_machines (0.4.0)
32
+ activesupport (>= 7.2)
33
+ concurrent-ruby (~> 1.3)
34
+ state_machines (>= 0.50.0)
35
+ zeitwerk (~> 2.7)
30
36
  concurrent-ruby (1.3.5)
31
37
  connection_pool (2.5.3)
32
38
  debug_me (1.1.1)
@@ -53,9 +59,11 @@ GEM
53
59
  shoulda-context (2.0.0)
54
60
  shoulda-matchers (4.5.1)
55
61
  activesupport (>= 4.2.0)
62
+ state_machines (0.100.1)
56
63
  tzinfo (2.0.6)
57
64
  concurrent-ruby (~> 1.0)
58
65
  uri (1.0.3)
66
+ zeitwerk (2.7.3)
59
67
 
60
68
  PLATFORMS
61
69
  arm64-darwin-24
@@ -92,7 +92,8 @@ class OrderService
92
92
  customer_id: customer_id,
93
93
  amount: amount,
94
94
  payment_method: payment_method,
95
- items: items
95
+ items: items,
96
+ from: 'OrderService'
96
97
  )
97
98
 
98
99
  puts "🏪 OrderService: Sending order to payment processing..."
@@ -132,7 +133,8 @@ class PaymentService
132
133
  payment_id: payment_id,
133
134
  status: success ? 'success' : 'failed',
134
135
  message: success ? 'Payment processed successfully' : 'Insufficient funds',
135
- processed_at: Time.now.iso8601
136
+ processed_at: Time.now.iso8601,
137
+ from: 'PaymentService'
136
138
  )
137
139
 
138
140
  puts "💳 PaymentService: Sending payment response..."
@@ -270,7 +270,8 @@ class UserManager
270
270
  user_email: user_email,
271
271
  user_name: user_name,
272
272
  timestamp: Time.now.iso8601,
273
- metadata: metadata
273
+ metadata: metadata,
274
+ from: 'UserManager'
274
275
  )
275
276
 
276
277
  puts "👤 UserManager: Publishing #{event_type} event..."
@@ -164,7 +164,8 @@ class HumanChatAgent
164
164
  message_type: 'user',
165
165
  timestamp: Time.now.iso8601,
166
166
  mentions: mentions,
167
- metadata: { client: 'human_agent' }
167
+ metadata: { client: 'human_agent' },
168
+ from: @user_id
168
169
  )
169
170
 
170
171
  message.publish
@@ -228,7 +229,8 @@ class HumanChatAgent
228
229
  user_id: @user_id,
229
230
  command: command,
230
231
  parameters: parameters,
231
- timestamp: Time.now.iso8601
232
+ timestamp: Time.now.iso8601,
233
+ from: @user_id
232
234
  )
233
235
 
234
236
  bot_command.publish
@@ -259,7 +261,8 @@ class HumanChatAgent
259
261
  notification_type: notification_type,
260
262
  content: content,
261
263
  timestamp: Time.now.iso8601,
262
- metadata: { triggered_by: @user_id }
264
+ metadata: { triggered_by: @user_id },
265
+ from: @user_id
263
266
  )
264
267
 
265
268
  notification.publish
@@ -444,7 +447,8 @@ class BotAgent
444
447
  message_type: 'bot',
445
448
  timestamp: Time.now.iso8601,
446
449
  mentions: [],
447
- metadata: { bot_type: 'service_bot' }
450
+ metadata: { bot_type: 'service_bot' },
451
+ from: @bot_id
448
452
  )
449
453
 
450
454
  message.publish
@@ -480,7 +484,8 @@ class RoomManager
480
484
  notification_type: 'room_created',
481
485
  content: "Room '#{name}' was created",
482
486
  timestamp: Time.now.iso8601,
483
- metadata: { description: description }
487
+ metadata: { description: description },
488
+ from: 'RoomManager'
484
489
  )
485
490
 
486
491
  notification.publish
@@ -234,7 +234,7 @@ class SmartThermostat
234
234
  @battery_level -= 0.1 if rand < 0.1 # Occasional battery drain
235
235
 
236
236
  # Publish sensor data
237
- SensorDataMessage.new(
237
+ sensor_msg = SensorDataMessage.new(
238
238
  device_id: @device_id,
239
239
  device_type: 'thermostat',
240
240
  location: @location,
@@ -242,8 +242,11 @@ class SmartThermostat
242
242
  value: @current_temp.round(1),
243
243
  unit: 'celsius',
244
244
  timestamp: Time.now.iso8601,
245
- battery_level: @battery_level.round(1)
246
- ).publish
245
+ battery_level: @battery_level.round(1),
246
+ from: @device_id
247
+ )
248
+ puts "🌡️ #{@device_id}: Publishing temperature #{@current_temp.round(1)}°C"
249
+ sensor_msg.publish
247
250
 
248
251
  # Check for alerts
249
252
  if @current_temp > 28.0
@@ -255,7 +258,8 @@ class SmartThermostat
255
258
  location: @location,
256
259
  message: "Temperature too high: #{@current_temp.round(1)}°C",
257
260
  timestamp: Time.now.iso8601,
258
- requires_action: true
261
+ requires_action: true,
262
+ from: @device_id
259
263
  ).publish
260
264
  end
261
265
 
@@ -325,7 +329,8 @@ class SecurityCamera
325
329
  value: true,
326
330
  unit: 'boolean',
327
331
  timestamp: Time.now.iso8601,
328
- battery_level: @battery_level.round(1)
332
+ battery_level: @battery_level.round(1),
333
+ from: @device_id
329
334
  ).publish
330
335
 
331
336
  # Send security alert
@@ -337,7 +342,8 @@ class SecurityCamera
337
342
  location: @location,
338
343
  message: "Motion detected in #{@location}",
339
344
  timestamp: Time.now.iso8601,
340
- requires_action: false
345
+ requires_action: false,
346
+ from: @device_id
341
347
  ).publish
342
348
 
343
349
  # Auto-start recording
@@ -346,7 +352,8 @@ class SecurityCamera
346
352
  command: 'start_recording',
347
353
  parameters: { duration: 30 },
348
354
  requested_by: 'motion_detector',
349
- timestamp: Time.now.iso8601
355
+ timestamp: Time.now.iso8601,
356
+ from: @device_id
350
357
  ).publish
351
358
 
352
359
  elsif !motion_detected && @motion_detected
@@ -361,7 +368,8 @@ class SecurityCamera
361
368
  value: false,
362
369
  unit: 'boolean',
363
370
  timestamp: Time.now.iso8601,
364
- battery_level: @battery_level.round(1)
371
+ battery_level: @battery_level.round(1),
372
+ from: @device_id
365
373
  ).publish
366
374
  end
367
375
 
@@ -424,7 +432,8 @@ class SmartDoorLock
424
432
  value: @locked ? 'locked' : 'unlocked',
425
433
  unit: 'string',
426
434
  timestamp: Time.now.iso8601,
427
- battery_level: @battery_level.round(1)
435
+ battery_level: @battery_level.round(1),
436
+ from: @device_id
428
437
  ).publish
429
438
 
430
439
  # Check for low battery
@@ -437,7 +446,8 @@ class SmartDoorLock
437
446
  location: @location,
438
447
  message: "Door lock battery low: #{@battery_level.round(1)}%",
439
448
  timestamp: Time.now.iso8601,
440
- requires_action: true
449
+ requires_action: true,
450
+ from: @device_id
441
451
  ).publish
442
452
  end
443
453
 
@@ -528,7 +538,8 @@ class IoTDashboard
528
538
  summary_stats: {
529
539
  total_devices: @@devices.size,
530
540
  avg_battery: @@devices.values.map { |d| d[:battery_level] || 100 }.sum / [@@devices.size, 1].max
531
- }
541
+ },
542
+ from: 'IoTDashboard'
532
543
  ).publish
533
544
  end
534
545
  end
@@ -592,13 +603,16 @@ class SmartHomeDemo
592
603
  puts "\n🎛️ Sending some manual commands..."
593
604
  sleep(2)
594
605
 
595
- DeviceCommandMessage.new(
606
+ cmd_msg = DeviceCommandMessage.new(
596
607
  device_id: "THERM-001",
597
608
  command: "set_temperature",
598
609
  parameters: { target: 24.0 },
599
610
  requested_by: "mobile_app",
600
- timestamp: Time.now.iso8601
601
- ).publish
611
+ timestamp: Time.now.iso8601,
612
+ from: 'SmartHomeDemo'
613
+ )
614
+ puts "🎛️ Publishing command: set_temperature for THERM-001"
615
+ cmd_msg.publish
602
616
 
603
617
  sleep(3)
604
618
 
@@ -607,7 +621,8 @@ class SmartHomeDemo
607
621
  command: "start_recording",
608
622
  parameters: { duration: 60 },
609
623
  requested_by: "security_schedule",
610
- timestamp: Time.now.iso8601
624
+ timestamp: Time.now.iso8601,
625
+ from: 'SmartHomeDemo'
611
626
  ).publish
612
627
 
613
628
  sleep(2)
@@ -617,7 +632,8 @@ class SmartHomeDemo
617
632
  command: "get_status",
618
633
  parameters: {},
619
634
  requested_by: "mobile_app",
620
- timestamp: Time.now.iso8601
635
+ timestamp: Time.now.iso8601,
636
+ from: 'SmartHomeDemo'
621
637
  ).publish
622
638
 
623
639
  puts "\n⏳ Monitoring for 30 seconds (watch the Redis channels!)..."
@@ -137,7 +137,7 @@ notifications = [
137
137
  notifications.each_with_index do |notification_data, index|
138
138
  puts "\n📤 Publishing notification #{index + 1}: #{notification_data[:title]}"
139
139
 
140
- notification = NotificationMessage.new(**notification_data)
140
+ notification = NotificationMessage.new(**notification_data, from: 'ProcHandlerDemo')
141
141
  notification.publish
142
142
 
143
143
  # Give time for all handlers to process
@@ -161,7 +161,8 @@ error_notification = NotificationMessage.new(
161
161
  title: 'Another Error',
162
162
  message: 'This error won\'t trigger the block handler',
163
163
  user_id: 'test_user',
164
- timestamp: Time.now.iso8601
164
+ timestamp: Time.now.iso8601,
165
+ from: 'ProcHandlerDemo'
165
166
  )
166
167
 
167
168
  error_notification.publish
@@ -470,7 +470,7 @@ class PriorityOrderService
470
470
 
471
471
  def process_priority_order(order_data)
472
472
  # Create message with instance-level logger override
473
- message = OrderProcessingMessage.new(**order_data)
473
+ message = OrderProcessingMessage.new(**order_data, from: 'PriorityOrderService')
474
474
 
475
475
  # Override the logger at instance level
476
476
  message.logger(@priority_logger)
@@ -506,7 +506,8 @@ class LoggerDemo
506
506
  DefaultLoggerMessage.subscribe
507
507
  default_msg = DefaultLoggerMessage.new(
508
508
  message: "Testing the built-in default logger",
509
- level: "info"
509
+ level: "info",
510
+ from: 'LoggerDemo'
510
511
  )
511
512
  default_msg.publish
512
513
  sleep(0.5)
@@ -518,7 +519,8 @@ class LoggerDemo
518
519
  customer_id: "CUST-123",
519
520
  amount: 99.99,
520
521
  status: "pending",
521
- items: ["Widget A", "Widget B"]
522
+ items: ["Widget A", "Widget B"],
523
+ from: 'LoggerDemo'
522
524
  )
523
525
  order1.publish
524
526
  sleep(0.5)
@@ -529,7 +531,8 @@ class LoggerDemo
529
531
  recipient: "customer@example.com",
530
532
  subject: "Order Confirmation",
531
533
  body: "Your order has been received",
532
- priority: "normal"
534
+ priority: "normal",
535
+ from: 'LoggerDemo'
533
536
  )
534
537
  notification.publish
535
538
  sleep(0.5)
@@ -542,7 +545,8 @@ class LoggerDemo
542
545
  customer_id: "VIP-456",
543
546
  amount: 299.99,
544
547
  status: "urgent",
545
- items: ["Premium Widget", "Express Shipping"]
548
+ items: ["Premium Widget", "Express Shipping"],
549
+ from: 'PriorityOrderService'
546
550
  )
547
551
  sleep(0.5)
548
552
 
@@ -555,7 +559,8 @@ class LoggerDemo
555
559
  # Create and send a message - watch for the Ruby logger output
556
560
  msg = StandardLoggerMessage.new(
557
561
  content: "Testing with Ruby's standard logger",
558
- level: "info"
562
+ level: "info",
563
+ from: 'LoggerDemo'
559
564
  )
560
565
 
561
566
  # The logger will output to STDOUT using Ruby's standard format
@@ -573,7 +578,8 @@ class LoggerDemo
573
578
  order_id: nil, # This might cause issues
574
579
  customer_id: "ERROR-TEST",
575
580
  amount: "invalid_amount",
576
- status: "error_demo"
581
+ status: "error_demo",
582
+ from: 'LoggerDemo'
577
583
  )
578
584
 
579
585
  # Simulate an error during processing
@@ -167,7 +167,8 @@ class ErrorDemonstrator
167
167
  missing_user_id = UserRegistrationMessage.new(
168
168
  email: "john@example.com",
169
169
  age: 25,
170
- username: "johndoe"
170
+ username: "johndoe",
171
+ from: 'ErrorDemonstrator'
171
172
  )
172
173
  puts "❌ ERROR: Should have failed but didn't!"
173
174
  rescue => e
@@ -181,7 +182,8 @@ class ErrorDemonstrator
181
182
  begin
182
183
  missing_multiple = UserRegistrationMessage.new(
183
184
  user_id: "USER-123",
184
- username: "johndoe"
185
+ username: "johndoe",
186
+ from: 'ErrorDemonstrator'
185
187
  )
186
188
  puts "❌ ERROR: Should have failed but didn't!"
187
189
  rescue => e
@@ -197,7 +199,8 @@ class ErrorDemonstrator
197
199
  user_id: "USER-123",
198
200
  email: "john@example.com",
199
201
  age: 25,
200
- username: "johndoe"
202
+ username: "johndoe",
203
+ from: 'ErrorDemonstrator'
201
204
  )
202
205
  puts "✅ Message created successfully"
203
206
  puts " User: #{valid_user.username} (#{valid_user.email})"
@@ -227,7 +230,8 @@ class ErrorDemonstrator
227
230
  user_id: "USER-124",
228
231
  email: "not-an-email", # Invalid format
229
232
  age: 25,
230
- username: "janedoe"
233
+ username: "janedoe",
234
+ from: 'ErrorDemonstrator'
231
235
  )
232
236
  puts " Message created, now validating..."
233
237
  invalid_email.validate! # Explicitly trigger validation
@@ -245,7 +249,8 @@ class ErrorDemonstrator
245
249
  user_id: "USER-125",
246
250
  email: "kid@example.com",
247
251
  age: 10, # Too young (< 13)
248
- username: "kiduser"
252
+ username: "kiduser",
253
+ from: 'ErrorDemonstrator'
249
254
  )
250
255
  puts " Message created, now validating..."
251
256
  invalid_age.validate! # Explicitly trigger validation
@@ -263,7 +268,8 @@ class ErrorDemonstrator
263
268
  user_id: "USER-126",
264
269
  email: "user@example.com",
265
270
  age: 30,
266
- username: "user@123!" # Contains invalid characters
271
+ username: "user@123!", # Contains invalid characters
272
+ from: 'ErrorDemonstrator'
267
273
  )
268
274
  puts " Message created, now validating..."
269
275
  invalid_username.validate! # Explicitly trigger validation
@@ -282,7 +288,8 @@ class ErrorDemonstrator
282
288
  email: "user@example.com",
283
289
  age: 30,
284
290
  username: "validuser",
285
- subscription_type: "platinum" # Not in allowed list
291
+ subscription_type: "platinum", # Not in allowed list
292
+ from: 'ErrorDemonstrator'
286
293
  )
287
294
  puts " Message created, now validating..."
288
295
  invalid_subscription.validate! # Explicitly trigger validation
@@ -301,7 +308,8 @@ class ErrorDemonstrator
301
308
  email: "valid@example.com",
302
309
  age: 25,
303
310
  username: "validuser123",
304
- subscription_type: "premium"
311
+ subscription_type: "premium",
312
+ from: 'ErrorDemonstrator'
305
313
  )
306
314
  puts " Message created, now validating..."
307
315
  valid_user.validate! # Explicitly trigger validation
@@ -339,7 +347,8 @@ class ErrorDemonstrator
339
347
  user_id: "USER-V1-001",
340
348
  email: "v1user@example.com",
341
349
  age: 28,
342
- username: "v1user"
350
+ username: "v1user",
351
+ from: 'ErrorDemonstrator'
343
352
  )
344
353
  puts "✅ V1 Message created (version #{v1_message._sm_header.version})"
345
354
  v1_message.publish
@@ -356,7 +365,8 @@ class ErrorDemonstrator
356
365
  email: "v2user@example.com",
357
366
  age: 32,
358
367
  username: "v2user",
359
- phone_number: "+1234567890"
368
+ phone_number: "+1234567890",
369
+ from: 'ErrorDemonstrator'
360
370
  )
361
371
  puts "✅ V2 Message created (version #{v2_message._sm_header.version})"
362
372
  v2_message.publish
@@ -374,7 +384,8 @@ class ErrorDemonstrator
374
384
  user_id: "USER-MISMATCH-001",
375
385
  email: "mismatch@example.com",
376
386
  age: 35,
377
- username: "mismatchuser"
387
+ username: "mismatchuser",
388
+ from: 'ErrorDemonstrator'
378
389
  )
379
390
 
380
391
  puts "✅ Original message created with version #{version_mismatch_message._sm_header.version}"
@@ -396,8 +407,8 @@ class ErrorDemonstrator
396
407
 
397
408
  # Test Case 4: Show version information
398
409
  puts "📊 Test Case 3D: Version information display"
399
- v1_msg = UserRegistrationMessage.new(user_id: "INFO-V1", email: "info@example.com", age: 25, username: "infouser")
400
- v2_msg = UserRegistrationMessageV2.new(user_id: "INFO-V2", email: "info@example.com", age: 25, username: "infouser", phone_number: "+1234567890")
410
+ v1_msg = UserRegistrationMessage.new(user_id: "INFO-V1", email: "info@example.com", age: 25, username: "infouser", from: 'ErrorDemonstrator')
411
+ v2_msg = UserRegistrationMessageV2.new(user_id: "INFO-V2", email: "info@example.com", age: 25, username: "infouser", phone_number: "+1234567890", from: 'ErrorDemonstrator')
401
412
 
402
413
  puts "📋 Version Information:"
403
414
  puts " UserRegistrationMessage (V1):"
@@ -424,7 +435,7 @@ class ErrorDemonstrator
424
435
  puts "Expected: Ideally should report all missing fields"
425
436
  puts "Actual Hashie::Dash behavior:"
426
437
  begin
427
- message = MultiRequiredMessage.new(optional_field: "present")
438
+ message = MultiRequiredMessage.new(optional_field: "present", from: 'ErrorDemonstrator')
428
439
  puts "❌ ERROR: Should have failed but didn't!"
429
440
  rescue => e
430
441
  puts "✅ Error caught: #{e.class.name}"
@@ -443,7 +454,7 @@ class ErrorDemonstrator
443
454
  test_data.each_with_index do |test_case, index|
444
455
  puts " #{index + 1}. #{test_case[:name]}:"
445
456
  begin
446
- message = MultiRequiredMessage.new(test_case[:data])
457
+ message = MultiRequiredMessage.new(test_case[:data].merge(from: 'ErrorDemonstrator'))
447
458
  puts " ✅ Success: Message created"
448
459
  rescue => e
449
460
  field_name = e.message.match(/property '([^']+)'/)[1] rescue 'unknown'
@@ -62,7 +62,8 @@ order = OrderMessage.new(
62
62
  order_id: "ORD-2024-001",
63
63
  customer_id: "CUST-12345",
64
64
  items: ["Widget A", "Widget B", "Gadget C"],
65
- total_amount: 299.99
65
+ total_amount: 299.99,
66
+ from: 'order-service'
66
67
  )
67
68
 
68
69
  puts "\n📤 Publishing point-to-point order message..."
@@ -111,7 +112,8 @@ SystemAnnouncementMessage.subscribe
111
112
  announcement = SystemAnnouncementMessage.new(
112
113
  message: "System maintenance scheduled for tonight at 2:00 AM EST",
113
114
  priority: 'high',
114
- effective_time: '2024-12-20 02:00:00 EST'
115
+ effective_time: '2024-12-20 02:00:00 EST',
116
+ from: 'admin-service'
115
117
  )
116
118
 
117
119
  puts "\n📤 Publishing broadcast announcement..."
@@ -192,7 +194,8 @@ UserLookupResponse.subscribe
192
194
  request = UserLookupRequest.new(
193
195
  user_id: "USER-789",
194
196
  request_id: SecureRandom.uuid,
195
- requested_fields: ['name', 'email', 'last_login']
197
+ requested_fields: ['name', 'email', 'last_login'],
198
+ from: 'web-service'
196
199
  )
197
200
 
198
201
  puts "\n📤 Publishing user lookup request..."
@@ -208,7 +211,8 @@ response = UserLookupResponse.new(
208
211
  email: "alice@example.com",
209
212
  last_login: "2024-12-19 14:30:00"
210
213
  },
211
- success: true
214
+ success: true,
215
+ from: 'user-service'
212
216
  )
213
217
  response.to('web-service') # Set reply destination
214
218
 
@@ -261,7 +265,8 @@ normal_payment = PaymentMessage.new(
261
265
  payment_id: "PAY-001",
262
266
  amount: 150.00,
263
267
  account_id: "ACCT-12345",
264
- payment_method: 'credit_card'
268
+ payment_method: 'credit_card',
269
+ from: 'payment-service'
265
270
  )
266
271
 
267
272
  puts "\n📤 Publishing normal payment (using class defaults)..."
@@ -275,7 +280,8 @@ backup_payment = PaymentMessage.new(
275
280
  payment_id: "PAY-002",
276
281
  amount: 75.50,
277
282
  account_id: "ACCT-67890",
278
- payment_method: 'debit_card'
283
+ payment_method: 'debit_card',
284
+ from: 'payment-service'
279
285
  )
280
286
 
281
287
  # Override instance addressing
@@ -338,7 +344,8 @@ external_message = ExternalAPIMessage.new(
338
344
  api_call: "PUT /api/v1/users/USER-123",
339
345
  payload_data: internal_data,
340
346
  authentication_token: "Bearer abc123xyz789",
341
- partner_id: "PARTNER-ALPHA"
347
+ partner_id: "PARTNER-ALPHA",
348
+ from: 'api-gateway'
342
349
  )
343
350
 
344
351
  # Gateway can override destination based on routing rules