smart_message 0.0.5 → 0.0.6

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.
@@ -18,6 +18,9 @@ module SmartMessage
18
18
  @@serializer = nil
19
19
  @@logger = nil
20
20
 
21
+ # Class-level addressing configuration - use a registry for per-class isolation
22
+ @@addressing_registry = {}
23
+
21
24
  # Registry for proc-based message handlers
22
25
  @@proc_handlers = {}
23
26
 
@@ -69,6 +72,11 @@ module SmartMessage
69
72
  @transport = nil
70
73
  @serializer = nil
71
74
  @logger = nil
75
+
76
+ # instance-level over ride of class addressing
77
+ @from = nil
78
+ @to = nil
79
+ @reply_to = nil
72
80
 
73
81
  # Create header with version validation specific to this message class
74
82
  header = SmartMessage::Header.new(
@@ -76,7 +84,10 @@ module SmartMessage
76
84
  message_class: self.class.to_s,
77
85
  published_at: Time.now,
78
86
  publisher_pid: Process.pid,
79
- version: self.class.version
87
+ version: self.class.version,
88
+ from: from,
89
+ to: to,
90
+ reply_to: reply_to
80
91
  )
81
92
 
82
93
  # Set up version validation to match the expected class version
@@ -234,6 +245,61 @@ module SmartMessage
234
245
  def reset_serializer; @serializer = nil; end
235
246
 
236
247
 
248
+ #########################################################
249
+ ## instance-level addressing configuration
250
+
251
+ def from(entity_id = nil)
252
+ if entity_id.nil?
253
+ @from || self.class.from
254
+ else
255
+ @from = entity_id
256
+ # Update the header with the new value
257
+ _sm_header.from = entity_id if _sm_header
258
+ end
259
+ end
260
+
261
+ def from_configured?; !from.nil?; end
262
+ def from_missing?; from.nil?; end
263
+ def reset_from;
264
+ @from = nil
265
+ _sm_header.from = nil if _sm_header
266
+ end
267
+
268
+ def to(entity_id = nil)
269
+ if entity_id.nil?
270
+ @to || self.class.to
271
+ else
272
+ @to = entity_id
273
+ # Update the header with the new value
274
+ _sm_header.to = entity_id if _sm_header
275
+ end
276
+ end
277
+
278
+ def to_configured?; !to.nil?; end
279
+ def to_missing?; to.nil?; end
280
+ def reset_to;
281
+ @to = nil
282
+ _sm_header.to = nil if _sm_header
283
+ end
284
+
285
+ def reply_to(entity_id = nil)
286
+ if entity_id.nil?
287
+ @reply_to || self.class.reply_to
288
+ else
289
+ @reply_to = entity_id
290
+ # Update the header with the new value
291
+ _sm_header.reply_to = entity_id if _sm_header
292
+ end
293
+ end
294
+
295
+ def reply_to_configured?; !reply_to.nil?; end
296
+ def reply_to_missing?; reply_to.nil?; end
297
+ def reset_reply_to;
298
+ @reply_to = nil
299
+ _sm_header.reply_to = nil if _sm_header
300
+ end
301
+
302
+
237
303
  #########################################################
238
304
  ## instance-level utility methods
239
305
 
@@ -358,6 +424,113 @@ module SmartMessage
358
424
  def reset_serializer; @@serializer = nil; end
359
425
 
360
426
 
