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.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.irbrc +24 -0
- data/CHANGELOG.md +119 -0
- data/Gemfile.lock +6 -1
- data/README.md +389 -17
- data/docs/README.md +3 -1
- data/docs/addressing.md +119 -13
- data/docs/architecture.md +184 -46
- 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_deduplication.md +488 -0
- 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/10_message_deduplication.rb +209 -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 +123 -599
- data/lib/smart_message/circuit_breaker.rb +2 -1
- data/lib/smart_message/configuration.rb +199 -0
- data/lib/smart_message/ddq/base.rb +71 -0
- data/lib/smart_message/ddq/memory.rb +109 -0
- data/lib/smart_message/ddq/redis.rb +168 -0
- data/lib/smart_message/ddq.rb +31 -0
- data/lib/smart_message/dead_letter_queue.rb +27 -10
- data/lib/smart_message/deduplication.rb +174 -0
- data/lib/smart_message/dispatcher.rb +259 -61
- 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 +196 -0
- data/lib/smart_message/transport/base.rb +72 -41
- 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 -28
- data/smart_message.gemspec +3 -0
- metadata +83 -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,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
|
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
|
-
|
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
|
-
|
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
|
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
|
-
#
|
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
|
-
})
|
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
|
-
#
|
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
|
-
}
|
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
|
-
|
138
|
+
JSON.generate(payload_props)
|
167
139
|
end
|
168
140
|
|
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)
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
170
|
+
# Use the class's configured serializer to decode the message
|
171
|
+
serializer = self.serializer || SmartMessage::Serializer.default
|
458
172
|
|
459
|
-
#
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
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
|
-
|
467
|
-
|
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
|
-
|
675
|
-
require_relative 'transport'
|
676
|
-
require_relative 'serializer'
|
677
|
-
require_relative 'logger'
|
201
|
+
# Zeitwerk will handle autoloading of these modules
|