smart_message 0.0.1

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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +3 -0
  3. data/.gitignore +8 -0
  4. data/.travis.yml +7 -0
  5. data/CHANGELOG.md +100 -0
  6. data/COMMITS.md +196 -0
  7. data/Gemfile +6 -0
  8. data/Gemfile.lock +71 -0
  9. data/README.md +303 -0
  10. data/Rakefile +10 -0
  11. data/bin/console +14 -0
  12. data/bin/setup +8 -0
  13. data/docs/README.md +52 -0
  14. data/docs/architecture.md +370 -0
  15. data/docs/dispatcher.md +593 -0
  16. data/docs/examples.md +808 -0
  17. data/docs/getting-started.md +235 -0
  18. data/docs/ideas_to_think_about.md +329 -0
  19. data/docs/serializers.md +575 -0
  20. data/docs/transports.md +501 -0
  21. data/docs/troubleshooting.md +582 -0
  22. data/examples/01_point_to_point_orders.rb +200 -0
  23. data/examples/02_publish_subscribe_events.rb +364 -0
  24. data/examples/03_many_to_many_chat.rb +608 -0
  25. data/examples/README.md +335 -0
  26. data/examples/tmux_chat/README.md +283 -0
  27. data/examples/tmux_chat/bot_agent.rb +272 -0
  28. data/examples/tmux_chat/human_agent.rb +197 -0
  29. data/examples/tmux_chat/room_monitor.rb +158 -0
  30. data/examples/tmux_chat/shared_chat_system.rb +295 -0
  31. data/examples/tmux_chat/start_chat_demo.sh +190 -0
  32. data/examples/tmux_chat/stop_chat_demo.sh +22 -0
  33. data/lib/simple_stats.rb +57 -0
  34. data/lib/smart_message/base.rb +284 -0
  35. data/lib/smart_message/dispatcher/.keep +0 -0
  36. data/lib/smart_message/dispatcher.rb +146 -0
  37. data/lib/smart_message/errors.rb +29 -0
  38. data/lib/smart_message/header.rb +20 -0
  39. data/lib/smart_message/logger/base.rb +8 -0
  40. data/lib/smart_message/logger.rb +7 -0
  41. data/lib/smart_message/serializer/base.rb +23 -0
  42. data/lib/smart_message/serializer/json.rb +22 -0
  43. data/lib/smart_message/serializer.rb +10 -0
  44. data/lib/smart_message/transport/base.rb +85 -0
  45. data/lib/smart_message/transport/memory_transport.rb +69 -0
  46. data/lib/smart_message/transport/registry.rb +59 -0
  47. data/lib/smart_message/transport/stdout_transport.rb +62 -0
  48. data/lib/smart_message/transport.rb +41 -0
  49. data/lib/smart_message/version.rb +7 -0
  50. data/lib/smart_message/wrapper.rb +43 -0
  51. data/lib/smart_message.rb +54 -0
  52. data/smart_message.gemspec +53 -0
  53. metadata +252 -0
