smart_message 0.0.8 → 0.0.9

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.irbrc +24 -0
  4. data/CHANGELOG.md +96 -0
  5. data/Gemfile.lock +6 -1
  6. data/README.md +289 -15
  7. data/docs/README.md +3 -1
  8. data/docs/addressing.md +119 -13
  9. data/docs/architecture.md +68 -0
  10. data/docs/dead_letter_queue.md +673 -0
  11. data/docs/dispatcher.md +87 -0
  12. data/docs/examples.md +59 -1
  13. data/docs/getting-started.md +8 -1
  14. data/docs/logging.md +382 -326
  15. data/docs/message_filtering.md +451 -0
  16. data/examples/01_point_to_point_orders.rb +54 -53
  17. data/examples/02_publish_subscribe_events.rb +14 -10
  18. data/examples/03_many_to_many_chat.rb +16 -8
  19. data/examples/04_redis_smart_home_iot.rb +20 -10
  20. data/examples/05_proc_handlers.rb +12 -11
  21. data/examples/06_custom_logger_example.rb +95 -100
  22. data/examples/07_error_handling_scenarios.rb +4 -2
  23. data/examples/08_entity_addressing_basic.rb +18 -6
  24. data/examples/08_entity_addressing_with_filtering.rb +27 -9
  25. data/examples/09_dead_letter_queue_demo.rb +559 -0
  26. data/examples/09_regex_filtering_microservices.rb +407 -0
  27. data/examples/10_header_block_configuration.rb +263 -0
  28. data/examples/11_global_configuration_example.rb +219 -0
  29. data/examples/README.md +102 -0
  30. data/examples/dead_letters.jsonl +12 -0
  31. data/examples/performance_metrics/benchmark_results_ractor_20250818_205603.json +135 -0
  32. data/examples/performance_metrics/benchmark_results_ractor_20250818_205831.json +135 -0
  33. data/examples/performance_metrics/benchmark_results_test_20250818_204942.json +130 -0
  34. data/examples/performance_metrics/benchmark_results_threadpool_20250818_204942.json +130 -0
  35. data/examples/performance_metrics/benchmark_results_threadpool_20250818_204959.json +130 -0
  36. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205044.json +130 -0
  37. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205109.json +130 -0
  38. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205252.json +130 -0
  39. data/examples/performance_metrics/benchmark_results_unknown_20250819_172852.json +130 -0
  40. data/examples/performance_metrics/compare_benchmarks.rb +519 -0
  41. data/examples/performance_metrics/dead_letters.jsonl +3100 -0
  42. data/examples/performance_metrics/performance_benchmark.rb +344 -0
  43. data/examples/show_logger.rb +367 -0
  44. data/examples/show_me.rb +145 -0
  45. data/examples/temp.txt +94 -0
  46. data/examples/tmux_chat/bot_agent.rb +4 -2
  47. data/examples/tmux_chat/human_agent.rb +4 -2
  48. data/examples/tmux_chat/room_monitor.rb +4 -2
  49. data/examples/tmux_chat/shared_chat_system.rb +6 -3
  50. data/lib/smart_message/addressing.rb +259 -0
  51. data/lib/smart_message/base.rb +121 -599
  52. data/lib/smart_message/circuit_breaker.rb +2 -1
  53. data/lib/smart_message/configuration.rb +199 -0
  54. data/lib/smart_message/dead_letter_queue.rb +27 -10
  55. data/lib/smart_message/dispatcher.rb +90 -49
  56. data/lib/smart_message/header.rb +5 -0
  57. data/lib/smart_message/logger/base.rb +21 -1
  58. data/lib/smart_message/logger/default.rb +88 -138
  59. data/lib/smart_message/logger/lumberjack.rb +324 -0
  60. data/lib/smart_message/logger/null.rb +81 -0
  61. data/lib/smart_message/logger.rb +17 -9
  62. data/lib/smart_message/messaging.rb +100 -0
  63. data/lib/smart_message/plugins.rb +132 -0
  64. data/lib/smart_message/serializer/base.rb +25 -8
  65. data/lib/smart_message/serializer/json.rb +5 -4
  66. data/lib/smart_message/subscription.rb +193 -0
  67. data/lib/smart_message/transport/base.rb +72 -41
  68. data/lib/smart_message/transport/memory_transport.rb +7 -5
  69. data/lib/smart_message/transport/redis_transport.rb +15 -45
  70. data/lib/smart_message/transport/stdout_transport.rb +18 -8
  71. data/lib/smart_message/transport.rb +1 -34
  72. data/lib/smart_message/utilities.rb +142 -0
  73. data/lib/smart_message/version.rb +1 -1
  74. data/lib/smart_message/versioning.rb +85 -0
  75. data/lib/smart_message/wrapper.rb.bak +132 -0
  76. data/lib/smart_message.rb +74 -28
  77. data/smart_message.gemspec +3 -0
  78. metadata +76 -3
  79. data/lib/smart_message/serializer.rb +0 -10
  80. data/lib/smart_message/wrapper.rb +0 -43
