smart_message 0.0.8 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.irbrc +24 -0
- data/CHANGELOG.md +96 -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 +2 -1
- data/lib/smart_message/configuration.rb +199 -0
- data/lib/smart_message/dead_letter_queue.rb +27 -10
- 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 +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 +76 -3
- data/lib/smart_message/serializer.rb +0 -10
- data/lib/smart_message/wrapper.rb +0 -43
@@ -10,7 +10,7 @@ module SmartMessage
|
|
10
10
|
# This defines the standard interface that all transports must implement
|
11
11
|
class Base
|
12
12
|
include BreakerMachines::DSL
|
13
|
-
|
13
|
+
|
14
14
|
attr_reader :options, :dispatcher
|
15
15
|
|
16
16
|
def initialize(**options)
|
@@ -18,7 +18,20 @@ module SmartMessage
|
|
18
18
|
@dispatcher = options[:dispatcher] || SmartMessage::Dispatcher.new
|
19
19
|
configure
|
20
20
|
configure_transport_circuit_breakers
|
21
|
+
|
22
|
+
logger.debug { "[SmartMessage::Transport::#{self.class.name.split('::').last}] Initialized with options: #{@options}" }
|
23
|
+
rescue => e
|
24
|
+
logger&.error { "[SmartMessage] Error in transport initialization: #{e.class.name} - #{e.message}" }
|
25
|
+
raise
|
21
26
|
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def logger
|
31
|
+
@logger ||= SmartMessage::Logger.default
|
32
|
+
end
|
33
|
+
|
34
|
+
public
|
22
35
|
|
23
36
|
# Transport-specific configuration
|
24
37
|
def configure
|
@@ -31,24 +44,27 @@ module SmartMessage
|
|
31
44
|
end
|
32
45
|
|
33
46
|
# Publish a message with circuit breaker protection
|
34
|
-
# @param
|
35
|
-
# @param
|
36
|
-
def publish(
|
47
|
+
# @param message_class [String] The message class name (used for channel routing)
|
48
|
+
# @param serialized_message [String] Complete serialized message content
|
49
|
+
def publish(message_class, serialized_message)
|
37
50
|
circuit(:transport_publish).wrap do
|
38
|
-
do_publish(
|
51
|
+
do_publish(message_class, serialized_message)
|
39
52
|
end
|
40
53
|
rescue => e
|
54
|
+
# Log the exception for debugging
|
55
|
+
logger.error { "[SmartMessage] Error in transport publish: #{e.class.name} - #{e.message}" }
|
56
|
+
|
41
57
|
# Re-raise if it's not a circuit breaker fallback
|
42
58
|
raise unless e.is_a?(Hash) && e[:circuit_breaker]
|
43
|
-
|
59
|
+
|
44
60
|
# Handle circuit breaker fallback
|
45
|
-
handle_publish_fallback(e,
|
61
|
+
handle_publish_fallback(e, message_class, serialized_message)
|
46
62
|
end
|
47
63
|
|
48
64
|
# Template method for actual publishing (implement in subclasses)
|
49
|
-
# @param
|
50
|
-
# @param
|
51
|
-
def do_publish(
|
65
|
+
# @param message_class [String] The message class name (used for channel routing)
|
66
|
+
# @param serialized_message [String] Complete serialized message content
|
67
|
+
def do_publish(message_class, serialized_message)
|
52
68
|
raise NotImplementedError, 'Transport must implement #do_publish'
|
53
69
|
end
|
54
70
|
|
@@ -61,7 +77,7 @@ module SmartMessage
|
|
61
77
|
end
|
62
78
|
|
63
79
|
# Unsubscribe from a specific message class and process method
|
64
|
-
# @param message_class [String] The message class name
|
80
|
+
# @param message_class [String] The message class name
|
65
81
|
# @param process_method [String] The processing method identifier
|
66
82
|
def unsubscribe(message_class, process_method)
|
67
83
|
@dispatcher.drop(message_class, process_method)
|
@@ -88,7 +104,7 @@ module SmartMessage
|
|
88
104
|
# Override in subclasses if connection setup is needed
|
89
105
|
end
|
90
106
|
|
91
|
-
# Disconnect from transport (if applicable)
|
107
|
+
# Disconnect from transport (if applicable)
|
92
108
|
def disconnect
|
93
109
|
# Override in subclasses if cleanup is needed
|
94
110
|
end
|
@@ -97,7 +113,7 @@ module SmartMessage
|
|
97
113
|
# @return [Hash] Circuit breaker statistics
|
98
114
|
def transport_circuit_stats
|
99
115
|
stats = {}
|
100
|
-
|
116
|
+
|
101
117
|
[:transport_publish, :transport_subscribe].each do |circuit_name|
|
102
118
|
begin
|
103
119
|
if respond_to?(:circuit)
|
@@ -118,7 +134,7 @@ module SmartMessage
|
|
118
134
|
stats[circuit_name] = { error: "Failed to get stats: #{e.message}" }
|
119
135
|
end
|
120
136
|
end
|
121
|
-
|
137
|
+
|
122
138
|
stats
|
123
139
|
end
|
124
140
|
|
@@ -135,41 +151,58 @@ module SmartMessage
|
|
135
151
|
end
|
136
152
|
|
137
153
|
# Receive and route a message (called by transport implementations)
|
138
|
-
# @param
|
139
|
-
# @param
|
154
|
+
# @param message_class [String] The message class name
|
155
|
+
# @param serialized_message [String] The serialized message content
|
140
156
|
protected
|
141
157
|
|
142
|
-
def receive(
|
143
|
-
|
158
|
+
def receive(message_class, serialized_message)
|
159
|
+
# Decode the message using the class's configured serializer
|
160
|
+
|
161
|
+
# Add defensive check for message_class type
|
162
|
+
unless message_class.respond_to?(:constantize)
|
163
|
+
logger.error { "[SmartMessage] Invalid message_class type: #{message_class.class.name} - #{message_class.inspect}" }
|
164
|
+
logger.error { "[SmartMessage] Expected String, got: #{message_class.class.name}" }
|
165
|
+
raise ArgumentError, "message_class must be a String, got #{message_class.class.name}"
|
166
|
+
end
|
167
|
+
|
168
|
+
message_class_obj = message_class.constantize
|
169
|
+
decoded_message = message_class_obj.decode(serialized_message)
|
170
|
+
|
171
|
+
@dispatcher.route(decoded_message)
|
172
|
+
rescue => e
|
173
|
+
logger.error { "[SmartMessage] Error in transport receive: #{e.class.name} - #{e.message}" }
|
174
|
+
logger.error { "[SmartMessage] message_class: #{message_class.inspect} (#{message_class.class.name})" }
|
175
|
+
logger.error { "[SmartMessage] serialized_message length: #{serialized_message&.length}" }
|
176
|
+
raise
|
144
177
|
end
|
145
178
|
|
146
179
|
# Configure circuit breakers for transport operations
|
147
180
|
def configure_transport_circuit_breakers
|
148
181
|
# Configure publish circuit breaker
|
149
182
|
publish_config = SmartMessage::CircuitBreaker::DEFAULT_CONFIGS[:transport_publish]
|
150
|
-
|
183
|
+
|
151
184
|
self.class.circuit :transport_publish do
|
152
|
-
threshold failures: publish_config[:threshold][:failures],
|
185
|
+
threshold failures: publish_config[:threshold][:failures],
|
153
186
|
within: publish_config[:threshold][:within].seconds
|
154
187
|
reset_after publish_config[:reset_after].seconds
|
155
|
-
|
188
|
+
|
156
189
|
# Use memory storage by default for transport circuits
|
157
190
|
storage BreakerMachines::Storage::Memory.new
|
158
|
-
|
191
|
+
|
159
192
|
# Fallback for publish failures - use DLQ fallback
|
160
193
|
fallback SmartMessage::CircuitBreaker::Fallbacks.dead_letter_queue
|
161
194
|
end
|
162
195
|
|
163
196
|
# Configure subscribe circuit breaker
|
164
197
|
subscribe_config = SmartMessage::CircuitBreaker::DEFAULT_CONFIGS[:transport_subscribe]
|
165
|
-
|
198
|
+
|
166
199
|
self.class.circuit :transport_subscribe do
|
167
|
-
threshold failures: subscribe_config[:threshold][:failures],
|
200
|
+
threshold failures: subscribe_config[:threshold][:failures],
|
168
201
|
within: subscribe_config[:threshold][:within].seconds
|
169
202
|
reset_after subscribe_config[:reset_after].seconds
|
170
|
-
|
203
|
+
|
171
204
|
storage BreakerMachines::Storage::Memory.new
|
172
|
-
|
205
|
+
|
173
206
|
# Fallback for subscribe failures - log and return error info
|
174
207
|
fallback do |exception|
|
175
208
|
{
|
@@ -189,34 +222,32 @@ module SmartMessage
|
|
189
222
|
|
190
223
|
# Handle publish circuit breaker fallback
|
191
224
|
# @param fallback_result [Hash] The circuit breaker fallback result
|
192
|
-
# @param
|
193
|
-
# @param
|
194
|
-
def handle_publish_fallback(fallback_result,
|
225
|
+
# @param message_class [String] The message class name
|
226
|
+
# @param serialized_message [String] The serialized message
|
227
|
+
def handle_publish_fallback(fallback_result, message_class, serialized_message)
|
195
228
|
# Log the circuit breaker activation
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
end
|
202
|
-
|
229
|
+
logger.error { "[SmartMessage::Transport] Circuit breaker activated: #{self.class.name}" }
|
230
|
+
logger.error { "[SmartMessage::Transport] Error: #{fallback_result[:circuit_breaker][:error]}" }
|
231
|
+
logger.error { "[SmartMessage::Transport] Message: #{message_class}" }
|
232
|
+
logger.info { "[SmartMessage::Transport] Sent to DLQ: #{fallback_result[:circuit_breaker][:sent_to_dlq]}" }
|
233
|
+
|
203
234
|
# If message wasn't sent to DLQ by circuit breaker, send it now
|
204
235
|
unless fallback_result.dig(:circuit_breaker, :sent_to_dlq)
|
205
236
|
begin
|
206
237
|
SmartMessage::DeadLetterQueue.default.enqueue(
|
207
|
-
|
208
|
-
|
238
|
+
message_class,
|
239
|
+
serialized_message,
|
209
240
|
error: fallback_result.dig(:circuit_breaker, :error) || 'Circuit breaker activated',
|
210
241
|
transport: self.class.name
|
211
242
|
)
|
212
243
|
rescue => dlq_error
|
213
|
-
|
244
|
+
logger.warn { "[SmartMessage] Warning: Failed to store message in DLQ: #{dlq_error.message}" }
|
214
245
|
end
|
215
246
|
end
|
216
|
-
|
247
|
+
|
217
248
|
# Return the fallback result to indicate failure
|
218
249
|
fallback_result
|
219
250
|
end
|
220
251
|
end
|
221
252
|
end
|
222
|
-
end
|
253
|
+
end
|
@@ -22,20 +22,22 @@ module SmartMessage
|
|
22
22
|
end
|
23
23
|
|
24
24
|
# Publish message to memory queue
|
25
|
-
def do_publish(
|
25
|
+
def do_publish(message_class, serialized_message)
|
26
26
|
@message_mutex.synchronize do
|
27
27
|
# Prevent memory overflow
|
28
28
|
@messages.shift if @messages.size >= @options[:max_messages]
|
29
29
|
|
30
30
|
@messages << {
|
31
|
-
|
32
|
-
|
31
|
+
message_class: message_class,
|
32
|
+
serialized_message: serialized_message,
|
33
33
|
published_at: Time.now
|
34
34
|
}
|
35
35
|
end
|
36
36
|
|
37
37
|
# Auto-process if enabled
|
38
|
-
|
38
|
+
if @options[:auto_process]
|
39
|
+
receive(message_class, serialized_message)
|
40
|
+
end
|
39
41
|
end
|
40
42
|
|
41
43
|
# Get all stored messages
|
@@ -57,7 +59,7 @@ module SmartMessage
|
|
57
59
|
def process_all
|
58
60
|
messages_to_process = @message_mutex.synchronize { @messages.dup }
|
59
61
|
messages_to_process.each do |msg|
|
60
|
-
receive(msg[:
|
62
|
+
receive(msg[:message_class], msg[:serialized_message])
|
61
63
|
end
|
62
64
|
end
|
63
65
|
|
@@ -36,20 +36,13 @@ module SmartMessage
|
|
36
36
|
end
|
37
37
|
|
38
38
|
# Publish message to Redis channel using message class name
|
39
|
-
def do_publish(
|
40
|
-
channel =
|
41
|
-
|
42
|
-
# Combine header and payload for Redis transport
|
43
|
-
# This ensures header information (from, to, reply_to, etc.) is preserved
|
44
|
-
redis_message = {
|
45
|
-
header: message_header.to_hash,
|
46
|
-
payload: message_payload
|
47
|
-
}.to_json
|
39
|
+
def do_publish(message_class, serialized_message)
|
40
|
+
channel = message_class
|
48
41
|
|
49
42
|
begin
|
50
|
-
@redis_pub.publish(channel,
|
43
|
+
@redis_pub.publish(channel, serialized_message)
|
51
44
|
rescue Redis::ConnectionError
|
52
|
-
retry_with_reconnect('publish') { @redis_pub.publish(channel,
|
45
|
+
retry_with_reconnect('publish') { @redis_pub.publish(channel, serialized_message) }
|
53
46
|
end
|
54
47
|
end
|
55
48
|
|
@@ -117,7 +110,7 @@ module SmartMessage
|
|
117
110
|
subscribe_to_channels
|
118
111
|
rescue => e
|
119
112
|
# Log error but don't crash the thread
|
120
|
-
|
113
|
+
logger.error { "[SmartMessage] Error in redis subscriber: #{e.class.name} - #{e.message}" }
|
121
114
|
retry_subscriber
|
122
115
|
end
|
123
116
|
end
|
@@ -143,51 +136,28 @@ module SmartMessage
|
|
143
136
|
|
144
137
|
begin
|
145
138
|
@redis_sub.subscribe(*@subscribed_channels) do |on|
|
146
|
-
on.message do |channel,
|
139
|
+
on.message do |channel, serialized_message|
|
147
140
|
begin
|
148
|
-
#
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
message_header = SmartMessage::Header.new(header_data)
|
155
|
-
message_payload = parsed_message['payload']
|
156
|
-
else
|
157
|
-
# Fallback for messages that don't have header/payload structure (legacy support)
|
158
|
-
message_header = SmartMessage::Header.new(
|
159
|
-
message_class: channel,
|
160
|
-
uuid: SecureRandom.uuid,
|
161
|
-
published_at: Time.now,
|
162
|
-
publisher_pid: 'redis_subscriber'
|
163
|
-
)
|
164
|
-
message_payload = redis_message
|
165
|
-
end
|
166
|
-
|
167
|
-
receive(message_header, message_payload)
|
168
|
-
rescue JSON::ParserError
|
169
|
-
# Handle malformed JSON - fallback to legacy behavior
|
170
|
-
message_header = SmartMessage::Header.new(
|
171
|
-
message_class: channel,
|
172
|
-
uuid: SecureRandom.uuid,
|
173
|
-
published_at: Time.now,
|
174
|
-
publisher_pid: 'redis_subscriber'
|
175
|
-
)
|
176
|
-
receive(message_header, redis_message)
|
141
|
+
# Channel name is the message class name
|
142
|
+
# Serialized message contains the complete message
|
143
|
+
receive(channel, serialized_message)
|
144
|
+
rescue => e
|
145
|
+
logger.error { "[SmartMessage] Error in redis message processing: #{e.class.name} - #{e.message}" }
|
146
|
+
# Continue processing other messages
|
177
147
|
end
|
178
148
|
end
|
179
149
|
|
180
150
|
on.subscribe do |channel, subscriptions|
|
181
|
-
|
151
|
+
logger.debug { "[SmartMessage::RedisTransport] Subscribed to Redis channel: #{channel} (#{subscriptions} total)" }
|
182
152
|
end
|
183
153
|
|
184
154
|
on.unsubscribe do |channel, subscriptions|
|
185
|
-
|
155
|
+
logger.debug { "[SmartMessage::RedisTransport] Unsubscribed from Redis channel: #{channel} (#{subscriptions} total)" }
|
186
156
|
end
|
187
157
|
end
|
188
158
|
rescue => e
|
189
159
|
# Silently handle connection errors during subscription
|
190
|
-
|
160
|
+
logger.error { "[SmartMessage] Error in redis subscription: #{e.class.name} - #{e.message}" }
|
191
161
|
retry_subscriber if @running
|
192
162
|
end
|
193
163
|
end
|
@@ -27,13 +27,22 @@ module SmartMessage
|
|
27
27
|
@options[:loopback]
|
28
28
|
end
|
29
29
|
|
30
|
-
# Publish message to STDOUT
|
31
|
-
def do_publish(
|
32
|
-
|
30
|
+
# Publish message to STDOUT (single-tier serialization)
|
31
|
+
def do_publish(message_class, serialized_message)
|
32
|
+
logger.debug { "[SmartMessage::StdoutTransport] do_publish called" }
|
33
|
+
logger.debug { "[SmartMessage::StdoutTransport] message_class: #{message_class}" }
|
34
|
+
|
35
|
+
@output.puts format_message(message_class, serialized_message)
|
33
36
|
@output.flush
|
34
37
|
|
35
38
|
# If loopback is enabled, route the message back through the dispatcher
|
36
|
-
|
39
|
+
if loopback?
|
40
|
+
logger.debug { "[SmartMessage::StdoutTransport] Loopback enabled, calling receive" }
|
41
|
+
receive(message_class, serialized_message)
|
42
|
+
end
|
43
|
+
rescue => e
|
44
|
+
logger.error { "[SmartMessage] Error in stdout transport do_publish: #{e.class.name} - #{e.message}" }
|
45
|
+
raise
|
37
46
|
end
|
38
47
|
|
39
48
|
def connected?
|
@@ -46,17 +55,18 @@ module SmartMessage
|
|
46
55
|
|
47
56
|
private
|
48
57
|
|
49
|
-
def format_message(
|
58
|
+
def format_message(message_class, serialized_message)
|
50
59
|
<<~MESSAGE
|
51
60
|
|
52
61
|
===================================================
|
53
62
|
== SmartMessage Published via STDOUT Transport
|
54
|
-
==
|
55
|
-
==
|
63
|
+
== Single-Tier Serialization:
|
64
|
+
== Message Class: #{message_class}
|
65
|
+
== Serialized Message: #{serialized_message}
|
56
66
|
===================================================
|
57
67
|
|
58
68
|
MESSAGE
|
59
69
|
end
|
60
70
|
end
|
61
71
|
end
|
62
|
-
end
|
72
|
+
end
|
@@ -6,37 +6,4 @@ require_relative 'transport/base'
|
|
6
6
|
require_relative 'transport/registry'
|
7
7
|
require_relative 'transport/stdout_transport'
|
8
8
|
require_relative 'transport/memory_transport'
|
9
|
-
require_relative 'transport/redis_transport'
|
10
|
-
|
11
|
-
module SmartMessage
|
12
|
-
# Transport layer abstraction for SmartMessage
|
13
|
-
module Transport
|
14
|
-
class << self
|
15
|
-
# Get the transport registry instance
|
16
|
-
def registry
|
17
|
-
@registry ||= Registry.new
|
18
|
-
end
|
19
|
-
|
20
|
-
# Register a transport adapter
|
21
|
-
def register(name, transport_class)
|
22
|
-
registry.register(name, transport_class)
|
23
|
-
end
|
24
|
-
|
25
|
-
# Get a transport by name
|
26
|
-
def get(name)
|
27
|
-
registry.get(name)
|
28
|
-
end
|
29
|
-
|
30
|
-
# Create a transport instance with options
|
31
|
-
def create(name, **options)
|
32
|
-
transport_class = get(name)
|
33
|
-
transport_class&.new(**options)
|
34
|
-
end
|
35
|
-
|
36
|
-
# List all registered transports
|
37
|
-
def available
|
38
|
-
registry.list
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
9
|
+
require_relative 'transport/redis_transport'
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# lib/smart_message/utilities.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require 'set' # STDLIB
|
6
|
+
|
7
|
+
module SmartMessage
|
8
|
+
# Utilities module for SmartMessage::Base
|
9
|
+
# Provides utility methods for message introspection and debugging
|
10
|
+
module Utilities
|
11
|
+
def self.included(base)
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
end
|
14
|
+
|
15
|
+
#########################################################
|
16
|
+
## instance-level utility methods
|
17
|
+
|
18
|
+
# return this class' name as a string
|
19
|
+
def whoami
|
20
|
+
self.class.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
# return this class' description
|
24
|
+
def description
|
25
|
+
self.class.description
|
26
|
+
end
|
27
|
+
|
28
|
+
# Clean accessor to the SmartMessage header object
|
29
|
+
# Provides more intuitive API than _sm_header
|
30
|
+
# Note: Renamed to avoid conflict with class-level header DSL
|
31
|
+
def message_header
|
32
|
+
_sm_header
|
33
|
+
end
|
34
|
+
|
35
|
+
# returns a collection of class Set that consists of
|
36
|
+
# the symbolized values of the property names of the message
|
37
|
+
# without the injected '_sm_' properties that support
|
38
|
+
# the behind-the-sceens operations of SmartMessage.
|
39
|
+
def fields
|
40
|
+
to_h.keys
|
41
|
+
.reject{|key| key.start_with?('_sm_')}
|
42
|
+
.map{|key| key.to_sym}
|
43
|
+
.to_set
|
44
|
+
end
|
45
|
+
|
46
|
+
# Pretty print the message content to STDOUT using amazing_print
|
47
|
+
# @param pp_or_include_header [PP, Boolean] Either a PP printer object (from Ruby's pp library)
|
48
|
+
# or include_header boolean (for our custom usage)
|
49
|
+
# @param include_header [Boolean] Whether to include the SmartMessage header (default: false)
|
50
|
+
def pretty_print(pp_or_include_header = nil, include_header: false)
|
51
|
+
# Handle Ruby's PP library calling convention: pretty_print(pp_object)
|
52
|
+
if pp_or_include_header.is_a?(Object) && pp_or_include_header.respond_to?(:text)
|
53
|
+
# This is Ruby's PP library calling us - delegate to standard object pretty printing
|
54
|
+
pp_or_include_header.text(self.inspect)
|
55
|
+
return
|
56
|
+
end
|
57
|
+
|
58
|
+
# Handle our custom calling convention: pretty_print(include_header: true)
|
59
|
+
if pp_or_include_header.is_a?(TrueClass) || pp_or_include_header.is_a?(FalseClass)
|
60
|
+
include_header = pp_or_include_header
|
61
|
+
end
|
62
|
+
|
63
|
+
require 'amazing_print'
|
64
|
+
|
65
|
+
if include_header
|
66
|
+
# Show both header and content
|
67
|
+
puts "Header:"
|
68
|
+
puts "-" * 20
|
69
|
+
|
70
|
+
# Get header data, converting to symbols and filtering out nils
|
71
|
+
header_data = _sm_header.to_h
|
72
|
+
.reject { |key, value| value.nil? }
|
73
|
+
header_data = deep_symbolize_keys(header_data)
|
74
|
+
ap header_data
|
75
|
+
|
76
|
+
puts "\nContent:"
|
77
|
+
puts "-" * 20
|
78
|
+
|
79
|
+
# Get payload data (message properties excluding header)
|
80
|
+
content_data = get_payload_data.reject { |key, value| value.nil? }
|
81
|
+
content_data = deep_symbolize_keys(content_data)
|
82
|
+
ap content_data
|
83
|
+
else
|
84
|
+
# Show only message content (excluding _sm_ properties and nil values)
|
85
|
+
content_data = get_payload_data.reject { |key, value| value.nil? }
|
86
|
+
content_data = deep_symbolize_keys(content_data)
|
87
|
+
ap content_data
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
# Extract payload data (all properties except _sm_header)
|
94
|
+
def get_payload_data
|
95
|
+
self.class.properties.each_with_object({}) do |prop, hash|
|
96
|
+
next if prop == :_sm_header
|
97
|
+
hash[prop.to_sym] = self[prop]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Recursively convert all string keys to symbols in nested hashes and arrays
|
102
|
+
def deep_symbolize_keys(obj)
|
103
|
+
case obj
|
104
|
+
when Hash
|
105
|
+
obj.each_with_object({}) do |(key, value), result|
|
106
|
+
result[key.to_sym] = deep_symbolize_keys(value)
|
107
|
+
end
|
108
|
+
when Array
|
109
|
+
obj.map { |item| deep_symbolize_keys(item) }
|
110
|
+
else
|
111
|
+
obj
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
module ClassMethods
|
116
|
+
#########################################################
|
117
|
+
## class-level description
|
118
|
+
|
119
|
+
def description(desc = nil)
|
120
|
+
if desc.nil?
|
121
|
+
@description || "#{self.name} is a SmartMessage"
|
122
|
+
else
|
123
|
+
@description = desc.to_s
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
#########################################################
|
128
|
+
## class-level utility methods
|
129
|
+
|
130
|
+
# return this class' name as a string
|
131
|
+
def whoami
|
132
|
+
ancestors.first.to_s
|
133
|
+
end
|
134
|
+
|
135
|
+
# Return a Set of symbols representing each defined property of
|
136
|
+
# this message class.
|
137
|
+
def fields
|
138
|
+
@properties.dup.delete_if{|item| item.to_s.start_with?('_sm_')}
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# lib/smart_message/versioning.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module SmartMessage
|
6
|
+
# Versioning module for SmartMessage::Base
|
7
|
+
# Handles schema versioning and version validation
|
8
|
+
module Versioning
|
9
|
+
def self.included(base)
|
10
|
+
base.extend(ClassMethods)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Validate that the header version matches the expected version for this class
|
14
|
+
def validate_header_version!
|
15
|
+
expected = self.class.expected_header_version
|
16
|
+
actual = _sm_header.version
|
17
|
+
unless actual == expected
|
18
|
+
raise SmartMessage::Errors::ValidationError,
|
19
|
+
"#{self.class.name} expects version #{expected}, but header has version #{actual}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Override PropertyValidations validate! to include header and version validation
|
24
|
+
def validate!
|
25
|
+
# Validate message properties using PropertyValidations
|
26
|
+
super
|
27
|
+
|
28
|
+
# Validate header properties
|
29
|
+
_sm_header.validate!
|
30
|
+
|
31
|
+
# Validate header version matches expected class version
|
32
|
+
validate_header_version!
|
33
|
+
end
|
34
|
+
|
35
|
+
# Override PropertyValidations validation_errors to include header errors
|
36
|
+
def validation_errors
|
37
|
+
errors = []
|
38
|
+
|
39
|
+
# Get message property validation errors using PropertyValidations
|
40
|
+
errors.concat(super.map { |err|
|
41
|
+
err.merge(source: 'message')
|
42
|
+
})
|
43
|
+
|
44
|
+
# Get header validation errors
|
45
|
+
errors.concat(_sm_header.validation_errors.map { |err|
|
46
|
+
err.merge(source: 'header')
|
47
|
+
})
|
48
|
+
|
49
|
+
# Check version mismatch
|
50
|
+
expected = self.class.expected_header_version
|
51
|
+
actual = _sm_header.version
|
52
|
+
unless actual == expected
|
53
|
+
errors << {
|
54
|
+
property: :version,
|
55
|
+
value: actual,
|
56
|
+
message: "Expected version #{expected}, got: #{actual}",
|
57
|
+
source: 'version_mismatch'
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
errors
|
62
|
+
end
|
63
|
+
|
64
|
+
module ClassMethods
|
65
|
+
# Class-level version setting
|
66
|
+
attr_accessor :_version
|
67
|
+
|
68
|
+
def version(v = nil)
|
69
|
+
if v.nil?
|
70
|
+
@_version || 1 # Default to version 1 if not set
|
71
|
+
else
|
72
|
+
@_version = v
|
73
|
+
|
74
|
+
# Set up version validation for the header
|
75
|
+
# This ensures that the header version matches the expected class version
|
76
|
+
@expected_header_version = v
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def expected_header_version
|
81
|
+
@expected_header_version || 1
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|