427
+ #########################################################
428
+ ## class-level addressing configuration
429
+
430
+ # Helper method to normalize filter values (string -> array, nil -> nil)
431
+ private def normalize_filter_value(value)
432
+ case value
433
+ when nil
434
+ nil
435
+ when String
436
+ [value]
437
+ when Array
438
+ value
439
+ else
440
+ raise ArgumentError, "Filter value must be a String, Array, or nil, got: #{value.class}"
441
+ end
442
+ end
443
+
444
+ # Helper method to find addressing values in the inheritance chain
445
+ private def find_addressing_value(field)
446
+ # Start with current class
447
+ current_class = self
448
+
449
+ while current_class && current_class.respond_to?(:name)
450
+ class_name = current_class.name || current_class.to_s
451
+
452
+ # Check registry for this class
453
+ result = @@addressing_registry.dig(class_name, field)
454
+ return result if result
455
+
456
+ # If we have a proper name but no result, also check the to_s version
457
+ if current_class.name
458
+ alternative_key = current_class.to_s
459
+ result = @@addressing_registry.dig(alternative_key, field)
460
+ return result if result
461
+ end
462
+
463
+ # Move up the inheritance chain
464
+ current_class = current_class.superclass
465
+
466
+ # Stop if we reach SmartMessage::Base or above
467
+ break if current_class == SmartMessage::Base || current_class.nil?
468
+ end
469
+
470
+ nil
471
+ end
472
+
473
+ def from(entity_id = nil)
474
+ class_name = self.name || self.to_s
475
+ if entity_id.nil?
476
+ # Try to find the value, checking inheritance chain
477
+ result = find_addressing_value(:from)
478
+ result
479
+ else
480
+ @@addressing_registry[class_name] ||= {}
481
+ @@addressing_registry[class_name][:from] = entity_id
482
+ end
483
+ end
484
+
485
+ def from_configured?; !from.nil?; end
486
+ def from_missing?; from.nil?; end
487
+ def reset_from;
488
+ class_name = self.name || self.to_s
489
+ @@addressing_registry[class_name] ||= {}
490
+ @@addressing_registry[class_name][:from] = nil
491
+ end
492
+
493
+ def to(entity_id = nil)
494
+ class_name = self.name || self.to_s
495
+ if entity_id.nil?
496
+ # Try to find the value, checking inheritance chain
497
+ result = find_addressing_value(:to)
498
+ result
499
+ else
500
+ @@addressing_registry[class_name] ||= {}
501
+ @@addressing_registry[class_name][:to] = entity_id
502
+ end
503
+ end
504
+
505
+ def to_configured?; !to.nil?; end
506
+ def to_missing?; to.nil?; end
507
+ def reset_to;
508
+ class_name = self.name || self.to_s
509
+ @@addressing_registry[class_name] ||= {}
510
+ @@addressing_registry[class_name][:to] = nil
511
+ end
512
+
513
+ def reply_to(entity_id = nil)
514
+ class_name = self.name || self.to_s
515
+ if entity_id.nil?
516
+ # Try to find the value, checking inheritance chain
517
+ result = find_addressing_value(:reply_to)
518
+ result
519
+ else
520
+ @@addressing_registry[class_name] ||= {}
521
+ @@addressing_registry[class_name][:reply_to] = entity_id
522
+ end
523
+ end
524
+
525
+ def reply_to_configured?; !reply_to.nil?; end
526
+ def reply_to_missing?; reply_to.nil?; end
527
+ def reset_reply_to;
528
+ class_name = self.name || self.to_s
529
+ @@addressing_registry[class_name] ||= {}
530
+ @@addressing_registry[class_name][:reply_to] = nil
531
+ end
532
+
533
+
361
534
  #########################################################
362
535
  ## class-level subscription management via the transport
363
536
 
@@ -369,25 +542,30 @@ module SmartMessage
369
542
  # - String: Method name like "MyService.handle_message"
370
543
  # - Proc: A proc/lambda that accepts (message_header, message_payload)
371
544
  # - nil: Uses default "MessageClass.process" method
545
+ # @param broadcast [Boolean, nil] Filter for broadcast messages (to: nil)
546
+ # @param to [String, Array, nil] Filter for messages directed to specific entities
547
+ # @param from [String, Array, nil] Filter for messages from specific entities
372
548
  # @param block [Proc] Alternative way to pass a processing block
373
549
  # @return [String] The identifier used for this subscription
374
550
  #
375
- # @example Using default handler
551
+ # @example Using default handler (all messages)
376
552
  # MyMessage.subscribe
377
553
  #
378
- # @example Using custom method name
379
- # MyMessage.subscribe("MyService.handle_message")
554
+ # @example Using custom method name with filtering
555
+ # MyMessage.subscribe("MyService.handle_message", to: 'my-service')
380
556
  #