@@ -4,51 +4,28 @@
4
4
 
5
5
  require 'securerandom' # STDLIB
6
6
 
7
- require_relative './wrapper.rb'
8
7
  require_relative './property_descriptions.rb'
9
8
  require_relative './property_validations.rb'
9
+ require_relative './plugins.rb'
10
+ require_relative './addressing.rb'
11
+ require_relative './subscription.rb'
12
+ require_relative './versioning.rb'
13
+ require_relative './messaging.rb'
14
+ require_relative './utilities.rb'
10
15
 
11
16
  module SmartMessage
12
17
  # The foundation class for the smart message
13
18
  class Base < Hashie::Dash
14
-
15
- # Supports multi-level plugins for transport, serializer and logger.
16
- # Plugins can be made at the class level and at the instance level.
17
- @@transport = nil
18
- @@serializer = nil
19
- @@logger = nil
20
-
21
- # Class-level addressing configuration - use a registry for per-class isolation
22
- @@addressing_registry = {}
23
-
24
- # Registry for proc-based message handlers
25
- @@proc_handlers = {}
26
-
27
- # Class-level version setting
28
- class << self
29
- attr_accessor :_version
30
-
31
- def version(v = nil)
32
- if v.nil?
33
- @_version || 1 # Default to version 1 if not set
34
- else
35
- @_version = v
36
-
37
- # Set up version validation for the header
38
- # This ensures that the header version matches the expected class version
39
- @expected_header_version = v
40
- end
41
- end
42
-
43
- def expected_header_version
44
- @expected_header_version || 1
45
- end
46
- end
47
-
48
19
  include Hashie::Extensions::Dash::PropertyTranslation
49
20
 
50
21
  include SmartMessage::PropertyDescriptions
51
22
  include SmartMessage::PropertyValidations
23
+ include SmartMessage::Plugins
24
+ include SmartMessage::Addressing
25
+ include SmartMessage::Subscription
26
+ include SmartMessage::Versioning
27
+ include SmartMessage::Messaging
28
+ include SmartMessage::Utilities
52
29
 
53
30
  include Hashie::Extensions::Coercion
54
31
  include Hashie::Extensions::DeepMerge
@@ -68,610 +45,155 @@ module SmartMessage
68
45
  # Constructor for a messsage definition that allows the
69
46
  # setting of initial values.
70
47
  def initialize(**props, &block)