@@ -0,0 +1,284 @@
1
+ # lib/smart_message/base.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require 'securerandom' # STDLIB
6
+
7
+ require_relative './wrapper.rb'
8
+
9
+ module SmartMessage
10
+ # The foundation class for the smart message
11
+ class Base < Hashie::Dash
12
+
13
+ # Supports multi-level plugins for transport, serializer and logger.
14
+ # Plugins can be made at the class level and at the instance level.
15
+ @@transport = nil
16
+ @@serializer = nil
17
+ @@logger = nil
18
+
19
+ include Hashie::Extensions::Dash::PropertyTranslation
20
+
21
+ include Hashie::Extensions::Coercion
22
+ include Hashie::Extensions::DeepMerge
23
+ include Hashie::Extensions::IgnoreUndeclared
24
+ include Hashie::Extensions::IndifferentAccess
25
+ include Hashie::Extensions::MergeInitializer
26
+ include Hashie::Extensions::MethodAccess
27
+
28
+ # Common attrubutes for all messages
29
+ # TODO: Need to change the SmartMessage::Header into a
30
+ # smartMessage::Wrapper concept where the message
31
+ # content is serialized into an element in the wrapper
32
+ # where the wrapper contains header/routing information
33
+ # in addition to the serialized message data.
34
+ property :_sm_header
35
+
36
+ # Constructor for a messsage definition that allows the
37
+ # setting of initial values.
38
+ def initialize(**props, &block)
39
+ # instance-level over ride of class plugins
40
+ @transport = nil
41
+ @serializer = nil
42
+ @logger = nil
43
+
44
+ attributes = {
45
+ _sm_header: SmartMessage::Header.new(
46
+ uuid: SecureRandom.uuid,
47
+ message_class: self.class.to_s,
48
+ published_at: 2,
49
+ publisher_pid: 3
50
+ )
51
+ }.merge(props)
52
+
53
+ super(attributes, &block)
54
+ end
55
+
56
+
57
+ ###################################################
58
+ ## Common instance methods
59
+
60
+ # SMELL: How does the transport know how to decode a message before
61
+ # it knows the message class? We need a wrapper around
62
+ # the entire message in a known serialization. That
63
+ # wrapper would contain two properties: _sm_header and
64
+ # _sm_payload
65
+
66
+ # NOTE: to publish a message it must first be encoded using a
67
+ # serializer. The receive a subscribed to message it must
68
+ # be decoded via a serializer from the transport to be processed.
69
+ def encode
70
+ raise Errors::SerializerNotConfigured if serializer_missing?
71
+
72
+ serializer.encode(self)
73
+ end
74
+
75
+
76
+ # NOTE: you publish instances; but, you subscribe/unsubscribe at
77
+ # the class-level
78
+ def publish
79
+ # TODO: move all of the _sm_ property processes into the wrapper
80
+ _sm_header.published_at = Time.now
81
+ _sm_header.publisher_pid = Process.pid
82
+
83
+ payload = encode
84
+
85
+ raise Errors::TransportNotConfigured if transport_missing?
86
+ transport.publish(_sm_header, payload)
87
+
88
+ SS.add(_sm_header.message_class, 'publish')
89
+ SS.get(_sm_header.message_class, 'publish')
90
+ end # def publish
91
+
92
+
93
+
94
+ #########################################################
95
+ ## instance-level configuration
96
+
97
+ # Configure the plugins for transport, serializer and logger
98
+ def config(&block)
99
+ instance_eval(&block) if block_given?
100
+ end
101
+
102
+
103
+ #########################################################
104
+ ## instance-level transport configuration
105
+
106
+ def transport(klass_or_instance = nil)
107
+ klass_or_instance.nil? ? @transport || @@transport : @transport = klass_or_instance
108
+ end
109
+
110
+ def transport_configured?; !transport.nil?; end
111
+ def transport_missing?; transport.nil?; end
112
+ def reset_transport; @transport = nil; end
113
+
114
+
115
+ #########################################################
116
+ ## instance-level logger configuration
117
+
118
+ def logger(klass_or_instance = nil)
119
+ klass_or_instance.nil? ? @logger || @@logger : @logger = klass_or_instance
120
+ end
121
+
122
+ def logger_configured?; !logger.nil?; end
123
+ def logger_missing?; logger.nil?; end
124
+ def reset_logger; @logger = nil; end
125
+
126
+
127
+ #########################################################
128
+ ## instance-level serializer configuration
129
+
130
+ def serializer(klass_or_instance = nil)
131
+ klass_or_instance.nil? ? @serializer || @@serializer : @serializer = klass_or_instance
132
+ end
133
+
134
+ def serializer_configured?; !serializer.nil?; end
135
+ def serializer_missing?; serializer.nil?; end
136
+ def reset_serializer; @serializer = nil; end
137
+
138
+
139
+ #########################################################
140
+ ## instance-level utility methods
141
+
142
+ # return this class' name as a string
143
+ def whoami
144
+ self.class.to_s
145
+ end
146
+
147
+
148
+ # returns a collection of class Set that consists of
149
+ # the symbolized values of the property names of the message
150
+ # without the injected '_sm_' properties that support
151
+ # the behind-the-sceens operations of SmartMessage.
152
+ def fields
153
+ to_h.keys
154
+ .reject{|key| key.start_with?('_sm_')}
155
+ .map{|key| key.to_sym}
156
+ .to_set
157
+ end
158
+
159
+
160
+ ###########################################################
161
+ ## class methods
162
+
163
+ class << self
164
+
165
+ #########################################################
166
+ ## class-level configuration
167
+
168
+ def config(&block)
169
+ class_eval(&block) if block_given?
170
+ end
171
+
172
+
173
+ #########################################################
174
+ ## class-level transport configuration
175
+
176
+ def transport(klass_or_instance = nil)
177
+ klass_or_instance.nil? ? @@transport : @@transport = klass_or_instance
178
+ end
179
+
180
+ def transport_configured?; !transport.nil?; end
181
+ def transport_missing?; transport.nil?; end
182
+ def reset_transport; @@transport = nil; end
183
+
184
+
185
+ #########################################################
186
+ ## class-level logger configuration
187
+
188
+ def logger(klass_or_instance = nil)
189
+ klass_or_instance.nil? ? @@logger : @@logger = klass_or_instance
190
+ end
191
+
192
+ def logger_configured?; !logger.nil?; end
193
+ def logger_missing?; logger.nil?; end
194
+ def reset_logger; @@logger = nil; end
195
+
196
+
197
+ #########################################################
198
+ ## class-level serializer configuration
199
+
200
+ def serializer(klass_or_instance = nil)
201
+ klass_or_instance.nil? ? @@serializer : @@serializer = klass_or_instance
202
+ end
203
+
204
+ def serializer_configured?; !serializer.nil?; end
205
+ def serializer_missing?; serializer.nil?; end
206
+ def reset_serializer; @@serializer = nil; end
207
+
208
+
209
+ #########################################################
210
+ ## class-level subscription management via the transport
211
+
212
+ # Add this message class to the transport's catalog of
213
+ # subscribed messages. If the transport is missing, raise
214
+ # an exception.
215
+ def subscribe(process_method = nil)
216
+ message_class = whoami
217
+ process_method = message_class + '.process' if process_method.nil?
218
+
219
+ # TODO: Add proper logging here
220
+
221
+ raise Errors::TransportNotConfigured if transport_missing?
222
+ transport.subscribe(message_class, process_method)
223
+ end
224
+
225
+
226
+ # Remove this process_method for this message class from the
227
+ # subscribers list.
228
+ def unsubscribe(process_method = nil)
229
+ message_class = whoami
230
+ process_method = message_class + '.process' if process_method.nil?
231
+ # TODO: Add proper logging here
232
+
233
+ transport.unsubscribe(message_class, process_method) if transport_configured?
234
+ end
235
+
236
+
237
+ # Remove this message class and all of its processing methods
238
+ # from the subscribers list.
239
+ def unsubscribe!
240
+ message_class = whoami
241
+
242
+ # TODO: Add proper logging here
243
+
244
+ transport.unsubscribe!(message_class) if transport_configured?
245
+ end
246
+
247
+
248
+
249
+ #########################################################
250
+ ## class-level utility methods
251
+
252
+ # return this class' name as a string
253
+ def whoami
254
+ ancestors.first.to_s
255
+ end
256
+
257
+ # Return a Set of symbols representing each defined property of
258
+ # this message class.
259
+ def fields
260
+ @properties.dup.delete_if{|item| item.to_s.start_with?('_sm_')}
261
+ end
262
+
263
+ ###################################################
264
+ ## Business Logic resides in the #process method.
265
+
266
+ # When a transport receives a subscribed to message it
267
+ # creates an instance of the message and then calls
268
+ # the process method on that instance.
269
+ #
270
+ # It is expected that SmartMessage classes over ride
271
+ # the SmartMessage::Base#process method with appropriate
272
+ # business logic to handle the received message content.
273
+ def process(message_instance)
274
+ raise Errors::NotImplemented
275
+ end
276
+
277
+ end # class << self
278
+ end # class Base
279
+ end # module SmartMessage
280
+
281
+ require_relative 'header'
282
+ require_relative 'transport'
283
+ require_relative 'serializer'
284
+ require_relative 'logger'
File without changes
@@ -0,0 +1,146 @@
1
+ # lib/smart_message/dispatcher.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require 'concurrent'
6
+
7
+ module SmartMessage
8
+
9
+ # The disoatcher routes incoming messages to all of the methods that
10
+ # have been subscribed to the message.
11
+ class Dispatcher
12
+
13
+ # TODO: setup forwardable for some @router_pool methods
14
+
15
+ def initialize
16
+ @subscribers = Hash.new(Array.new)
17
+ @router_pool = Concurrent::CachedThreadPool.new
18
+ at_exit do
19
+ print "Shuttingdown down the dispatcher's @router_pool ..."
20
+ @router_pool.shutdown
21
+ while @router_pool.shuttingdown?
22
+ print '.'
23
+ sleep 1
24
+ end
25
+ puts " done."
26
+ end
27
+ end
28
+
29
+
30
+ def what_can_i_do?
31
+ # TODO: Return pool methods list for debugging
32
+ @router_pool.methods.sort
33
+ end
34
+
35
+
36
+ def status
37
+ # TODO: Return proper status hash
38
+ {
39
+ scheduled_task_count: @router_pool.scheduled_task_count,
40
+ completed_task_count: @router_pool.completed_task_count,
41
+ queue_length: @router_pool.queue_length,
42
+ length: @router_pool.length,
43
+ running: @router_pool.running?
44
+ }
45
+ rescue NoMethodError
46
+ what_can_i_do?
47
+ end
48
+
49
+
50
+ def pool
51
+ @router_pool.instance_variable_get('@pool'.to_sym)
52
+ end
53
+
54
+ def scheduled_task_count
55
+ @router_pool.scheduled_task_count
56
+ end
57
+
58
+ def worker_task_completed
59
+ @router_pool.worker_task_completed
60
+ end
61
+
62
+ def completed_task_count
63
+ @router_pool.completed_task_count
64
+ end
65
+
66
+ def queue_length
67
+ @router_pool.queue_length
68
+ end
69
+
70
+
71
+ def current_length
72
+ @router_pool.length
73
+ end
74
+
75
+
76
+ def running?
77
+ @router_pool.running?
78
+ end
79
+
80
+
81
+ def subscribers
82
+ @subscribers
83
+ end
84
+
85
+
86
+ def add(message_class, process_method_as_string)
87
+ klass = String(message_class)
88
+ unless @subscribers[klass].include? process_method_as_string
89
+ @subscribers[klass] += [process_method_as_string]
90
+ end
91
+ end
92
+
93
+
94
+ # drop a processer from a subscribed message
95
+ def drop(message_class, process_method_as_string)
96
+ @subscribers[String(message_class)].delete process_method_as_string
97
+ end
98
+
99
+
100
+ # drop all processer from a subscribed message
101
+ def drop_all(message_class)
102
+ @subscribers.delete String(message_class)
103
+ end
104
+
105
+
106
+ # complete reset all subscriptions
107
+ def drop_all!
108
+ @subscribers = Hash.new(Array.new)
109
+ end
110
+
111
+
112
+ # message_header is of class SmartMessage::Header
113
+ # message_payload is a string buffer that is a serialized
114
+ # SmartMessage
115
+ def route(message_header, message_payload)
116
+ message_klass = message_header.message_class
117
+ return nil if @subscribers[message_klass].empty?
118
+ @subscribers[message_klass].each do |message_processor|
119
+ SS.add(message_klass, message_processor, 'routed' )
120
+ @router_pool.post do
121
+ parts = message_processor.split('.')
122
+ target_klass = parts[0]
123
+ class_method = parts[1]
124
+ begin
125
+ target_klass.constantize
126
+ .method(class_method)
127
+ .call(message_header, message_payload)
128
+ rescue Exception => e
129
+ # TODO: Add proper exception logging
130
+ # Exception details: #{e.message}
131
+ # Processor: #{message_processor}
132
+ puts "Error processing message: #{e.message}" if $DEBUG
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+
139
+ #######################################################
140
+ ## Class methods
141
+
142
+ class << self
143
+ # TODO: may want a class-level config method
144
+ end # class << self
145
+ end # class Dispatcher
146
+ end # module SmartMessage
@@ -0,0 +1,29 @@
1
+ # smart_message/lib/smart_message/errors.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ module SmartMessage
6
+ module Errors
7
+ # A message can't be very smart if it does not know how to
8
+ # send and receive itself using a message transport
9
+ class TransportNotConfigured < RuntimeError; end
10
+
11
+ # A message can't be very smart if it does not know how to
12
+ # send and receive itself using a message transport
13
+ class TransportNotConfigured < RuntimeError; end
14
+
15
+ # A message can't be very smart if it does not know how to
16
+ # encode and decode itself using a message serializer
17
+ class SerializerNotConfigured < RuntimeError; end
18
+
19
+ # The functionality has not be implemented
20
+ class NotImplemented < RuntimeError; end
21
+
22
+ # A message was received to which there is no subscription
23
+ class ReceivedMessageNotSubscribed < RuntimeError; end
24
+
25
+ # A received message is of an unknown class
26
+ class UnknownMessageClass < RuntimeError; end
27
+
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ # lib/smart_message/header.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ module SmartMessage
6
+ # Every smart message has a common header format that contains
7
+ # information used to support the dispatching of subscribed
8
+ # messages upon receipt from a transport.
9
+ class Header < Hashie::Dash
10
+ include Hashie::Extensions::IndifferentAccess
11
+ include Hashie::Extensions::MergeInitializer
12
+ include Hashie::Extensions::MethodAccess
13
+
14
+ # Common attributes of the smart message standard header
15
+ property :uuid
16
+ property :message_class
17
+ property :published_at
18
+ property :publisher_pid
19
+ end
20
+ end
@@ -0,0 +1,8 @@
1
+ # lib/smart_message/logger/base.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ module SmartMessage::Logger
6
+ class Base
7
+ end
8
+ end # module SmartMessage::Logger
@@ -0,0 +1,7 @@
1
+ # lib/smart_message/logger.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ module SmartMessage::Logger
6
+ # TODO: write this
7
+ end # module SmartMessage::Logger
@@ -0,0 +1,23 @@
1
+ # lib/smart_message/serializer/base.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ module SmartMessage::Serializer
6
+ # the standard super class
7
+ class Base
8
+ # provide basic configuration
9
+ def initialize
10
+ # TODO: write this
11
+ end
12
+
13
+ def encode(message_instance)
14
+ # TODO: Add proper logging here
15
+ raise ::SmartMessage::Errors::NotImplemented
16
+ end
17
+
18
+ def decode(payload)
19
+ # TODO: Add proper logging here
20
+ raise ::SmartMessage::Errors::NotImplemented
21
+ end
22
+ end # class Base
23
+ end # module SmartMessage::Serializer
@@ -0,0 +1,22 @@
1
+ # lib/smart_message/serializer/json.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require 'json' # STDLIB
6
+
7
+ module SmartMessage::Serializer
8
+ class JSON < Base
9
+ def encode(message_instance)
10
+ # TODO: is this the right place to insert an automated-invisible
11
+ # message header?
12
+ message_instance.to_json
13
+ end
14
+
15
+ def decode(payload)
16
+ # TODO: so how do I know to which message class this payload
17
+ # belongs? The class needs to be in some kind of message
18
+ # header.
19
+ ::JSON.parse payload
20
+ end
21
+ end # class JSON < Base
22
+ end # module SmartMessage::Serializer
@@ -0,0 +1,10 @@
1
+ # lib/smart_message/serializer.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require_relative 'serializer/base'
6
+ require_relative 'serializer/json'
7
+
8
+ module SmartMessage::Serializer
9
+ # Serializer module for message encoding/decoding
10
+ end # module SmartMessage::Serializer
@@ -0,0 +1,85 @@
1
+ # lib/smart_message/transport/base.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ module SmartMessage
6
+ module Transport
7
+ # Base class for all transport implementations
8
+ # This defines the standard interface that all transports must implement
9
+ class Base
10
+ attr_reader :options, :dispatcher
11
+
12
+ def initialize(**options)
13
+ @options = default_options.merge(options)
14
+ @dispatcher = options[:dispatcher] || SmartMessage::Dispatcher.new
15
+ configure
16
+ end
17
+
18
+ # Transport-specific configuration
19
+ def configure
20
+ # Override in subclasses for specific setup
21
+ end
22
+
23
+ # Default options for this transport
24
+ def default_options
25
+ {}
26
+ end
27
+
28
+ # Publish a message
29
+ # @param message_header [SmartMessage::Header] Message routing information
30
+ # @param message_payload [String] Serialized message content
31
+ def publish(message_header, message_payload)
32
+ raise NotImplementedError, 'Transport must implement #publish'
33
+ end
34
+
35
+ # Subscribe to a message class
36
+ # @param message_class [String] The message class name
37
+ # @param process_method [String] The processing method identifier
38
+ def subscribe(message_class, process_method)
39
+ @dispatcher.add(message_class, process_method)
40
+ end
41
+
42
+ # Unsubscribe from a specific message class and process method
43
+ # @param message_class [String] The message class name
44
+ # @param process_method [String] The processing method identifier
45
+ def unsubscribe(message_class, process_method)
46
+ @dispatcher.drop(message_class, process_method)
47
+ end
48
+
49
+ # Unsubscribe from all process methods for a message class
50
+ # @param message_class [String] The message class name
51
+ def unsubscribe!(message_class)
52
+ @dispatcher.drop_all(message_class)
53
+ end
54
+
55
+ # Get current subscriptions
56
+ def subscribers
57
+ @dispatcher.subscribers
58
+ end
59
+
60
+ # Check if transport is connected/available
61
+ def connected?
62
+ true
63
+ end
64
+
65
+ # Connect to transport (if applicable)
66
+ def connect
67
+ # Override in subclasses if connection setup is needed
68
+ end
69
+
70
+ # Disconnect from transport (if applicable)
71
+ def disconnect
72
+ # Override in subclasses if cleanup is needed
73
+ end
74
+
75
+ # Receive and route a message (called by transport implementations)
76
+ # @param message_header [SmartMessage::Header] Message routing information
77
+ # @param message_payload [String] Serialized message content
78
+ protected
79
+
80
+ def receive(message_header, message_payload)
81
+ @dispatcher.route(message_header, message_payload)
82
+ end
83
+ end
84
+ end
85
+ end