smart_message 0.0.12 → 0.0.16

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.
Files changed (117) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +155 -1
  4. data/Gemfile.lock +6 -6
  5. data/README.md +71 -25
  6. data/docs/core-concepts/architecture.md +5 -10
  7. data/docs/getting-started/examples.md +0 -12
  8. data/docs/getting-started/quick-start.md +4 -9
  9. data/docs/index.md +6 -4
  10. data/docs/reference/serializers.md +160 -488
  11. data/docs/reference/transports.md +47 -146
  12. data/docs/transports/memory-transport.md +2 -1
  13. data/docs/transports/multi-transport.md +484 -0
  14. data/docs/transports/redis-transport-comparison.md +215 -350
  15. data/docs/transports/redis-transport.md +3 -22
  16. data/examples/README.md +6 -9
  17. data/examples/city_scenario/README.md +1 -1
  18. data/examples/city_scenario/messages/emergency_911_message.rb +0 -1
  19. data/examples/city_scenario/messages/emergency_resolved_message.rb +0 -1
  20. data/examples/city_scenario/messages/fire_dispatch_message.rb +0 -1
  21. data/examples/city_scenario/messages/fire_emergency_message.rb +0 -1
  22. data/examples/city_scenario/messages/health_check_message.rb +0 -1
  23. data/examples/city_scenario/messages/health_status_message.rb +0 -1
  24. data/examples/city_scenario/messages/police_dispatch_message.rb +0 -1
  25. data/examples/city_scenario/messages/silent_alarm_message.rb +0 -1
  26. data/examples/file/00_run_all_file_demos.rb +260 -0
  27. data/examples/file/01_basic_file_transport_demo.rb +237 -0
  28. data/examples/file/02_fifo_transport_demo.rb +289 -0
  29. data/examples/file/03_file_watching_demo.rb +332 -0
  30. data/examples/file/04_multi_transport_file_demo.rb +432 -0
  31. data/examples/file/README.md +257 -0
  32. data/examples/memory/00_run_all_demos.rb +317 -0
  33. data/examples/memory/01_message_deduplication_demo.rb +18 -32
  34. data/examples/memory/02_dead_letter_queue_demo.rb +9 -12
  35. data/examples/memory/03_point_to_point_orders.rb +3 -5
  36. data/examples/memory/04_publish_subscribe_events.rb +15 -16
  37. data/examples/memory/05_many_to_many_chat.rb +19 -22
  38. data/examples/memory/06_stdout_publish_only.rb +145 -0
  39. data/examples/memory/07_proc_handlers_demo.rb +13 -14
  40. data/examples/memory/08_custom_logger_demo.rb +136 -140
  41. data/examples/memory/09_error_handling_demo.rb +7 -10
  42. data/examples/memory/10_entity_addressing_basic.rb +25 -31
  43. data/examples/memory/11_entity_addressing_with_filtering.rb +32 -36
  44. data/examples/memory/12_regex_filtering_microservices.rb +10 -11
  45. data/examples/memory/13_header_block_configuration.rb +0 -5
  46. data/examples/memory/14_global_configuration_demo.rb +12 -14
  47. data/examples/memory/15_logger_demo.rb +0 -1
  48. data/examples/memory/README.md +37 -20
  49. data/examples/memory/log/demo_app.log.1 +100 -0
  50. data/examples/memory/log/demo_app.log.2 +100 -0
  51. data/examples/multi_transport_example.rb +114 -0
  52. data/examples/redis/01_smart_home_iot_demo.rb +20 -24
  53. data/examples/redis/README.md +0 -2
  54. data/examples/utilities/box_it.rb +12 -0
  55. data/examples/utilities/doing.rb +19 -0
  56. data/examples/utilities/temp.md +28 -0
  57. data/lib/smart_message/base.rb +24 -17
  58. data/lib/smart_message/configuration.rb +2 -23
  59. data/lib/smart_message/dead_letter_queue.rb +1 -1
  60. data/lib/smart_message/errors.rb +3 -0
  61. data/lib/smart_message/header.rb +1 -1
  62. data/lib/smart_message/logger/default.rb +1 -1
  63. data/lib/smart_message/messaging.rb +37 -66
  64. data/lib/smart_message/plugins.rb +42 -41
  65. data/lib/smart_message/serializer/base.rb +1 -1
  66. data/lib/smart_message/serializer.rb +3 -2
  67. data/lib/smart_message/subscription.rb +18 -20
  68. data/lib/smart_message/transport/async_publish_queue.rb +284 -0
  69. data/lib/smart_message/transport/base.rb +42 -8
  70. data/lib/smart_message/transport/fifo_operations.rb +264 -0
  71. data/lib/smart_message/transport/file_operations.rb +200 -0
  72. data/lib/smart_message/transport/file_transport.rb +149 -0
  73. data/lib/smart_message/transport/file_watching.rb +72 -0
  74. data/lib/smart_message/transport/memory_transport.rb +23 -4
  75. data/lib/smart_message/transport/partitioned_files.rb +46 -0
  76. data/lib/smart_message/transport/redis_transport.rb +11 -0
  77. data/lib/smart_message/transport/registry.rb +0 -1
  78. data/lib/smart_message/transport/stdout_transport.rb +73 -41
  79. data/lib/smart_message/transport/stdout_transport.rb.backup +88 -0
  80. data/lib/smart_message/transport.rb +0 -1
  81. data/lib/smart_message/version.rb +1 -1
  82. metadata +25 -37
  83. data/docs/guides/redis-queue-getting-started.md +0 -697
  84. data/docs/guides/redis-queue-patterns.md +0 -889
  85. data/docs/guides/redis-queue-production.md +0 -1091
  86. data/docs/transports/redis-enhanced-transport.md +0 -524
  87. data/docs/transports/redis-queue-transport.md +0 -1304
  88. data/examples/redis_enhanced/README.md +0 -319
  89. data/examples/redis_enhanced/enhanced_01_basic_patterns.rb +0 -233
  90. data/examples/redis_enhanced/enhanced_02_fluent_api.rb +0 -331
  91. data/examples/redis_enhanced/enhanced_03_dual_publishing.rb +0 -281
  92. data/examples/redis_enhanced/enhanced_04_advanced_routing.rb +0 -419
  93. data/examples/redis_queue/01_basic_messaging.rb +0 -221
  94. data/examples/redis_queue/01_comprehensive_examples.rb +0 -508
  95. data/examples/redis_queue/02_pattern_routing.rb +0 -405
  96. data/examples/redis_queue/03_fluent_api.rb +0 -422
  97. data/examples/redis_queue/04_load_balancing.rb +0 -486
  98. data/examples/redis_queue/05_microservices.rb +0 -735
  99. data/examples/redis_queue/06_emergency_alerts.rb +0 -777
  100. data/examples/redis_queue/07_queue_management.rb +0 -587
  101. data/examples/redis_queue/README.md +0 -366
  102. data/examples/redis_queue/enhanced_01_basic_patterns.rb +0 -233
  103. data/examples/redis_queue/enhanced_02_fluent_api.rb +0 -331
  104. data/examples/redis_queue/enhanced_03_dual_publishing.rb +0 -281
  105. data/examples/redis_queue/enhanced_04_advanced_routing.rb +0 -419
  106. data/examples/redis_queue/redis_queue_architecture.svg +0 -148
  107. data/ideas/README.md +0 -41
  108. data/ideas/agents.md +0 -1001
  109. data/ideas/database_transport.md +0 -980
  110. data/ideas/improvement.md +0 -359
  111. data/ideas/meshage.md +0 -1788
  112. data/ideas/message_discovery.md +0 -178
  113. data/ideas/message_schema.md +0 -1381
  114. data/lib/smart_message/transport/redis_enhanced_transport.rb +0 -399
  115. data/lib/smart_message/transport/redis_queue_transport.rb +0 -555
  116. data/lib/smart_message/wrapper.rb.bak +0 -132
  117. /data/examples/memory/{06_pretty_print_demo.rb → 16_pretty_print_demo.rb} +0 -0
