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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.irbrc +24 -0
  4. data/CHANGELOG.md +96 -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 +2 -1
  53. data/lib/smart_message/configuration.rb +199 -0
  54. data/lib/smart_message/dead_letter_queue.rb +27 -10
  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 +72 -41
  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 -28
  77. data/smart_message.gemspec +3 -0
  78. metadata +76 -3
  79. data/lib/smart_message/serializer.rb +0 -10
  80. 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 message_header [SmartMessage::Header] Message routing information
35
- # @param message_payload [String] Serialized message content
36
- def publish(message_header, message_payload)
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(message_header, message_payload)
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, message_header, message_payload)
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 message_header [SmartMessage::Header] Message routing information
50
- # @param message_payload [String] Serialized message content
51
- def do_publish(message_header, message_payload)
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 message_header [SmartMessage::Header] Message routing information
139
- # @param message_payload [String] Serialized message content
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(message_header, message_payload)
143
- @dispatcher.route(message_header, message_payload)
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 message_header [SmartMessage::Header] The message header
193
- # @param message_payload [String] The message payload
194
- def handle_publish_fallback(fallback_result, message_header, message_payload)
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
- if $DEBUG
197
- puts "Transport publish circuit breaker activated: #{self.class.name}"
198
- puts "Error: #{fallback_result[:circuit_breaker][:error]}"
199
- puts "Message: #{message_header.message_class}"
200
- puts "Sent to DLQ: #{fallback_result[:circuit_breaker][:sent_to_dlq]}"
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
- message_header,
208
- message_payload,
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
- puts "Warning: Failed to store message in DLQ: #{dlq_error.message}" if $DEBUG
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(message_header, message_payload)
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
- header: message_header,
32
- payload: message_payload,
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
- receive(message_header, message_payload) if @options[:auto_process]
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[:header], msg[:payload])
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(message_header, message_payload)
40
- channel = message_header.message_class
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, redis_message)
43
+ @redis_pub.publish(channel, serialized_message)
51
44
  rescue Redis::ConnectionError
52
- retry_with_reconnect('publish') { @redis_pub.publish(channel, redis_message) }
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
- puts "Redis subscriber error: #{e.message}" if @options[:debug]
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, redis_message|
139
+ on.message do |channel, serialized_message|
147
140
  begin
148
- # Parse the Redis message to extract header and payload
149
- parsed_message = JSON.parse(redis_message)
150
-
151
- if parsed_message.is_a?(Hash) && parsed_message.has_key?('header') && parsed_message.has_key?('payload')
152
- # Reconstruct the original header from the parsed data
153
- header_data = parsed_message['header']
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
- puts "Subscribed to Redis channel: #{channel} (#{subscriptions} total)" if @options[:debug]
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
- puts "Unsubscribed from Redis channel: #{channel} (#{subscriptions} total)" if @options[:debug]
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
- puts "Redis subscription error: #{e.class.name}" if @options[:debug]
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(message_header, message_payload)
32
- @output.puts format_message(message_header, message_payload)
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
- receive(message_header, message_payload) if loopback?
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(message_header, message_payload)
58
+ def format_message(message_class, serialized_message)
50
59
  <<~MESSAGE
51
60
 
52
61
  ===================================================
53
62
  == SmartMessage Published via STDOUT Transport
54
- == Header: #{message_header.inspect}
55
- == Payload: #{message_payload}
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
@@ -3,5 +3,5 @@
3
3
  # frozen_string_literal: true
4
4
 
5
5
  module SmartMessage
6
- VERSION = '0.0.8'
6
+ VERSION = '0.0.9'
7
7
  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