smart_message 0.0.7 → 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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.irbrc +24 -0
- data/CHANGELOG.md +143 -0
- data/Gemfile.lock +6 -1
- data/README.md +289 -15
- data/docs/README.md +3 -1
- data/docs/addressing.md +119 -13
- data/docs/architecture.md +68 -0
- data/docs/dead_letter_queue.md +673 -0
- data/docs/dispatcher.md +87 -0
- data/docs/examples.md +59 -1
- data/docs/getting-started.md +8 -1
- data/docs/logging.md +382 -326
- data/docs/message_filtering.md +451 -0
- data/examples/01_point_to_point_orders.rb +54 -53
- data/examples/02_publish_subscribe_events.rb +14 -10
- data/examples/03_many_to_many_chat.rb +16 -8
- data/examples/04_redis_smart_home_iot.rb +20 -10
- data/examples/05_proc_handlers.rb +12 -11
- data/examples/06_custom_logger_example.rb +95 -100
- data/examples/07_error_handling_scenarios.rb +4 -2
- data/examples/08_entity_addressing_basic.rb +18 -6
- data/examples/08_entity_addressing_with_filtering.rb +27 -9
- data/examples/09_dead_letter_queue_demo.rb +559 -0
- data/examples/09_regex_filtering_microservices.rb +407 -0
- data/examples/10_header_block_configuration.rb +263 -0
- data/examples/11_global_configuration_example.rb +219 -0
- data/examples/README.md +102 -0
- data/examples/dead_letters.jsonl +12 -0
- data/examples/performance_metrics/benchmark_results_ractor_20250818_205603.json +135 -0
- data/examples/performance_metrics/benchmark_results_ractor_20250818_205831.json +135 -0
- data/examples/performance_metrics/benchmark_results_test_20250818_204942.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_204942.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_204959.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_205044.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_205109.json +130 -0
- data/examples/performance_metrics/benchmark_results_threadpool_20250818_205252.json +130 -0
- data/examples/performance_metrics/benchmark_results_unknown_20250819_172852.json +130 -0
- data/examples/performance_metrics/compare_benchmarks.rb +519 -0
- data/examples/performance_metrics/dead_letters.jsonl +3100 -0
- data/examples/performance_metrics/performance_benchmark.rb +344 -0
- data/examples/show_logger.rb +367 -0
- data/examples/show_me.rb +145 -0
- data/examples/temp.txt +94 -0
- data/examples/tmux_chat/bot_agent.rb +4 -2
- data/examples/tmux_chat/human_agent.rb +4 -2
- data/examples/tmux_chat/room_monitor.rb +4 -2
- data/examples/tmux_chat/shared_chat_system.rb +6 -3
- data/lib/smart_message/addressing.rb +259 -0
- data/lib/smart_message/base.rb +121 -599
- data/lib/smart_message/circuit_breaker.rb +23 -6
- data/lib/smart_message/configuration.rb +199 -0
- data/lib/smart_message/dead_letter_queue.rb +361 -0
- data/lib/smart_message/dispatcher.rb +90 -49
- data/lib/smart_message/header.rb +5 -0
- data/lib/smart_message/logger/base.rb +21 -1
- data/lib/smart_message/logger/default.rb +88 -138
- data/lib/smart_message/logger/lumberjack.rb +324 -0
- data/lib/smart_message/logger/null.rb +81 -0
- data/lib/smart_message/logger.rb +17 -9
- data/lib/smart_message/messaging.rb +100 -0
- data/lib/smart_message/plugins.rb +132 -0
- data/lib/smart_message/serializer/base.rb +25 -8
- data/lib/smart_message/serializer/json.rb +5 -4
- data/lib/smart_message/subscription.rb +193 -0
- data/lib/smart_message/transport/base.rb +84 -53
- data/lib/smart_message/transport/memory_transport.rb +7 -5
- data/lib/smart_message/transport/redis_transport.rb +15 -45
- data/lib/smart_message/transport/stdout_transport.rb +18 -8
- data/lib/smart_message/transport.rb +1 -34
- data/lib/smart_message/utilities.rb +142 -0
- data/lib/smart_message/version.rb +1 -1
- data/lib/smart_message/versioning.rb +85 -0
- data/lib/smart_message/wrapper.rb.bak +132 -0
- data/lib/smart_message.rb +74 -27
- data/smart_message.gemspec +3 -0
- metadata +77 -3
- data/lib/smart_message/serializer.rb +0 -10
- data/lib/smart_message/wrapper.rb +0 -43
data/lib/smart_message/base.rb
CHANGED
@@ -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
|
72
|
-
|
73
|
-
@
|
74
|
-
@
|
75
|
-
|
76
|
-
#
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
from
|
92
|
-
to
|
93
|
-
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
#
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
-
#
|
150
|
-
|
151
|
-
|
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
|
-
|
136
|
+
JSON.generate(payload_props)
|
167
137
|
end
|
168
138
|
|
169
|
-
#
|
170
|
-
#
|
171
|
-
|
172
|
-
|
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
|
-
|
186
|
-
|
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
|
-
|
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
|
-
|
352
|
-
|
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
|
-
#
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
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
|
-
|
467
|
-
|
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
|
-
|
675
|
-
require_relative 'transport'
|
676
|
-
require_relative 'serializer'
|
677
|
-
require_relative 'logger'
|
199
|
+
# Zeitwerk will handle autoloading of these modules
|