smart_message 0.0.8 → 0.0.10

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.irbrc +24 -0
  4. data/CHANGELOG.md +119 -0
  5. data/Gemfile.lock +6 -1
  6. data/README.md +389 -17
  7. data/docs/README.md +3 -1
  8. data/docs/addressing.md +119 -13
  9. data/docs/architecture.md +184 -46
  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_deduplication.md +488 -0
  16. data/docs/message_filtering.md +451 -0
  17. data/examples/01_point_to_point_orders.rb +54 -53
  18. data/examples/02_publish_subscribe_events.rb +14 -10
  19. data/examples/03_many_to_many_chat.rb +16 -8
  20. data/examples/04_redis_smart_home_iot.rb +20 -10
  21. data/examples/05_proc_handlers.rb +12 -11
  22. data/examples/06_custom_logger_example.rb +95 -100
  23. data/examples/07_error_handling_scenarios.rb +4 -2
  24. data/examples/08_entity_addressing_basic.rb +18 -6
  25. data/examples/08_entity_addressing_with_filtering.rb +27 -9
  26. data/examples/09_dead_letter_queue_demo.rb +559 -0
  27. data/examples/09_regex_filtering_microservices.rb +407 -0
  28. data/examples/10_header_block_configuration.rb +263 -0
  29. data/examples/10_message_deduplication.rb +209 -0
  30. data/examples/11_global_configuration_example.rb +219 -0
  31. data/examples/README.md +102 -0
  32. data/examples/dead_letters.jsonl +12 -0
  33. data/examples/performance_metrics/benchmark_results_ractor_20250818_205603.json +135 -0
  34. data/examples/performance_metrics/benchmark_results_ractor_20250818_205831.json +135 -0
  35. data/examples/performance_metrics/benchmark_results_test_20250818_204942.json +130 -0
  36. data/examples/performance_metrics/benchmark_results_threadpool_20250818_204942.json +130 -0
  37. data/examples/performance_metrics/benchmark_results_threadpool_20250818_204959.json +130 -0
  38. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205044.json +130 -0
  39. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205109.json +130 -0
  40. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205252.json +130 -0
  41. data/examples/performance_metrics/benchmark_results_unknown_20250819_172852.json +130 -0
  42. data/examples/performance_metrics/compare_benchmarks.rb +519 -0
  43. data/examples/performance_metrics/dead_letters.jsonl +3100 -0
  44. data/examples/performance_metrics/performance_benchmark.rb +344 -0
  45. data/examples/show_logger.rb +367 -0
  46. data/examples/show_me.rb +145 -0
  47. data/examples/temp.txt +94 -0
  48. data/examples/tmux_chat/bot_agent.rb +4 -2
  49. data/examples/tmux_chat/human_agent.rb +4 -2
  50. data/examples/tmux_chat/room_monitor.rb +4 -2
  51. data/examples/tmux_chat/shared_chat_system.rb +6 -3
  52. data/lib/smart_message/addressing.rb +259 -0
  53. data/lib/smart_message/base.rb +123 -599
  54. data/lib/smart_message/circuit_breaker.rb +2 -1
  55. data/lib/smart_message/configuration.rb +199 -0
  56. data/lib/smart_message/ddq/base.rb +71 -0
  57. data/lib/smart_message/ddq/memory.rb +109 -0
  58. data/lib/smart_message/ddq/redis.rb +168 -0
  59. data/lib/smart_message/ddq.rb +31 -0
  60. data/lib/smart_message/dead_letter_queue.rb +27 -10
  61. data/lib/smart_message/deduplication.rb +174 -0
  62. data/lib/smart_message/dispatcher.rb +259 -61
  63. data/lib/smart_message/header.rb +5 -0
  64. data/lib/smart_message/logger/base.rb +21 -1
  65. data/lib/smart_message/logger/default.rb +88 -138
  66. data/lib/smart_message/logger/lumberjack.rb +324 -0
  67. data/lib/smart_message/logger/null.rb +81 -0
  68. data/lib/smart_message/logger.rb +17 -9
  69. data/lib/smart_message/messaging.rb +100 -0
  70. data/lib/smart_message/plugins.rb +132 -0
  71. data/lib/smart_message/serializer/base.rb +25 -8
  72. data/lib/smart_message/serializer/json.rb +5 -4
  73. data/lib/smart_message/subscription.rb +196 -0
  74. data/lib/smart_message/transport/base.rb +72 -41
  75. data/lib/smart_message/transport/memory_transport.rb +7 -5
  76. data/lib/smart_message/transport/redis_transport.rb +15 -45
  77. data/lib/smart_message/transport/stdout_transport.rb +18 -8
  78. data/lib/smart_message/transport.rb +1 -34
  79. data/lib/smart_message/utilities.rb +142 -0
  80. data/lib/smart_message/version.rb +1 -1
  81. data/lib/smart_message/versioning.rb +85 -0
  82. data/lib/smart_message/wrapper.rb.bak +132 -0
  83. data/lib/smart_message.rb +74 -28
  84. data/smart_message.gemspec +3 -0
  85. metadata +83 -3
  86. data/lib/smart_message/serializer.rb +0 -10
  87. data/lib/smart_message/wrapper.rb +0 -43
