tavern 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,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in tavern.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Darcy Laycock
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # Tavern
2
+
3
+ TODO: Write a gem description
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'tavern'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install tavern
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rake'
4
+ require 'rspec/core'
5
+ require 'rspec/core/rake_task'
6
+ require 'bundler/gem_tasks'
7
+
8
+ task :default => :spec
9
+
10
+ begin
11
+ require 'ci/reporter/rake/rspec'
12
+ rescue LoadError
13
+ end
14
+
15
+
16
+ desc "Run all specs in spec directory (excluding plugin specs)"
17
+ RSpec::Core::RakeTask.new :spec
data/lib/tavern/hub.rb ADDED
@@ -0,0 +1,117 @@
1
+ require 'tavern/subscription'
2
+ require 'tavern/subscriptions'
3
+ require 'active_support/core_ext/module/delegation'
4
+ require 'active_support/core_ext/object/blank'
5
+
6
+ module Tavern
7
+
8
+ class << self
9
+
10
+ # Gets the current application wide hub, initializing
11
+ # a new one if it hasn't been set yet.
12
+ # @return [Hub] the subscription hub
13
+ def hub
14
+ @hub ||= Hub.new.tap { |h| h.primary = true }
15
+ end
16
+
17
+ def hub=(value)
18
+ old_hub = @hub
19
+ @hub = value.presence
20
+ if old_hub != @hub
21
+ old_hub.primary = false if old_hub
22
+ @hub.primary = true if @hub
23
+ end
24
+ end
25
+
26
+ # We delegate the subscription management methods to the default hub.
27
+ # Note: This does not replace having an application-wide hub which is still
28
+ # a good idea.
29
+ delegate :subscribe, :unsubscribe, :publish, :to => :hub
30
+
31
+ end
32
+
33
+ # Implements a simplified Pub / Sub hub for in-application notifications.
34
+ # Used inside smeghead as a general replacement for observers and a way
35
+ # for items to hook into events.
36
+ class Hub
37
+
38
+ attr_reader :subscriptions
39
+
40
+ # Initializes the given hub with an empty set of subscriptions.
41
+ def initialize
42
+ @subscriptions = Subscriptions.new
43
+ @primary = false
44
+ end
45
+
46
+ # Subscribes to a given path string and either a proc callback or
47
+ # any object responding to #call.
48
+ # @param [String] path the subscription path
49
+ # @param [#call] object if present, the callback to invoke
50
+ # @param [Proc] blk the block to use for the callback (if the object is nil)
51
+ # @example Subscribing with a block
52
+ # hub.subscribe 'hello:world' do |ctx|
53
+ # puts "Context is #{ctx.inspect}"
54
+ # end
55
+ # @example Subscribing with an object
56
+ # hub.subscribe 'hello:world', MyCallableClass
57
+ def subscribe(path, object = nil, &blk)
58
+ if object and not object.respond_to?(:call)
59
+ raise ArgumentError, "you provided an object as an argument but it doesn't respond to #call"
60
+ end
61
+ subscription = Subscription.new(path, (object || blk))
62
+ level = subscriptions.sublevel_at subscription.to_subscribe_keys
63
+ level.add subscription
64
+ subscription
65
+ end
66
+
67
+ # Deletes the given subscription from this pub sub hub.
68
+ # @param [Subscription] subscription the subscription to delete
69
+ # @return [Subscription] the deleted subscription
70
+ def unsubscribe(subscription)
71
+ return if subscription.blank?
72
+ level = subscriptions.sublevel_at subscription.to_subscribe_keys
73
+ level.delete subscription
74
+ subscription
75
+ end
76
+
77
+ # Publishes a message to the given path and with a given hash context.
78
+ # @param [String] path the pubsub path
79
+ # @param [Hash{Symbol => Object}] context the message context
80
+ # @return [true,false] whether or not all callbacks executed successfully.
81
+ # @example Publishing a message
82
+ # hub.publish 'hello:world', :hello => 'world'
83
+ def publish(path, context = {})
84
+ path_parts = path.split(":")
85
+ context = merge_path_context path_parts, context
86
+ # Actually handle publishing the subscription
87
+ subscriptions.call(context.merge(:path_parts => path_parts, :full_path => path)) != false
88
+ end
89
+
90
+ def primary?
91
+ !!@primary
92
+ end
93
+
94
+ def primary=(value)
95
+ value = !!value
96
+ if value != @value
97
+ @value = value
98
+ ActiveSupport.run_load_hooks :smeg_head_hub, self
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ def merge_path_context(path_parts, context)
105
+ if context.has_key?(:path_keys)
106
+ context = context.dup
107
+ path_keys = Array(context.delete(:path_keys))
108
+ path_keys.each_with_index do |part, idx|
109
+ next if part.blank?
110
+ context[part.to_sym] = path_parts[idx]
111
+ end
112
+ end
113
+ context
114
+ end
115
+
116
+ end
117
+ end
@@ -0,0 +1,54 @@
1
+ require 'tavern/hub'
2
+
3
+ module Tavern
4
+ # A simple hub you can use to completely disable the pub / sub process
5
+ # but still record what happens.
6
+ class MockHub < Hub
7
+
8
+ class Publication < Struct.new(:path, :context)
9
+ end
10
+
11
+ attr_reader :subscriptions, :unsubscriptions, :publications
12
+
13
+ def initialize
14
+ super
15
+ @subscriptions, @unsubscriptions, @publications = [], [], []
16
+ end
17
+
18
+ # Subscribes to a given path string and either a proc callback or
19
+ # any object responding to #call.
20
+ # @param [String] path the subscription path
21
+ # @param [#call] object if present, the callback to invoke
22
+ # @param [Proc] blk the block to use for the callback (if the object is nil)
23
+ # @example Subscribing with a block
24
+ # hub.subscribe 'hello:world' do |ctx|
25
+ # puts "Context is #{ctx.inspect}"
26
+ # end
27
+ # @example Subscribing with an object
28
+ # hub.subscribe 'hello:world', MyCallableClass
29
+ def subscribe(path, object = nil, &blk)
30
+ subscription = Subscription.new(path, (object || blk))
31
+ subscriptions << subscription
32
+ subscription
33
+ end
34
+
35
+ # Deletes the given subscription from this pub sub hub.
36
+ # @param [Subscription] subscription the subscription to delete
37
+ # @return [Subscription] the deleted subscription
38
+ def unsubscribe(subscription)
39
+ return if subscription.blank?
40
+ subscriptions.delete subscription
41
+ subscription
42
+ end
43
+
44
+ # Publishes a message to the given path and with a given hash context.
45
+ # @param [String] path the pubsub path
46
+ # @param [Hash{Symbol => Object}] context the message context
47
+ # @example Publishing a message
48
+ # hub.publish 'hello:world', :hello => 'world'
49
+ def publish(path, context = {})
50
+ publications << Publication.new(path, context)
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,34 @@
1
+ module Tavern
2
+ # A general subscription in a given hub, including a name and a given callback
3
+ # block. As part of this, it provides tools to make it easy to handle the subscriptions.
4
+ class Subscription
5
+
6
+ attr_reader :name, :callback
7
+
8
+ # Initializes a new subscription with a given name and callback.
9
+ # @param [String] name the name of this subscription
10
+ # @param [#call] callback the callback for this subscription
11
+ def initialize(name, callback)
12
+ @name = name.to_s
13
+ @callback = callback
14
+ @_subscribe_keys = @name.to_s.split(":")
15
+ end
16
+
17
+ # Returns the list of subscription sublevel keys.
18
+ # @return [Array<String>] the list of sublevel keys
19
+ def to_subscribe_keys
20
+ @_subscribe_keys
21
+ end
22
+
23
+ # Invokes the callback, returning whatever it returns.
24
+ # @param [Hash] context the callback context.
25
+ def call(context)
26
+ @callback.call context
27
+ end
28
+
29
+ def to_proc
30
+ proc { |ctx| call ctx }
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,79 @@
1
+ module Tavern
2
+ # Tavern::Subscriptions implements a simple tree-like structure for handling subscriptions, facilitating
3
+ # the efficient depth-first processing of callbacks based on a given path.
4
+ #
5
+ # Each level of the tree is composed of some callbacks as well as a set of sublevels, each represented by
6
+ # part of a subscription namespace. Namely, given a key of "a:b:c", there will be four levels:
7
+ #
8
+ # 1. The root level - Currently, nothing subscribes to this level
9
+ # 2. The "a" level - this is nested under the root level.
10
+ # 3. The "b" level - this is nested under the "a" level.
11
+ # 4. The "c" level - this is nested under the "b" level.
12
+ #
13
+ # When an event is published to "a:b:c", the top level Subscriptions instance will recursively ``#call`` the
14
+ # child levels, breaking if at any point a subscription returns false. Ideally, this means that for a normal
15
+ # event, we will get events (if present) invoked on all four levels, allowing a nice and structured event
16
+ # dispatch.
17
+ class Subscriptions
18
+
19
+ # Creates a new subscription with an empty list of subscriptions and an empty subkeys mapping.
20
+ def initialize
21
+ @subscriptions = []
22
+ @subkeys = {}
23
+ end
24
+
25
+ # Given a specified context, calls all nested matching subcontexts and then invokes the
26
+ # callbacks on this level, breaking if it encounters false (like terminators in AS callbacks).
27
+ #
28
+ # This means that if your subscription returns false, it will halt further callbacks. Also,
29
+ # It also means that dispatching events are depth first based on the key. E.g., given a callback
30
+ # key of "a:b:c", the callbacks for "a:b:c" will be called first followed by those for "a:b" and "a".
31
+ #
32
+ # @param [Hash] context the given context for this publish call
33
+ # @option context [Array<String>] :path_parts an array of the current children keys to dispatch on
34
+ def call(context = {})
35
+ path_parts = context[:path_parts].dup
36
+ # Call the sublevel, breaking when a value returns false
37
+ if path_parts.any? and (subkey = @subkeys[path_parts.shift])
38
+ result = subkey.call context.merge(:path_parts => path_parts)
39
+ return result if result == false
40
+ end
41
+ # Iterate over the subscriptions, breaking when one of them returns false
42
+ @subscriptions.each do |subscription|
43
+ return false if subscription.call(context) == false
44
+ end
45
+ # Otherwise, return nil
46
+ true
47
+ end
48
+
49
+ # Adds a new subscription to this subscriptions list.
50
+ # @param [Subscription] subscription the new subscription to add
51
+ def add(subscription)
52
+ @subscriptions << subscription
53
+ end
54
+
55
+ # Removes a given subscription from this level.
56
+ # @param [Subscription] subscription the old subscription to remove
57
+ def delete(subscription)
58
+ @subscriptions.delete subscription
59
+ end
60
+
61
+ # Gets the sublevel with the given key, initializing a new
62
+ # sublevel if it is as of yet unknown.
63
+ # @param [String] key the key of the given sublevel
64
+ # @return [Subscriptions] the returned subscription level
65
+ def sublevel(key)
66
+ @subkeys[key] ||= Subscriptions.new
67
+ end
68
+
69
+ # Given a list of subkeys, will return the association sublevel.
70
+ # @param [Array<String>] parts the list of path-parts to reach the desired sublevel.
71
+ # @return [Subscriptions] the given sublevel subscriptions object.
72
+ def sublevel_at(*parts)
73
+ parts.flatten.inject(self) do |level, part|
74
+ level.sublevel part
75
+ end
76
+ end
77
+
78
+ end
79
+ end
@@ -0,0 +1,3 @@
1
+ module Tavern
2
+ VERSION = "0.0.1"
3
+ end
data/lib/tavern.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'tavern/version'
2
+
3
+ module Tavern
4
+ require 'tavern/hub'
5
+ require 'tavern/mock_hub'
6
+ end
data/spec/hub_spec.rb ADDED
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tavern::Hub do
4
+
5
+ let(:hub) { Tavern::Hub.new }
6
+
7
+ let(:subscriber_klass) do
8
+ Class.new do
9
+
10
+ def published
11
+ @published ||= []
12
+ end
13
+
14
+ def call(ctx)
15
+ published << ctx
16
+ end
17
+
18
+ end
19
+ end
20
+
21
+ let(:ctx_from_proc) { [] }
22
+
23
+ let(:subscriber_proc) do
24
+ proc { |ctx| ctx_from_proc << ctx }
25
+ end
26
+
27
+ describe 'the primary hub' do
28
+
29
+ it 'should let you query if something is the primary hub'
30
+
31
+ it 'should run the load hook when the hub is changed'
32
+
33
+ it 'should unset it when changing the hub'
34
+
35
+ it 'should set it to primary when setting it to the hub value'
36
+
37
+ it 'always set the default to be primary'
38
+
39
+ end
40
+
41
+ describe '#subscribe' do
42
+
43
+ it 'should let you subscribe to a top level item'
44
+
45
+ it 'should let you subscribe to a nested item'
46
+
47
+ it 'should let you pass an object'
48
+
49
+ it 'should let you pass a block'
50
+
51
+ it 'should return a subscription'
52
+
53
+ it 'should automatically subscribe to lower level nested events'
54
+
55
+ it 'should raise an error when subscribing with an object that does not provide call'
56
+
57
+ end
58
+
59
+ describe '#unsubscribe' do
60
+
61
+ it 'should remove an object from the subscription pool'
62
+
63
+ it 'should return the subscription'
64
+
65
+ it 'should do nothing with a blank item'
66
+
67
+ end
68
+
69
+ describe '#publish' do
70
+
71
+ let(:nested_a) { subscriber_klass.new }
72
+ let(:nested_b) { subscriber_klass.new }
73
+ let(:nested_c) { subscriber_klass.new }
74
+ let(:top_level_a) { subscriber_klass.new }
75
+ let(:top_level_b) { subscriber_klass.new }
76
+
77
+ before :each do
78
+ hub.subscribe 'hello', top_level_a
79
+ hub.subscribe 'hello:world', nested_a
80
+ hub.subscribe 'foo', top_level_b
81
+ hub.subscribe 'foo:bar', nested_b
82
+ end
83
+
84
+ it 'should add the path parts for a top level call' do
85
+ mock(top_level_a).call(hash_including(:path_parts => %w()))
86
+ hub.publish 'hello', {}
87
+ end
88
+
89
+ it 'should add the path parts for a nested call' do
90
+ mock(top_level_a).call(hash_including(:path_parts => %w(world)))
91
+ mock(nested_a).call(hash_including(:path_parts => %w()))
92
+ hub.publish 'hello:world', {}
93
+ end
94
+
95
+ it 'should unpack path keys if provided' do
96
+ mock(top_level_a).call(hash_including(:model_name => 'world'))
97
+ mock(nested_a).call(hash_including(:model_name => 'world'))
98
+ hub.publish 'hello:world', :path_keys => [nil, :model_name]
99
+ end
100
+
101
+ it 'should add the full path to the publish' do
102
+ mock(top_level_a).call(hash_including(:full_path => 'hello:world'))
103
+ mock(nested_a).call(hash_including(:full_path => 'hello:world'))
104
+ hub.publish 'hello:world', {}
105
+ end
106
+
107
+ it 'should notify all subscriptions under the path'
108
+
109
+ it 'should not notify unmatched subscriptions on a simple case'
110
+
111
+ it 'should not notify unmatched subscriptions on a nested case'
112
+
113
+ end
114
+
115
+ end
@@ -0,0 +1,8 @@
1
+ require 'rspec'
2
+ require 'rr'
3
+
4
+ require 'tavern'
5
+
6
+ RSpec.configure do |config|
7
+ config.mock_with :rr
8
+ end
data/tavern.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/tavern/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Darcy Laycock"]
6
+ gem.email = ["sutto@sutto.net"]
7
+ gem.description = %q{Tavern implements simple pub / sub systems for Rails applications with a simple, extendable architecture and minimal api surface area.}
8
+ gem.summary = %q{Simple pubsub for Ruby apps.}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "tavern"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Tavern::VERSION
17
+
18
+ gem.add_dependency 'activesupport', '~> 3.0'
19
+ gem.add_development_dependency 'rake'
20
+ gem.add_development_dependency 'rr'
21
+ gem.add_development_dependency 'rspec', '~> 2.0'
22
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tavern
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Darcy Laycock
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-04-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activesupport
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.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: '3.0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rr
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: rspec
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: '2.0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: '2.0'
78
+ description: Tavern implements simple pub / sub systems for Rails applications with
79
+ a simple, extendable architecture and minimal api surface area.
80
+ email:
81
+ - sutto@sutto.net
82
+ executables: []
83
+ extensions: []
84
+ extra_rdoc_files: []
85
+ files:
86
+ - .gitignore
87
+ - .rspec
88
+ - Gemfile
89
+ - LICENSE
90
+ - README.md
91
+ - Rakefile
92
+ - lib/tavern.rb
93
+ - lib/tavern/hub.rb
94
+ - lib/tavern/mock_hub.rb
95
+ - lib/tavern/subscription.rb
96
+ - lib/tavern/subscriptions.rb
97
+ - lib/tavern/version.rb
98
+ - spec/hub_spec.rb
99
+ - spec/spec_helper.rb
100
+ - tavern.gemspec
101
+ homepage: ''
102
+ licenses: []
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ! '>='
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ! '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ requirements: []
120
+ rubyforge_project:
121
+ rubygems_version: 1.8.21
122
+ signing_key:
123
+ specification_version: 3
124
+ summary: Simple pubsub for Ruby apps.
125
+ test_files:
126
+ - spec/hub_spec.rb
127
+ - spec/spec_helper.rb