observatory 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ doc
2
+ .yardoc
3
+ _site
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "http://rubygems.org"
2
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,20 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ observatory (0.0.1)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ bluecloth (2.1.0)
10
+ rake (0.8.7)
11
+ yard (0.6.8)
12
+
13
+ PLATFORMS
14
+ ruby
15
+
16
+ DEPENDENCIES
17
+ bluecloth (~> 2.1)
18
+ observatory!
19
+ rake (~> 0.8)
20
+ yard (~> 0.6)
data/HISTORY ADDED
@@ -0,0 +1,3 @@
1
+ # 0.0.1
2
+
3
+ First alpha version.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2011 Arjan van der Gaag
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # Observatory
2
+
3
+ ## Overview
4
+
5
+ ### Description
6
+
7
+ Observatory is a simple Ruby gem that implements the observer design pattern to facilitate loosely coupled communication between objects in your program. It allows one object to publish an event, and others to respond to that.
8
+
9
+ Using Observatory you can apply filters to method arguments, respond to events in your program or dynamically inject new functionality.
10
+
11
+ ### What's new?
12
+
13
+ See HISTORY for a list of changes per version.
14
+
15
+ ## Usage
16
+
17
+ ### Installation
18
+
19
+ Observatory is distributed as a Ruby gem, so installation is simple:
20
+
21
+ $ gem install observatory
22
+
23
+ Then, you only need to load it in your program using Bundler or a manual require, like so:
24
+
25
+ require 'observatory'
26
+
27
+ ### Quick start
28
+
29
+ For full documentation refer to the inline API docs (which you can generate using the `yard` rake task). A quick overview:
30
+
31
+ class Post
32
+ include Observatory::Observable
33
+
34
+ attr_reader :dispatcher, :title
35
+
36
+ def initialize(title, dispatcher)
37
+ @title = title
38
+ @dispatcher = dispatcher
39
+ end
40
+
41
+ def publish
42
+ notify 'post.publish', :title => title
43
+ end
44
+
45
+ def title
46
+ filter('post.title', @title).return_value
47
+ end
48
+ end
49
+
50
+ class Logger
51
+ include Observatory::Observer
52
+
53
+ def initialize(dispatcher)
54
+ @dispatcher = dispatcher
55
+ end
56
+
57
+ observe 'post.publish'
58
+ def log(event)
59
+ "Post published: #{event[:title]}"
60
+ end
61
+ end
62
+
63
+ dispatcher = Observatory::Dispatcher.new
64
+
65
+ dispatcher.connect('post.title') do |event, title|
66
+ "Title: #{title}"
67
+ end
68
+
69
+ p = Post.new('foo', dispatcher)
70
+ l = Logger.new(dispatcher)
71
+
72
+ p.publish
73
+ # => Outputs: 'Post published: Title: foo'
74
+
75
+ ## More information
76
+
77
+ ### To Do
78
+
79
+ * Complete unit tests for mixins.
80
+
81
+ ### Note on Patches/Pull Requests
82
+
83
+ * Fork the project.
84
+ * Make your feature addition or bug fix.
85
+ * Add tests for it. This is important so I don't break it in a future version unintentionally.
86
+ * Commit, do not mess with rakefile, version, or history. (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
87
+ * Send me a pull request. Bonus points for topic branches.
88
+
89
+ ### Credits
90
+
91
+ By Arjan van der Gaag <arjan@arjanvandergaag.nl>. Based on the [Event Dispatcher Symfonoy Component][1].
92
+
93
+ ### License
94
+
95
+ Observatory is released under the same license as Ruby. See LICENSE for more information.
96
+
97
+ [1]: http://components.symfony-project.org/event-dispatcher/
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+ Bundler.require(:default, :development)
4
+ require 'rake'
5
+ require 'rake/testtask'
6
+
7
+ task :default => :test
8
+
9
+ Rake::TestTask.new do |t|
10
+ t.libs << 'test'
11
+ t.test_files = FileList['test/*_test.rb']
12
+ t.verbose = true
13
+ end
14
+
15
+ YARD::Rake::YardocTask.new do |t|
16
+ t.files = ['lib/**/*.rb', 'app/**/*.rb', '-', 'LICENSE', 'HISTORY']
17
+ t.options = %w{--title Observatory -m markdown}
18
+ end
@@ -0,0 +1,7 @@
1
+ module Observatory
2
+ autoload :Dispatcher, 'observatory/dispatcher'
3
+ autoload :Event, 'observatory/event'
4
+ autoload :Observable, 'observatory/observable'
5
+ autoload :Observer, 'observatory/observer'
6
+ autoload :VERSION, 'observatory/version'
7
+ end
@@ -0,0 +1,175 @@
1
+ module Observatory
2
+ # The Dispatcher is the central repository of all registered observers, and
3
+ # is used by observables to send out signals to these observers.
4
+ #
5
+ # A dispatcher is not a singleton object, which means you may very well have
6
+ # several dispatcher objects in your program, keeping track of different
7
+ # stacks of observers and observables. Note that this requires you to pass
8
+ # your dispatcher object around using dependency injection.
9
+ #
10
+ # ## For observables
11
+ #
12
+ # The stack of observers for any given signal is kept in {#observers}. When
13
+ # using {#notify}, {#notify_until} or {#filter} all observers in the stack
14
+ # will be called.
15
+ #
16
+ # ### Notification methods
17
+ #
18
+ # Observable objects may use the following methods to trigger their
19
+ # observers:
20
+ #
21
+ # * {#notify} to call all observers.
22
+ # * {#notify_until} to call observers until one stops the chain.
23
+ # * {#filter} to let all observers alter a given value.
24
+ #
25
+ # ## For observers
26
+ #
27
+ # An object that observes another object is an observer, and it can
28
+ # register itself with the {Dispatcher} to listen to a signal that
29
+ # observable objects may issue.
30
+ #
31
+ # @example Using {#connect} to register a new observer
32
+ # class Logger
33
+ # def log(event)
34
+ # puts "Post published by #{event.observable}"
35
+ # end
36
+ # end
37
+ # logger = Logger.new
38
+ # dispatcher.connect('post.publish', logger.method(:log))
39
+ #
40
+ # @example Using {#disconnect} to unregister an observer
41
+ # dispatcher.disconnect('post.publish', logger.method(:log))
42
+ #
43
+ # @example Using {#notify} to let other objects know something has happened
44
+ # class Post
45
+ # include Observable
46
+ # attr_reader :title
47
+ # def publish
48
+ # notify 'post.publish', :title => title
49
+ # # do publication stuff here
50
+ # end
51
+ # end
52
+ #
53
+ # @example Using {#notify_until} to delegate saving a record to another object
54
+ # class Post
55
+ # def save
56
+ # notify_until 'post.save', :title => title
57
+ # end
58
+ # end
59
+ #
60
+ # @example Using {#filter} to let observers modify the output of the title attribute
61
+ # class Post
62
+ # def title
63
+ # filter('post.title', @title).return_value
64
+ # end
65
+ # end
66
+ class Dispatcher
67
+ # A list of all registered observers grouped by signal.
68
+ # @return [Hash]
69
+ attr_reader :observers
70
+
71
+ def initialize
72
+ @observers = {}
73
+ end
74
+
75
+ # Register a observer for a given signal.
76
+ #
77
+ # Instead of adding a method or Proc object to the stack, you could
78
+ # also use a block. Either the observer argument or the block is required.
79
+ #
80
+ # @example Using a block as an observer
81
+ # dispatcher.connect('post.publish') do |event|
82
+ # puts "Post was published"
83
+ # end
84
+ #
85
+ # @example Using a method as an observer
86
+ # class Reporter
87
+ # def log(event)
88
+ # puts "Post published"
89
+ # end
90
+ # end
91
+ # dispatcher.connect('post.publish', Reporter.new.method(:log))
92
+ #
93
+ # @param [String] signal is the name used by the observable to trigger
94
+ # observers
95
+ # @param [#call] observer is the Proc or method that will react to
96
+ # an event issued by an observable.
97
+ # @return [#call] the added observer
98
+ def connect(signal, observer = nil, &block)
99
+ if observer.nil?
100
+ if block_given?
101
+ observer = block
102
+ else
103
+ raise ArgumentError, 'Use a block, method or proc to specify an observer'
104
+ end
105
+ end
106
+ observers[signal] ||= []
107
+ observers[signal] << observer
108
+ end
109
+
110
+ # Removes an observer from a signal stack, so it no longer gets triggered.
111
+ #
112
+ # @param [String] signal is the name of the stack to remove the observer
113
+ # from.
114
+ # @param [#call] observer is the original observer to remove.
115
+ # @return [#call, nil] the removed observer or nil if it could not be found
116
+ def disconnect(signal, observer)
117
+ return nil unless observers.key?(signal)
118
+ observers[signal].delete(observer)
119
+ end
120
+
121
+ # Send out a signal to all registered observers using a new {Event}
122
+ # instance. The {Event#signal} will be used to determine the stack of
123
+ # {#observers} to use.
124
+ #
125
+ # Using {#notify} allows observers to take action at a given time during
126
+ # program execution, such as logging important events.
127
+ #
128
+ # @param [Event]
129
+ # @return [Event]
130
+ def notify(event)
131
+ each(event.signal) do |observer|
132
+ observer.call(event)
133
+ end
134
+ event
135
+ end
136
+
137
+ # Same as {#notify}, but halt execution as soon as an observer has
138
+ # indicated it has handled the event by returning a non-falsy value.
139
+ #
140
+ # An event that was acted upon by an observer will be marked as processed.
141
+ #
142
+ # @param [Event]
143
+ # @see Event#process!
144
+ # @return [Event]
145
+ def notify_until(event)
146
+ each(event.signal) do |observer|
147
+ event.process! and break if observer.call(event)
148
+ end
149
+ event
150
+ end
151
+
152
+ # Let all registered observers modify a given value. The observable can
153
+ # then use the {Event#return_value} to get the filtered result back.
154
+ #
155
+ # You could use {#filter} to let observers modify arguments to a method
156
+ # before continuing to work on them (just an example).
157
+ #
158
+ # @param [Event]
159
+ # @param [Object] value
160
+ # @return [Event]
161
+ def filter(event, value)
162
+ each(event.signal) do |observer|
163
+ value = observer.call(event, value)
164
+ end
165
+ event.return_value = value
166
+ event
167
+ end
168
+
169
+ private
170
+
171
+ def each(signal, &block)
172
+ (observers[signal] || []).each(&block)
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,70 @@
1
+ module Observatory
2
+ # An Event is a value object that the observable passes along to the
3
+ # observers. It gives observers easy access to the original observable
4
+ # and any additional values it may have wanted to pass along
5
+ # (the parameters).
6
+ #
7
+ # The event object is also returned back to the observable that originally
8
+ # issued it, so it also works as a value object to pass information from
9
+ # the observers to the observable. The event knows whether it has been acted
10
+ # upon, and it can remember a return value that the observable may want to
11
+ # use (and other observers may want to act upon).
12
+ #
13
+ # The event works just like a regular Ruby Hash, so you can access any
14
+ # parameters just like you would with a hash.
15
+ #
16
+ # @example Accessing parameters like a Hash
17
+ # event = Event.new(self, 'post.publish', :title => 'My new post')
18
+ # event[:title] # => 'My new post'
19
+ #
20
+ # @note An event is essentially a Hash with some extra properties, so
21
+ # you can use all the regular Hash and Enumerable methods to your liking.
22
+ class Event < Hash
23
+
24
+ # The original observable object that issued the event.
25
+ # @return [Object]
26
+ attr_reader :observable
27
+
28
+ # The name of the signal that the observable triggered. Namespaced with
29
+ # periods.
30
+ # @return [String]
31
+ attr_reader :signal
32
+
33
+ # The return value for the observable, that observers may modify.
34
+ # @return [Object]
35
+ attr_accessor :return_value
36
+
37
+ # Create a new event instance with the given observable and signal. Any
38
+ # parameters are stored, which you can later access like a hash.
39
+ #
40
+ # @param [Object] observable
41
+ # @param [String, #to_s] signal
42
+ # @param [Hash] parameters is a hash of additional information that
43
+ # observers may want to use.
44
+ def initialize(observable, signal, parameters = {})
45
+ @observable, @signal = observable, signal.to_s
46
+ merge! parameters
47
+ @processed = false
48
+ super()
49
+ end
50
+
51
+ # See if this event has been processed by an observer. Useful when using
52
+ # {Dispatcher#notify_until} to see if there was any observer that actually
53
+ # did something.
54
+ #
55
+ # @see Dispatcher#notify_until
56
+ # @return [Boolean]
57
+ def processed?
58
+ @processed
59
+ end
60
+
61
+ # Mark this event as processed, so the observable knows an observer was
62
+ # active when using {Dispatcher#notify_until}.
63
+ #
64
+ # @see Dispatcher#notify_until
65
+ # @return [Boolean] true
66
+ def process!
67
+ @processed = true
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,63 @@
1
+ module Observatory
2
+ # An observable object can publish events to registered observers. This
3
+ # module provides some simple helper methods as syntactic sugar.
4
+ #
5
+ # Using these shortcut methods will default the observable object of the
6
+ # events raised to `self`, so the method signatures are the same as in
7
+ # {Dispatcher} but without the first one, the observable object.
8
+ #
9
+ # @note Including this module will create a read-only attribute `dispatcher`
10
+ # but not set it. You need to populate it yourself.
11
+ #
12
+ # @example Manually triggering events in your code
13
+ # class Post
14
+ # attr_reader :dispatcher
15
+ #
16
+ # def initialize(dispatcher)
17
+ # @dispatcher = dispatcher
18
+ # end
19
+ #
20
+ # def publish
21
+ # event = Observatory::Event.new(self, 'post.publish')
22
+ # dispatcher.notify(event)
23
+ # end
24
+ # end
25
+ #
26
+ # @example Using the Observable shortcut methods
27
+ # class Post
28
+ # include Observatory::Observable
29
+ #
30
+ # def publish
31
+ # notify('post.publish') # => instance of Event
32
+ # end
33
+ # end
34
+ #
35
+ # @see Observer
36
+ module Observable
37
+ def self.included(base)
38
+ base.send(:attr_reader, :dispatcher)
39
+ end
40
+
41
+ # @see Dispatcher#notify
42
+ def notify(*args)
43
+ Observatory::Event.new(self, *args).tap do |e|
44
+ dispatcher.notify(e)
45
+ end
46
+ end
47
+
48
+ # @see Dispatcher#filter
49
+ def filter(*args)
50
+ value = args.pop
51
+ Observatory::Event.new(self, *args).tap do |e|
52
+ dispatcher.filter(e, value)
53
+ end
54
+ end
55
+
56
+ # @see Dispatcher#notify_until
57
+ def notify_until(*args)
58
+ Observatory::Event.new(self, *args).tap do |e|
59
+ dispatcher.notify_until(e)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,91 @@
1
+ module Observatory
2
+ # @note Including the Observer module in your object will override your
3
+ # initializer.
4
+ #
5
+ # The Observer module enhances your classes with some simple syntactic sugar
6
+ # to register methods as observers. You may very well register your
7
+ # methods manually, but these methods may increase your code readability.
8
+ #
9
+ # This module will override your `initialize` method to automatically
10
+ # register all observer methods with the dispatcher. In order to do so,
11
+ # it will alias your own `initialize` method and create a new one that
12
+ # both calls the old initializer and registers observers. This does mean
13
+ # that your initializer needs to set up a dispatcher object, probably
14
+ # via constructor dependency injection.
15
+ #
16
+ # The main benefit of using this module in your classes is you get the
17
+ # class macro `observe` which will set up the next method that is declared
18
+ # in the class as an observer for the given signal name.
19
+ #
20
+ # @example Registering a method as an observer for a signal
21
+ # class Logger
22
+ # include Observatory::Observer
23
+ #
24
+ # def initialize(dispatcher)
25
+ # @dispatcher = dispatcher
26
+ # end
27
+ #
28
+ # observe 'post.publish'
29
+ # def log(event)
30
+ # puts "Event #{event.signal} happened at #{Time.now}"
31
+ # end
32
+ # end
33
+ #
34
+ # @todo allow registering a single method to mulitple signals, or even
35
+ # matching by regular expression...?
36
+ # @todo this is pretty magicky. Might need to refactor to make things more
37
+ # explicit and obvious.
38
+ module Observer
39
+ def self.included(base)
40
+ base.extend ClassMethods
41
+ base.overwrite_initialize
42
+ base.class_eval do
43
+ attr_reader :dispatcher
44
+ end
45
+
46
+ base.instance_eval do
47
+ def method_added(name)
48
+ if name == :initialize
49
+ overwrite_initialize
50
+ else
51
+ if @observer_next_event_name_to_observe
52
+ @observers_to_set_up ||= {}
53
+ @observers_to_set_up[@observer_next_event_name_to_observe] ||= []
54
+ @observers_to_set_up[@observer_next_event_name_to_observe] << name
55
+ @observer_next_event_name_to_observe = nil
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ module ClassMethods
63
+ def overwrite_initialize
64
+ class_eval do
65
+ unless method_defined?(:initialize_and_setup_observers)
66
+ define_method(:initialize_and_setup_observers) do |*args|
67
+ initialize_without_observer *args
68
+ self.class.observers_to_set_up.each_pair do |name, methods|
69
+ methods.each do |m|
70
+ @dispatcher.connect(name, method(m))
71
+ end
72
+ end
73
+ end
74
+ end
75
+ if instance_method(:initialize) != instance_method(:initialize_and_setup_observers)
76
+ alias_method :initialize_without_observer, :initialize
77
+ alias_method :initialize, :initialize_and_setup_observers
78
+ end
79
+ end
80
+ end
81
+
82
+ def observers_to_set_up
83
+ @observers_to_set_up ||= {}
84
+ end
85
+
86
+ def observe(event_name)
87
+ @observer_next_event_name_to_observe = event_name
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,3 @@
1
+ module Observatory
2
+ VERSION = '0.0.1'
3
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require 'observatory'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'observatory'
7
+ s.version = Observatory::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ['Arjan van der Gaag']
10
+ s.email = ['arjan@arjanvandergaag.nl']
11
+ s.homepage = ""
12
+ s.summary = "A simple implementation of the observer pattern for Ruby programs."
13
+ s.description = %q{Observatory is a simple gem to facilitate loosely-coupled communication between Ruby objects. It implements the observer design pattern so that your objects can publish events that other objects can subscribe to. Observatory provides some syntactic sugar and methods to notify events, filter values and allow observing objects to stop the filter chain. Observatory is inspired by the Event Dispatcher Symfony component.}
14
+
15
+ s.rubyforge_project = 'observatory'
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_development_dependency 'yard', '~>0.6'
23
+ s.add_development_dependency 'bluecloth', '~>2.1'
24
+ s.add_development_dependency 'rake', '~>0.8'
25
+ end
@@ -0,0 +1,78 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class DispatcherTest < Test::Unit::TestCase
4
+ def setup
5
+ @dispatcher = Dispatcher.new
6
+ @method = method(:example_observer_method)
7
+ end
8
+
9
+ def example_observer_method
10
+ # does nothing
11
+ end
12
+
13
+ def test_should_start_with_empty_list_of_observers
14
+ assert_equal({}, @dispatcher.observers)
15
+ end
16
+
17
+ def test_connecting_an_observer_using_a_method
18
+ @dispatcher.connect('signal', @method)
19
+ assert @dispatcher.observers['signal'].include?(@method)
20
+ end
21
+
22
+ def test_connecting_an_oversver_using_a_block
23
+ @dispatcher.connect('signal') do
24
+ # does nothing
25
+ end
26
+ assert_equal 1, @dispatcher.observers['signal'].size
27
+ end
28
+
29
+ def test_connecting_nothing_should_raise_exception
30
+ assert_raise(ArgumentError) { @dispatcher.connect('signal') }
31
+ end
32
+
33
+ def test_disconnecting_a_new_observer_returns_nil
34
+ assert_nil @dispatcher.disconnect('signal', @method)
35
+ end
36
+
37
+ def test_disconnecting_an_exisiting_observer_removes_it_from_stack
38
+ @dispatcher.connect('signal', @method)
39
+ assert_equal 1, @dispatcher.observers['signal'].size
40
+ @dispatcher.disconnect('signal', @method)
41
+ assert_equal 0, @dispatcher.observers['signal'].size
42
+ end
43
+
44
+ def test_notify_calls_all_observers
45
+ flag1 = false
46
+ flag2 = false
47
+ @dispatcher.connect('signal') { flag1 = true }
48
+ @dispatcher.connect('signal') { flag2 = true }
49
+ @dispatcher.notify(Event.new('observable', 'signal'))
50
+ assert flag1
51
+ assert flag2
52
+ end
53
+
54
+ def test_notify_calls_all_observers_in_order
55
+ output = ''
56
+ @dispatcher.connect('signal') { output << 'a' }
57
+ @dispatcher.connect('signal') { output << 'b' }
58
+ @dispatcher.notify(Event.new('observable', 'signal'))
59
+ assert_equal('ab', output)
60
+ end
61
+
62
+ def test_using_notify_until_calls_all_observers_until_one_returns_true
63
+ output = ''
64
+ @dispatcher.connect('signal') { output << 'a'; true }
65
+ @dispatcher.connect('signal') { output << 'b' }
66
+ @dispatcher.notify_until(Event.new('observable', 'signal'))
67
+ assert_equal('a', output)
68
+ end
69
+
70
+ def test_using_filter_uses_original_value_as_default_return_value
71
+ assert_equal 'foo', @dispatcher.filter(Event.new('observable', 'signal'), 'foo').return_value
72
+ end
73
+
74
+ def test_using_filter_uses_adjusted_value_as_default_return_value
75
+ @dispatcher.connect('signal') { |e,v| v.upcase }
76
+ assert_equal 'FOO', @dispatcher.filter(Event.new('observable', 'signal'), 'foo').return_value
77
+ end
78
+ end
@@ -0,0 +1,40 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class EventTest < Test::Unit::TestCase
4
+ def setup
5
+ @event = Event.new('observable', 'signal', :foo => 'bar')
6
+ end
7
+
8
+ def test_works_like_a_hash
9
+ assert_equal('bar', @event[:foo])
10
+ assert_kind_of(Hash, @event)
11
+ end
12
+
13
+ def test_should_be_processed_by_default
14
+ assert !@event.processed?
15
+ end
16
+
17
+ def test_should_be_marked_processed
18
+ @event.process!
19
+ assert @event.processed?
20
+ end
21
+
22
+ def test_require_observable_and_signal
23
+ assert_raise(ArgumentError) { Event.new }
24
+ assert_raise(ArgumentError) { Event.new('foo') }
25
+ end
26
+
27
+ def test_do_not_require_parameters
28
+ assert_nothing_raised { Event.new('foo', 'bar') }
29
+ end
30
+
31
+ def test_use_signal_as_string
32
+ event = Event.new('foo', 123)
33
+ assert_equal('123', event.signal)
34
+ end
35
+
36
+ def test_has_a_return_value_attribute
37
+ @event.return_value = :foo
38
+ assert_equal(:foo, @event.return_value)
39
+ end
40
+ end
@@ -0,0 +1,78 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+ require 'stringio'
3
+
4
+ class Post
5
+ include Observatory::Observable
6
+
7
+ attr_reader :title
8
+
9
+ def initialize(title, dispatcher)
10
+ @title, @dispatcher = title, dispatcher
11
+ end
12
+
13
+ def publish
14
+ notify('post.publish', :title => title)
15
+ end
16
+
17
+ def title
18
+ filter('post.title', @title).return_value
19
+ end
20
+
21
+ def save
22
+ event = notify_until('post.save', :title => title)
23
+ raise Exception, 'Saving is not implemented!' unless event.processed?
24
+ end
25
+ end
26
+
27
+ class Spy
28
+ include Observatory::Observer
29
+
30
+ attr_reader :buffer
31
+
32
+ def initialize(dispatcher, buffer)
33
+ @dispatcher, @buffer = dispatcher, buffer
34
+ end
35
+
36
+ observe 'post.publish'
37
+ def log_publication(event)
38
+ buffer.puts "Post titled #{event[:title]} was published"
39
+ end
40
+
41
+ observe 'post.title'
42
+ def title_filter(event, value)
43
+ value.upcase
44
+ end
45
+
46
+ observe 'post.save'
47
+ def save_post_to_output(event)
48
+ buffer.puts "Saving post titled #{event[:title]}"
49
+ true
50
+ end
51
+ end
52
+
53
+ class IntegrationTest < Test::Unit::TestCase
54
+ def setup
55
+ @buffer = StringIO.new
56
+ @dispatcher = Observatory::Dispatcher.new
57
+ @post = Post.new('My new post', @dispatcher)
58
+ @spy = Spy.new(@dispatcher, @buffer)
59
+ end
60
+
61
+ def test_notify
62
+ @dispatcher.disconnect('post.title', @spy.method(:title_filter))
63
+ @post.publish
64
+ assert_equal("Post titled My new post was published\n", @buffer.string)
65
+ end
66
+
67
+ def test_filter
68
+ assert_equal("MY NEW POST", @post.title)
69
+ end
70
+
71
+ def test_notify_until
72
+ @dispatcher.disconnect('post.title', @spy.method(:title_filter))
73
+ assert_nothing_raised(Exception) { @post.save }
74
+ assert_equal("Saving post titled My new post\n", @buffer.string)
75
+ @dispatcher.disconnect('post.save', @spy.method(:save_post_to_output))
76
+ assert_raise(Exception) { @post.save }
77
+ end
78
+ end
@@ -0,0 +1,7 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class ObservableTest < Test::Unit::TestCase
4
+ def test_truth
5
+ assert true
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class ObserverTest < Test::Unit::TestCase
4
+ def test_truth
5
+ assert true
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ require 'test/unit'
2
+ require 'rubygems'
3
+ require 'bundler'
4
+ Bundler.require(:default, :development)
5
+
6
+ class Test::Unit::TestCase
7
+ include Observatory
8
+ end
data/watch_tests.rb ADDED
@@ -0,0 +1,62 @@
1
+ def run(cmd)
2
+ puts cmd
3
+ `#{cmd}`
4
+ end
5
+
6
+ def run_all_tests
7
+ system 'rake test'
8
+ end
9
+
10
+ def growl(message)
11
+ growlnotify = `which growlnotify`.chomp
12
+ title = "Watchr Test Results"
13
+ passed = message.include?('0 failures, 0 errors')
14
+ image = passed ? "~/.watchr_images/passed.png" : "~/.watchr_images/failed.png"
15
+ severity = passed ? "-1" : "1"
16
+ options = "-w -n Watchr --image '#{File.expand_path(image)}'"
17
+ options << " -m '#{message}' '#{title}' -p #{severity}"
18
+ system %(#{growlnotify} #{options} &)
19
+ end
20
+
21
+ # --------------------------------------------------
22
+ # Watchr Rules
23
+ # --------------------------------------------------
24
+ watch('^lib.*/(.*)\.rb') do |m|
25
+ result = run("ruby test/#{m[1]}_test.rb")
26
+ growl result.split("\n").last
27
+ end
28
+
29
+ watch('test.*/teststrap\.rb') do
30
+ run_all_tests
31
+ end
32
+
33
+ watch('^test/(.*)_test\.rb') do |m|
34
+ result = run("ruby test/#{m[1]}_test.rb")
35
+ growl result.split("\n").last
36
+ end
37
+
38
+
39
+ # --------------------------------------------------
40
+ # Signal Handling
41
+ # --------------------------------------------------
42
+ # Ctrl-\
43
+ Signal.trap('QUIT') do
44
+ puts " --- Running all tests ---\n\n"
45
+ run_all_tests
46
+ end
47
+
48
+ @interrupted = false
49
+
50
+ # Ctrl-C
51
+ Signal.trap 'INT' do
52
+ if @interrupted then
53
+ @wants_to_quit = true
54
+ abort("\n")
55
+ else
56
+ puts "Interrupt a second time to quit"
57
+ @interrupted = true
58
+ Kernel.sleep 1.5
59
+ # raise Interrupt, nil # let the run loop catch it
60
+ run_suite
61
+ end
62
+ end
metadata ADDED
@@ -0,0 +1,136 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: observatory
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Arjan van der Gaag
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-05-01 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: yard
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 7
29
+ segments:
30
+ - 0
31
+ - 6
32
+ version: "0.6"
33
+ type: :development
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: bluecloth
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ hash: 1
44
+ segments:
45
+ - 2
46
+ - 1
47
+ version: "2.1"
48
+ type: :development
49
+ version_requirements: *id002
50
+ - !ruby/object:Gem::Dependency
51
+ name: rake
52
+ prerelease: false
53
+ requirement: &id003 !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ~>
57
+ - !ruby/object:Gem::Version
58
+ hash: 27
59
+ segments:
60
+ - 0
61
+ - 8
62
+ version: "0.8"
63
+ type: :development
64
+ version_requirements: *id003
65
+ description: Observatory is a simple gem to facilitate loosely-coupled communication between Ruby objects. It implements the observer design pattern so that your objects can publish events that other objects can subscribe to. Observatory provides some syntactic sugar and methods to notify events, filter values and allow observing objects to stop the filter chain. Observatory is inspired by the Event Dispatcher Symfony component.
66
+ email:
67
+ - arjan@arjanvandergaag.nl
68
+ executables: []
69
+
70
+ extensions: []
71
+
72
+ extra_rdoc_files: []
73
+
74
+ files:
75
+ - .gitignore
76
+ - Gemfile
77
+ - Gemfile.lock
78
+ - HISTORY
79
+ - LICENSE
80
+ - README.md
81
+ - Rakefile
82
+ - lib/observatory.rb
83
+ - lib/observatory/dispatcher.rb
84
+ - lib/observatory/event.rb
85
+ - lib/observatory/observable.rb
86
+ - lib/observatory/observer.rb
87
+ - lib/observatory/version.rb
88
+ - observatory.gemspec
89
+ - test/dispatcher_test.rb
90
+ - test/event_test.rb
91
+ - test/integration_test.rb
92
+ - test/observable_test.rb
93
+ - test/observer_test.rb
94
+ - test/test_helper.rb
95
+ - watch_tests.rb
96
+ homepage: ""
97
+ licenses: []
98
+
99
+ post_install_message:
100
+ rdoc_options: []
101
+
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ hash: 3
110
+ segments:
111
+ - 0
112
+ version: "0"
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ hash: 3
119
+ segments:
120
+ - 0
121
+ version: "0"
122
+ requirements: []
123
+
124
+ rubyforge_project: observatory
125
+ rubygems_version: 1.7.2
126
+ signing_key:
127
+ specification_version: 3
128
+ summary: A simple implementation of the observer pattern for Ruby programs.
129
+ test_files:
130
+ - test/dispatcher_test.rb
131
+ - test/event_test.rb
132
+ - test/integration_test.rb
133
+ - test/observable_test.rb
134
+ - test/observer_test.rb
135
+ - test/test_helper.rb
136
+ has_rdoc: