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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +67 -0
- data/Gemfile.lock +1 -1
- data/README.md +58 -4
- data/docs/README.md +1 -0
- data/docs/addressing.md +364 -0
- data/docs/getting-started.md +38 -0
- data/examples/08_entity_addressing_basic.rb +366 -0
- data/examples/08_entity_addressing_with_filtering.rb +418 -0
- data/examples/README.md +68 -0
- data/lib/smart_message/base.rb +201 -12
- data/lib/smart_message/dispatcher.rb +53 -5
- data/lib/smart_message/header.rb +14 -0
- data/lib/smart_message/transport/base.rb +3 -2
- data/lib/smart_message/transport/redis_transport.rb +2 -2
- data/lib/smart_message/version.rb +1 -1
- metadata +4 -1
data/lib/smart_message/base.rb
CHANGED
@@ -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
|
388
|
-
#
|
389
|
-
#
|
390
|
-
|
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
|
-
|
89
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/smart_message/header.rb
CHANGED
@@ -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
|
-
|
39
|
-
|
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)
|
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.
|
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
|