tavern-history 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.
@@ -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-history.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.
@@ -0,0 +1,38 @@
1
+ # tavern-history
2
+
3
+ Tavern history adds basic history to the events published with tavern,
4
+ letting you retrieve a historical record of things that have happened.
5
+
6
+ How does it work you ask? easy! Tavern history uses a backend to store
7
+ entities (out of the box, it's built to work with Redis at the moment)
8
+ as well as a generaled channel to events mapping.
9
+
10
+ The redis backend is the primary backend at the moment and makes uses of redis
11
+ hashes (for the actual event storage) and sorted sets for the actual
12
+ historical channel tracking.
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ gem 'tavern-history'
19
+
20
+ And then execute:
21
+
22
+ $ bundle
23
+
24
+ Or install it yourself as:
25
+
26
+ $ gem install tavern-history
27
+
28
+ ## Usage
29
+
30
+ TODO: Write usage instructions here
31
+
32
+ ## Contributing
33
+
34
+ 1. Fork it
35
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
36
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
37
+ 4. Push to the branch (`git push origin my-new-feature`)
38
+ 5. Create new Pull Request
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rake'
4
+ require 'rspec/core'
5
+ require 'rspec/core/rake_task'
6
+
7
+ task :default => :spec
8
+ desc "Run all specs in spec directory (excluding plugin specs)"
9
+ RSpec::Core::RakeTask.new :spec
@@ -0,0 +1 @@
1
+ require 'tavern/history'
@@ -0,0 +1,9 @@
1
+ require "tavern"
2
+ require "tavern/history/version"
3
+
4
+ module Tavern
5
+ module History
6
+ require 'tavern/history/base'
7
+ require 'tavern/history/redis'
8
+ end
9
+ end
@@ -0,0 +1,65 @@
1
+ module Tavern
2
+ module History
3
+ class Base
4
+
5
+ attr_reader :hub
6
+
7
+ def initialize(hub)
8
+ @hub = hub
9
+ hub.subscribe('') { |event| persist event }
10
+ end
11
+
12
+ def history(channel, limit = 10)
13
+ identifiers = identifiers_for channel, limit
14
+ fetch identifiers
15
+ end
16
+
17
+ def persist(event)
18
+ identifier = record event
19
+ each_subchannel event do |channel|
20
+ add_record channel, identifier
21
+ end
22
+ true
23
+ end
24
+
25
+ def subscribe(*args, &blk); hub.subscribe(*args, &blk); end
26
+ def unsubscribe(*args, &blk); hub.unsubscribe(*args, &blk); end
27
+ def publish(*args, &blk); hub.publish(*args, &blk); end
28
+
29
+ # TODO: We need to delegate all normal hub methods to the hub.
30
+ # On creation, we need to subscribe to the lowest level hub,
31
+ # and then have something to record it.
32
+
33
+ private
34
+
35
+ # Given a single event, records them and returns the identifiers that match them.
36
+ def record(entry)
37
+ raise NotImplementedError
38
+ end
39
+
40
+ def fetch(identifiers)
41
+ raise NotImplementedError
42
+ end
43
+
44
+ # Given a record identifier and a channel name, will
45
+ # add the specified record to the given channel.
46
+ def add_record(channel_name, identifier)
47
+ raise NotImplementedError
48
+ end
49
+
50
+ # Gets the specified number of identifers from the given channel.
51
+ def identifiers_for(channel, limit)
52
+ raise NotImplementedError
53
+ end
54
+
55
+ def each_subchannel(event)
56
+ parts = event[:path_parts].dup
57
+ while parts.any?
58
+ yield parts.join ':'
59
+ parts.pop
60
+ end
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,56 @@
1
+ require 'redis'
2
+ require 'multi_json'
3
+
4
+ module Tavern
5
+ module History
6
+ class Redis < Base
7
+
8
+ attr_reader :redis
9
+
10
+ def initialize(hub, keyspace, redis = ::Redis.current)
11
+ super hub
12
+ @redis = redis
13
+ @base_key = "#{keyspace}:history"
14
+ @counter = "#{@base_key}:counter"
15
+ @records = "#{@base_key}:records"
16
+ @stream_base = "#{@base_key}:entries"
17
+ end
18
+
19
+ private
20
+
21
+ def generate_id
22
+ @redis.incr @counter
23
+ end
24
+
25
+ # Given a single event, records them and returns the identifiers that match them.
26
+ def record(entry)
27
+ dumped = MultiJson.encode(entry)
28
+ identifier = generate_id
29
+ @redis.hset @records, identifier, dumped
30
+ identifier
31
+ end
32
+
33
+ def fetch(identifiers)
34
+ records = identifiers.any? ? @redis.hmget(@records, identifiers) : []
35
+ records.map do |record|
36
+ MultiJson.decode record
37
+ end.compact
38
+ end
39
+
40
+ # Given a record identifier and a channel name, will
41
+ # add the specified record to the given channel.
42
+ def add_record(channel_name, identifier)
43
+ stream = "#{@stream_base}:#{channel_name}"
44
+ current_score = (Time.now.to_f * 1000000).round
45
+ @redis.zadd stream, current_score, identifier
46
+ end
47
+
48
+ # Gets the specified number of identifers from the given channel.
49
+ def identifiers_for(channel_name, limit)
50
+ stream = "#{@stream_base}:#{channel_name}"
51
+ @redis.zrevrange stream, 0, limit - 1
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,5 @@
1
+ module Tavern
2
+ module History
3
+ VERSION = "0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe Tavern::History::Redis do
4
+
5
+ let(:redis) { Redis.new(database: 4) }
6
+
7
+ let(:base_hub) { Tavern::Hub.new }
8
+ let(:hub) { Tavern::History::Redis.new base_hub, 'redis-tests', redis }
9
+ after(:each) { redis.flushdb }
10
+
11
+ it_should_behave_like 'a hub with history'
12
+
13
+ end
@@ -0,0 +1,6 @@
1
+ require 'tavern-history'
2
+
3
+ Dir[File.expand_path('../support/**/*.rb', __FILE__)].each { |f| require f }
4
+
5
+ RSpec.configure do |c|
6
+ end
@@ -0,0 +1,62 @@
1
+ shared_examples_for 'a hub with history' do
2
+
3
+ it 'should maintain a list of events' do
4
+ hub.history('test-event').should == []
5
+ (1..3).each { |i| hub.publish 'test-event', index: i }
6
+ history = hub.history('test-event')
7
+ history.should be_a Array
8
+ history.should have(3).entries
9
+ history.map { |h| h['index'] }.should == [3, 2, 1]
10
+ end
11
+
12
+ it 'should let you control the number' do
13
+ (1..3).each { |i| hub.publish 'test-event', index: i }
14
+ result = hub.history('test-event', 1)
15
+ result.should be_a Array
16
+ result.should have(1).item
17
+ result.first['index'].should == 3
18
+ end
19
+
20
+ it 'should give a sliding window' do
21
+ (1..3).each { |i| hub.publish 'test-event', index: i }
22
+ history = -> { hub.history 'test-event', 3 }
23
+ expect do
24
+ hub.publish 'test-event', index: 4
25
+ end.to change(history, :call)
26
+ end
27
+
28
+ it 'should work correctly with top level lists' do
29
+ hub.publish 'x', type: 'x'
30
+ hub.publish 'x', type: 'x'
31
+ hub.history('x').map { |r| r['type'] }.should == %w(x x)
32
+ end
33
+
34
+ it 'should work correctly with nested lists' do
35
+ hub.publish 'x', type: 'x'
36
+ hub.publish 'x:y:z', type: 'x:y:z'
37
+ hub.publish 'x:y', type: 'x:y'
38
+ hub.history('x:y:z').map { |r| r['type'] }.should == %w(x:y:z)
39
+ hub.history('x:y').map { |r| r['type'] }.should == %w(x:y x:y:z)
40
+ hub.history('x').map { |r| r['type'] }.should == %w(x:y x:y:z x)
41
+ end
42
+
43
+ it 'should return an empty array with any objects' do
44
+ hub.history('blah').should == []
45
+ end
46
+
47
+ it "should return the correct data if it doesn't fill the limit" do
48
+ (1..3).each { |i| hub.publish('other', index: i) }
49
+ hub.history('other', 10).map { |i| i['index'] }.should == [3, 2, 1]
50
+ end
51
+
52
+ it "should return the full set if it fits the limit" do
53
+ (1..10).each { |i| hub.publish('other', index: i) }
54
+ hub.history('other', 10).map { |i| i['index'] }.should == (1..10).to_a.reverse
55
+ end
56
+
57
+ it "should return a cut down set if it's more than the limit" do
58
+ (1..15).each { |i| hub.publish('other', index: i) }
59
+ hub.history('other', 10).map { |i| i['index'] }.should == (6..15).to_a.reverse
60
+ end
61
+
62
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/tavern/history/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Darcy Laycock"]
6
+ gem.email = ["sutto@sutto.net"]
7
+ gem.description = %q{Adds support for history to tavern.}
8
+ gem.summary = %q{Adds support for history to tavern.}
9
+
10
+ gem.files = `git ls-files`.split($\)
11
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
12
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
13
+ gem.name = "tavern-history"
14
+ gem.require_paths = ["lib"]
15
+ gem.version = Tavern::History::VERSION
16
+
17
+ gem.add_dependency 'multi_json'
18
+ gem.add_dependency 'redis'
19
+ gem.add_dependency 'tavern'
20
+
21
+ gem.add_development_dependency 'rspec'
22
+ gem.add_development_dependency 'rake'
23
+
24
+ end
metadata ADDED
@@ -0,0 +1,143 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tavern-history
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-06-29 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: multi_json
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'
30
+ - !ruby/object:Gem::Dependency
31
+ name: redis
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
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: tavern
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
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: '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: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: rake
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ description: Adds support for history to tavern.
95
+ email:
96
+ - sutto@sutto.net
97
+ executables: []
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - .gitignore
102
+ - .rspec
103
+ - Gemfile
104
+ - LICENSE
105
+ - README.md
106
+ - Rakefile
107
+ - lib/tavern-history.rb
108
+ - lib/tavern/history.rb
109
+ - lib/tavern/history/base.rb
110
+ - lib/tavern/history/redis.rb
111
+ - lib/tavern/history/version.rb
112
+ - spec/history/redis_spec.rb
113
+ - spec/spec_helper.rb
114
+ - spec/support/shared_examples.rb
115
+ - tavern-history.gemspec
116
+ homepage:
117
+ licenses: []
118
+ post_install_message:
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ none: false
124
+ requirements:
125
+ - - ! '>='
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ requirements: []
135
+ rubyforge_project:
136
+ rubygems_version: 1.8.24
137
+ signing_key:
138
+ specification_version: 3
139
+ summary: Adds support for history to tavern.
140
+ test_files:
141
+ - spec/history/redis_spec.rb
142
+ - spec/spec_helper.rb
143
+ - spec/support/shared_examples.rb