tavern 0.0.1

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/.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