diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..74c8f2e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/.rbenv-version +/pkg +Gemfile.lock diff --git a/.kick b/.kick new file mode 100644 index 0000000..a3b9522 --- /dev/null +++ b/.kick @@ -0,0 +1,15 @@ +require "kicker/utils" + +process do |files| + if files.any? { |f| /\.rb$/ =~ f } + execute "rake test" + end + + files.clear +end + +module Kicker::Utils + def log(message) + nil + end +end diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..8b4de24 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: ruby +rvm: + - 1.8.7 + - 1.9.2 + - 1.9.3 + - jruby-18mode + - jruby-19mode + - jruby-head + - rbx-18mode + - rbx-19mode + - ree + - ruby-head diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..d4998a9 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in watchable.gemspec +gemspec diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..c1b1e1a --- /dev/null +++ b/README.markdown @@ -0,0 +1,125 @@ +[![Build Status](https://secure.travis-ci.org/jbarnette/watchable.png)](http://travis-ci.org/jbarnette/watchable) + +# Watchable + +A simple event/notification mixin, reluctantly extracted to a gem. +This is code I've had floating around for a few years now, but I've +also incorporated a few extras from node.js' [EventEmitter][ee], +[jQuery][jq], and [Backbone.Events][be]. + +[ee]: http://nodejs.org/api/events.html#events_class_events_eventemitter +[jq]: http://api.jquery.com/on +[be]: http://documentcloud.github.com/backbone/#Events + +## Example + +### Fixtures + +```ruby +require "watchable" + +class Frob + include Watchable +end + +class Callable + def call *args + p :called! => args + end +end + +``` + +### Watching and Firing + +Events can have any number of watchers. Each watcher will be called +in order, and any args provided when the event is fired will be passed +along. Watchers will most commonly be blocks, but any object that +responds to `call` can be used instead. + +```ruby +frob = Frob.new + +frob.on :twiddle do |name| + puts "#{name} twiddled the frob!" +end + +frob.on :twiddle do |name| + puts "(not that there's anything wrong with that)" +end + +frob.on :twiddle, Callable.new +frob.fire :twiddle, "John" +``` + +#### Result + + John twiddled the frob! + (not that there's anything wrong with that) + { :called! => ["John"] } + +### Watching Once + +Only want to be notified the first time something happens? `once` is +like `on`, but fickle. + +```ruby +frob = Frob.new + +frob.on :twiddle do + p :twiddled! +end + +frob.fire :twiddle +frob.fire :twiddle +``` + +#### Result + + :twiddled! + +### Unwatching + +Specific blocks or callable objects can be removed from an event's +watchers, or all the event's watchers can be removed. + +```ruby +b = lambda {} +frob = Frob.new + +frob.on :twiddle, &b + +frob.off :twiddle, b # removes the 'b' watcher, same as frob.off :twiddle, &b +frob.off :twiddle # removes all watchers for the 'twiddle' event +``` + +## Compatibility + +Watchable is actively developed against MRI Ruby 1.8.7 as a least common +denominator, but is widely tested against other Ruby versions and +implementations. Check the [travis-ci][] page for details. + +[travis-ci]: http://travis-ci.org/jbarnette/watchable + +## License (MIT) + +Copyright 2012 John Barnette (john@jbarnette.com) + +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. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..7a3daea --- /dev/null +++ b/Rakefile @@ -0,0 +1,9 @@ +require "bundler/gem_tasks" + +desc "Run the tests." +task :test do + $: << "lib" << "test" + Dir["test/*_test.rb"].each { |f| require f[5..-4] } +end + +task :default => :test diff --git a/lib/watchable.rb b/lib/watchable.rb new file mode 100644 index 0000000..ade2444 --- /dev/null +++ b/lib/watchable.rb @@ -0,0 +1,33 @@ +module Watchable + def watchers + @watchers ||= Hash.new { |h, k| h[k] = [] } + end + + def fire event, *args + watchers[event].each { |w| w && w.call(*args) } + + self + end + + def on event, callable = nil, &block + watchers[event] << (callable || block) + + self + end + + def once event, callable = nil, &block + wrapper = lambda do |*args| + off event, wrapper + (callable || block).call *args + end + + on event, wrapper + end + + def off event, callable = nil, &block + watcher = callable || block + watcher ? watchers[event].delete(watcher) : watchers[event].clear + + self + end +end diff --git a/test/watchable_test.rb b/test/watchable_test.rb new file mode 100644 index 0000000..3260279 --- /dev/null +++ b/test/watchable_test.rb @@ -0,0 +1,110 @@ +require "minitest/autorun" +require "mocha" +require "watchable" + +describe Watchable do + before do + @obj = Object.new + @obj.extend Watchable + end + + it "has an empty list of watchers by default" do + assert @obj.watchers.empty? + end + + it "returns an empty array of watchers for any event" do + assert_equal [], @obj.watchers[:foo] + end + + describe :fire do + it "calls each watcher with optional args" do + @obj.on :foo, mock { expects(:call).with :bar, :baz } + @obj.fire :foo, :bar, :baz + end + + it "calls multiple watchers in order" do + fires = sequence "fires" + + @obj.on :foo, mock { expects(:call).in_sequence fires } + @obj.on :foo, mock { expects(:call).in_sequence fires } + + @obj.fire :foo + end + + it "ignores nil watchers" do + @obj.on :foo, nil + @obj.fire :foo + end + + it "returns the watchable" do + assert_same @obj, @obj.fire(:foo) + end + end + + describe :off do + it "can unregister a block" do + b = lambda {} + + @obj.on :foo, &b + @obj.off :foo, &b + + assert @obj.watchers[:foo].empty? + end + + it "can unregister an object" do + b = lambda {} + + @obj.on :foo, &b + @obj.off :foo, &b + + assert @obj.watchers[:foo].empty? + end + + it "can unregister all watchers for an event" do + @obj.on(:foo) {} + @obj.on(:foo) {} + + assert_equal 2, @obj.watchers[:foo].size + + @obj.off :foo + assert @obj.watchers[:foo].empty? + end + + it "returns the watchable" do + assert_same @obj, @obj.off(:foo) + end + end + + describe :on do + it "can register a block" do + b = lambda {} + + @obj.on :foo, &b + assert_equal [b], @obj.watchers[:foo] + end + + it "can register an object" do + b = lambda {} + + @obj.on :foo, b + assert_equal [b], @obj.watchers[:foo] + end + + it "returns the watchable" do + assert_same @obj, @obj.on(:foo) {} + end + end + + describe :once do + it "registers a watcher that's only called on the first fire" do + @obj.once :foo, mock { expects :call } + + @obj.fire :foo + @obj.fire :foo + end + + it "returns the watchable" do + assert_same @obj, @obj.once(:foo) {} + end + end +end diff --git a/watchable.gemspec b/watchable.gemspec new file mode 100644 index 0000000..3ef712a --- /dev/null +++ b/watchable.gemspec @@ -0,0 +1,16 @@ +Gem::Specification.new do |gem| + gem.authors = ["John Barnette"] + gem.email = ["john@jbarnette.com"] + gem.description = "A simple event mixin, reluctantly extracted to a gem." + gem.summary = "Watch an object for events." + gem.homepage = "https://github.com/jbarnette/watchable" + + gem.files = `git ls-files`.split "\n" + gem.test_files = `git ls-files -- test/*`.split "\n" + gem.name = "watchable" + gem.require_paths = ["lib"] + gem.version = "0.0.0" + + gem.add_development_dependency "minitest" + gem.add_development_dependency "mocha" +end