71
- # instance-level over ride of class plugins
72
- @transport = nil
73
- @serializer = nil
74
- @logger = nil
75
-
76
- # Extract addressing information from props before creating header
77
- addressing_props = props.extract!(:from, :to, :reply_to)
78
-
79
- # instance-level over ride of class addressing
80
- @from = addressing_props[:from]
81
- @to = addressing_props[:to]
82
- @reply_to = addressing_props[:reply_to]
83
-
84
- # Create header with version validation specific to this message class
85
- header = SmartMessage::Header.new(
86
- uuid: SecureRandom.uuid,
87
- message_class: self.class.to_s,
88
- published_at: Time.now,
89
- publisher_pid: Process.pid,
90
- version: self.class.version,
91
- from: from,
92
- to: to,
93
- reply_to: reply_to
94
- )
95
-
48
+ # instance-level override of class plugins
49
+ # Don't use fallback defaults here - let the methods handle fallbacks when actually used
50
+ @transport = (self.class.class_variable_get(:@@transport) rescue nil)
51
+ @serializer = (self.class.class_variable_get(:@@serializer) rescue nil)
52
+
53
+ # Check if we're reconstructing from serialized data (complete header provided)
54
+ if props[:_sm_header]
55
+ # Deserialization path: use provided header and payload
56
+ existing_header = props[:_sm_header]
57
+
58
+ # Convert to Header object if it's a hash (from deserialization)
59
+ if existing_header.is_a?(Hash)
60
+ # Convert string keys to symbols
61
+ header_hash = existing_header.transform_keys(&:to_sym)
62
+ header = SmartMessage::Header.new(**header_hash)
63
+ else
64
+ header = existing_header
65
+ end
66
+
67
+ # Extract addressing from header for instance variables
68
+ @from = header.from
69
+ @to = header.to
70
+ @reply_to = header.reply_to
71
+
72
+ # Extract payload properties
73
+ payload_props = props[:_sm_payload] || {}
74
+
75
+ attributes = {
76
+ _sm_header: header
77
+ }.merge(payload_props)
78
+ else
79
+ # Normal creation path: create new header
80
+ # Extract addressing information from props before creating header
81
+ addressing_props = props.extract!(:from, :to, :reply_to)
82
+
83
+ # instance-level over ride of class addressing
84
+ @from = addressing_props[:from]
85
+ @to = addressing_props[:to]
86
+ @reply_to = addressing_props[:reply_to]
87
+
88
+ # Create header with version validation specific to this message class
89
+ header = SmartMessage::Header.new(
90
+ uuid: SecureRandom.uuid,
91
+ message_class: self.class.to_s,
92
+ published_at: Time.now,
93
+ publisher_pid: Process.pid,
94
+ version: self.class.version,
95
+ from: from,
96
+ to: to,
97
+ reply_to: reply_to
98
+ )
99
+
100
+ attributes = {
101
+ _sm_header: header
102
+ }.merge(props)
103
+ end
104
+
96
105
  # Set up version validation to match the expected class version
97
106
  expected_version = self.class.expected_header_version
98
107
  header.singleton_class.class_eval do
99
108
  define_method(:validate_version!) do
100
109
  unless self.version == expected_version
101
- raise SmartMessage::Errors::ValidationError,
110
+ raise SmartMessage::Errors::ValidationError,
102
111
  "Header version must be #{expected_version}, got: #{self.version}"
103
112
  end
104
113
  end
105
114
  end
106
-
107
- attributes = {
108
- _sm_header: header
109
- }.merge(props)
110
115
 
111
116
  super(attributes, &block)
112
- end
113
-
114
117
 
115
- ###################################################
116
- ## Common instance methods
117
-
118
- # Validate that the header version matches the expected version for this class
119
- def validate_header_version!
120
- expected = self.class.expected_header_version
121
- actual = _sm_header.version
122
- unless actual == expected
123
- raise SmartMessage::Errors::ValidationError,
124
- "#{self.class.name} expects version #{expected}, but header has version #{actual}"
125
- end
118
+ # Log message creation
119
+ (self.class.logger || SmartMessage::Logger.default).debug { "[SmartMessage] Created: #{self.class.name}" }
120
+ rescue => e
121
+ (self.class.logger || SmartMessage::Logger.default).error { "[SmartMessage] Error in message initialization: #{e.class.name} - #{e.message}" }
122
+ raise
126
123
  end
127
124
 
