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
@@ -0,0 +1,324 @@
|
|
1
|
+
# lib/smart_message/logger/lumberjack.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require 'lumberjack'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'colorize'
|
8
|
+
|
9
|
+
module SmartMessage
|
10
|
+
module Logger
|
11
|
+
# Lumberjack-based logger implementation for SmartMessage
|
12
|
+
#
|
13
|
+
# This logger uses the Lumberjack gem to provide enhanced structured logging
|
14
|
+
# with automatic source location tracking, structured data support, and
|
15
|
+
# better performance characteristics.
|
16
|
+
#
|
17
|
+
# Features:
|
18
|
+
# - Automatic source location tracking (file:line) for every log entry
|
19
|
+
# - Structured data logging for message headers and payloads
|
20
|
+
# - Better performance than standard Ruby Logger
|
21
|
+
# - JSON output support for machine-readable logs
|
22
|
+
# - Customizable formatters and devices
|
23
|
+
#
|
24
|
+
# Usage:
|
25
|
+
# # Basic usage with file logging
|
26
|
+
# config do
|
27
|
+
# logger SmartMessage::Logger::Lumberjack.new
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# # Custom configuration
|
31
|
+
# config do
|
32
|
+
# logger SmartMessage::Logger::Lumberjack.new(
|
33
|
+
# log_file: 'custom/smart_message.log',
|
34
|
+
# level: :debug,
|
35
|
+
# format: :json, # :text or :json
|
36
|
+
# include_source: true, # Include source location
|
37
|
+
# structured_data: true, # Log structured message data
|
38
|
+
# colorize: true, # Enable colorized output (console only)
|
39
|
+
# roll_by_date: true, # Enable date-based log rolling
|
40
|
+
# max_file_size: 50 * 1024 * 1024 # Max file size before rolling (50 MB)
|
41
|
+
# )
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# # Log to STDOUT with colorized JSON format
|
45
|
+
# config do
|
46
|
+
# logger SmartMessage::Logger::Lumberjack.new(
|
47
|
+
# log_file: STDOUT,
|
48
|
+
# format: :json,
|
49
|
+
# colorize: true
|
50
|
+
# )
|
51
|
+
# end
|
52
|
+
class Lumberjack < Base
|
53
|
+
attr_reader :logger, :log_file, :level, :format, :include_source, :structured_data, :colorize
|
54
|
+
|
55
|
+
def initialize(log_file: nil, level: nil, format: :text, include_source: true, structured_data: true, colorize: false, **options)
|
56
|
+
@log_file = log_file || default_log_file
|
57
|
+
@level = level || default_log_level
|
58
|
+
@format = format
|
59
|
+
@include_source = include_source
|
60
|
+
@structured_data = structured_data
|
61
|
+
@options = options
|
62
|
+
|
63
|
+
# Set colorize after @log_file is set so console_output? works correctly
|
64
|
+
@colorize = colorize && console_output?
|
65
|
+
|
66
|
+
@logger = setup_lumberjack_logger
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
# General purpose logging methods matching Ruby's Logger interface
|
71
|
+
# These methods capture caller information and add structured data
|
72
|
+
|
73
|
+
def debug(message = nil, **structured_data, &block)
|
74
|
+
return unless message || block_given?
|
75
|
+
structured_data[:message] = message if message
|
76
|
+
log_with_caller(:debug, 1, structured_data, &block)
|
77
|
+
end
|
78
|
+
|
79
|
+
def info(message = nil, **structured_data, &block)
|
80
|
+
return unless message || block_given?
|
81
|
+
structured_data[:message] = message if message
|
82
|
+
log_with_caller(:info, 1, structured_data, &block)
|
83
|
+
end
|
84
|
+
|
85
|
+
def warn(message = nil, **structured_data, &block)
|
86
|
+
return unless message || block_given?
|
87
|
+
structured_data[:message] = message if message
|
88
|
+
log_with_caller(:warn, 1, structured_data, &block)
|
89
|
+
end
|
90
|
+
|
91
|
+
def error(message = nil, **structured_data, &block)
|
92
|
+
return unless message || block_given?
|
93
|
+
structured_data[:message] = message if message
|
94
|
+
log_with_caller(:error, 1, structured_data, &block)
|
95
|
+
end
|
96
|
+
|
97
|
+
def fatal(message = nil, **structured_data, &block)
|
98
|
+
return unless message || block_given?
|
99
|
+
structured_data[:message] = message if message
|
100
|
+
log_with_caller(:fatal, 1, structured_data, &block)
|
101
|
+
end
|
102
|
+
|
103
|
+
# Check if output is going to console (STDOUT/STDERR)
|
104
|
+
def console_output?
|
105
|
+
@log_file == STDOUT || @log_file == STDERR
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def setup_lumberjack_logger
|
111
|
+
# Create log directory if needed
|
112
|
+
if @log_file.is_a?(String) && !@log_file.start_with?('/dev/')
|
113
|
+
log_dir = File.dirname(@log_file)
|
114
|
+
FileUtils.mkdir_p(log_dir) unless Dir.exist?(log_dir)
|
115
|
+
end
|
116
|
+
|
117
|
+
# For STDOUT/STDERR, use Device::Writer with template for colorization
|
118
|
+
if console_output?
|
119
|
+
device = ::Lumberjack::Device::Writer.new(@log_file, template: create_template)
|
120
|
+
::Lumberjack::Logger.new(device, level: normalize_level(@level))
|
121
|
+
else
|
122
|
+
# Build Lumberjack options for file-based logging
|
123
|
+
lumberjack_options = {
|
124
|
+
level: normalize_level(@level),
|
125
|
+
formatter: create_formatter,
|
126
|
+
buffer_size: 0 # Disable buffering for immediate output
|
127
|
+
}
|
128
|
+
|
129
|
+
# Add log rolling options if specified
|
130
|
+
if @options[:roll_by_date]
|
131
|
+
lumberjack_options[:date_pattern] = @options[:date_pattern] || '%Y-%m-%d'
|
132
|
+
end
|
133
|
+
|
134
|
+
if @options[:roll_by_size]
|
135
|
+
lumberjack_options[:max_size] = @options[:max_file_size] || (10 * 1024 * 1024) # 10 MB
|
136
|
+
lumberjack_options[:keep] = @options[:keep_files] || 5
|
137
|
+
end
|
138
|
+
|
139
|
+
# Configure the Lumberjack logger
|
140
|
+
::Lumberjack::Logger.new(@log_file, lumberjack_options)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def create_template
|
145
|
+
# Template for Device::Writer (used for console output)
|
146
|
+
colorize_enabled = @colorize
|
147
|
+
include_source = @include_source
|
148
|
+
|
149
|
+
case @format
|
150
|
+
when :json
|
151
|
+
# JSON template - return lambda that formats as JSON
|
152
|
+
lambda do |entry|
|
153
|
+
data = {
|
154
|
+
timestamp: entry.time.strftime('%Y-%m-%d %H:%M:%S.%3N'),
|
155
|
+
level: entry.severity_label,
|
156
|
+
message: entry.message
|
157
|
+
}
|
158
|
+
|
159
|
+
# Add source location if available
|
160
|
+
if include_source && entry.tags[:source]
|
161
|
+
data[:source] = entry.tags[:source]
|
162
|
+
end
|
163
|
+
|
164
|
+
# Add any structured data
|
165
|
+
entry.tags.each do |key, value|
|
166
|
+
next if key == :source
|
167
|
+
data[key] = value
|
168
|
+
end
|
169
|
+
|
170
|
+
data.to_json + "\n"
|
171
|
+
end
|
172
|
+
else
|
173
|
+
# Text template with optional colorization
|
174
|
+
lambda do |entry|
|
175
|
+
require 'colorize' if colorize_enabled
|
176
|
+
|
177
|
+
timestamp = entry.time.strftime('%Y-%m-%d %H:%M:%S.%3N')
|
178
|
+
level = entry.severity_label.ljust(5)
|
179
|
+
source_info = include_source && entry.tags[:source] ? " #{entry.tags[:source]}" : ""
|
180
|
+
|
181
|
+
line = "[#{timestamp}] #{level} --#{source_info} : #{entry.message}"
|
182
|
+
|
183
|
+
if colorize_enabled
|
184
|
+
# Apply colorization with custom color scheme
|
185
|
+
case entry.severity_label.downcase.to_sym
|
186
|
+
when :debug
|
187
|
+
# Debug: dark green background, white foreground, bold
|
188
|
+
line.white.bold.on_green
|
189
|
+
when :info
|
190
|
+
# Info: bright white foreground (no background)
|
191
|
+
line.light_white
|
192
|
+
when :warn
|
193
|
+
# Warn: yellow background, white foreground, bold
|
194
|
+
line.white.bold.on_yellow
|
195
|
+
when :error
|
196
|
+
# Error: red background, white foreground, bold
|
197
|
+
line.white.bold.on_red
|
198
|
+
when :fatal
|
199
|
+
# Fatal: bright red background, yellow foreground, bold
|
200
|
+
line.yellow.bold.on_light_red
|
201
|
+
else
|
202
|
+
line
|
203
|
+
end
|
204
|
+
else
|
205
|
+
line
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def create_formatter
|
212
|
+
case @format
|
213
|
+
when :json
|
214
|
+
# JSON formatter with structured data support
|
215
|
+
::Lumberjack::Formatter.new do |entry|
|
216
|
+
data = {
|
217
|
+
timestamp: entry.time.strftime('%Y-%m-%d %H:%M:%S.%3N'),
|
218
|
+
level: entry.severity_label,
|
219
|
+
message: entry.message
|
220
|
+
}
|
221
|
+
|
222
|
+
# Add source location if available
|
223
|
+
if @include_source && entry.tags[:source]
|
224
|
+
data[:source] = entry.tags[:source]
|
225
|
+
end
|
226
|
+
|
227
|
+
# Add any structured data
|
228
|
+
entry.tags.each do |key, value|
|
229
|
+
next if key == :source
|
230
|
+
data[key] = value
|
231
|
+
end
|
232
|
+
|
233
|
+
data.to_json + "\n"
|
234
|
+
end
|
235
|
+
else
|
236
|
+
# Text formatter for file output (no colorization)
|
237
|
+
::Lumberjack::Formatter.new do |entry|
|
238
|
+
timestamp = entry.time.strftime('%Y-%m-%d %H:%M:%S.%3N')
|
239
|
+
level = entry.severity_label.ljust(5)
|
240
|
+
source_info = @include_source && entry.tags[:source] ? " #{entry.tags[:source]}" : ""
|
241
|
+
|
242
|
+
"[#{timestamp}] #{level} --#{source_info} : #{entry.message}\n"
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def log_with_caller(level, caller_depth, structured_data = {}, &block)
|
248
|
+
return unless @logger.send("#{level}?")
|
249
|
+
|
250
|
+
caller_info = get_caller_info(caller_depth + 1)
|
251
|
+
tags = structured_data.dup
|
252
|
+
tags[:source] = caller_info if @include_source && caller_info
|
253
|
+
|
254
|
+
if block_given?
|
255
|
+
message = block.call(caller_info)
|
256
|
+
else
|
257
|
+
message = structured_data.delete(:message) || ""
|
258
|
+
end
|
259
|
+
|
260
|
+
if tags.empty?
|
261
|
+
@logger.send(level, message)
|
262
|
+
else
|
263
|
+
@logger.tag(tags) do
|
264
|
+
@logger.send(level, message)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
|
270
|
+
def get_caller_info(depth)
|
271
|
+
return nil unless @include_source
|
272
|
+
|
273
|
+
caller_location = caller_locations(depth + 1, 1).first
|
274
|
+
return nil unless caller_location
|
275
|
+
|
276
|
+
filename = File.basename(caller_location.path)
|
277
|
+
line_number = caller_location.lineno
|
278
|
+
"#{filename}:#{line_number}"
|
279
|
+
end
|
280
|
+
|
281
|
+
def normalize_level(level)
|
282
|
+
case level
|
283
|
+
when Symbol
|
284
|
+
case level
|
285
|
+
when :debug then ::Lumberjack::Severity::DEBUG
|
286
|
+
when :info then ::Lumberjack::Severity::INFO
|
287
|
+
when :warn then ::Lumberjack::Severity::WARN
|
288
|
+
when :error then ::Lumberjack::Severity::ERROR
|
289
|
+
when :fatal then ::Lumberjack::Severity::FATAL
|
290
|
+
else ::Lumberjack::Severity::INFO
|
291
|
+
end
|
292
|
+
when Integer
|
293
|
+
level
|
294
|
+
else
|
295
|
+
::Lumberjack::Severity::INFO
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def default_log_file
|
300
|
+
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
301
|
+
Rails.root.join('log', 'smart_message.log').to_s
|
302
|
+
else
|
303
|
+
'log/smart_message.log'
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def default_log_level
|
308
|
+
if defined?(Rails) && Rails.respond_to?(:env)
|
309
|
+
case Rails.env
|
310
|
+
when 'development', 'test'
|
311
|
+
:info
|
312
|
+
when 'production'
|
313
|
+
:info
|
314
|
+
else
|
315
|
+
:info
|
316
|
+
end
|
317
|
+
else
|
318
|
+
:info
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# lib/smart_message/logger/null.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module SmartMessage
|
6
|
+
module Logger
|
7
|
+
# Null logger implementation that discards all log messages
|
8
|
+
#
|
9
|
+
# This logger provides a no-op implementation of the standard logging
|
10
|
+
# interface. All log messages are silently discarded, making it useful
|
11
|
+
# for applications that want to completely disable SmartMessage logging.
|
12
|
+
#
|
13
|
+
# Usage:
|
14
|
+
# # Disable all SmartMessage logging
|
15
|
+
# SmartMessage.configure do |config|
|
16
|
+
# config.logger = SmartMessage::Logger::Null.new
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# # Or set logger to nil (framework will use Null logger automatically)
|
20
|
+
# SmartMessage.configure do |config|
|
21
|
+
# config.logger = nil
|
22
|
+
# end
|
23
|
+
class Null < Base
|
24
|
+
def initialize
|
25
|
+
# No setup needed for null logger
|
26
|
+
end
|
27
|
+
|
28
|
+
# All logging methods are no-ops that accept any arguments
|
29
|
+
# and return nil immediately without processing
|
30
|
+
|
31
|
+
def debug(*args, &block)
|
32
|
+
# Silently discard
|
33
|
+
nil
|
34
|
+
end
|
35
|
+
|
36
|
+
def info(*args, &block)
|
37
|
+
# Silently discard
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def warn(*args, &block)
|
42
|
+
# Silently discard
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def error(*args, &block)
|
47
|
+
# Silently discard
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def fatal(*args, &block)
|
52
|
+
# Silently discard
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
|
56
|
+
# Additional methods that might be called on loggers
|
57
|
+
|
58
|
+
def level
|
59
|
+
::Logger::FATAL + 1 # Higher than FATAL to ensure nothing logs
|
60
|
+
end
|
61
|
+
|
62
|
+
def level=(value)
|
63
|
+
# Ignore level changes
|
64
|
+
end
|
65
|
+
|
66
|
+
def close
|
67
|
+
# Nothing to close
|
68
|
+
end
|
69
|
+
|
70
|
+
def respond_to_missing?(method_name, include_private = false)
|
71
|
+
# Pretend to respond to any logging method
|
72
|
+
true
|
73
|
+
end
|
74
|
+
|
75
|
+
def method_missing(method_name, *args, &block)
|
76
|
+
# Silently handle any other logging calls
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/smart_message/logger.rb
CHANGED
@@ -2,14 +2,22 @@
|
|
2
2
|
# encoding: utf-8
|
3
3
|
# frozen_string_literal: true
|
4
4
|
|
5
|
-
module SmartMessage::Logger
|
6
|
-
# Logger module provides logging capabilities for SmartMessage
|
7
|
-
# The Default logger automatically uses Rails.logger if available,
|
8
|
-
# otherwise falls back to a standard Ruby Logger
|
9
|
-
end # module SmartMessage::Logger
|
10
|
-
|
11
|
-
# Load the base class first
|
12
5
|
require_relative 'logger/base'
|
13
|
-
|
14
|
-
# Load the default logger implementation
|
15
6
|
require_relative 'logger/default'
|
7
|
+
require_relative 'logger/lumberjack'
|
8
|
+
|
9
|
+
module SmartMessage
|
10
|
+
module Logger
|
11
|
+
class << self
|
12
|
+
# Global default logger instance
|
13
|
+
def default
|
14
|
+
@default ||= Lumberjack.new
|
15
|
+
end
|
16
|
+
|
17
|
+
# Set the default logger
|
18
|
+
def default=(logger)
|
19
|
+
@default = logger
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# lib/smart_message/messaging.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module SmartMessage
|
6
|
+
# Messaging module for SmartMessage::Base
|
7
|
+
# Handles message encoding and publishing operations
|
8
|
+
module Messaging
|
9
|
+
# SMELL: How does the transport know how to decode a message before
|
10
|
+
# it knows the message class? We need a wrapper around
|
11
|
+
# the entire message in a known serialization. That
|
12
|
+
# wrapper would contain two properties: _sm_header and
|
13
|
+
# _sm_payload
|
14
|
+
|
15
|
+
# NOTE: to publish a message it must first be encoded using a
|
16
|
+
# serializer. The receive a subscribed to message it must
|
17
|
+
# be decoded via a serializer from the transport to be processed.
|
18
|
+
def encode
|
19
|
+
raise Errors::SerializerNotConfigured if serializer_missing?
|
20
|
+
|
21
|
+
serializer.encode(self)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Convert message to hash with _sm_header and _sm_payload structure
|
25
|
+
# This is the foundation for wrapper architecture
|
26
|
+
def to_h
|
27
|
+
# Update header with serializer info before converting
|
28
|
+
_sm_header.serializer = serializer.class.to_s if serializer_configured?
|
29
|
+
|
30
|
+
{
|
31
|
+
:_sm_header => header_hash_with_symbols,
|
32
|
+
:_sm_payload => payload_hash_with_symbols
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
# NOTE: you publish instances; but, you subscribe/unsubscribe at
|
38
|
+
# the class-level
|
39
|
+
def publish
|
40
|
+
begin
|
41
|
+
# Validate the complete message before publishing (now uses overridden validate!)
|
42
|
+
validate!
|
43
|
+
|
44
|
+
# Update header with current publication info
|
45
|
+
_sm_header.published_at = Time.now
|
46
|
+
_sm_header.publisher_pid = Process.pid
|
47
|
+
_sm_header.serializer = serializer.class.to_s if serializer_configured?
|
48
|
+
|
49
|
+
# Single-tier serialization: serialize entire message with designated serializer
|
50
|
+
serialized_message = encode
|
51
|
+
|
52
|
+
raise Errors::TransportNotConfigured if transport_missing?
|
53
|
+
|
54
|
+
# Transport receives the message class name (for channel routing) and serialized message
|
55
|
+
(self.class.logger || SmartMessage::Logger.default).debug { "[SmartMessage::Messaging] About to call transport.publish" }
|
56
|
+
transport.publish(_sm_header.message_class, serialized_message)
|
57
|
+
(self.class.logger || SmartMessage::Logger.default).debug { "[SmartMessage::Messaging] transport.publish completed" }
|
58
|
+
|
59
|
+
# Log the message publish
|
60
|
+
(self.class.logger || SmartMessage::Logger.default).info { "[SmartMessage] Published: #{self.class.name} via #{transport.class.name.split('::').last}" }
|
61
|
+
|
62
|
+
SS.add(_sm_header.message_class, 'publish')
|
63
|
+
SS.get(_sm_header.message_class, 'publish')
|
64
|
+
rescue => e
|
65
|
+
(self.class.logger || SmartMessage::Logger.default).error { "[SmartMessage] Error in message publishing: #{e.class.name} - #{e.message}" }
|
66
|
+
raise
|
67
|
+
end
|
68
|
+
end # def publish
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# Convert header to hash with symbol keys
|
73
|
+
def header_hash_with_symbols
|
74
|
+
_sm_header.to_hash.transform_keys(&:to_sym)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Extract all non-header properties into a hash with symbol keys
|
78
|
+
# Performs deep symbolization on nested structures
|
79
|
+
def payload_hash_with_symbols
|
80
|
+
self.class.properties.each_with_object({}) do |prop, hash|
|
81
|
+
next if prop == :_sm_header
|
82
|
+
hash[prop.to_sym] = deep_symbolize_keys(self[prop])
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Recursively convert all string keys to symbols in nested hashes and arrays
|
87
|
+
def deep_symbolize_keys(obj)
|
88
|
+
case obj
|
89
|
+
when Hash
|
90
|
+
obj.each_with_object({}) do |(key, value), result|
|
91
|
+
result[key.to_sym] = deep_symbolize_keys(value)
|
92
|
+
end
|
93
|
+
when Array
|
94
|
+
obj.map { |item| deep_symbolize_keys(item) }
|
95
|
+
else
|
96
|
+
obj
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# lib/smart_message/plugins.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module SmartMessage
|
6
|
+
# Plugin configuration module for SmartMessage::Base
|
7
|
+
# Handles transport, serializer, and logger configuration at both
|
8
|
+
# class and instance levels
|
9
|
+
module Plugins
|
10
|
+
def self.included(base)
|
11
|
+
base.extend(ClassMethods)
|
12
|
+
base.class_eval do
|
13
|
+
# Class-level plugin storage
|
14
|
+
class_variable_set(:@@transport, nil) unless class_variable_defined?(:@@transport)
|
15
|
+
class_variable_set(:@@serializer, nil) unless class_variable_defined?(:@@serializer)
|
16
|
+
class_variable_set(:@@logger, nil) unless class_variable_defined?(:@@logger)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
#########################################################
|
21
|
+
## instance-level configuration
|
22
|
+
|
23
|
+
# Configure the plugins for transport, serializer and logger
|
24
|
+
def config(&block)
|
25
|
+
instance_eval(&block) if block_given?
|
26
|
+
end
|
27
|
+
|
28
|
+
#########################################################
|
29
|
+
## instance-level transport configuration
|
30
|
+
|
31
|
+
def transport(klass_or_instance = nil)
|
32
|
+
if klass_or_instance.nil?
|
33
|
+
# Return instance transport, class transport, or global configuration
|
34
|
+
@transport || self.class.class_variable_get(:@@transport) || SmartMessage::Transport.default
|
35
|
+
else
|
36
|
+
@transport = klass_or_instance
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def transport_configured?; !transport_missing?; end
|
41
|
+
def transport_missing?
|
42
|
+
# Check if transport is explicitly configured (without fallback to defaults)
|
43
|
+
@transport.nil? &&
|
44
|
+
(self.class.class_variable_get(:@@transport) rescue nil).nil?
|
45
|
+
end
|
46
|
+
def reset_transport; @transport = nil; end
|
47
|
+
|
48
|
+
|
49
|
+
#########################################################
|
50
|
+
## instance-level serializer configuration
|
51
|
+
|
52
|
+
def serializer(klass_or_instance = nil)
|
53
|
+
if klass_or_instance.nil?
|
54
|
+
# Return instance serializer, class serializer, or global configuration
|
55
|
+
@serializer || self.class.class_variable_get(:@@serializer) || SmartMessage::Serializer.default
|
56
|
+
else
|
57
|
+
@serializer = klass_or_instance
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def serializer_configured?; !serializer_missing?; end
|
62
|
+
def serializer_missing?
|
63
|
+
# Check if serializer is explicitly configured (without fallback to defaults)
|
64
|
+
@serializer.nil? &&
|
65
|
+
(self.class.class_variable_get(:@@serializer) rescue nil).nil?
|
66
|
+
end
|
67
|
+
def reset_serializer; @serializer = nil; end
|
68
|
+
|
69
|
+
module ClassMethods
|
70
|
+
#########################################################
|
71
|
+
## class-level configuration
|
72
|
+
|
73
|
+
def config(&block)
|
74
|
+
class_eval(&block) if block_given?
|
75
|
+
end
|
76
|
+
|
77
|
+
#########################################################
|
78
|
+
## class-level transport configuration
|
79
|
+
|
80
|
+
def transport(klass_or_instance = nil)
|
81
|
+
if klass_or_instance.nil?
|
82
|
+
# Return class-level transport or fall back to global configuration
|
83
|
+
class_variable_get(:@@transport) || SmartMessage::Transport.default
|
84
|
+
else
|
85
|
+
class_variable_set(:@@transport, klass_or_instance)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def transport_configured?; !transport_missing?; end
|
90
|
+
def transport_missing?
|
91
|
+
# Check if class-level transport is explicitly configured (without fallback to defaults)
|
92
|
+
(class_variable_get(:@@transport) rescue nil).nil?
|
93
|
+
end
|
94
|
+
def reset_transport; class_variable_set(:@@transport, nil); end
|
95
|
+
|
96
|
+
#########################################################
|
97
|
+
## class-level logger configuration
|
98
|
+
|
99
|
+
def logger(klass_or_instance = nil)
|
100
|
+
if klass_or_instance.nil?
|
101
|
+
# Return class-level logger or fall back to global configuration
|
102
|
+
class_variable_get(:@@logger) || SmartMessage::Logger.default
|
103
|
+
else
|
104
|
+
class_variable_set(:@@logger, klass_or_instance)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def logger_configured?; !logger.nil?; end
|
109
|
+
def logger_missing?; logger.nil?; end
|
110
|
+
def reset_logger; class_variable_set(:@@logger, nil); end
|
111
|
+
|
112
|
+
#########################################################
|
113
|
+
## class-level serializer configuration
|
114
|
+
|
115
|
+
def serializer(klass_or_instance = nil)
|
116
|
+
if klass_or_instance.nil?
|
117
|
+
# Return class-level serializer or fall back to global configuration
|
118
|
+
class_variable_get(:@@serializer) || SmartMessage::Serializer.default
|
119
|
+
else
|
120
|
+
class_variable_set(:@@serializer, klass_or_instance)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def serializer_configured?; !serializer_missing?; end
|
125
|
+
def serializer_missing?
|
126
|
+
# Check if class-level serializer is explicitly configured (without fallback to defaults)
|
127
|
+
(class_variable_get(:@@serializer) rescue nil).nil?
|
128
|
+
end
|
129
|
+
def reset_serializer; class_variable_set(:@@serializer, nil); end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|