cross-talk 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 1.9.3
5
+ - jruby-19mode # JRuby in 1.9 mode
6
+ - rbx-19mode
data/Gemfile CHANGED
@@ -1,4 +1,14 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in cross-talk.gemspec
3
+ gem 'rake'
4
+
5
+ gem 'rspec'
6
+ gem 'rspec-spies'
7
+ gem 'coveralls', require: false
8
+ gem 'flay'
9
+ gem 'flog'
10
+ gem 'mutant'
11
+
12
+ gem 'pry'
13
+
4
14
  gemspec
data/NOTES.md ADDED
@@ -0,0 +1,58 @@
1
+ # Cross::Talk -- Event Pub/Sub for PORC
2
+
3
+ ## Design Notes
4
+
5
+ At require-time, create a `Cross::Talk::Manager` instance (a singleton). This
6
+ serves as the clearinghouse for all events. It is a celluloid actor.
7
+
8
+ NB. Though cross-talk is perfectly happy to run on MRI, you'll probably have
9
+ performance issues unless you run on RBX/JRuby, because we are likely sending
10
+ _lots_ of messages at any given point in time, and that means we're probably
11
+ eating a lot of CPU cycles.
12
+
13
+ Every public method call triggers and asynchronous notification to the
14
+ `Cross::Talk::Manager` (henceforth, `CTM`). Containing the method invoked, the
15
+ object that invoked it, and `:before` or `:after` (depending on which side of
16
+ the method you're on).
17
+
18
+ Another class can subscribe to any event by simply using the class macro
19
+ `#listen`, a la:
20
+
21
+
22
+ class Foo
23
+ include Celluloid::Actor
24
+ include Celluloid::Logger
25
+ include Cross::Talk
26
+
27
+ def bar
28
+ info "baz"
29
+ end
30
+ end
31
+
32
+ class Listener
33
+ include Celluloid::Actor
34
+ include Celluloid::Logger
35
+ include Cross::Talk
36
+
37
+ listen Foo, :bar, :before do
38
+ info "before Foo#bar"
39
+ end
40
+
41
+ listen "Foo#bar:after" do |obj|
42
+ info "Calling terminate! on Foo instance"
43
+ obj.terminate!
44
+ end
45
+ end
46
+
47
+
48
+ Listener.new
49
+ Foo.new.bar
50
+
51
+ #=> before Foo#bar
52
+ #=> baz
53
+ #=> Calling terminate! on Foo instance
54
+ #=> <Celluloid::Actor Foo Terminated>
55
+
56
+ (mod output formatting).
57
+
58
+
data/README.md CHANGED
@@ -1,9 +1,19 @@
1
- # Cross::Talk
1
+ # Cross::Talk [![Gem Version](https://badge.fury.io/rb/cross-talk.png)](http://badge.fury.io/rb/cross-talk) [![Build Status](https://travis-ci.org/jfredett/cross-talk.png?branch=master)](http://travis-ci.org/jfredett/cross-talk) [![Code Climate](https://codeclimate.com/github/jfredett/cross-talk.png)](https://codeclimate.com/github/jfredett/cross-talk) [![Coverage Status](https://coveralls.io/repos/jfredett/cross-talk/badge.png?branch=master)](https://coveralls.io/r/jfredett/cross-talk)
2
2
 
3
3
  NOTA BENE:
4
4
 
5
- This is primarily lies. This is not production ready, this isn't even really
6
- worthy of being read. But it'll be cool when it's done.
5
+ This is not production ready, the basic functionality is there, but use in
6
+ critical code is at your own risk.
7
+
8
+ Also, this thing is almost certainly going to give you performance problems of
9
+ the most severe variety.
10
+
11
+ ## Support
12
+
13
+ Check CI for edge support, but ideally we support MRI > 1.9, (including 2.0),
14
+ Reasonably recent RBX, and JRuby.
15
+
16
+ JRuby is broken right now though for unknown reasons.
7
17
 
8
18
  ## Installation
9
19
 
@@ -21,7 +31,52 @@ Or install it yourself as:
21
31
 
22
32
  ## Usage
23
33
 
24
- TODO: Write usage instructions here
34
+ For any object (not just Celluloid Actors, though they'll work the best) simply
35
+ include `Cross::Talk` and enjoy the following features for that class:
36
+
37
+ 1. Any public method will automatically send out two events -- one at the
38
+ beginning of execution, the other at the end. These are identified by the
39
+ schema: `<class>#<method>:<time>`. Eg. for a class `Foo`, and a method `bar`,
40
+ calling `Foo.new.bar` would send an event `Foo#bar:before`, and then a
41
+ `Foo#bar:after`. Note that the event is the same for any instance of the
42
+ class, then read the "Plans" section, item 2.
43
+
44
+ 2. Any Cross::Talk class can bind to an event by using the `listen` macro at
45
+ define-time
46
+
47
+ 3. Any instance of a Cross::Talk class can bind to an event later, without
48
+ forcing every other instance to also bind to that event. Think of the
49
+ difference between `define_method` and `define_singleton_method` (in fact
50
+ they are implemented precisely that way)
51
+
52
+ 4. Celluloid Actors which include `Cross::Talk` will have the events sent to
53
+ them asynchronously, so the event handler won't block while trying to
54
+ dispatch those events
55
+
56
+
57
+ ## Plans
58
+
59
+ 1. Improve the `listen` macro so you don't always need to supply an argument --
60
+ it should just be ignored if it's not there.
61
+
62
+ 2. Allow `listen` to bind to a specific _instance_ of an event, rather than just
63
+ the whole class of events.
64
+
65
+ 3. Refactor the codebase, it's a bit sprawling right now
66
+
67
+ 4. Optimize for dispatch speed -- basically make it as lightweight as possible
68
+
69
+ 5. Make the Event Dispatcher maybe use some thread primitives during dispatch
70
+ around non-actors, so that we can join at the end and still send them
71
+ asynchronous events?
72
+
73
+ ### Pipe dreams
74
+
75
+ 1. Optionally back the event manager with a message queue, because why the hell
76
+ not? It's worth a try, maybe it'll do something neat.
77
+
78
+ 2. Experiment with making this usable efficiently over DCell. Including making
79
+ the Event Manager run as a cluster of actors, notifying remote actors, etc.
25
80
 
26
81
  ## Contributing
27
82
 
data/Rakefile CHANGED
@@ -1 +1,35 @@
1
- require "bundler/gem_tasks"
1
+ #!/usr/bin/env rake
2
+ require 'bundler/setup'
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ desc 'run specs'
7
+ RSpec::Core::RakeTask.new do |task|
8
+ task.rspec_opts = ["-c", "-f progress"]
9
+ end
10
+
11
+ task :default => :spec
12
+
13
+
14
+ task :flog do
15
+ puts "#### FLOG ####"
16
+ system 'flog lib/*'
17
+ puts "##############"
18
+ end
19
+
20
+ task :flay do
21
+ puts "#### FLAY ####"
22
+ system 'flay lib/*'
23
+ puts "##############"
24
+ end
25
+
26
+ task :mutant, [:klass] do |_, args|
27
+ puts "#### MUTANT TESTING ####"
28
+ system "mutant -I lib -r cross-talk --rspec-full #{args[:name] || '::Katuv'}"
29
+ puts "########################"
30
+ end
31
+
32
+ task :metrics => [:flog, :flay]
33
+
34
+
35
+ task :all => [:spec, :mutant, :metrics]
data/cross-talk.gemspec CHANGED
@@ -16,4 +16,6 @@ Gem::Specification.new do |gem|
16
16
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
  gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency 'celluloid'
19
21
  end
data/lib/cross-talk.rb CHANGED
@@ -1,7 +1,20 @@
1
+ require 'celluloid'
2
+
1
3
  require "cross-talk/version"
4
+ require 'cross-talk/manager'
5
+ require 'cross-talk/listener'
2
6
 
3
7
  module Cross
4
8
  module Talk
5
- # Your code goes here...
9
+ # Access the Event Manager
10
+ def self.manager
11
+ Celluloid::Actor[:manager] ||= Cross::Talk::Manager.new
12
+ end
13
+
14
+ def self.included(base)
15
+ base.send(:include, Listener)
16
+ end
6
17
  end
7
18
  end
19
+
20
+
@@ -0,0 +1,123 @@
1
+ module Cross
2
+ module Talk
3
+ module Listener
4
+ def self.included(base)
5
+ base.send(:include, InstanceMethods)
6
+ base.send(:extend, ClassMethods)
7
+ base.send(:include, Celluloid::Logger)
8
+ base.send(:extend, Celluloid::Logger)
9
+
10
+ # This makes sure that when we add a new hook at runtime, we rebuild all
11
+ # of our hooks so that they're up-to-date.
12
+ base.instance_eval do
13
+ listen base, :__new_hook, :before do
14
+ register_hooks!
15
+ end
16
+ end
17
+ end
18
+
19
+ module InstanceMethods
20
+ def register_hooks!
21
+ self.class.registration_hooks.each { |hook| hook.call(__get_receiver) }
22
+ end
23
+
24
+ def initialize(*_)
25
+ register_hooks!
26
+ super
27
+ end
28
+
29
+ def silenced?
30
+ @__silenced
31
+ end
32
+
33
+ def silence!
34
+ @__silenced = true
35
+ end
36
+
37
+ def unsilence!
38
+ @__silenced = false
39
+ end
40
+
41
+ def silently(&block)
42
+ silence!
43
+ block.call
44
+ unsilence!
45
+ end
46
+
47
+ def listen(klass, event, timing, &block)
48
+ event_name = "#{klass}##{event}:#{timing}"
49
+ Cross::Talk.manager.notify('__new_hook:before', event_name)
50
+ define_singleton_method event_name, &block
51
+ Cross::Talk.manager.register(event_name, __get_receiver)
52
+ Cross::Talk.manager.notify('__new_hook:after', event_name)
53
+ end
54
+
55
+ private
56
+
57
+ def __notify_event(method, timing)
58
+ return if Object.methods.include?(method)
59
+ return unless public_methods(false).include?(method)
60
+ return if silenced?
61
+
62
+ receiver = __get_receiver
63
+ event_name = "#{receiver.class}##{method}:#{timing}"
64
+
65
+ Cross::Talk.manager.notify(event_name, receiver)
66
+ end
67
+
68
+ def __get_receiver
69
+ return Celluloid::Actor.current if is_a?(Celluloid)
70
+ return self
71
+ end
72
+ end
73
+
74
+ module ClassMethods
75
+ def registration_hooks
76
+ @registration_hooks ||= []
77
+ end
78
+
79
+ def listen(klass, event, timing, &block)
80
+ event_name = "#{klass}##{event}:#{timing}"
81
+ Cross::Talk.manager.notify('__new_hook:before', event_name)
82
+ define_hook! event_name, &block
83
+ registration_hooks << proc { |actor| Cross::Talk.manager.register(event_name, actor) }
84
+ Cross::Talk.manager.notify('__new_hook:after', event_name)
85
+ end
86
+
87
+ def notify(method_name)
88
+ class_eval %{
89
+ alias __old_#{method_name} #{method_name}
90
+
91
+ def #{method_name}(*args, &block)
92
+ __notify_event(#{method_name.inspect}, :before)
93
+ result = __old_#{method_name}(*args, &block)
94
+ __notify_event(#{method_name.inspect}, :after)
95
+ result
96
+ end
97
+ }
98
+
99
+ send(:private, method_name) if private_instance_methods.include?("__old_#{method_name}".to_sym)
100
+ send(:protected, method_name) if protected_instance_methods.include?("__old_#{method_name}".to_sym)
101
+
102
+ nil
103
+ end
104
+
105
+ def method_added(method)
106
+ # if we're redefining a method, the lock is set to true, so bug out.
107
+ return if @lock
108
+ #don't notify hook methods
109
+ return if method =~ /^.*#.*:.*$/
110
+ # don't notify pseudoprivate methods
111
+ return if method =~ /^__/
112
+ @lock = true
113
+ notify(method)
114
+ @lock = false
115
+ end
116
+
117
+ def define_hook!(method, &block)
118
+ define_method(method, &block)
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,38 @@
1
+ module Cross
2
+ module Talk
3
+ class Manager
4
+ include Celluloid
5
+ include Celluloid::Logger
6
+
7
+ def initialize
8
+ @event_table = {}
9
+ end
10
+
11
+ def notify(event, sender)
12
+ receivers_for(event).each do |receiver|
13
+ next unless receiver.respond_to?(event)
14
+ if receiver.respond_to?(:async)
15
+ receiver.async
16
+ else
17
+ receiver
18
+ end.send(event, sender)
19
+ end
20
+ end
21
+
22
+ def register(event, receiver)
23
+ receivers_for(event) << receiver
24
+ nil
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :event_table
30
+
31
+ def receivers_for(event)
32
+ # We need to ||= here to avoid a weird behavior with Hash.new { [] } --
33
+ # you can't destructively update the first element to it.
34
+ event_table[event] ||= Set.new
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,5 +1,5 @@
1
1
  module Cross
2
2
  module Talk
3
- VERSION = "0.0.1"
3
+ VERSION = "0.1.0"
4
4
  end
5
5
  end
@@ -0,0 +1,17 @@
1
+ module RSpec
2
+ module Cross
3
+ module Talk
4
+ module DSL
5
+ module Macros
6
+ def the(sym, &block)
7
+ context sym do
8
+ subject { if sym.is_a? Class then sym else send sym end }
9
+ it &block
10
+ end
11
+ end
12
+ alias the_class the
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+
3
+
4
+ describe Cross::Talk::Listener do
5
+ before :all do
6
+ class Sender
7
+ include Celluloid
8
+ include Cross::Talk
9
+
10
+ def test
11
+ end
12
+
13
+ def late
14
+ end
15
+
16
+ protected
17
+
18
+ def protected_method
19
+ end
20
+
21
+ private
22
+
23
+ def private_method
24
+ end
25
+ end
26
+
27
+ class Receiver
28
+ include Celluloid
29
+ include Cross::Talk
30
+
31
+ listen Sender, :test, :after do |sender|
32
+ @notified = true
33
+ end
34
+
35
+ listen Sender, :private_method, :after do |sender|
36
+ debug "Should never occur"
37
+ @notified = true
38
+ end
39
+
40
+ listen Sender, :protected_method, :after do |sender|
41
+ debug "Should never occur"
42
+ @notified = true
43
+ end
44
+
45
+ def has_been_notified?
46
+ @notified
47
+ end
48
+ end
49
+ end
50
+ after :all do
51
+ Object.send(:remove_const, :Receiver)
52
+ Object.send(:remove_const, :Sender)
53
+ end
54
+
55
+ let!(:receiver) { Receiver.new }
56
+ let!(:other_receiver) { Receiver.new }
57
+ let!(:sender) { Sender.new }
58
+
59
+ subject { receiver }
60
+
61
+ context 'late-bound listen' do
62
+
63
+ before do
64
+ receiver.listen(Sender, :late, :after) do |*_|
65
+ @notified = true
66
+ end
67
+ end
68
+
69
+ the(:receiver) { should_not have_been_notified }
70
+ the(:other_receiver) { should_not have_been_notified }
71
+
72
+ describe 'when the message is sent' do
73
+ before { sender.late }
74
+
75
+ the(:receiver) { should have_been_notified }
76
+ the(:other_receiver) { should_not have_been_notified }
77
+ end
78
+ end
79
+
80
+ describe 'define-time bound listen' do
81
+ it { should respond_to :"Sender#test:after" }
82
+ it { should_not have_been_notified }
83
+
84
+ describe 'calling the method' do
85
+ before { sender.test }
86
+
87
+ it { should have_been_notified }
88
+ end
89
+
90
+ describe 'calling a private method on the sender' do
91
+ before { sender.send(:private_method) }
92
+
93
+ it { should_not have_been_notified }
94
+ end
95
+
96
+ describe 'calling a protected method on the sender' do
97
+ before { sender.send(:protected_method) }
98
+
99
+ it { should_not have_been_notified }
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,76 @@
1
+ require 'spec_helper'
2
+
3
+
4
+ describe Cross::Talk::Manager do
5
+ subject(:manager) { Cross::Talk.manager }
6
+
7
+ before :all do
8
+ class Receiver
9
+ include Celluloid
10
+ include Celluloid::Logger
11
+
12
+ def initialize
13
+ Cross::Talk.manager.register('an_event', Actor.current)
14
+ end
15
+
16
+ def an_event(*_)
17
+ @notification = true
18
+ end
19
+
20
+ def has_received_notification?
21
+ @notification
22
+ end
23
+ end
24
+
25
+ class StupidReceiver
26
+ include Celluloid
27
+
28
+ def initialize
29
+ Cross::Talk.manager.register('an_event', Actor.current)
30
+ end
31
+
32
+ # I'm dumb because I didn't implement the event method!
33
+ end
34
+
35
+ class NonReceiver
36
+ include Celluloid
37
+
38
+ def initialize
39
+ Cross::Talk.manager.register('another_event', Actor.current)
40
+ end
41
+
42
+ def another_event(*_)
43
+ end
44
+ end
45
+ end
46
+
47
+ after :all do
48
+ Object.send(:remove_const, :Receiver)
49
+ Object.send(:remove_const, :NonReceiver)
50
+ Object.send(:remove_const, :StupidReceiver)
51
+ end
52
+
53
+
54
+ describe 'api' do
55
+ it { should respond_to :notify }
56
+ it { should respond_to :register }
57
+ end
58
+
59
+ let!(:receiver) { Receiver.new }
60
+ let!(:non_receiver) { NonReceiver.new }
61
+ let!(:stupid_receiver) { StupidReceiver.new }
62
+
63
+ before { manager.notify('an_event', nil) }
64
+
65
+ it 'notifies registered receivers when an event occurs' do
66
+ receiver.should have_received_notification
67
+ end
68
+
69
+ it 'only notifies registered receivers which actually define the event method' do
70
+ stupid_receiver.should_not have_received :an_event
71
+ end
72
+
73
+ it 'does not notify receivers of other messages of that message' do
74
+ non_receiver.should_not have_received :an_event
75
+ end
76
+ end
@@ -0,0 +1,32 @@
1
+ require 'bundler/setup'
2
+
3
+ require 'pry'
4
+
5
+ require 'cross-talk'
6
+
7
+ require 'rspec-spies'
8
+
9
+ require 'coveralls'
10
+ Coveralls.wear!
11
+
12
+ #include helpers
13
+ Dir["./spec/helpers/*.rb"].each { |file| require file }
14
+
15
+ #include shared examples
16
+ Dir["./spec/shared/*_examples.rb"].each { |file| require file }
17
+
18
+ RSpec.configure do |config|
19
+ config.before do
20
+ allow_message_expectations_on_nil
21
+ end
22
+
23
+ config.treat_symbols_as_metadata_keys_with_true_values = true
24
+
25
+ config.extend(RSpec::Cross::Talk::DSL::Macros)
26
+ end
27
+
28
+ class RSpec::Mocks::Mock
29
+ def inspect
30
+ "double(#{@name.inspect})"
31
+ end
32
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cross-talk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,8 +9,24 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-04-11 00:00:00.000000000 Z
13
- dependencies: []
12
+ date: 2013-04-13 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: celluloid
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
14
30
  description: A Pub/Sub Event Management Service for Ruby Classes
15
31
  email:
16
32
  - jfredett@gmail.com
@@ -19,13 +35,21 @@ extensions: []
19
35
  extra_rdoc_files: []
20
36
  files:
21
37
  - .gitignore
38
+ - .travis.yml
22
39
  - Gemfile
23
40
  - LICENSE.txt
41
+ - NOTES.md
24
42
  - README.md
25
43
  - Rakefile
26
44
  - cross-talk.gemspec
27
45
  - lib/cross-talk.rb
46
+ - lib/cross-talk/listener.rb
47
+ - lib/cross-talk/manager.rb
28
48
  - lib/cross-talk/version.rb
49
+ - spec/helpers/the.rb
50
+ - spec/listener_spec.rb
51
+ - spec/manager_spec.rb
52
+ - spec/spec_helper.rb
29
53
  homepage: ''
30
54
  licenses: []
31
55
  post_install_message:
@@ -38,16 +62,26 @@ required_ruby_version: !ruby/object:Gem::Requirement
38
62
  - - ! '>='
39
63
  - !ruby/object:Gem::Version
40
64
  version: '0'
65
+ segments:
66
+ - 0
67
+ hash: 635870557992844421
41
68
  required_rubygems_version: !ruby/object:Gem::Requirement
42
69
  none: false
43
70
  requirements:
44
71
  - - ! '>='
45
72
  - !ruby/object:Gem::Version
46
73
  version: '0'
74
+ segments:
75
+ - 0
76
+ hash: 635870557992844421
47
77
  requirements: []
48
78
  rubyforge_project:
49
79
  rubygems_version: 1.8.24
50
80
  signing_key:
51
81
  specification_version: 3
52
82
  summary: A Pub/Sub Event Management Service for Ruby Classes
53
- test_files: []
83
+ test_files:
84
+ - spec/helpers/the.rb
85
+ - spec/listener_spec.rb
86
+ - spec/manager_spec.rb
87
+ - spec/spec_helper.rb