drama_queen 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a1cb78ce2e3af6d28e68255b0505b9d704c86ed5
4
+ data.tar.gz: 64f553c705c27367ebc8b17cf6229c589e371b3f
5
+ SHA512:
6
+ metadata.gz: 343d156ca2cbf4d87ba4846c316ce5d3c51763a870d688744ce22982940942f02f2052ae00bae6f0d03fa69b20335545ec35388e3582bd287844c64aa45e6c55
7
+ data.tar.gz: 220dd484df1bc26b0c438b752f5883d27a13fbe61ed14fc21d2f70e84ac2ad09748791cd3fcfe528e438418445ee2b6752792b929c3beca6d593e17bb271481d
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+
19
+ .idea/
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - jruby-19mode
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in drama_queen.gemspec
4
+ gemspec
5
+
6
+ gem 'yard'
7
+
data/History.md ADDED
@@ -0,0 +1,3 @@
1
+ ### 0.1.0 / 11-Nov-2013
2
+
3
+ * Initial release.
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Steve Loveless
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # DramaQueen
2
+
3
+ A simple, synchronous pub-sub/observer library that allows objects to observe or
4
+ publish and subscribe to topics.
5
+
6
+ [![Build Status](https://travis-ci.org/turboladen/drama_queen.png?branch=master)](https://travis-ci.org/turboladen/drama_queen)
7
+
8
+ ## Features
9
+
10
+ * Observe Ruby objects and receive updates when they change
11
+ ([Observer Pattern](http://en.wikipedia.org/wiki/Observer_pattern))
12
+ * Subscribe and publish on a topic or routing key
13
+ ([Publish-Subscribe Pattern](http://en.wikipedia.org/wiki/Publish–subscribe_pattern))
14
+ * Synchronous, no threading
15
+
16
+
17
+ ## Usage
18
+
19
+ My initial desire for this library was to replace Ruby's built-in Observer
20
+ module with something that allowed objects to observe specific topics, as
21
+ opposed to simply just observing objects. DramaQueen lets you do both.
22
+
23
+ ### Subscribe to an object
24
+
25
+ ```ruby
26
+ class MyProducer
27
+ include DramaQueen::Producer
28
+
29
+ def do_stuff
30
+ publish(self, "I did some stuff!", [1, 2, 3])
31
+ end
32
+ end
33
+
34
+ class MyConsumer
35
+ include DramaQueen::Consumer
36
+
37
+ def initialize(topic)
38
+ subscribe(topic, :call_me!)
39
+ end
40
+
41
+ def call_me!(message, neat_array)
42
+ puts "He did it! -> #{message}"
43
+ puts "A present for me?! #{neat_array}"
44
+ end
45
+ end
46
+
47
+ producer = MyProducer.new
48
+ consumer = MyConsumer.new(producer)
49
+ producer.do_stuff
50
+
51
+ # "He did it! -> I did some stuff!"
52
+ # "A present for me?! [1, 2, 3]"
53
+ #=> true
54
+ ```
55
+
56
+ `consumer` subscribes to the `producer` object and `producer` publishes on the
57
+ topic of itself, thus publishing to `consumer`. Also notice that the consumer
58
+ passed in `:call_me!` as the second parameter to `#subscribe`: that registers
59
+ the `#call_me!` method to be called when the producer publishes. We can see
60
+ that when we call `producer.do_stuff`, `consumer.call_me!` gets called by
61
+ the strings that get output to the console. Also notice that `consumer.call_me!`
62
+ gets the same parameters passed to it that are passed in to `producer`'s call
63
+ to `#publish`.
64
+
65
+
66
+ ### Subscribe to a topic
67
+
68
+ Subscribing to a topic is no different than subscribing to an object.
69
+
70
+ ```ruby
71
+ class ThingsProducer
72
+ include DramaQueen::Producer
73
+
74
+ def do_stuff
75
+ publish('things', "I did some stuff!", [1, 2, 3])
76
+ end
77
+ end
78
+
79
+ class ThingsConsumer
80
+ include DramaQueen::Consumer
81
+
82
+ def initialize(topic)
83
+ subscribe(topic, :call_me!)
84
+ end
85
+
86
+ def call_me!(message, neat_array)
87
+ puts "He did it! -> #{message}"
88
+ puts "A present for me?! #{neat_array}"
89
+ end
90
+ end
91
+
92
+ producer = ThingsProducer.new
93
+ consumer = ThingsConsumer.new('things')
94
+ producer.do_stuff
95
+
96
+ # "He did it! -> I did some stuff!"
97
+ # "A present for me?! [1, 2, 3]"
98
+ #=> true
99
+ ```
100
+
101
+ Moral of the story: The topic that you publish/subscribe on can be any Ruby
102
+ object--how you use this is up to you.
103
+
104
+ ### Routing Key Matching
105
+
106
+ Thus far, we've talked about subscribing to a "topic".
107
+
108
+ ```ruby
109
+ class A
110
+ include DramaQueen::Consumer
111
+
112
+ def initialize
113
+ subscribe 'root.*.children', :call_me
114
+ end
115
+
116
+ def call_me(*args)
117
+ puts "A got called with args: #{args}"
118
+ end
119
+ end
120
+
121
+ class B
122
+ include DramaQueen::Producer
123
+
124
+ def do_stuff
125
+ publish 'root.parent', 1, 2, 3
126
+ end
127
+ end
128
+
129
+ class C
130
+ include DramaQueen::Producer
131
+
132
+ def do_stuff
133
+ publish 'root.parent.children', 1, 2, 3
134
+ end
135
+ end
136
+
137
+ a = A.new
138
+ b = B.new
139
+ c = C.new
140
+
141
+ b.do_stuff
142
+
143
+ # (A does not get called)
144
+ c.do_stuff
145
+ # "A got called with args: 1, 2, 3
146
+ #=> true
147
+ ```
148
+
149
+ See {DramaQueen::Exchange} for more.
150
+
151
+ ### Synchronous Only!
152
+
153
+ DramaQueen does not use any threading or fancy asynchronous stuff. When your
154
+ producer publishes, you can expect your subscribers to receive the notifications:
155
+
156
+ * in the order the publishing was triggered, and
157
+ * in the order that consumers subscribed.
158
+
159
+ This is intentional. If you want publishing to take place in a thread, you can
160
+ thread your call to #publish in your code.
161
+
162
+ ### Application-wide
163
+
164
+ DramaQueen stores all of its exchanges in a singleton, which is thus accessible
165
+ throughout your app/library. This makes it possible to subscribe to a topic in
166
+ one object and publish to that topic in another unrelated object from anywhere
167
+ in your code. And while you have access to `DramaQueen`, you shouldn't ever
168
+ need to deal with it directly--just use `#publish` and `#subscribe` and let it
169
+ do its thing.
170
+
171
+
172
+ ## Installation
173
+
174
+ Add this line to your application's Gemfile:
175
+
176
+ gem 'drama_queen'
177
+
178
+ And then execute:
179
+
180
+ $ bundle
181
+
182
+ Or install it yourself as:
183
+
184
+ $ gem install drama_queen
185
+
186
+
187
+ ## Contributing
188
+
189
+ 1. Fork it
190
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
191
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
192
+ 4. Push to the branch (`git push origin my-new-feature`)
193
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new
5
+
6
+ task default: :spec
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'drama_queen/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'drama_queen'
8
+ spec.version = DramaQueen::VERSION
9
+ spec.author = 'Steve Loveless'
10
+ spec.email = 'steve.loveless@gmail.com'
11
+ spec.description = %q{A simple, non-threaded, local-object pub-sub/observer
12
+ with the ability to pub-sub on topics. Topics can be any Ruby object.}
13
+ spec.summary = spec.description
14
+ spec.homepage = 'https://githb.com/turboladen/drama_queen'
15
+ spec.license = 'MIT'
16
+
17
+ spec.files = `git ls-files`.split($/)
18
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
19
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
20
+ spec.require_paths = %w[lib]
21
+
22
+ spec.add_development_dependency 'bundler', '~> 1.3'
23
+ spec.add_development_dependency 'rake'
24
+ spec.add_development_dependency 'rspec', '~> 3.0.0.beta1'
25
+ end
@@ -0,0 +1,37 @@
1
+ require_relative 'drama_queen/version'
2
+
3
+
4
+ # This is the singleton that maintains the list of active exchanges.
5
+ module DramaQueen
6
+
7
+ # The list of all exchanges that DramaQueen knows about. This is updated
8
+ # by DramaQueen::Consumers as they subscribe to topics.
9
+ #
10
+ # @return [Array<DramaQueen::Exchange>]
11
+ def self.exchanges
12
+ @exchanges ||= Array.new
13
+ end
14
+
15
+ # Finds the DramaQueen::Exchange for the given +routing_key+.
16
+ #
17
+ # @param [Object] routing_key
18
+ # @return [DramaQueen::Exchange]
19
+ def self.exchange_for(routing_key)
20
+ exchanges.find do |exchange|
21
+ exchange.routing_key == routing_key
22
+ end
23
+ end
24
+
25
+ # @param [Object] routing_key
26
+ # @return [Boolean]
27
+ def self.routes_to?(routing_key)
28
+ !!exchange_for(routing_key)
29
+ end
30
+
31
+ # Removes all exchanges from the exchanges list.
32
+ #
33
+ # @return [Array]
34
+ def self.unsubscribe_all
35
+ @exchanges = Array.new
36
+ end
37
+ end
@@ -0,0 +1,34 @@
1
+ require_relative '../drama_queen'
2
+ require_relative 'exchange'
3
+
4
+
5
+ module DramaQueen
6
+
7
+ # A +consumer+ is simply an object that receives messages from a +producer+.
8
+ # In order to sign up to receive messages from a producer, the consumer
9
+ # subscribes to a +topic+ that they're interested in. When the producer
10
+ # +publish+es something on that topic, the consumer's +callback+ will get
11
+ # called.
12
+ module Consumer
13
+
14
+ # @param [Object] routing_key The routing key that represents the Exchange
15
+ # to subscribe to.
16
+ # @param [Symbol,Method,Proc] callback If given a Symbol, this will be
17
+ # converted to a Method that gets called on the includer of Consumer. If
18
+ # +callback+ is not a Symbol, it simply must just respond to +call+.
19
+ def subscribe(routing_key, callback)
20
+ callable_callback = callback.is_a?(Symbol) ? method(callback) : callback
21
+
22
+ unless callable_callback.respond_to?(:call)
23
+ raise "The given callback is not a Symbol, nor responds to #call: #{callback}"
24
+ end
25
+
26
+ unless DramaQueen.routes_to? routing_key
27
+ DramaQueen.exchanges << Exchange.new(routing_key)
28
+ end
29
+
30
+ exchange = DramaQueen.exchange_for(routing_key)
31
+ exchange.subscribers << callable_callback
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,137 @@
1
+ module DramaQueen
2
+
3
+ # An Exchange determines which objects will receive messages from a Producer.
4
+ # It is merely a wrapper around the +routing_key+ that is given on
5
+ # initialization, allowing to more easily determine which routing_keys are
6
+ # related, and thus if any subscribers to the related exchanges should get
7
+ # notified in addition to the subscribers to this exchange.
8
+ #
9
+ # A Exchange routing_key is what Producers and Consumers refer to when they're
10
+ # looking to publish or subscribe. The Exchange +routing_key+ can be any
11
+ # object, but some extra semantics & functionality come along if you use
12
+ # DramaQueen's route_key globbing.
13
+ #
14
+ # === Ruby Objects
15
+ #
16
+ # First, the simplest case: a +routing_key+ that is any Ruby object.
17
+ # Producers and subscribers will use this case when observing that Ruby
18
+ # object. #related_exchanges will be empty; all messages published using this
19
+ # routing_key will only get delivered to consumers subscribing to this
20
+ # Exchange's routing_key. If you only need this approach, you might be better
21
+ # off just using Ruby's build-in +Observer+ library--it accomplishes this, and
22
+ # is much simpler than DramaQueen.
23
+ #
24
+ # === RouteKey Globbing
25
+ #
26
+ # Now, the fun stuff: routing key globbing uses period-delimited strings to
27
+ # infer some hierarchy of Exchanges. Using this approach lets you tie
28
+ # together other Exchanges via +#related_keys+, thus letting you build
29
+ # some organization/structure into your whole system of routing messages.
30
+ # These globs (somewhat similar to using +Dir.glob+ for file systems) let your
31
+ # producers and consumers pub/sub to large numbers of topics, yet organize
32
+ # those topics. The structure that you build is up to you. Here's a
33
+ # contrived example. You could use set up a routing key scheme like:
34
+ #
35
+ # * +"my_library.bob.pants"+
36
+ # * +"my_library.bob.shirts"+
37
+ # * +"my_library.sally.pants"+
38
+ # * +"my_library.sally.shirts"+
39
+ #
40
+ # When producers publish to +"my_library.bob.pants"+, only consumers of
41
+ # +"my_library.bob.pants"+ get notified. If a producer publishes to
42
+ # +"my_library.*.pants"+, consumers of both +"my_library.bob.pants"+ _and_
43
+ # +"my_library.sally.pants"+ get notifications. Going the other way, if
44
+ # a consumer subscribes to +"my_library.*.pants"+, then when a producer on
45
+ # +"my_library.bob.pants"+, _or_ +"my_library.sally.pants"+, _or_
46
+ # +"my_library.*.pants"+, that consumer gets all of those messages.
47
+ #
48
+ # Further, producers and consumers can use a single asterisk at multiple
49
+ # levels: +"\*.\*.shirts"+ would resolve to both +"my_library.bob.shirts"+ and
50
+ # +"my_library.sally.shirts"+; if there was a +"someone_else.vendor.shirts"+,
51
+ # for example, that would route as well.
52
+ #
53
+ # Lastly, you can you a double-asterisk to denote that you want everything
54
+ # from that level and deeper. So, +"**"+ would match _all_ existing routing
55
+ # keys, including single-object (non-glob style) keys. +"my_library.**"+
56
+ # would match all four of our +"my_library"+ keys above.
57
+ #
58
+ # As you're devising your routing key scheme, consider naming like you would
59
+ # name classes/modules in a Ruby library: use namespaces to avoid messing
60
+ # others up! Notice the use of +"my_library..."+ above... that was on
61
+ # purpose.
62
+ class Exchange
63
+
64
+ # @return [Object]
65
+ attr_reader :routing_key
66
+
67
+ # @return [Array]
68
+ attr_reader :subscribers
69
+
70
+ # @param [Object] routing_key
71
+ def initialize(routing_key)
72
+ @routing_key = routing_key
73
+ @subscribers = []
74
+ end
75
+
76
+ # @param [Object] routing_key
77
+ # @return [Boolean]
78
+ def routes_to?(routing_key)
79
+ related_exchanges.include? routing_key
80
+ end
81
+
82
+ # All other DramaQueen::Exchange objects that the current Exchange routes
83
+ # to.
84
+ #
85
+ # @return [Array<DramaQueen::Exchange]
86
+ def related_exchanges
87
+ return DramaQueen.exchanges if self.routing_key == '**'
88
+
89
+ DramaQueen.exchanges.find_all do |exchange|
90
+ next if exchange.routing_key == '**'
91
+ next if exchange.routing_key == self.routing_key
92
+
93
+ if self.routing_key.is_a?(String) && exchange.routing_key.is_a?(String)
94
+ routing_key_match?(exchange.routing_key, self.routing_key) ||
95
+ routing_key_match?(self.routing_key, exchange.routing_key)
96
+ else
97
+ exchange == self
98
+ end
99
+ end
100
+ end
101
+
102
+ # Calls each subscriber's callback with the given arguments.
103
+ #
104
+ # @param args
105
+ def notify_with(*args)
106
+ @subscribers.each do |subscriber|
107
+ subscriber.call(*args)
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ # @return [Boolean]
114
+ def routing_key_match?(first, second)
115
+ self_matcher = make_matchable(second)
116
+
117
+ !!first.match(Regexp.new(self_matcher))
118
+ end
119
+
120
+ # @param [String] string
121
+ # @return [String]
122
+ def make_matchable(string)
123
+ matcher = if string =~ %r[\*\*]
124
+ string.sub(%r[\.?\*\*], '\..+')
125
+ elsif string =~ %r[\*]
126
+ string.sub(%r[\*], '[^\.]+')
127
+ else
128
+ string
129
+ end
130
+
131
+ matcher = matcher.gsub('.*', '\.\w+')
132
+ matcher = "\\A#{matcher}\\z"
133
+
134
+ matcher
135
+ end
136
+ end
137
+ end