Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Bringing publisher into atomicobject.rb project

git-svn-id: http://atomicobjectrb.rubyforge.org/svn/trunk/publisher@19 7bd720c3-caf9-44c9-a48d-612f81e63739
  • Loading branch information...
commit bdbcc94d87dd47b289ac203386ea4d7b903be126 0 parents
dcrosby42 authored
3  History.txt
@@ -0,0 +1,3 @@
+== 1.1.0 / 2007-11-21
+
+* Moved publisher out of internal AO repository into atomicobject.rb project.
7 Manifest.txt
@@ -0,0 +1,7 @@
+History.txt
+Manifest.txt
+README.txt
+Rakefile
+lib/publisher.rb
+test/publisher_test.rb
+test/test_helper.rb
75 README.txt
@@ -0,0 +1,75 @@
+publisher
+* http://rubyforge.org/projects/atomicobjectrb/
+* http://atomicobjectrb.rubyforge.org/constructor
+
+== DESCRIPTION:
+
+publisher is a module for extending a class with event subscription and firing capabilities. This is helpful for implementing objects that participate in the Observer design pattern.
+
+== FEATURES/PROBLEMS:
+
+* Nice syntax for declaring events that can be subscribed for
+* Convenient event firing syntax
+* Subscribe / unsubscribe functionality
+* Several method name aliases give you the flexibility to make your code more readable (eg, *fire*, *notify*, *emit*)
+
+== SYNOPSIS:
+
+ require 'rubygems'
+ require 'publisher'
+
+ class CustomerListHolder
+ extend Publisher
+ can_fire :customer_added, :list_cleared
+
+ def add_customer(cust)
+ (@list ||= []) << cust
+ fire :customer_added, cust
+ end
+
+ def clear_list
+ @list.clear
+ fire :list_cleared
+ end
+ end
+
+ holder = CustomerListHolder.new
+ holder.when :customer_added do |cust|
+ puts "Customer added! #{cust}"
+ end
+ holder.when :list_cleared do
+ puts "All gone."
+ end
+
+ holder.add_customer("Croz")
+ holder.add_customer("Matt")
+ holder.clear_list
+
+== INSTALL:
+
+* sudo gem install publisher
+
+== LICENSE:
+
+(The MIT License)
+
+Copyright (c) 2007 Atomic Object
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+'Software'), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27 Rakefile
@@ -0,0 +1,27 @@
+require 'rubygems'
+require 'hoe'
+require './lib/publisher.rb'
+require 'rake'
+require 'rake/testtask'
+
+task :default => [ :test ]
+
+desc "Run the unit tests in test"
+Rake::TestTask.new("test") { |t|
+ t.libs << "test"
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+}
+
+Hoe.new('publisher', Publisher::VERSION) do |p|
+ p.rubyforge_name = 'atomicobjectrb'
+ p.author = 'Atomic Object'
+ p.email = 'dev@atomicobject.com'
+ p.summary = 'Event subscription and firing mechanism'
+ p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
+ p.url = p.paragraphs_of('README.txt', 1).first.gsub(/\* /,'').split(/\n/)
+# p.url = p.paragraphs_of('README.txt', 1).first.split(/\n/)[1..-1]
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
+end
+
+# vim: syntax=Ruby
73 lib/publisher.rb
@@ -0,0 +1,73 @@
+# See README.txt for synopsis
+module Publisher
+ VERSION = "1.1.0" #:nodoc:#
+
+ # Use this method (or one of the aliases) to declare which events you support
+ # Once invoked, your class will have the neccessary supporting methods for subscribing and firing.
+ def has_events(*args)
+ include InstanceMethods unless @published_events
+ @published_events ||= []
+ @published_events << args
+ @published_events.flatten!
+ end
+ alias :has_event :has_events
+ alias :can_fire :has_events
+
+ # Use this method to allow subscription and firing of arbitrary events.
+ # This is convenient if, eg, your class has dynamic event names.
+ # Don't use this unless you have to; it's better to declare your events if you
+ # can.
+ def has_any_event
+ include InstanceMethods unless @published_events
+ @published_events = :any_event_is_ok
+ end
+ alias :can_fire_anything :has_any_event
+
+ # Container for the instance methods that will be mixed-in to extenders of Publisher.
+ # These methods get mixed in when you use the 'has_events' call.
+ module InstanceMethods
+ # Sign up a code block to be executed when an event is fired.
+ # It's important to know the signature of the event, as your proc needs
+ # to accept incoming parameters accordingly.
+ def subscribe(event, &block)
+ ensure_valid event
+ @subscriptions ||= {}
+ listeners = @subscriptions[event]
+ listeners ||= []
+ listeners << block
+ @subscriptions[event] = listeners
+ end
+ alias :when :subscribe
+ alias :on :subscribe
+
+ # Unsubscribe for an event. 'listener' is a reference to the object who enacted the
+ # subscription... often, this is 'self'. If this object has subsribed more than once
+ # for the given event (unusual), all of the subscriptions will be removed.
+ def unsubscribe(event, listener)
+ ensure_valid event
+ if @subscriptions && @subscriptions[event]
+ @subscriptions[event].delete_if do |block|
+ eval('self',block.binding).equal?(listener)
+ end
+ end
+ end
+
+ protected
+ # Fire an event with 0 or more outbound parameters
+ def fire(event, *args) #:nod
+ ensure_valid event
+ listeners = @subscriptions[event] if @subscriptions
+ listeners.each do |l| l.call(*args) end if listeners
+ end
+ alias :emit :fire
+ alias :notify :fire
+
+ # Does nothing if the current class supports the named event.
+ # Raises RuntimeError otherwise.
+ def ensure_valid(event) #:nodoc:#
+ events = self.class.class_eval { @published_events }
+ return if events == :any_event_is_ok
+ raise "Event '#{event}' not available" unless events and events.include?(event)
+ end
+ end
+end
29 sample_code/synopsis.rb
@@ -0,0 +1,29 @@
+require 'rubygems'
+require 'publisher'
+
+class CustomerListHolder
+ extend Publisher
+ can_fire :customer_added, :list_cleared
+
+ def add_customer(cust)
+ (@list ||= []) << cust
+ fire :customer_added, cust
+ end
+
+ def clear_list
+ @list.clear
+ fire :list_cleared
+ end
+end
+
+holder = CustomerListHolder.new
+holder.when :customer_added do |cust|
+ puts "Customer added! #{cust}"
+end
+holder.when :list_cleared do
+ puts "All gone."
+end
+
+holder.add_customer("Croz")
+holder.add_customer("Matt")
+holder.clear_list
358 test/publisher_test.rb
@@ -0,0 +1,358 @@
+require File.expand_path(File.dirname(__FILE__) + '/test_helper')
+require 'publisher'
+
+class PublisherTest < Test::Unit::TestCase
+
+
+ #
+ # TESTS
+ #
+
+ def test_unsubscribe_will_remove_subscription_for_correct_event
+ obj = SeveralWays.new
+ out = []
+
+ obj.on :eviction do
+ out << 'boom'
+ end
+ obj.on :cry do
+ out << 'pow'
+ end
+ obj.on :employee do
+ out << 'kaboom'
+ end
+
+ obj.unsubscribe :cry, self
+
+ obj.do_eviction
+ obj.do_cry
+ obj.do_employee
+ assert_equal ['boom','kaboom'], out
+ end
+
+ def test_unsubsribe_will_raise_if_event_not_valid
+ obj = SeveralWays.new
+ assert_raise RuntimeError do
+ obj.unsubscribe :not_there, self
+ end
+ end
+
+ def test_unsubscribe_will_not_care_if_no_previous_subscription_was_made
+ obj = SeveralWays.new
+ obj.unsubscribe :cry, self
+ end
+
+ def test_unsubscribe_can_be_called_multiple_times
+ obj = SeveralWays.new
+ out = []
+ obj.on :cry do
+ out << 'a'
+ end
+ obj.unsubscribe :cry, self
+ obj.unsubscribe :cry, self
+ obj.do_cry
+ assert_equal [], out
+ end
+
+ def test_unsubscribe_only_unsubscribes_the_unsubscriber
+ something = Something.new
+ watcher = SomethingWatcher.new something
+
+ out = []
+ something.on :boom do
+ out << 'boom'
+ end
+
+ something.do_boom
+ assert_equal ['boom'], out
+ assert_equal ['boom'], watcher.observations
+
+ something.unsubscribe :boom, watcher
+ something.do_boom
+ assert_equal ['boom','boom'], out
+ assert_equal ['boom'], watcher.observations
+ end
+
+ def test_unsubscribe_removes_all_subscriptions_for_the_event_being_listened_for
+ something = Something.new
+ out = []
+ something.on :boom do
+ out << 'a'
+ end
+ something.on :boom do
+ out << 'b'
+ end
+ something.on :boom do
+ out << 'c'
+ end
+ something.do_boom
+ assert_equal ['a','b','c'], out
+ something.unsubscribe :boom, self
+ something.do_boom
+ assert_equal ['a','b','c'], out
+ end
+
+ def test_subscribe_and_fire
+ obj = Something.new
+
+ out = []
+ obj.subscribe :boom do out << 'boom' end
+ obj.do_boom
+ assert_equal ['boom'], out
+
+ out = []
+ obj.subscribe :pow do out << 'pow' end
+ obj.do_pow
+ assert_equal ['pow'], out
+ end
+
+ def test_alternate_event_declarators
+ obj = ManyEvents.new
+ [:a, :b, :c, :crunch, :rip, :shred, :rend].each do |event|
+ out = nil
+ obj.subscribe event do out = event end
+ obj.relay(event)
+ assert_equal event, out
+ end
+ end
+
+ def test_alternate_subscribe_and_fire_methods
+ obj = SeveralWays.new
+ out = nil
+ obj.subscribe :employee do out = :employee end
+ obj.on :cry do out = :cry end
+ obj.when :eviction do out = :eviction end
+
+ out = nil
+ obj.do_employee
+ assert_equal :employee, out
+
+ out = nil
+ obj.do_cry
+ assert_equal :cry, out
+
+ out = nil
+ obj.do_eviction
+ assert_equal :eviction, out
+ end
+
+ def test_fire_is_not_public
+ obj = Something.new
+ err = assert_raise NoMethodError do
+ obj.fire :boom
+ end
+ assert_match(/protected/i, err.message)
+ end
+
+ def test_subscribe_for_non_event
+ obj = Something.new
+ assert_raise RuntimeError do
+ obj.subscribe :not_there
+ end
+
+ obj = Nobody.new
+ assert_raise NoMethodError do
+ obj.subscribe :huh
+ end
+ end
+
+ def test_fire_non_event
+ obj = Broken.new
+ assert_raise RuntimeError do
+ obj.go
+ end
+ end
+
+ def test_fire_with_parameters
+ obj = Somebody.new
+ out = nil
+ obj.on :eat_this do |food|
+ out = food
+ end
+ obj.go "burger"
+ assert_equal "burger", out
+ end
+
+ def test_fire_when_nobodys_listening
+ # Make sure this doesn't explode
+ Something.new.do_boom
+ end
+
+ def test_parameter_mismatch_between_event_and_handler
+ obj = Somebody.new
+ out = nil
+ obj.on :eat_this do
+ out = 'huh'
+ end
+ obj.go "ignore me"
+ assert_equal 'huh', out
+ end
+
+# Cannot inherit events right now
+# class SomebodyKid < Somebody
+# end
+#
+# def test_inheriting_events
+# obj = SomebodyKid.new
+# out = nil
+# obj.on :eat_this do |food|
+# out = food
+# end
+# obj.go "taco"
+# assert_equal "taco", out
+# end
+
+ def test_extending_publisher_doesnt_affect_normal_inheritance
+ obj = Billy.new('wheel')
+ assert_equal 'wheel', obj.chair
+ out = nil
+ obj.subscribe :weapons do out = 'bang' end
+
+ obj.go
+ assert_equal 'bang', out
+ end
+
+ def test_has_any_event
+ sh = SuperHoss.new
+ got = nil
+ sh.when :awesome do |data|
+ got = data
+ end
+
+ sh.go "right on"
+ assert_equal "right on", got
+ end
+
+ def test_can_fire_anything
+ sdh = SuperDuperHoss.new
+ got = nil
+ sdh.when :really_awesome do |data|
+ got = data
+ end
+
+ sdh.go "right on"
+ assert_equal "right on", got
+ end
+
+ def test_multiple_subscriptions_for_same_event
+ obj = Somebody.new
+ out1 = nil
+ out2 = nil
+ obj.on :eat_this do |food|
+ out1 = food
+ end
+ obj.on :eat_this do |food|
+ out2 = food
+ end
+ obj.go "burger"
+ assert_equal "burger", out1, "First subscription no go"
+ assert_equal "burger", out2, "Second subscription no go"
+ end
+
+ #
+ # HELPERS
+ #
+
+ class SomethingWatcher
+ attr_reader :observations
+ def initialize(something)
+ @observations = []
+ something.on :boom do
+ @observations << 'boom'
+ end
+ end
+ end
+
+ class Something
+ extend Publisher
+ has_events :boom, :pow
+
+ def do_boom
+ fire :boom
+ end
+
+ def do_pow
+ fire :pow
+ end
+ end
+
+ class ManyEvents
+ extend Publisher
+ has_events :a, :b, :c
+ has_event :crunch
+ can_fire :rip
+ can_fire :shred, :rend
+ def relay(evt_sym)
+ fire evt_sym
+ end
+ end
+
+ class SeveralWays
+ extend Publisher
+ can_fire :cry, :eviction, :employee
+
+ def do_employee
+ fire :employee
+ end
+ def do_eviction
+ notify :eviction
+ end
+ def do_cry
+ emit :cry
+ end
+ end
+
+ class Broken
+ extend Publisher
+ can_fire :stuff
+
+ def go
+ fire :oops
+ end
+ end
+
+ class Nobody
+ extend Publisher
+ end
+
+ class Somebody
+ extend Publisher
+ can_fire :eat_this
+ def go(food)
+ fire :eat_this, food
+ end
+ end
+
+ class Grampa
+ attr_accessor :chair
+ def initialize(ch)
+ @chair = ch
+ end
+ end
+ class Billy < Grampa
+ extend Publisher
+ can_fire :weapons
+ def go
+ fire :weapons
+ end
+ end
+
+ class SuperHoss
+ extend Publisher
+ has_any_event
+
+ def go(arg)
+ fire :awesome, arg
+ end
+ end
+
+ class SuperDuperHoss
+ extend Publisher
+ can_fire_anything
+
+ def go(arg)
+ fire :really_awesome, arg
+ end
+ end
+
+end
20 test/test_helper.rb
@@ -0,0 +1,20 @@
+here = File.expand_path(File.dirname(__FILE__))
+$: << "#{here}/../lib"
+$: << "#{here}/../test"
+
+require 'test/unit'
+
+class Test::Unit::TestCase
+ # Prevent duplicate test methods
+ self.instance_eval { alias :old_method_added :method_added }
+ def self.method_added(method)
+ method = method.to_s
+ case method
+ when /^test_/
+ @_tracked_tests ||= {}
+ raise "Duplicate test #{method}" if @_tracked_tests[method]
+ @_tracked_tests[method] = true
+ end
+ old_method_added method
+ end
+end
Please sign in to comment.
Something went wrong with that request. Please try again.