381
- # @example Using a block
382
- # MyMessage.subscribe do |header, payload|
557
+ # @example Using a block with broadcast filtering
558
+ # MyMessage.subscribe(broadcast: true) do |header, payload|
383
559
  # data = JSON.parse(payload)
384
- # puts "Received: #{data}"
560
+ # puts "Received broadcast: #{data}"
385
561
  # end
386
562
  #
387
- # @example Using a proc
388
- # handler = proc { |header, payload| puts "Processing..." }
389
- # MyMessage.subscribe(handler)
390
- def subscribe(process_method = nil, &block)
563
+ # @example Entity-specific filtering
564
+ # MyMessage.subscribe(to: 'order-service', from: ['payment', 'user'])
565
+ #
566
+ # @example Broadcast + directed messages
567
+ # MyMessage.subscribe(to: 'my-service', broadcast: true)
568
+ def subscribe(process_method = nil, broadcast: nil, to: nil, from: nil, &block)
391
569
  message_class = whoami
392
570
 
393
571
  # Handle different parameter types
@@ -405,10 +583,21 @@ module SmartMessage
405
583
  end
406
584
  # If process_method is a String, use it as-is
407
585
 
586
+ # Normalize string filters to arrays
587
+ to_filter = normalize_filter_value(to)
588
+ from_filter = normalize_filter_value(from)
589
+
590
+ # Create filter options
591
+ filter_options = {
592
+ broadcast: broadcast,
593
+ to: to_filter,
594
+ from: from_filter
595
+ }
596
+
408
597
  # TODO: Add proper logging here
409
598
 
410
599
  raise Errors::TransportNotConfigured if transport_missing?
411
- transport.subscribe(message_class, process_method)
600
+ transport.subscribe(message_class, process_method, filter_options)
412
601
 
413
602
  process_method
414
603
  end
@@ -83,17 +83,30 @@ module SmartMessage
83
83
  end
84
84
 
85
85
 
86
- def add(message_class, process_method_as_string)
86
+ def add(message_class, process_method_as_string, filter_options = {})
87
87
  klass = String(message_class)
88
- unless @subscribers[klass].include? process_method_as_string
89
- @subscribers[klass] += [process_method_as_string]
88
+
89
+ # Create subscription entry with filter options
90
+ subscription = {
91
+ process_method: process_method_as_string,
92
+ filters: filter_options
93
+ }
94
+
95
+ # Check if this exact subscription already exists
96
+ existing_subscription = @subscribers[klass].find do |sub|
97
+ sub[:process_method] == process_method_as_string && sub[:filters] == filter_options
98
+ end
99
+
100
+ unless existing_subscription
101
+ @subscribers[klass] += [subscription]
90
102
  end
91
103
  end
92
104
 
93
105
 
94
106
  # drop a processer from a subscribed message
95
107
  def drop(message_class, process_method_as_string)
96
- @subscribers[String(message_class)].delete process_method_as_string
108
+ klass = String(message_class)
109
+ @subscribers[klass].reject! { |sub| sub[:process_method] == process_method_as_string }
97
110
  end
98
111
 
99
112
 
@@ -115,7 +128,15 @@ module SmartMessage
115
128
  def route(message_header, message_payload)
116
129
  message_klass = message_header.message_class
117
130
  return nil if @subscribers[message_klass].empty?
118
- @subscribers[message_klass].each do |message_processor|
131
+
132
+ @subscribers[message_klass].each do |subscription|
133
+ # Extract subscription details
134
+ message_processor = subscription[:process_method]
135
+ filters = subscription[:filters]
136
+
137
+ # Check if message matches filters
138
+ next unless message_matches_filters?(message_header, filters)
139
+
119
140
  SS.add(message_klass, message_processor, 'routed' )
120
141
  @router_pool.post do
121
142
  begin
@@ -144,6 +165,33 @@ module SmartMessage
144
165
 
145
166
  private
146
167
 