128
- # Override PropertyValidations validate! to include header and version validation
129
- def validate!
130
- # Validate message properties using PropertyValidations
131
- super
132
-
133
- # Validate header properties
134
- _sm_header.validate!
135
-
136
- # Validate header version matches expected class version
137
- validate_header_version!
138
- end
139
-
140
- # Override PropertyValidations validation_errors to include header errors
141
- def validation_errors
142
- errors = []
143
-
144
- # Get message property validation errors using PropertyValidations
145
- errors.concat(super.map { |err|
146
- err.merge(source: 'message')
147
- })
125
+ # Backward compatibility method for proc handlers that expect _sm_payload as JSON string
126
+ # In the new single-tier approach, this recreates the expected format
127
+ def _sm_payload
128
+ require 'json'
148
129
 
149
- # Get header validation errors
150
- errors.concat(_sm_header.validation_errors.map { |err|
151
- err.merge(source: 'header')
152
- })
153
-
154
- # Check version mismatch
155
- expected = self.class.expected_header_version
156
- actual = _sm_header.version
157
- unless actual == expected
158
- errors << {
159
- property: :version,
160
- value: actual,
161
- message: "Expected version #{expected}, got: #{actual}",
162
- source: 'version_mismatch'
163
- }
130
+ # Extract payload properties (non-header properties)
131
+ payload_props = self.class.properties.each_with_object({}) do |prop, hash|
132
+ next if prop == :_sm_header
133
+ hash[prop.to_s] = self[prop] # Use string keys to match old format
164
134
  end
165
135
 
166
- errors
136
+ JSON.generate(payload_props)
167
137
  end
168
138
 
169
- # SMELL: How does the transport know how to decode a message before
170
- # it knows the message class? We need a wrapper around
171
- # the entire message in a known serialization. That
172
- # wrapper would contain two properties: _sm_header and
173
- # _sm_payload
174
-
175
- # NOTE: to publish a message it must first be encoded using a
176
- # serializer. The receive a subscribed to message it must
177
- # be decoded via a serializer from the transport to be processed.
178
- def encode
179
- raise Errors::SerializerNotConfigured if serializer_missing?
180
-
181
- serializer.encode(self)
139
+ # Backward compatibility method for handlers that expect wrapper.split
140
+ # Returns [header, payload_json] in the old wrapper format
141
+ def split
142
+ [_sm_header, _sm_payload]
182
143
  end
183
144
 
184
145
 
