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
@@ -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.7'
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
@@ -0,0 +1,132 @@
1
+ # lib/smart_message/wrapper.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require 'securerandom' # STDLIB
6
+ require_relative './header.rb'
7
+
8
+ module SmartMessage
9
+ module Wrapper
10
+ # Every smart message has a common wrapper format that contains
11
+ # information used to support the dispatching of subscribed
12
+ # messages upon receipt from a transport as well as the serialized
13
+ # payload.
14
+ #
15
+ # The wrapper consolidates header and payload into a single object
16
+ # for cleaner method signatures throughout the SmartMessage dataflow.
17
+ class Base < Hashie::Dash
18
+ include Hashie::Extensions::IndifferentAccess
19
+ include Hashie::Extensions::MethodAccess
20
+ include Hashie::Extensions::DeepMerge
21
+
22
+ # Core wrapper properties
23
+ # Using '_sm_' prefix to avoid collision with user message definitions
24
+ property :_sm_header,
25
+ required: true,
26
+ description: "SmartMessage header containing routing and metadata information"
27
+
28
+ property :_sm_payload,
29
+ required: true,
30
+ description: "Serialized message payload containing the business data"
31
+
32
+ # Create wrapper from header and payload
33
+ def initialize(header: nil, payload: nil, **props, &block)
34
+ # Handle different initialization patterns
35
+ if header && payload
36
+ attributes = {
37
+ _sm_header: header,
38
+ _sm_payload: payload
39
+ }
40
+ else
41
+ # Create default header if not provided
42
+ default_header = SmartMessage::Header.new(
43
+ uuid: SecureRandom.uuid,
44
+ message_class: 'SmartMessage::Wrapper::Base',
45
+ published_at: Time.now,
46
+ publisher_pid: Process.pid,
47
+ version: 1
48
+ )
49
+
50
+ attributes = {
51
+ _sm_header: default_header,
52
+ _sm_payload: nil
53
+ }.merge(props)
54
+ end
55
+
56
+ super(attributes, &block)
57
+ end
58
+
59
+ # Convenience accessors for header and payload
60
+ def header
61
+ _sm_header
62
+ end
63
+
64
+ def payload
65
+ _sm_payload
66
+ end
67
+
68
+ # Check if this is a broadcast message (to field is nil)
69
+ def broadcast?
70
+ _sm_header.to.nil?
71
+ end
72
+
73
+ # Check if this is a directed message (to field is present)
74
+ def directed?
75
+ !broadcast?
76
+ end
77
+
78
+ # Get message class from header
79
+ def message_class
80
+ _sm_header.message_class
81
+ end
82
+
83
+ # Get sender from header
84
+ def from
85
+ _sm_header.from
86
+ end
87
+
88
+ # Get recipient from header
89
+ def to
90
+ _sm_header.to
91
+ end
92
+
93
+ # Get reply destination from header
94
+ def reply_to
95
+ _sm_header.reply_to
96
+ end
97
+
98
+ # Get message version from header
99
+ def version
100
+ _sm_header.version
101
+ end
102
+
103
+ # Get UUID from header
104
+ def uuid
105
+ _sm_header.uuid
106
+ end
107
+
108
+ # Convert wrapper to hash for serialization/transport
109
+ def to_hash
110
+ {
111
+ '_sm_header' => _sm_header.to_hash,
112
+ '_sm_payload' => _sm_payload
113
+ }
114
+ end
115
+
116
+ alias_method :to_h, :to_hash
117
+
118
+ # Outer-level JSON serialization for the wrapper
119
+ # This is level 2 serialization - always JSON for routing/monitoring
120
+ def to_json(*args)
121
+ require 'json'
122
+ to_hash.to_json(*args)
123
+ end
124
+
125
+ # Split wrapper into header and payload components
126
+ # Enables destructuring assignment: header, payload = wrapper.split
127
+ def split
128
+ [_sm_header, _sm_payload]
129
+ end
130
+ end
131
+ end
132
+ end
data/lib/smart_message.rb CHANGED
@@ -2,6 +2,8 @@
2
2
  # encoding: utf-8
3
3
  # frozen_string_literal: true
4
4
 
5
+ require 'zeitwerk'
6
+
5
7
  # FIXME: handle this better
6
8
  class MilClass # IMO nil is the same as an empty String