@@ -4,51 +4,30 @@
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'
15
+ require_relative './deduplication.rb'
10
16
 
11
17
  module SmartMessage
12
18
  # The foundation class for the smart message
13
19
  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
20
  include Hashie::Extensions::Dash::PropertyTranslation
49
21
 
50
22
  include SmartMessage::PropertyDescriptions
51
23
  include SmartMessage::PropertyValidations
24
+ include SmartMessage::Plugins
25
+ include SmartMessage::Addressing
26
+ include SmartMessage::Subscription
27
+ include SmartMessage::Versioning
28
+ include SmartMessage::Messaging
29
+ include SmartMessage::Utilities
30
+ include SmartMessage::Deduplication
52
31
 
53
32
  include Hashie::Extensions::Coercion
54
33
  include Hashie::Extensions::DeepMerge
@@ -68,610 +47,155 @@ module SmartMessage
68
47
  # Constructor for a messsage definition that allows the
69
48
  # setting of initial values.
70
49
  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
-
50
+ # instance-level override of class plugins
51
+ # Don't use fallback defaults here - let the methods handle fallbacks when actually used
52
+ @transport = (self.class.class_variable_get(:@@transport) rescue nil)
53
+ @serializer = (self.class.class_variable_get(:@@serializer) rescue nil)
54
+
55
+ # Check if we're reconstructing from serialized data (complete header provided)
56
+ if props[:_sm_header]
57
+ # Deserialization path: use provided header and payload
58
+ existing_header = props[:_sm_header]
59
+
60
+ # Convert to Header object if it's a hash (from deserialization)
61
+ if existing_header.is_a?(Hash)
62
+ # Convert string keys to symbols
63
+ header_hash = existing_header.transform_keys(&:to_sym)
64
+ header = SmartMessage::Header.new(**header_hash)
65
+ else
66
+ header = existing_header
67
+ end
68
+
69
+ # Extract addressing from header for instance variables
70
+ @from = header.from
71
+ @to = header.to
72
+ @reply_to = header.reply_to
73
+
74
+ # Extract payload properties
75
+ payload_props = props[:_sm_payload] || {}
76
+
77
+ attributes = {
78
+ _sm_header: header
79
+ }.merge(payload_props)
80
+ else
81
+ # Normal creation path: create new header
82
+ # Extract addressing information from props before creating header
83
+ addressing_props = props.extract!(:from, :to, :reply_to)
84
+
85
+ # instance-level over ride of class addressing
86
+ @from = addressing_props[:from]
87
+ @to = addressing_props[:to]
88
+ @reply_to = addressing_props[:reply_to]
89
+
90
+ # Create header with version validation specific to this message class
91
+ header = SmartMessage::Header.new(
92
+ uuid: SecureRandom.uuid,
93
+ message_class: self.class.to_s,
94
+ published_at: Time.now,
95
+ publisher_pid: Process.pid,
96
+ version: self.class.version,
97
+ from: from,
98
+ to: to,
99
+ reply_to: reply_to
100
+ )
101
+
102
+ attributes = {
103
+ _sm_header: header
104
+ }.merge(props)
105
+ end
106
+
96
107
  # Set up version validation to match the expected class version
97
108
  expected_version = self.class.expected_header_version
98
109
  header.singleton_class.class_eval do
99
110
  define_method(:validate_version!) do