185
- # NOTE: you publish instances; but, you subscribe/unsubscribe at
186
- # the class-level
187
- def publish
188
- # Validate the complete message before publishing (now uses overridden validate!)
189
- validate!
190
-
191
- # TODO: move all of the _sm_ property processes into the wrapper
192
- _sm_header.published_at = Time.now
193
- _sm_header.publisher_pid = Process.pid
194
-
195
- payload = encode
196
-
197
- raise Errors::TransportNotConfigured if transport_missing?
198
- transport.publish(_sm_header, payload)
199
-
200
- SS.add(_sm_header.message_class, 'publish')
201
- SS.get(_sm_header.message_class, 'publish')
202
- end # def publish
203
-
204
-
205
-
206
- #########################################################
207
- ## instance-level configuration
208
-
209
- # Configure the plugins for transport, serializer and logger
210
- def config(&block)
211
- instance_eval(&block) if block_given?
212
- end
213
-
214
-
215
- #########################################################
216
- ## instance-level transport configuration
217
-
218
- def transport(klass_or_instance = nil)
219
- klass_or_instance.nil? ? @transport || @@transport : @transport = klass_or_instance
220
- end
221
-
222
- def transport_configured?; !transport.nil?; end
223
- def transport_missing?; transport.nil?; end
224
- def reset_transport; @transport = nil; end
225
-
226
-
227
- #########################################################
228
- ## instance-level logger configuration
229
-
230
- def logger(klass_or_instance = nil)
231
- klass_or_instance.nil? ? @logger || @@logger : @logger = klass_or_instance
232
- end
233
-
234
- def logger_configured?; !logger.nil?; end
235
- def logger_missing?; logger.nil?; end
236
- def reset_logger; @logger = nil; end
237
-
238
-
239
- #########################################################
240
- ## instance-level serializer configuration
241
-
242
- def serializer(klass_or_instance = nil)
243
- klass_or_instance.nil? ? @serializer || @@serializer : @serializer = klass_or_instance
244
- end
245
-
246
- def serializer_configured?; !serializer.nil?; end
247
- def serializer_missing?; serializer.nil?; end
248
- def reset_serializer; @serializer = nil; end
249
-
250
-
251
- #########################################################
252
- ## instance-level addressing configuration
253
-
254
- def from(entity_id = nil)
255
- if entity_id.nil?
256
- @from || self.class.from
257
- else
258
- @from = entity_id
259
- # Update the header with the new value
260
- _sm_header.from = entity_id if _sm_header
261
- end
262
- end
263
-
264
- def from_configured?; !from.nil?; end
265
- def from_missing?; from.nil?; end
266
- def reset_from;
267
- @from = nil
268
- _sm_header.from = nil if _sm_header
269
- end
270
-
271
- def to(entity_id = nil)
272
- if entity_id.nil?
273
- @to || self.class.to
274
- else
275
- @to = entity_id
276
- # Update the header with the new value
277
- _sm_header.to = entity_id if _sm_header
278
- end
279
- end
280
-
281
- def to_configured?; !to.nil?; end
282
- def to_missing?; to.nil?; end
283
- def reset_to;
284
- @to = nil
285
- _sm_header.to = nil if _sm_header
286
- end
287
-
288
- def reply_to(entity_id = nil)
289
- if entity_id.nil?
290
- @reply_to || self.class.reply_to
291
- else
292
- @reply_to = entity_id
293
- # Update the header with the new value
294
- _sm_header.reply_to = entity_id if _sm_header
295
- end
296
- end
297
-
298
- def reply_to_configured?; !reply_to.nil?; end
299
- def reply_to_missing?; reply_to.nil?; end
300
- def reset_reply_to;
301
- @reply_to = nil
302
- _sm_header.reply_to = nil if _sm_header
303
- end
304
-
146
+ ###################################################
147
+ ## Common instance methods
305
148
 
306
- #########################################################
307
- ## instance-level utility methods
308
149
 
309
- # return this class' name as a string
310
- def whoami
311
- self.class.to_s
312
- end
313
150
 
314
- # return this class' description
315
- def description
316
- self.class.description
317
- end
318
151
 
319
152
 
320
- # returns a collection of class Set that consists of
321
- # the symbolized values of the property names of the message
322
- # without the injected '_sm_' properties that support
323
- # the behind-the-sceens operations of SmartMessage.
324
- def fields
325
- to_h.keys
326
- .reject{|key| key.start_with?('_sm_')}
327
- .map{|key| key.to_sym}
328
- .to_set
329
- end
330
153
 
331
154
 
332
155
  ###########################################################
333
156
  ## class methods
334
157
 
335
158
  class << self
159
+ # Decode a complete serialized message back to a message instance
160
+ # @param serialized_message [String] The serialized message content
161
+ # @return [SmartMessage::Base] The decoded message instance
162
+ def decode(serialized_message)
163
+ logger = SmartMessage::Logger.default
336
164
 
337
- #########################################################
338
- ## class-level description
339
-
340
- def description(desc = nil)
341
- if desc.nil?
342
- @description || "#{self.name} is a SmartMessage"
343
- else
344
- @description = desc.to_s
345
- end
346
- end
347
-
348
- #########################################################
349
- ## class-level configuration
165
+ begin
166
+ (self.logger || SmartMessage::Logger.default).info { "[SmartMessage] Received: #{self.name} (#{serialized_message.bytesize} bytes)" }
350
167
 