168
+ # Check if a message matches the subscription filters
169
+ # @param message_header [SmartMessage::Header] The message header
170
+ # @param filters [Hash] The filter criteria
171
+ # @return [Boolean] True if the message matches all filters
172
+ def message_matches_filters?(message_header, filters)
173
+ # If no filters specified, accept all messages (backward compatibility)
174
+ return true if filters.nil? || filters.empty? || filters.values.all?(&:nil?)
175
+
176
+ # Check from filter
177
+ if filters[:from]
178
+ from_match = filters[:from].include?(message_header.from)
179
+ return false unless from_match
180
+ end
181
+
182
+ # Check to/broadcast filters (OR logic between them)
183
+ if filters[:broadcast] || filters[:to]
184
+ broadcast_match = filters[:broadcast] && message_header.to.nil?
185
+ to_match = filters[:to] && filters[:to].include?(message_header.to)
186
+
187
+ # If either broadcast or to filter is specified, at least one must match
188
+ combined_match = (broadcast_match || to_match)
189
+ return false unless combined_match
190
+ end
191
+
192
+ true
193
+ end
194
+
147
195
  # Check if a message processor is a proc handler
148
196
  # @param message_processor [String] The message processor identifier
149
197
  # @return [Boolean] True if this is a proc handler
@@ -43,5 +43,19 @@ module SmartMessage
43
43
  description: "Schema version of the message format, used for schema evolution and compatibility checking",
44
44
  validate: ->(v) { v.is_a?(Integer) && v > 0 },
45
45
  validation_message: "Header version must be a positive integer"
46
+
47
+ # Message addressing properties for entity-to-entity communication
48
+ property :from,
49
+ required: true,
50
+ message: "From entity ID is required for message routing and replies",
51
+ description: "Unique identifier of the entity sending this message, used for routing responses and audit trails"
52
+
53
+ property :to,
54
+ required: false,
55
+ description: "Optional unique identifier of the intended recipient entity. When nil, message is broadcast to all subscribers"
56
+
57
+ property :reply_to,
58
+ required: false,
59
+ description: "Optional unique identifier of the entity that should receive replies to this message. Defaults to 'from' entity if not specified"
46
60
  end
47
61
  end
@@ -35,8 +35,9 @@ module SmartMessage
35
35
  # Subscribe to a message class
36
36
  # @param message_class [String] The message class name
37
37
  # @param process_method [String] The processing method identifier
38
- def subscribe(message_class, process_method)
39
- @dispatcher.add(message_class, process_method)
38
+ # @param filter_options [Hash] Optional filtering criteria
39
+ def subscribe(message_class, process_method, filter_options = {})
40
+ @dispatcher.add(message_class, process_method, filter_options)
40
41
  end
41
42
 
42
43
  # Unsubscribe from a specific message class and process method
@@ -46,8 +46,8 @@ module SmartMessage
46
46
  end
47
47
 
48
48
  # Subscribe to a message class (Redis channel)
49
- def subscribe(message_class, process_method)
50
- super(message_class, process_method)
49
+ def subscribe(message_class, process_method, filter_options = {})
50
+ super(message_class, process_method, filter_options)
51
51
 
52
52
  @mutex.synchronize do
53
53
  @subscribed_channels.add(message_class)
@@ -3,5 +3,5 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  module SmartMessage
6
- VERSION = '0.0.5'
6
+ VERSION = '0.0.6'
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smart_message
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -199,6 +199,7 @@ files:
199
199
  - bin/console
200
200
  - bin/setup
201
201
  - docs/README.md
202
+ - docs/addressing.md
202
203
  - docs/architecture.md
203
204
  - docs/dispatcher.md
204
205
  - docs/examples.md
@@ -219,6 +220,8 @@ files:
219
220
  - examples/05_proc_handlers.rb
220
221
  - examples/06_custom_logger_example.rb
221
222
  - examples/07_error_handling_scenarios.rb
223
+ - examples/08_entity_addressing_basic.rb
224
+ - examples/08_entity_addressing_with_filtering.rb
222
225
  - examples/README.md
223
226
  - examples/smart_home_iot_dataflow.md
224
227
  - examples/tmux_chat/README.md