cross-talk 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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