7
9
  def to_s
@@ -9,47 +11,92 @@ class MilClass # IMO nil is the same as an empty String
9
11
  end
10
12
  end
11
13
 
12
-
13
14
  require 'active_support/core_ext/string/inflections'
14
15
  require 'date' # STDLIB
15
-
16
- # Production logging should use the logger framework, not debug_me
17
-
18
16
  require 'hashie' # Your friendly neighborhood hash library.
19
17
 
20
- require_relative './simple_stats'
21
-
22
- require_relative './smart_message/version'
23
- require_relative './smart_message/errors'
24
- require_relative './smart_message/circuit_breaker'
18
+ # Set up Zeitwerk autoloader
19
+ loader = Zeitwerk::Loader.for_gem
20
+ loader.tag = "smart_message"
21
+ loader.ignore("#{__dir__}/simple_stats.rb")
22
+ loader.setup
25
23
 
26
- require_relative './smart_message/dispatcher.rb'
27
- require_relative './smart_message/transport.rb'
28
- require_relative './smart_message/base.rb'
24
+ # Load simple_stats manually since it's not following the naming convention
25
+ require_relative './simple_stats'
29
26
 
30
27
  # SmartMessage abstracts messages from the backend transport process
31
28
  module SmartMessage
32
- # The super class of all smart messages
33
- # class Base < Dash from the Hashie gem plus mixins
34
- # end
35
-
36
- # encapsulates the message transport plugin
37
- # module Transport is defined in transport.rb
38
-
39
- # encapsulates the message code/decode serializer
29
+ class << self
30
+ # Global configuration for SmartMessage
31
+ #
32
+ # Usage:
33
+ # SmartMessage.configure do |config|
34
+ # config.logger = MyApp::Logger.new
35
+ # config.transport = MyApp::Transport.new
36
+ # config.serializer = MyApp::Serializer.new
37
+ # end
38
+ def configure
39
+ yield(configuration)
40
+ end
41
+
42
+ # Get the global configuration instance
43
+ def configuration
44
+ @configuration ||= Configuration.new
45
+ end
46
+
47
+ # Reset global configuration to defaults
48
+ def reset_configuration!
49
+ @configuration = Configuration.new
50
+ end
51
+ end
52
+ # Module definitions for Zeitwerk to populate
40
53
  module Serializer
41
- # the super class of the message serializer
42
- class Base
54
+ class << self
55
+ def default
56
+ # Check global configuration first, then fall back to framework default
57
+ SmartMessage.configuration.default_serializer
58
+ end
43
59
  end
44
60
  end
45
61
 
46
- # encapsulates the message logging capability
47
62
  module Logger
48
- # the super class of the message logger
49
- class Base
63
+ class << self
64
+ def default
65
+ # Check global configuration first, then fall back to framework default
66
+ SmartMessage.configuration.default_logger
67
+ end
68
+ end
69
+ end
70
+
71
+ module Transport
72
+ class << self
73
+ def default
74
+ # Check global configuration first, then fall back to framework default
75
+ SmartMessage.configuration.default_transport
76
+ end
77
+
78
+ def registry
79
+ @registry ||= Registry.new
80
+ end
81
+
82
+ def register(name, transport_class)
83
+ registry.register(name, transport_class)
84
+ end
85
+
86
+ def get(name)
87
+ registry.get(name)
88
+ end
89
+
90
+ def create(name, **options)
91
+ transport_class = get(name)
92
+ transport_class&.new(**options)
93
+ end
94
+
95
+ def available
96
+ registry.list
97
+ end
50
98
  end
51
99
  end
52
100
  end # module SmartMessage
53
101
 
54
- require_relative './smart_message/serializer'
55
- require_relative './smart_message/logger'
102
+ # Don't eager load initially - let Zeitwerk handle lazy loading
@@ -41,6 +41,9 @@ Gem::Specification.new do |spec|
41
41
  spec.add_dependency 'concurrent-ruby'
42
42
  spec.add_dependency 'redis'
43
43
  spec.add_dependency 'breaker_machines'
44
+ spec.add_dependency 'zeitwerk'
45
+ spec.add_dependency 'lumberjack'
46
+ spec.add_dependency 'colorize'
44
47
 
45
48
  spec.add_development_dependency 'bundler'
46
49
  spec.add_development_dependency 'rake'