@@ -1,555 +0,0 @@
1
- # lib/smart_message/transport/redis_queue_transport_async.rb
2
- # encoding: utf-8
3
- # frozen_string_literal: true
4
-
5
- require 'async'
6
- require 'async/redis'
7
- require 'json'
8
-
9
- module SmartMessage
10
- module Transport
11
- # Redis Queue Transport - Async-powered routing with RabbitMQ-style patterns
12
- # This transport provides intelligent routing using Redis Lists as queues with pattern matching
13
- # Built on Ruby's Async framework for modern concurrency and testing
14
- #
15
- # Key Features:
16
- # - Async/Fiber-based concurrency (thousands of subscriptions)
17
- # - RabbitMQ-style topic exchange pattern matching (#.*.my_uuid)
18
- # - Load balancing via consumer groups
19
- # - Queue persistence using Redis Lists
20
- # - FIFO message ordering
21
- # - 10x faster than RabbitMQ with same routing intelligence
22
- # - Test-friendly with proper async lifecycle management
23
- #
24
- # Usage:
25
- # Async do
26
- # transport = SmartMessage::Transport::RedisQueueTransport.new
27
- # transport.subscribe_pattern("#.*.my_service") do |msg_class, data|
28
- # puts "Processing: #{msg_class}"
29
- # end
30
- # end
31
- class RedisQueueTransport < Base
32
-
33
- DEFAULT_CONFIG = {
34
- url: 'redis://localhost:6379',
35
- db: 0,
36
- exchange_name: 'smart_message',
37
- queue_prefix: 'smart_message.queue',
38
- routing_prefix: 'smart_message.routing',
39
- consumer_timeout: 1, # 1 second blocking pop timeout
40
- max_queue_size: 10000, # Max messages per queue (circular buffer)
41
- cleanup_on_disconnect: true, # Remove queues on shutdown
42
- reconnect_attempts: 5,
43
- reconnect_delay: 1,
44
- async_timeout: 30 # Global async task timeout
45
- }.freeze
46
-
47
- attr_reader :redis, :exchange_name, :active_queues, :consumer_tasks
48
-
49
- def initialize(**options)
50
- @active_queues = {} # queue_name => consumer_info
51
- @consumer_tasks = {} # queue_name => async task
52
- @routing_table = {} # pattern => [queue_names]
53
- @shutdown = false
54
- # Don't initialize @redis to nil here since super() calls configure which sets it
55
- super(**options)
56
- end
57
-
58
- def configure
59
- @exchange_name = @options[:exchange_name]
60
-
61
- # Setup Redis connection synchronously for immediate availability
62
- begin
63
- redis_client = nil
64
- Async do
65
- endpoint = Async::Redis::Endpoint.parse(@options[:url], db: @options[:db])
66
- redis_client = Async::Redis::Client.new(endpoint)
67
-
68
- # Test connection
69
- redis_client.call('PING')
70
-
71
- # Initialize exchange metadata with local redis client
72
- @redis = redis_client # Set instance variable
73
- setup_exchange
74
-
75
- logger.info { "[RedisQueue] Async transport configured with exchange: #{@exchange_name}" }
76
- end.wait
77
-
78
- # Ensure @redis is set outside the async block too
79
- @redis = redis_client if redis_client
80
- rescue => e
81
- logger.error { "[RedisQueue] Failed to configure transport: #{e.message}" }
82
- @redis = nil
83
- raise
84
- end
85
- end
86
-
87
- def default_options
88
- DEFAULT_CONFIG
89
- end
90
-
91
- # Publish message with intelligent routing to matching queues (Async)
92
- # @param message_class [String] The message class name
93
- # @param serialized_message [String] The serialized message content
94
- def do_publish(message_class, serialized_message)
95
- async_task do
96
- routing_info = extract_routing_info(serialized_message)
97
- routing_key = build_enhanced_routing_key(message_class, routing_info)
98
-
99
- # Find all queues that match this routing key (like RabbitMQ topic exchange)
100
- matching_queues = find_matching_queues(routing_key)
101
-
102
- if matching_queues.empty?
103
- logger.debug { "[RedisQueue] No queues match routing key: #{routing_key}" }
104
- next
105
- end
106
-
107
- # Create message envelope with metadata
108
- message_envelope = {
109
- routing_key: routing_key,
110
- message_class: message_class.to_s,
111
- data: serialized_message,
112
- timestamp: Time.now.to_f,
113
- headers: routing_info
114
- }.to_json
115
-
116
- # Publish to all matching queues atomically using async redis
117
- published_count = 0
118
-
119
- # Use pipelined operations for better performance
120
- commands = []
121
- matching_queues.each do |queue_name|
122
- commands << [:lpush, queue_name, message_envelope]
123
- commands << [:ltrim, queue_name, 0, @options[:max_queue_size] - 1]
124
- published_count += 1
125
- end
126
-
127
- # Execute all commands in pipeline
128
- @redis.pipelined(commands) if commands.any?
129
-
130
- logger.debug { "[RedisQueue] Published to #{published_count} queues with key: #{routing_key}" }
131
- rescue => e
132
- logger.error { "[RedisQueue] Publish error: #{e.message}" }
133
- raise
134
- end
135
- end
136
-
137
- # Subscribe to messages using RabbitMQ-style pattern matching (Async)
138
- # @param pattern [String] Routing pattern (e.g., "#.*.my_service", "order.#.*.*")
139
- # @param process_method [String] Method identifier for processing
140
- # @param filter_options [Hash] Additional filtering options
141
- # @param block [Proc] Optional block for message processing
142
- def subscribe_pattern(pattern, process_method = :process, filter_options = {}, &block)
143
- queue_name = derive_queue_name(pattern, filter_options)
144
-
145
- # Add pattern to routing table (no mutex needed with Fibers)
146
- @routing_table[pattern] ||= []
147
- @routing_table[pattern] << queue_name unless @routing_table[pattern].include?(queue_name)
148
-
149
- # Store queue metadata
150
- @active_queues[queue_name] = {
151
- pattern: pattern,
152
- process_method: process_method,
153
- filter_options: filter_options,
154
- created_at: Time.now,
155
- block_handler: block
156
- }
157
-
158
- # Start async consumer task for this queue (unless in test mode)
159
- start_queue_consumer(queue_name) unless @options[:test_mode]
160
-
161
- logger.info { "[RedisQueue] Subscribed to pattern '#{pattern}' via queue '#{queue_name}'" }
162
- end
163
-
164
- # Subscribe to all messages sent to a specific recipient
165
- # @param recipient_id [String] The recipient identifier
166
- def subscribe_to_recipient(recipient_id, process_method = :process, &block)
167
- pattern = "#.*.#{sanitize_for_routing_key(recipient_id)}"
168
- subscribe_pattern(pattern, process_method, {}, &block)
169
- end
170
-
171
- # Subscribe to all messages from a specific sender
172
- # @param sender_id [String] The sender identifier
173
- def subscribe_from_sender(sender_id, process_method = :process, &block)
174
- pattern = "#.#{sanitize_for_routing_key(sender_id)}.*"
175
- subscribe_pattern(pattern, process_method, {}, &block)
176
- end
177
-
178
- # Subscribe to all messages of a specific type regardless of routing
179
- # @param message_type [String] The message class name
180
- def subscribe_to_type(message_type, process_method = :process, &block)
181
- pattern = "*.#{message_type.to_s.gsub('::', '.').downcase}.*.*"
182
- subscribe_pattern(pattern, process_method, {}, &block)
183
- end
184
-
185
- # Subscribe to all alert/emergency messages
186
- def subscribe_to_alerts(process_method = :process, &block)
187
- patterns = [
188
- "emergency.#.*.*",
189
- "#.alert.*.*",
190
- "#.alarm.*.*",
191
- "#.critical.*.*"
192
- ]
193
-
194
- patterns.each { |pattern| subscribe_pattern(pattern, process_method, {}, &block) }
195
- end
196
-
197
- # Subscribe to all broadcast messages
198
- def subscribe_to_broadcasts(process_method = :process, &block)
199
- pattern = "#.*.broadcast"
200
- subscribe_pattern(pattern, process_method, {}, &block)
201
- end
202
-
203
- # Test-friendly disconnect method
204
- def disconnect
205
- @shutdown = true
206
-
207
- # Stop all consumer tasks
208
- @consumer_tasks.each do |queue_name, task|
209
- task.stop if task&.running?
210
- rescue => e
211
- logger.debug { "[RedisQueue] Error stopping consumer task for #{queue_name}: #{e.message}" }
212
- end
213
-
214
- @consumer_tasks.clear
215
-
216
- # Close Redis connection
217
- async_task do
218
- @redis&.close
219
- @redis = nil
220
- rescue => e
221
- logger.debug { "[RedisQueue] Error closing Redis connection: #{e.message}" }
222
- end
223
- end
224
-
225
- def connected?
226
- return false unless @redis
227
-
228
- # Test connection with async ping
229
- begin
230
- if Async::Task.current?
231
- # Already in async context
232
- return @redis.call('PING') == 'PONG'
233
- else
234
- # Need to create async context
235
- result = Async do
236
- @redis.call('PING') == 'PONG'
237
- end.wait
238
- return result
239
- end
240
- rescue => e
241
- logger.debug { "[RedisQueue] Connection test failed: #{e.message}" }
242
- return false
243
- end
244
- end
245
-
246
- # Get statistics about all active queues
247
- def queue_stats
248
- return {} unless @redis
249
-
250
- stats = {}
251
-
252
- begin
253
- Async do
254
- @active_queues.each do |queue_name, queue_info|
255
- length = @redis.llen(queue_name).to_i
256
- stats[queue_name] = {
257
- length: length,
258
- pattern: queue_info[:pattern],
259
- created_at: queue_info[:created_at],
260
- consumers: @consumer_tasks.key?(queue_name) ? 1 : 0
261
- }
262
- end
263
- end.wait
264
- rescue => e
265
- logger.error { "[RedisQueue] Error getting queue stats: #{e.message}" }
266
- end
267
-
268
- stats
269
- end
270
-
271
- # Get the current routing table
272
- def routing_table
273
- @routing_table.dup
274
- end
275
-
276
- # Fluent API builder for complex subscriptions
277
- def where
278
- RedisQueueSubscriptionBuilder.new(self)
279
- end
280
-
281
- private
282
-
283
- # Ensure async operations run in proper context
284
- def async_task(&block)
285
- if Async::Task.current?
286
- # Already in async context
287
- yield
288
- else
289
- # Need to create async context
290
- Async { yield }
291
- end
292
- end
293
-
294
- # Start async consumer for a queue
295
- def start_queue_consumer(queue_name)
296
- return if @consumer_tasks.key?(queue_name)
297
-
298
- @consumer_tasks[queue_name] = async_task do |task|
299
- begin
300
- consume_from_queue(queue_name)
301
- rescue => e
302
- logger.error { "[RedisQueue] Consumer task error for #{queue_name}: #{e.message}" }
303
- ensure
304
- @consumer_tasks.delete(queue_name)
305
- end
306
- end
307
- end
308
-
309
- # Async consumer loop for a specific queue
310
- def consume_from_queue(queue_name)
311
- queue_info = @active_queues[queue_name]
312
- return unless queue_info
313
-
314
- while !@shutdown
315
- begin
316
- # Use BRPOP for blocking read with timeout (cooperative blocking)
317
- result = @redis.brpop(queue_name, timeout: @options[:consumer_timeout])
318
-
319
- if result && result.length >= 2
320
- _, message_envelope = result
321
- process_queue_message(message_envelope, queue_info)
322
- end
323
-
324
- rescue => e
325
- logger.error { "[RedisQueue] Redis connection error in consumer: #{e.message}" }
326
- # Async will handle reconnection automatically
327
- sleep(1) unless @shutdown
328
- rescue => e
329
- logger.error { "[RedisQueue] Consumer error for #{queue_name}: #{e.message}" }
330
- sleep(1) unless @shutdown
331
- end
332
- end
333
- end
334
-
335
- # Process a message from the queue
336
- def process_queue_message(message_envelope, queue_info)
337
- begin
338
- message_data = JSON.parse(message_envelope)
339
-
340
- message_class = message_data['message_class']
341
- payload = message_data['data']
342
- headers = message_data['headers'] || {}
343
-
344
- # Apply additional filtering if specified
345
- if should_process_message?(headers, queue_info[:filter_options])
346
- # Use block handler if provided, otherwise route through dispatcher
347
- if queue_info[:block_handler]
348
- queue_info[:block_handler].call(message_class, payload)
349
- else
350
- # Route through dispatcher (inherited from Base)
351
- receive(message_class, payload)
352
- end
353
- else
354
- logger.debug { "[RedisQueue] Message filtered out by queue rules" }
355
- end
356
-
357
- rescue JSON::ParserError => e
358
- logger.error { "[RedisQueue] Invalid message envelope: #{e.message}" }
359
- rescue => e
360
- logger.error { "[RedisQueue] Error processing message: #{e.message}" }
361
- end
362
- end
363
-
364
- # Check if message should be processed based on filters
365
- def should_process_message?(headers, filter_options)
366
- return true if filter_options.empty?
367
-
368
- # Apply from/to filters if specified
369
- if filter_options[:from] && headers['from'] != filter_options[:from]
370
- return false
371
- end
372
-
373
- if filter_options[:to] && headers['to'] != filter_options[:to]
374
- return false
375
- end
376
-
377
- true
378
- end
379
-
380
- # Setup exchange metadata in Redis
381
- def setup_exchange
382
- exchange_key = "#{@options[:routing_prefix]}:#{@exchange_name}:metadata"
383
- @redis.hset(exchange_key, 'type', 'topic')
384
- @redis.hset(exchange_key, 'created_at', Time.now.to_f)
385
- @redis.expire(exchange_key, 86400) # Expire in 24 hours
386
- end
387
-
388
- # Generate queue name from pattern and options
389
- def derive_queue_name(pattern, filter_options = {})
390
- base_name = pattern.gsub(/[#*]/, 'wildcard')
391
- filter_suffix = filter_options.empty? ? '' : "_#{filter_options.hash.abs}"
392
- "#{@options[:queue_prefix]}.#{base_name}#{filter_suffix}"
393
- end
394
-
395
- # Extract routing information from message
396
- def extract_routing_info(serialized_message)
397
- data = JSON.parse(serialized_message)
398
- header = data['_sm_header'] || {}
399
- {
400
- from: header['from'],
401
- to: header['to']
402
- }
403
- rescue JSON::ParserError
404
- { from: nil, to: nil }
405
- end
406
-
407
- # Build enhanced routing key similar to RabbitMQ topic exchange
408
- def build_enhanced_routing_key(message_class, routing_info)
409
- # Create hierarchical routing key: namespace.message_type.from.to
410
- parts = []
411
-
412
- # Add exchange name as namespace
413
- parts << @exchange_name
414
-
415
- # Add message class (normalized)
416
- normalized_class = message_class.to_s.gsub('::', '.').downcase
417
- parts << normalized_class
418
-
419
- # Add from/to if present (default to 'any')
420
- parts << (routing_info[:from] || 'any')
421
- parts << (routing_info[:to] || 'any')
422
-
423
- parts.join('.')
424
- end
425
-
426
- # Find queues that match the routing key pattern
427
- def find_matching_queues(routing_key)
428
- matching = []
429
-
430
- @routing_table.each do |pattern, queue_names|
431
- if routing_key_matches_pattern?(routing_key, pattern)
432
- matching.concat(queue_names)
433
- end
434
- end
435
-
436
- matching.uniq
437
- end
438
-
439
- # Check if routing key matches the pattern (RabbitMQ-style)
440
- def routing_key_matches_pattern?(routing_key, pattern)
441
- return false if routing_key.nil? || routing_key.empty?
442
- return false if pattern.nil? || pattern.empty?
443
-
444
- # Split into segments for proper RabbitMQ-style matching
445
- routing_segments = routing_key.split('.')
446
- pattern_segments = pattern.split('.')
447
-
448
- match_segments(routing_segments, pattern_segments)
449
- end
450
-
451
- # Recursively match routing key segments against pattern segments
452
- def match_segments(routing_segments, pattern_segments)
453
- return routing_segments.empty? && pattern_segments.empty? if pattern_segments.empty?
454
- return pattern_segments.all? { |seg| seg == '#' } if routing_segments.empty?
455
-
456
- pattern_seg = pattern_segments[0]
457
-
458
- case pattern_seg
459
- when '#'
460
- # # matches zero or more segments
461
- # Try matching with zero segments (skip the # pattern)
462
- if match_segments(routing_segments, pattern_segments[1..-1])
463
- return true
464
- end
465
- # Try matching with one or more segments (consume one routing segment)
466
- return match_segments(routing_segments[1..-1], pattern_segments)
467
-
468
- when '*'
469
- # * matches exactly one segment
470
- return false if routing_segments.empty?
471
- return match_segments(routing_segments[1..-1], pattern_segments[1..-1])
472
-
473
- else
474
- # Literal match required
475
- return false if routing_segments.empty? || routing_segments[0] != pattern_seg
476
- return match_segments(routing_segments[1..-1], pattern_segments[1..-1])
477
- end
478
- end
479
-
480
- # Sanitize string for use in routing key
481
- def sanitize_for_routing_key(str)
482
- str.to_s.gsub(/[^a-zA-Z0-9_-]/, '_').downcase
483
- end
484
- end
485
-
486
- # Fluent API builder for complex subscription patterns
487
- class RedisQueueSubscriptionBuilder
488
- def initialize(transport)
489
- @transport = transport
490
- @conditions = {}
491
- end
492
-
493
- def from(sender_uuid)
494
- @conditions[:from] = sender_uuid
495
- self
496
- end
497
-
498
- def to(recipient_uuid)
499
- @conditions[:to] = recipient_uuid
500
- self
501
- end
502
-
503
- def type(message_type)
504
- @conditions[:type] = message_type
505
- self
506
- end
507
-
508
- def broadcast
509
- @conditions[:to] = 'broadcast'
510
- self
511
- end
512
-
513
- def alerts
514
- @conditions[:alerts] = true
515
- self
516
- end
517
-
518
- def consumer_group(group_name)
519
- @conditions[:consumer_group] = group_name
520
- self
521
- end
522
-
523
- def build
524
- parts = []
525
-
526
- # Build pattern based on conditions
527
- if @conditions[:alerts]
528
- return "alert.#.*.*"
529
- end
530
-
531
- if @conditions[:type]
532
- parts << "*"
533
- parts << @conditions[:type].to_s.gsub('::', '.').downcase
534
- else
535
- parts << "#"
536
- end
537
-
538
- parts << (@conditions[:from] || "*")
539
- parts << (@conditions[:to] || "*")
540
-
541
- parts.join(".")
542
- end
543
-
544
- def subscribe(process_method = :process, &block)
545
- pattern = build
546
-
547
- filter_options = {}
548
- filter_options[:from] = @conditions[:from] if @conditions[:from]
549
- filter_options[:to] = @conditions[:to] if @conditions[:to]
550
-
551
- @transport.subscribe_pattern(pattern, process_method, filter_options, &block)
552
- end
553
- end
554
- end
555
- end
@@ -1,132 +0,0 @@
1
- # lib/smart_message/wrapper.rb
2
- # encoding: utf-8
3
- # frozen_string_literal: true
4
-
5
- require 'securerandom' # STDLIB
6
- require_relative './header.rb'
7
-
8
- module SmartMessage
9
- module Wrapper
10
- # Every smart message has a common wrapper format that contains
11
- # information used to support the dispatching of subscribed
12
- # messages upon receipt from a transport as well as the serialized
13
- # payload.
14
- #
15
- # The wrapper consolidates header and payload into a single object
16
- # for cleaner method signatures throughout the SmartMessage dataflow.
17
- class Base < Hashie::Dash
18
- include Hashie::Extensions::IndifferentAccess
19
- include Hashie::Extensions::MethodAccess
20
- include Hashie::Extensions::DeepMerge
21
-
22
- # Core wrapper properties
23
- # Using '_sm_' prefix to avoid collision with user message definitions
24
- property :_sm_header,
25
- required: true,
26
- description: "SmartMessage header containing routing and metadata information"
27
-
28
- property :_sm_payload,
29
- required: true,
30
- description: "Serialized message payload containing the business data"
31
-
32
- # Create wrapper from header and payload
33
- def initialize(header: nil, payload: nil, **props, &block)
34
- # Handle different initialization patterns
35
- if header && payload
36
- attributes = {
37
- _sm_header: header,
38
- _sm_payload: payload
39
- }
40
- else
41
- # Create default header if not provided
42
- default_header = SmartMessage::Header.new(
43
- uuid: SecureRandom.uuid,
44
- message_class: 'SmartMessage::Wrapper::Base',
45
- published_at: Time.now,
46
- publisher_pid: Process.pid,
47
- version: 1
48
- )
49
-
50
- attributes = {
51
- _sm_header: default_header,
52
- _sm_payload: nil
53
- }.merge(props)
54
- end
55
-
56
- super(attributes, &block)
57
- end
58
-
59
- # Convenience accessors for header and payload
60
- def header
61
- _sm_header
62
- end
63
-
64
- def payload
65
- _sm_payload
66
- end
67
-
68
- # Check if this is a broadcast message (to field is nil)
69
- def broadcast?
70
- _sm_header.to.nil?
71
- end
72
-
73
- # Check if this is a directed message (to field is present)
74
- def directed?
75
- !broadcast?
76
- end
77
-
78
- # Get message class from header
79
- def message_class
80
- _sm_header.message_class
81
- end
82
-
83
- # Get sender from header
84
- def from
85
- _sm_header.from
86
- end
87
-
88
- # Get recipient from header
89
- def to
90
- _sm_header.to
91
- end
92
-
93
- # Get reply destination from header
94
- def reply_to
95
- _sm_header.reply_to
96
- end
97
-
98
- # Get message version from header
99
- def version
100
- _sm_header.version
101
- end
102
-
103
- # Get UUID from header
104
- def uuid
105
- _sm_header.uuid
106
- end
107
-
108
- # Convert wrapper to hash for serialization/transport
109
- def to_hash
110
- {
111
- '_sm_header' => _sm_header.to_hash,
112
- '_sm_payload' => _sm_payload
113
- }
114
- end
115
-
116
- alias_method :to_h, :to_hash
117
-
118
- # Outer-level JSON serialization for the wrapper
119
- # This is level 2 serialization - always JSON for routing/monitoring
120
- def to_json(*args)
121
- require 'json'
122
- to_hash.to_json(*args)
123
- end
124
-
125
- # Split wrapper into header and payload components
126
- # Enables destructuring assignment: header, payload = wrapper.split
127
- def split
128
- [_sm_header, _sm_payload]
129
- end
130
- end
131
- end
132
- end