smart_message 0.0.1 → 0.0.3
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/CHANGELOG.md +31 -1
- data/Gemfile.lock +6 -1
- data/README.md +103 -21
- data/docs/README.md +5 -4
- data/docs/architecture.md +41 -8
- data/docs/dispatcher.md +52 -16
- data/docs/getting-started.md +64 -2
- data/docs/message_processing.md +423 -0
- data/docs/proc_handlers_summary.md +247 -0
- data/docs/properties.md +471 -0
- data/docs/transports.md +202 -8
- data/examples/04_redis_smart_home_iot.rb +649 -0
- data/examples/05_proc_handlers.rb +181 -0
- data/examples/README.md +118 -3
- data/examples/smart_home_iot_dataflow.md +257 -0
- data/lib/smart_message/base.rb +108 -4
- data/lib/smart_message/dispatcher.rb +22 -6
- data/lib/smart_message/property_descriptions.rb +41 -0
- data/lib/smart_message/transport/redis_transport.rb +190 -0
- data/lib/smart_message/transport/registry.rb +1 -0
- data/lib/smart_message/transport.rb +1 -0
- data/lib/smart_message/version.rb +1 -1
- data/smart_message.gemspec +1 -0
- metadata +23 -1
data/lib/smart_message/base.rb
CHANGED
@@ -5,6 +5,7 @@
|
|
5
5
|
require 'securerandom' # STDLIB
|
6
6
|
|
7
7
|
require_relative './wrapper.rb'
|
8
|
+
require_relative './property_descriptions.rb'
|
8
9
|
|
9
10
|
module SmartMessage
|
10
11
|
# The foundation class for the smart message
|
@@ -15,9 +16,14 @@ module SmartMessage
|
|
15
16
|
@@transport = nil
|
16
17
|
@@serializer = nil
|
17
18
|
@@logger = nil
|
19
|
+
|
20
|
+
# Registry for proc-based message handlers
|
21
|
+
@@proc_handlers = {}
|
18
22
|
|
19
23
|
include Hashie::Extensions::Dash::PropertyTranslation
|
20
24
|
|
25
|
+
include SmartMessage::PropertyDescriptions
|
26
|
+
|
21
27
|
include Hashie::Extensions::Coercion
|
22
28
|
include Hashie::Extensions::DeepMerge
|
23
29
|
include Hashie::Extensions::IgnoreUndeclared
|
@@ -162,6 +168,17 @@ module SmartMessage
|
|
162
168
|
|
163
169
|
class << self
|
164
170
|
|
171
|
+
#########################################################
|
172
|
+
## class-level description
|
173
|
+
|
174
|
+
def description(desc = nil)
|
175
|
+
if desc.nil?
|
176
|
+
@description
|
177
|
+
else
|
178
|
+
@description = desc.to_s
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
165
182
|
#########################################################
|
166
183
|
## class-level configuration
|
167
184
|
|
@@ -170,6 +187,44 @@ module SmartMessage
|
|
170
187
|
end
|
171
188
|
|
172
189
|
|
190
|
+
#########################################################
|
191
|
+
## proc handler management
|
192
|
+
|
193
|
+
# Register a proc handler and return a unique identifier for it
|
194
|
+
# @param message_class [String] The message class name
|
195
|
+
# @param handler_proc [Proc] The proc to register
|
196
|
+
# @return [String] Unique identifier for this handler
|
197
|
+
def register_proc_handler(message_class, handler_proc)
|
198
|
+
handler_id = "#{message_class}.proc_#{SecureRandom.hex(8)}"
|
199
|
+
@@proc_handlers[handler_id] = handler_proc
|
200
|
+
handler_id
|
201
|
+
end
|
202
|
+
|
203
|
+
# Call a registered proc handler
|
204
|
+
# @param handler_id [String] The handler identifier
|
205
|
+
# @param message_header [SmartMessage::Header] The message header
|
206
|
+
# @param message_payload [String] The message payload
|
207
|
+
def call_proc_handler(handler_id, message_header, message_payload)
|
208
|
+
handler_proc = @@proc_handlers[handler_id]
|
209
|
+
return unless handler_proc
|
210
|
+
|
211
|
+
handler_proc.call(message_header, message_payload)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Remove a proc handler from the registry
|
215
|
+
# @param handler_id [String] The handler identifier to remove
|
216
|
+
def unregister_proc_handler(handler_id)
|
217
|
+
@@proc_handlers.delete(handler_id)
|
218
|
+
end
|
219
|
+
|
220
|
+
# Check if a handler ID refers to a proc handler
|
221
|
+
# @param handler_id [String] The handler identifier
|
222
|
+
# @return [Boolean] True if this is a proc handler
|
223
|
+
def proc_handler?(handler_id)
|
224
|
+
@@proc_handlers.key?(handler_id)
|
225
|
+
end
|
226
|
+
|
227
|
+
|
173
228
|
#########################################################
|
174
229
|
## class-level transport configuration
|
175
230
|
|
@@ -212,25 +267,74 @@ module SmartMessage
|
|
212
267
|
# Add this message class to the transport's catalog of
|
213
268
|
# subscribed messages. If the transport is missing, raise
|
214
269
|
# an exception.
|
215
|
-
|
216
|
-
|
217
|
-
|
270
|
+
#
|
271
|
+
# @param process_method [String, Proc, nil] The processing method:
|
272
|
+
# - String: Method name like "MyService.handle_message"
|
273
|
+
# - Proc: A proc/lambda that accepts (message_header, message_payload)
|
274
|
+
# - nil: Uses default "MessageClass.process" method
|
275
|
+
# @param block [Proc] Alternative way to pass a processing block
|
276
|
+
# @return [String] The identifier used for this subscription
|
277
|
+
#
|
278
|
+
# @example Using default handler
|
279
|
+
# MyMessage.subscribe
|
280
|
+
#
|
281
|
+
# @example Using custom method name
|
282
|
+
# MyMessage.subscribe("MyService.handle_message")
|
283
|
+
#
|
284
|
+
# @example Using a block
|
285
|
+
# MyMessage.subscribe do |header, payload|
|
286
|
+
# data = JSON.parse(payload)
|
287
|
+
# puts "Received: #{data}"
|
288
|
+
# end
|
289
|
+
#
|
290
|
+
# @example Using a proc
|
291
|
+
# handler = proc { |header, payload| puts "Processing..." }
|
292
|
+
# MyMessage.subscribe(handler)
|
293
|
+
def subscribe(process_method = nil, &block)
|
294
|
+
message_class = whoami
|
295
|
+
|
296
|
+
# Handle different parameter types
|
297
|
+
if block_given?
|
298
|
+
# Block was passed - use it as the handler
|
299
|
+
handler_proc = block
|
300
|
+
process_method = register_proc_handler(message_class, handler_proc)
|
301
|
+
elsif process_method.respond_to?(:call)
|
302
|
+
# Proc/lambda was passed as first parameter
|
303
|
+
handler_proc = process_method
|
304
|
+
process_method = register_proc_handler(message_class, handler_proc)
|
305
|
+
elsif process_method.nil?
|
306
|
+
# Use default handler
|
307
|
+
process_method = message_class + '.process'
|
308
|
+
end
|
309
|
+
# If process_method is a String, use it as-is
|
218
310
|
|
219
311
|
# TODO: Add proper logging here
|
220
312
|
|
221
313
|
raise Errors::TransportNotConfigured if transport_missing?
|
222
314
|
transport.subscribe(message_class, process_method)
|
315
|
+
|
316
|
+
process_method
|
223
317
|
end
|
224
318
|
|
225
319
|
|
226
320
|
# Remove this process_method for this message class from the
|
227
321
|
# subscribers list.
|
322
|
+
# @param process_method [String, nil] The processing method identifier to remove
|
323
|
+
# - String: Method name like "MyService.handle_message" or proc handler ID
|
324
|
+
# - nil: Uses default "MessageClass.process" method
|
228
325
|
def unsubscribe(process_method = nil)
|
229
326
|
message_class = whoami
|
230
327
|
process_method = message_class + '.process' if process_method.nil?
|
231
328
|
# TODO: Add proper logging here
|
232
329
|
|
233
|
-
|
330
|
+
if transport_configured?
|
331
|
+
transport.unsubscribe(message_class, process_method)
|
332
|
+
|
333
|
+
# If this was a proc handler, clean it up from the registry
|
334
|
+
if proc_handler?(process_method)
|
335
|
+
unregister_proc_handler(process_method)
|
336
|
+
end
|
337
|
+
end
|
234
338
|
end
|
235
339
|
|
236
340
|
|
@@ -118,13 +118,20 @@ module SmartMessage
|
|
118
118
|
@subscribers[message_klass].each do |message_processor|
|
119
119
|
SS.add(message_klass, message_processor, 'routed' )
|
120
120
|
@router_pool.post do
|
121
|
-
parts = message_processor.split('.')
|
122
|
-
target_klass = parts[0]
|
123
|
-
class_method = parts[1]
|
124
121
|
begin
|
125
|
-
|
126
|
-
|
127
|
-
|
122
|
+
# Check if this is a proc handler or a regular method call
|
123
|
+
if proc_handler?(message_processor)
|
124
|
+
# Call the proc handler via SmartMessage::Base
|
125
|
+
SmartMessage::Base.call_proc_handler(message_processor, message_header, message_payload)
|
126
|
+
else
|
127
|
+
# Original method call logic
|
128
|
+
parts = message_processor.split('.')
|
129
|
+
target_klass = parts[0]
|
130
|
+
class_method = parts[1]
|
131
|
+
target_klass.constantize
|
132
|
+
.method(class_method)
|
133
|
+
.call(message_header, message_payload)
|
134
|
+
end
|
128
135
|
rescue Exception => e
|
129
136
|
# TODO: Add proper exception logging
|
130
137
|
# Exception details: #{e.message}
|
@@ -135,6 +142,15 @@ module SmartMessage
|
|
135
142
|
end
|
136
143
|
end
|
137
144
|
|
145
|
+
private
|
146
|
+
|
147
|
+
# Check if a message processor is a proc handler
|
148
|
+
# @param message_processor [String] The message processor identifier
|
149
|
+
# @return [Boolean] True if this is a proc handler
|
150
|
+
def proc_handler?(message_processor)
|
151
|
+
SmartMessage::Base.proc_handler?(message_processor)
|
152
|
+
end
|
153
|
+
|
138
154
|
|
139
155
|
#######################################################
|
140
156
|
## Class methods
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# lib/smart_message/property_descriptions.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module SmartMessage
|
6
|
+
module PropertyDescriptions
|
7
|
+
def self.included(base)
|
8
|
+
base.extend(ClassMethods)
|
9
|
+
base.class_eval do
|
10
|
+
@property_descriptions = {}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module ClassMethods
|
15
|
+
def property(property_name, options = {})
|
16
|
+
description = options.delete(:description)
|
17
|
+
|
18
|
+
# Store description if provided
|
19
|
+
if description
|
20
|
+
@property_descriptions ||= {}
|
21
|
+
@property_descriptions[property_name.to_sym] = description
|
22
|
+
end
|
23
|
+
|
24
|
+
# Call original property method
|
25
|
+
super(property_name, options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def property_description(property_name)
|
29
|
+
@property_descriptions&.[](property_name.to_sym)
|
30
|
+
end
|
31
|
+
|
32
|
+
def property_descriptions
|
33
|
+
@property_descriptions&.dup || {}
|
34
|
+
end
|
35
|
+
|
36
|
+
def described_properties
|
37
|
+
@property_descriptions&.keys || []
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
# lib/smart_message/transport/redis_transport.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require 'redis'
|
6
|
+
require 'securerandom'
|
7
|
+
require 'set'
|
8
|
+
|
9
|
+
module SmartMessage
|
10
|
+
module Transport
|
11
|
+
# Redis pub/sub transport for SmartMessage
|
12
|
+
# Uses message class name as the Redis channel name
|
13
|
+
class RedisTransport < Base
|
14
|
+
attr_reader :redis_pub, :redis_sub, :subscriber_thread
|
15
|
+
|
16
|
+
def default_options
|
17
|
+
{
|
18
|
+
url: 'redis://localhost:6379',
|
19
|
+
db: 0,
|
20
|
+
auto_subscribe: true,
|
21
|
+
reconnect_attempts: 5,
|
22
|
+
reconnect_delay: 1
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def configure
|
27
|
+
@redis_pub = Redis.new(url: @options[:url], db: @options[:db])
|
28
|
+
@redis_sub = Redis.new(url: @options[:url], db: @options[:db])
|
29
|
+
@subscribed_channels = Set.new
|
30
|
+
@subscriber_thread = nil
|
31
|
+
@running = false
|
32
|
+
@mutex = Mutex.new
|
33
|
+
|
34
|
+
start_subscriber if @options[:auto_subscribe]
|
35
|
+
end
|
36
|
+
|
37
|
+
# Publish message to Redis channel using message class name
|
38
|
+
def publish(message_header, message_payload)
|
39
|
+
channel = message_header.message_class
|
40
|
+
|
41
|
+
begin
|
42
|
+
@redis_pub.publish(channel, message_payload)
|
43
|
+
rescue Redis::ConnectionError
|
44
|
+
retry_with_reconnect('publish') { @redis_pub.publish(channel, message_payload) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Subscribe to a message class (Redis channel)
|
49
|
+
def subscribe(message_class, process_method)
|
50
|
+
super(message_class, process_method)
|
51
|
+
|
52
|
+
@mutex.synchronize do
|
53
|
+
@subscribed_channels.add(message_class)
|
54
|
+
restart_subscriber if @running
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Unsubscribe from a specific message class and process method
|
59
|
+
def unsubscribe(message_class, process_method)
|
60
|
+
super(message_class, process_method)
|
61
|
+
|
62
|
+
@mutex.synchronize do
|
63
|
+
# If no more subscribers for this message class, unsubscribe from channel
|
64
|
+
if @dispatcher.subscribers[message_class].nil? || @dispatcher.subscribers[message_class].empty?
|
65
|
+
@subscribed_channels.delete(message_class)
|
66
|
+
restart_subscriber if @running
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Unsubscribe from all process methods for a message class
|
72
|
+
def unsubscribe!(message_class)
|
73
|
+
super(message_class)
|
74
|
+
|
75
|
+
@mutex.synchronize do
|
76
|
+
@subscribed_channels.delete(message_class)
|
77
|
+
restart_subscriber if @running
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def connected?
|
82
|
+
begin
|
83
|
+
@redis_pub.ping == 'PONG' && @redis_sub.ping == 'PONG'
|
84
|
+
rescue Redis::ConnectionError
|
85
|
+
false
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def connect
|
90
|
+
@redis_pub.ping
|
91
|
+
@redis_sub.ping
|
92
|
+
start_subscriber unless @running
|
93
|
+
end
|
94
|
+
|
95
|
+
def disconnect
|
96
|
+
stop_subscriber
|
97
|
+
@redis_pub.quit if @redis_pub
|
98
|
+
@redis_sub.quit if @redis_sub
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def start_subscriber
|
104
|
+
return if @running
|
105
|
+
|
106
|
+
@running = true
|
107
|
+
@subscriber_thread = Thread.new do
|
108
|
+
begin
|
109
|
+
subscribe_to_channels
|
110
|
+
rescue => e
|
111
|
+
# Log error but don't crash the thread
|
112
|
+
puts "Redis subscriber error: #{e.message}" if @options[:debug]
|
113
|
+
retry_subscriber
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def stop_subscriber
|
119
|
+
@running = false
|
120
|
+
|
121
|
+
if @subscriber_thread
|
122
|
+
@subscriber_thread.kill
|
123
|
+
@subscriber_thread.join(5) # Wait up to 5 seconds
|
124
|
+
@subscriber_thread = nil
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def restart_subscriber
|
129
|
+
stop_subscriber
|
130
|
+
start_subscriber if @subscribed_channels.any?
|
131
|
+
end
|
132
|
+
|
133
|
+
def subscribe_to_channels
|
134
|
+
return unless @subscribed_channels.any?
|
135
|
+
|
136
|
+
begin
|
137
|
+
@redis_sub.subscribe(*@subscribed_channels) do |on|
|
138
|
+
on.message do |channel, message_payload|
|
139
|
+
# Create a header with the channel as message_class
|
140
|
+
message_header = SmartMessage::Header.new(
|
141
|
+
message_class: channel,
|
142
|
+
uuid: SecureRandom.uuid,
|
143
|
+
published_at: Time.now,
|
144
|
+
publisher_pid: 'redis_subscriber'
|
145
|
+
)
|
146
|
+
|
147
|
+
receive(message_header, message_payload)
|
148
|
+
end
|
149
|
+
|
150
|
+
on.subscribe do |channel, subscriptions|
|
151
|
+
puts "Subscribed to Redis channel: #{channel} (#{subscriptions} total)" if @options[:debug]
|
152
|
+
end
|
153
|
+
|
154
|
+
on.unsubscribe do |channel, subscriptions|
|
155
|
+
puts "Unsubscribed from Redis channel: #{channel} (#{subscriptions} total)" if @options[:debug]
|
156
|
+
end
|
157
|
+
end
|
158
|
+
rescue => e
|
159
|
+
# Silently handle connection errors during subscription
|
160
|
+
puts "Redis subscription error: #{e.class.name}" if @options[:debug]
|
161
|
+
retry_subscriber if @running
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def retry_subscriber
|
166
|
+
return unless @running
|
167
|
+
|
168
|
+
sleep(@options[:reconnect_delay])
|
169
|
+
subscribe_to_channels if @running
|
170
|
+
end
|
171
|
+
|
172
|
+
def retry_with_reconnect(operation)
|
173
|
+
attempts = 0
|
174
|
+
begin
|
175
|
+
yield
|
176
|
+
rescue Redis::ConnectionError => e
|
177
|
+
attempts += 1
|
178
|
+
if attempts <= @options[:reconnect_attempts]
|
179
|
+
sleep(@options[:reconnect_delay])
|
180
|
+
# Reconnect
|
181
|
+
@redis_pub = Redis.new(url: @options[:url], db: @options[:db])
|
182
|
+
retry
|
183
|
+
else
|
184
|
+
raise e
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
@@ -6,6 +6,7 @@ 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'
|
9
10
|
|
10
11
|
module SmartMessage
|
11
12
|
# Transport layer abstraction for SmartMessage
|
data/smart_message.gemspec
CHANGED
@@ -39,6 +39,7 @@ Gem::Specification.new do |spec|
|
|
39
39
|
spec.add_dependency 'hashie'
|
40
40
|
spec.add_dependency 'activesupport'
|
41
41
|
spec.add_dependency 'concurrent-ruby'
|
42
|
+
spec.add_dependency 'redis'
|
42
43
|
|
43
44
|
spec.add_development_dependency 'bundler'
|
44
45
|
spec.add_development_dependency 'rake'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smart_message
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dewayne VanHoozer
|
@@ -51,6 +51,20 @@ dependencies:
|
|
51
51
|
- - ">="
|
52
52
|
- !ruby/object:Gem::Version
|
53
53
|
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: redis
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
54
68
|
- !ruby/object:Gem::Dependency
|
55
69
|
name: bundler
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
@@ -190,13 +204,19 @@ files:
|
|
190
204
|
- docs/examples.md
|
191
205
|
- docs/getting-started.md
|
192
206
|
- docs/ideas_to_think_about.md
|
207
|
+
- docs/message_processing.md
|
208
|
+
- docs/proc_handlers_summary.md
|
209
|
+
- docs/properties.md
|
193
210
|
- docs/serializers.md
|
194
211
|
- docs/transports.md
|
195
212
|
- docs/troubleshooting.md
|
196
213
|
- examples/01_point_to_point_orders.rb
|
197
214
|
- examples/02_publish_subscribe_events.rb
|
198
215
|
- examples/03_many_to_many_chat.rb
|
216
|
+
- examples/04_redis_smart_home_iot.rb
|
217
|
+
- examples/05_proc_handlers.rb
|
199
218
|
- examples/README.md
|
219
|
+
- examples/smart_home_iot_dataflow.md
|
200
220
|
- examples/tmux_chat/README.md
|
201
221
|
- examples/tmux_chat/bot_agent.rb
|
202
222
|
- examples/tmux_chat/human_agent.rb
|
@@ -213,12 +233,14 @@ files:
|
|
213
233
|
- lib/smart_message/header.rb
|
214
234
|
- lib/smart_message/logger.rb
|
215
235
|
- lib/smart_message/logger/base.rb
|
236
|
+
- lib/smart_message/property_descriptions.rb
|
216
237
|
- lib/smart_message/serializer.rb
|
217
238
|
- lib/smart_message/serializer/base.rb
|
218
239
|
- lib/smart_message/serializer/json.rb
|
219
240
|
- lib/smart_message/transport.rb
|
220
241
|
- lib/smart_message/transport/base.rb
|
221
242
|
- lib/smart_message/transport/memory_transport.rb
|
243
|
+
- lib/smart_message/transport/redis_transport.rb
|
222
244
|
- lib/smart_message/transport/registry.rb
|
223
245
|
- lib/smart_message/transport/stdout_transport.rb
|
224
246
|
- lib/smart_message/version.rb
|