351
- def config(&block)
352
- class_eval(&block) if block_given?
353
- end
354
-
355
-
356
- #########################################################
357
- ## proc handler management
358
-
359
- # Register a proc handler and return a unique identifier for it
360
- # @param message_class [String] The message class name
361
- # @param handler_proc [Proc] The proc to register
362
- # @return [String] Unique identifier for this handler
363
- def register_proc_handler(message_class, handler_proc)
364
- handler_id = "#{message_class}.proc_#{SecureRandom.hex(8)}"
365
- @@proc_handlers[handler_id] = handler_proc
366
- handler_id
367
- end
368
-
369
- # Call a registered proc handler
370
- # @param handler_id [String] The handler identifier
371
- # @param message_header [SmartMessage::Header] The message header
372
- # @param message_payload [String] The message payload
373
- def call_proc_handler(handler_id, message_header, message_payload)
374
- handler_proc = @@proc_handlers[handler_id]
375
- return unless handler_proc
376
-
377
- handler_proc.call(message_header, message_payload)
378
- end
379
-
380
- # Remove a proc handler from the registry
381
- # @param handler_id [String] The handler identifier to remove
382
- def unregister_proc_handler(handler_id)
383
- @@proc_handlers.delete(handler_id)
384
- end
385
-
386
- # Check if a handler ID refers to a proc handler
387
- # @param handler_id [String] The handler identifier
388
- # @return [Boolean] True if this is a proc handler
389
- def proc_handler?(handler_id)
390
- @@proc_handlers.key?(handler_id)
391
- end
392
-
393
-
394
- #########################################################
395
- ## class-level transport configuration
396
-
397
- def transport(klass_or_instance = nil)
398
- klass_or_instance.nil? ? @@transport : @@transport = klass_or_instance
399
- end
400
-
401
- def transport_configured?; !transport.nil?; end
402
- def transport_missing?; transport.nil?; end
403
- def reset_transport; @@transport = nil; end
404
-
405
-
406
- #########################################################
407
- ## class-level logger configuration
408
-
409
- def logger(klass_or_instance = nil)
410
- klass_or_instance.nil? ? @@logger : @@logger = klass_or_instance
411
- end
412
-
413
- def logger_configured?; !logger.nil?; end
414
- def logger_missing?; logger.nil?; end
415
- def reset_logger; @@logger = nil; end
416
-
417
-
418
- #########################################################
419
- ## class-level serializer configuration
420
-
421
- def serializer(klass_or_instance = nil)
422
- klass_or_instance.nil? ? @@serializer : @@serializer = klass_or_instance
423
- end
424
-
425
- def serializer_configured?; !serializer.nil?; end
426
- def serializer_missing?; serializer.nil?; end
427
- def reset_serializer; @@serializer = nil; end
428
-
429
-
430
- #########################################################
431
- ## class-level addressing configuration
432
-
433
- # Helper method to normalize filter values (string -> array, nil -> nil)
434
- private def normalize_filter_value(value)
435
- case value
436
- when nil
437
- nil
438
- when String
439
- [value]
440
- when Array
441
- value
442
- else
443
- raise ArgumentError, "Filter value must be a String, Array, or nil, got: #{value.class}"
444
- end
445
- end
446
-
447
- # Helper method to find addressing values in the inheritance chain
448
- private def find_addressing_value(field)
449
- # Start with current class
450
- current_class = self
451
-
452
- while current_class && current_class.respond_to?(:name)
453
- class_name = current_class.name || current_class.to_s
454
-
455
- # Check registry for this class
456
- result = @@addressing_registry.dig(class_name, field)
457
- return result if result
168
+ # Use the class's configured serializer to decode the message
169
+ serializer = self.serializer || SmartMessage::Serializer.default
458
170
 
459
- # If we have a proper name but no result, also check the to_s version
460
- if current_class.name
461
- alternative_key = current_class.to_s
462
- result = @@addressing_registry.dig(alternative_key, field)
463
- return result if result
171
+ # Deserialize the complete message
172
+ deserialized_data = serializer.decode(serialized_message)
173
+
174
+ # Create new message instance with the complete deserialized data
175
+ if deserialized_data.is_a?(Hash)
176
+ # Convert string keys to symbols for compatibility with keyword arguments
177
+ symbol_props = deserialized_data.transform_keys(&:to_sym)
178
+
179
+ # With single-tier serialization, use the complete deserialized message structure
180
+ message = self.new(**symbol_props)
181
+
182
+ (self.logger || SmartMessage::Logger.default).debug { "[SmartMessage] Deserialized message: #{self.name}" }
183
+ message
184
+ else
185
+ # If it's already a message object, return it
186
+ (self.logger || SmartMessage::Logger.default).debug { "[SmartMessage] Returning existing message object: #{self.name}" }
187
+ deserialized_data
464
188
  end