100
111
  unless self.version == expected_version
101
- raise SmartMessage::Errors::ValidationError,
112
+ raise SmartMessage::Errors::ValidationError,
102
113
  "Header version must be #{expected_version}, got: #{self.version}"
103
114
  end
104
115
  end
105
116
  end
106
-
107
- attributes = {
108
- _sm_header: header
109
- }.merge(props)
110
117
 
111
118
  super(attributes, &block)
112
- end
113
-
114
119
 
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
120
+ # Log message creation
121
+ (self.class.logger || SmartMessage::Logger.default).debug { "[SmartMessage] Created: #{self.class.name}" }
122
+ rescue => e
123
+ (self.class.logger || SmartMessage::Logger.default).error { "[SmartMessage] Error in message initialization: #{e.class.name} - #{e.message}" }
124
+ raise
126
125
  end
127
126
 
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
- })
127
+ # Backward compatibility method for proc handlers that expect _sm_payload as JSON string
128
+ # In the new single-tier approach, this recreates the expected format
129
+ def _sm_payload
130
+ require 'json'
148
131
 
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
- }
132
+ # Extract payload properties (non-header properties)
133
+ payload_props = self.class.properties.each_with_object({}) do |prop, hash|
134
+ next if prop == :_sm_header
135
+ hash[prop.to_s] = self[prop] # Use string keys to match old format
164
136
  end
165
137
 
166
- errors
138
+ JSON.generate(payload_props)
167
139
  end
168
140
 
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)
141
+ # Backward compatibility method for handlers that expect wrapper.split
142
+ # Returns [header, payload_json] in the old wrapper format
143
+ def split
144
+ [_sm_header, _sm_payload]
182
145
  end
183
146
 
184
147
 
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
-
148
+ ###################################################
149
+ ## Common instance methods
305
150
 
306
- #########################################################
307
- ## instance-level utility methods
308
151
 
309
- # return this class' name as a string
310
- def whoami
311
- self.class.to_s
312
- end
313
152
 
314
- # return this class' description
315
- def description
316
- self.class.description
317
- end
318
153
 
319
154
 
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
155
 
331
156
 
332
157
  ###########################################################
333
158
  ## class methods
334
159
 
335
160
  class << self
161
+ # Decode a complete serialized message back to a message instance
162
+ # @param serialized_message [String] The serialized message content
163
+ # @return [SmartMessage::Base] The decoded message instance
164
+ def decode(serialized_message)
165
+ logger = SmartMessage::Logger.default
336
166
 
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
167
+ begin
168
+ (self.logger || SmartMessage::Logger.default).info { "[SmartMessage] Received: #{self.name} (#{serialized_message.bytesize} bytes)" }
350
169
 
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
170
+ # Use the class's configured serializer to decode the message
171
+ serializer = self.serializer || SmartMessage::Serializer.default
458
172
 
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
173
+ # Deserialize the complete message
174
+ deserialized_data = serializer.decode(serialized_message)
175
+
176
+ # Create new message instance with the complete deserialized data
177
+ if deserialized_data.is_a?(Hash)
178
+ # Convert string keys to symbols for compatibility with keyword arguments
179
+ symbol_props = deserialized_data.transform_keys(&:to_sym)
180
+
181
+ # With single-tier serialization, use the complete deserialized message structure
182
+ message = self.new(**symbol_props)
183
+
184
+ (self.logger || SmartMessage::Logger.default).debug { "[SmartMessage] Deserialized message: #{self.name}" }
185
+ message
186
+ else
187
+ # If it's already a message object, return it
188
+ (self.logger || SmartMessage::Logger.default).debug { "[SmartMessage] Returning existing message object: #{self.name}" }
189
+ deserialized_data
464
190
  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?
191
+ rescue => e
192
+ (self.logger || SmartMessage::Logger.default).error { "[SmartMessage] Error in message deserialization: #{e.class.name} - #{e.message}" }
193
+ raise
471
194
  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
195
  end
196
+ end
669
197
 
670
- end # class << self
671
198
  end # class Base
672
199
  end # module SmartMessage
673
200
 
674
- require_relative 'header'
675
- require_relative 'transport'
676
- require_relative 'serializer'
677
- require_relative 'logger'
201
+ # Zeitwerk will handle autoloading of these modules