drama_queen 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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