465
-
466
- # Move up the inheritance chain
467
- current_class = current_class.superclass
468
-
469
- # Stop if we reach SmartMessage::Base or above
470
- break if current_class == SmartMessage::Base || current_class.nil?
189
+ rescue => e
190
+ (self.logger || SmartMessage::Logger.default).error { "[SmartMessage] Error in message deserialization: #{e.class.name} - #{e.message}" }
191
+ raise
471
192
  end
472
-
473
- nil
474
- end
475
-
476
- def from(entity_id = nil)
477
- class_name = self.name || self.to_s
478
- if entity_id.nil?
479
- # Try to find the value, checking inheritance chain
480
- result = find_addressing_value(:from)
481
- result
482
- else
483
- @@addressing_registry[class_name] ||= {}
484
- @@addressing_registry[class_name][:from] = entity_id
485
- end
486
- end
487
-
488
- def from_configured?; !from.nil?; end
489
- def from_missing?; from.nil?; end
490
- def reset_from;
491
- class_name = self.name || self.to_s
492
- @@addressing_registry[class_name] ||= {}
493
- @@addressing_registry[class_name][:from] = nil
494
- end
495
-
496
- def to(entity_id = nil)
497
- class_name = self.name || self.to_s
498
- if entity_id.nil?
499
- # Try to find the value, checking inheritance chain
500
- result = find_addressing_value(:to)
501
- result
502
- else
503
- @@addressing_registry[class_name] ||= {}
504
- @@addressing_registry[class_name][:to] = entity_id
505
- end
506
- end
507
-
508
- def to_configured?; !to.nil?; end
509
- def to_missing?; to.nil?; end
510
- def reset_to;
511
- class_name = self.name || self.to_s
512
- @@addressing_registry[class_name] ||= {}
513
- @@addressing_registry[class_name][:to] = nil
514
- end
515
-
516
- def reply_to(entity_id = nil)
517
- class_name = self.name || self.to_s
518
- if entity_id.nil?
519
- # Try to find the value, checking inheritance chain
520
- result = find_addressing_value(:reply_to)
521
- result
522
- else
523
- @@addressing_registry[class_name] ||= {}
524
- @@addressing_registry[class_name][:reply_to] = entity_id
525
- end
526
- end
527
-
528
- def reply_to_configured?; !reply_to.nil?; end
529
- def reply_to_missing?; reply_to.nil?; end
530
- def reset_reply_to;
531
- class_name = self.name || self.to_s
532
- @@addressing_registry[class_name] ||= {}
533
- @@addressing_registry[class_name][:reply_to] = nil
534
- end
535
-
536
-
537
- #########################################################
538
- ## class-level subscription management via the transport
539
-
540
- # Add this message class to the transport's catalog of
541
- # subscribed messages. If the transport is missing, raise
542
- # an exception.
543
- #
544
- # @param process_method [String, Proc, nil] The processing method:
545
- # - String: Method name like "MyService.handle_message"
546
- # - Proc: A proc/lambda that accepts (message_header, message_payload)
547
- # - nil: Uses default "MessageClass.process" method
548
- # @param broadcast [Boolean, nil] Filter for broadcast messages (to: nil)
549
- # @param to [String, Array, nil] Filter for messages directed to specific entities
550
- # @param from [String, Array, nil] Filter for messages from specific entities
551
- # @param block [Proc] Alternative way to pass a processing block
552
- # @return [String] The identifier used for this subscription
553
- #
554
- # @example Using default handler (all messages)
555
- # MyMessage.subscribe
556
- #
557
- # @example Using custom method name with filtering
558
- # MyMessage.subscribe("MyService.handle_message", to: 'my-service')
559
- #
560
- # @example Using a block with broadcast filtering
561
- # MyMessage.subscribe(broadcast: true) do |header, payload|
562
- # data = JSON.parse(payload)
563
- # puts "Received broadcast: #{data}"
564
- # end
565
- #
566
- # @example Entity-specific filtering
567
- # MyMessage.subscribe(to: 'order-service', from: ['payment', 'user'])
568
- #
569
- # @example Broadcast + directed messages
570
- # MyMessage.subscribe(to: 'my-service', broadcast: true)
571
- def subscribe(process_method = nil, broadcast: nil, to: nil, from: nil, &block)
572
- message_class = whoami
573
-
574
- # Handle different parameter types
575
- if block_given?
576
- # Block was passed - use it as the handler
577
- handler_proc = block
578
- process_method = register_proc_handler(message_class, handler_proc)
579
- elsif process_method.respond_to?(:call)
580
- # Proc/lambda was passed as first parameter
581
- handler_proc = process_method
582
- process_method = register_proc_handler(message_class, handler_proc)
583
- elsif process_method.nil?
584
- # Use default handler
585
- process_method = message_class + '.process'
586
- end
587
- # If process_method is a String, use it as-is
588
-
589
- # Normalize string filters to arrays
590
- to_filter = normalize_filter_value(to)
591
- from_filter = normalize_filter_value(from)
592
-
593
- # Create filter options
594
- filter_options = {
595
- broadcast: broadcast,
596
- to: to_filter,
597
- from: from_filter
598
- }
599
-
600
- # TODO: Add proper logging here
601
-
602
- raise Errors::TransportNotConfigured if transport_missing?
603
- transport.subscribe(message_class, process_method, filter_options)
604
-
605
- process_method
606
- end
607
-
608
-
609
- # Remove this process_method for this message class from the
610
- # subscribers list.
611
- # @param process_method [String, nil] The processing method identifier to remove
612
- # - String: Method name like "MyService.handle_message" or proc handler ID
613
- # - nil: Uses default "MessageClass.process" method
614
- def unsubscribe(process_method = nil)
615
- message_class = whoami
616
- process_method = message_class + '.process' if process_method.nil?
617
- # TODO: Add proper logging here
618
-
619
- if transport_configured?
620
- transport.unsubscribe(message_class, process_method)
621
-
622
- # If this was a proc handler, clean it up from the registry
623
- if proc_handler?(process_method)
624
- unregister_proc_handler(process_method)
625
- end
626
- end
627
- end
628
-
629
-
630
- # Remove this message class and all of its processing methods
631
- # from the subscribers list.
632
- def unsubscribe!
633
- message_class = whoami
634
-
635
- # TODO: Add proper logging here
636
-
637
- transport.unsubscribe!(message_class) if transport_configured?
638
- end
639
-
640
-
641
-
642
- #########################################################
643
- ## class-level utility methods
644
-
645
- # return this class' name as a string
646
- def whoami
647
- ancestors.first.to_s
648
- end
649
-
650
- # Return a Set of symbols representing each defined property of
651
- # this message class.
652
- def fields
653
- @properties.dup.delete_if{|item| item.to_s.start_with?('_sm_')}
654
- end
655
-
656
- ###################################################
657
- ## Business Logic resides in the #process method.
658
-
659
- # When a transport receives a subscribed to message it
660
- # creates an instance of the message and then calls
661
- # the process method on that instance.
662
- #
663
- # It is expected that SmartMessage classes over ride
664
- # the SmartMessage::Base#process method with appropriate
665
- # business logic to handle the received message content.
666
- def process(message_instance)
667
- raise Errors::NotImplemented
668
193
  end
194
+ end
669
195
 
670
- end # class << self
671
196
  end # class Base
672
197
  end # module SmartMessage
673
198
 
674
- require_relative 'header'
675
- require_relative 'transport'
676
- require_relative 'serializer'
677
- require_relative 'logger'
199
+ # Zeitwerk will handle autoloading of these modules