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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.irbrc +24 -0
  4. data/CHANGELOG.md +143 -0
  5. data/Gemfile.lock +6 -1
  6. data/README.md +289 -15
  7. data/docs/README.md +3 -1
  8. data/docs/addressing.md +119 -13
  9. data/docs/architecture.md +68 -0
  10. data/docs/dead_letter_queue.md +673 -0
  11. data/docs/dispatcher.md +87 -0
  12. data/docs/examples.md +59 -1
  13. data/docs/getting-started.md +8 -1
  14. data/docs/logging.md +382 -326
  15. data/docs/message_filtering.md +451 -0
  16. data/examples/01_point_to_point_orders.rb +54 -53
  17. data/examples/02_publish_subscribe_events.rb +14 -10
  18. data/examples/03_many_to_many_chat.rb +16 -8
  19. data/examples/04_redis_smart_home_iot.rb +20 -10
  20. data/examples/05_proc_handlers.rb +12 -11
  21. data/examples/06_custom_logger_example.rb +95 -100
  22. data/examples/07_error_handling_scenarios.rb +4 -2
  23. data/examples/08_entity_addressing_basic.rb +18 -6
  24. data/examples/08_entity_addressing_with_filtering.rb +27 -9
  25. data/examples/09_dead_letter_queue_demo.rb +559 -0
  26. data/examples/09_regex_filtering_microservices.rb +407 -0
  27. data/examples/10_header_block_configuration.rb +263 -0
  28. data/examples/11_global_configuration_example.rb +219 -0
  29. data/examples/README.md +102 -0
  30. data/examples/dead_letters.jsonl +12 -0
  31. data/examples/performance_metrics/benchmark_results_ractor_20250818_205603.json +135 -0
  32. data/examples/performance_metrics/benchmark_results_ractor_20250818_205831.json +135 -0
  33. data/examples/performance_metrics/benchmark_results_test_20250818_204942.json +130 -0
  34. data/examples/performance_metrics/benchmark_results_threadpool_20250818_204942.json +130 -0
  35. data/examples/performance_metrics/benchmark_results_threadpool_20250818_204959.json +130 -0
  36. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205044.json +130 -0
  37. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205109.json +130 -0
  38. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205252.json +130 -0
  39. data/examples/performance_metrics/benchmark_results_unknown_20250819_172852.json +130 -0
  40. data/examples/performance_metrics/compare_benchmarks.rb +519 -0
  41. data/examples/performance_metrics/dead_letters.jsonl +3100 -0
  42. data/examples/performance_metrics/performance_benchmark.rb +344 -0
  43. data/examples/show_logger.rb +367 -0
  44. data/examples/show_me.rb +145 -0
  45. data/examples/temp.txt +94 -0
  46. data/examples/tmux_chat/bot_agent.rb +4 -2
  47. data/examples/tmux_chat/human_agent.rb +4 -2
  48. data/examples/tmux_chat/room_monitor.rb +4 -2
  49. data/examples/tmux_chat/shared_chat_system.rb +6 -3
  50. data/lib/smart_message/addressing.rb +259 -0
  51. data/lib/smart_message/base.rb +121 -599
  52. data/lib/smart_message/circuit_breaker.rb +23 -6
  53. data/lib/smart_message/configuration.rb +199 -0
  54. data/lib/smart_message/dead_letter_queue.rb +361 -0
  55. data/lib/smart_message/dispatcher.rb +90 -49
  56. data/lib/smart_message/header.rb +5 -0
  57. data/lib/smart_message/logger/base.rb +21 -1
  58. data/lib/smart_message/logger/default.rb +88 -138
  59. data/lib/smart_message/logger/lumberjack.rb +324 -0
  60. data/lib/smart_message/logger/null.rb +81 -0
  61. data/lib/smart_message/logger.rb +17 -9
  62. data/lib/smart_message/messaging.rb +100 -0
  63. data/lib/smart_message/plugins.rb +132 -0
  64. data/lib/smart_message/serializer/base.rb +25 -8
  65. data/lib/smart_message/serializer/json.rb +5 -4
  66. data/lib/smart_message/subscription.rb +193 -0
  67. data/lib/smart_message/transport/base.rb +84 -53
  68. data/lib/smart_message/transport/memory_transport.rb +7 -5
  69. data/lib/smart_message/transport/redis_transport.rb +15 -45
  70. data/lib/smart_message/transport/stdout_transport.rb +18 -8
  71. data/lib/smart_message/transport.rb +1 -34
  72. data/lib/smart_message/utilities.rb +142 -0
  73. data/lib/smart_message/version.rb +1 -1
  74. data/lib/smart_message/versioning.rb +85 -0
  75. data/lib/smart_message/wrapper.rb.bak +132 -0
  76. data/lib/smart_message.rb +74 -27
  77. data/smart_message.gemspec +3 -0
  78. metadata +77 -3
  79. data/lib/smart_message/serializer.rb +0 -10
  80. 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
@@ -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