von 0.1.0

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/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.2
4
+ - 1.9.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in von.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 blahed
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,105 @@
1
+ # Von [![Build Status](https://travis-ci.org/blahed/von.png?branch=master)](https://travis-ci.org/blahed/von)
2
+
3
+ Von is an opinionated Redis stats tracker. It works with keys, you choose one, Von increments it. It has a few built in conveniences:
4
+
5
+ ## Requirements
6
+
7
+ Von uses Redis for storing counters so you'll need it get going. If you're on OS X you can use homebrew:
8
+
9
+ ```bash
10
+ $ brew install redis
11
+ ```
12
+
13
+ ## Auto Incrementing Parent Keys
14
+
15
+ Keys are namespaced and every parent key is incremented when you increment a child key, for example:
16
+
17
+ ```ruby
18
+ Von.increment('downloads') # bumps the 'downloads' key 1 time
19
+ Von.increment('downloads:app123') # bumps the 'downloads:app123' key 1 time AND the 'downloads' key 1 time
20
+ ```
21
+
22
+ ## Time Period Grouping and Limiting
23
+
24
+ By default Von will only bump a "total" counter for the given key. This is great, but what makes Von really useful is that it can be configured to group certain keys by hour, day, week, month, and year. And you can set limits on how many of each you want to keep around. Here's how it works:
25
+
26
+ ### Configuring Time Periods
27
+ ```ruby
28
+ Von.configure do |config|
29
+ # Keep daily stats for 30 days
30
+ config.counter 'downloads', :daily => 30
31
+
32
+ # Keep monthly stats for 3 months and yearly stats for 2 years
33
+ config.counter 'uploads', :monthly => 3, :yearly => 2
34
+ end
35
+ ```
36
+
37
+ ### Incrementing Time Periods
38
+
39
+ Once you've configured the keys you want to use Time Periods on, you just increment them like normal, Von handles the rest.
40
+
41
+ ```ruby
42
+ Von.increment('downloads')
43
+ Von.increment('uploads')
44
+ ```
45
+
46
+ ## Getting Stats
47
+
48
+ ```ruby
49
+ # get the total downloads (returns an Integer)
50
+ Von.count('downloads') #=> 4
51
+ # get the monthly counts (returns an Array of Hashes)
52
+ Von.count('uploads', :monthly) #=> [ { '2012-03 => 3}, { '2013-04' => 1 }, { '2013-05' => 0 }]
53
+
54
+ ```
55
+
56
+ One nice thing to note, if you're counting a time period and there wasn't a value stored for the particular hour/day/week/etc, it'll be populated with a zero, this ensures that if you want 30 days of stats, you get 30 days of stats.
57
+
58
+ ## Configuration
59
+
60
+ There are a few things you might want to configure in Von, you can do this in the configure block where you would also set time periods and expirations.
61
+
62
+ ```ruby
63
+ Von.configure do |config|
64
+ # set the Redis connection to an already existing connection
65
+ config.redis = Redis.current
66
+ # Initialize a new Redis connection given options
67
+ config.redis = { :host => 'localhost', :port => 6379 }
68
+
69
+ # rescue Redis connection errors
70
+ # if the connection fails, no errors are raised by default
71
+ config.raise_connection_errors = false
72
+
73
+ # set the top level Redis key namespace
74
+ config.namespace = 'von'
75
+
76
+ # set the various formatting for time periods (defaults shown)
77
+ config.yearly_format = '%Y'
78
+ config.monthly_format = '%Y-%m'
79
+ config.weekly_format = '%Y-%m-%d'
80
+ config.daily_format = '%Y-%m-%d'
81
+ config.hourly_format = '%Y-%m-%d %H:00'
82
+ end
83
+ ```
84
+
85
+ ## Installation
86
+
87
+ Add this line to your application's Gemfile:
88
+
89
+ gem 'von'
90
+
91
+ And then execute:
92
+
93
+ $ bundle
94
+
95
+ Or install it yourself as:
96
+
97
+ $ gem install von
98
+
99
+ ## Contributing
100
+
101
+ 1. Fork it
102
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
103
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
104
+ 4. Push to the branch (`git push origin my-new-feature`)
105
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+ require 'rake/testtask'
3
+ require 'bundler/gem_tasks'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << 'test'
7
+ t.test_files = FileList['test/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
data/lib/von/config.rb ADDED
@@ -0,0 +1,81 @@
1
+ require 'yaml'
2
+
3
+ module Von
4
+ module Config
5
+ extend self
6
+
7
+ attr_accessor :namespace
8
+ attr_accessor :raise_connection_errors
9
+ attr_accessor :yearly_format
10
+ attr_accessor :monthly_format
11
+ attr_accessor :weekly_format
12
+ attr_accessor :daily_format
13
+ attr_accessor :hourly_format
14
+
15
+ attr_reader :periods
16
+
17
+ def init!
18
+ @counter_options = {}
19
+ @periods = {}
20
+ # all keys are prefixed with this namespace
21
+ self.namespace = 'von'
22
+ # rescue Redis connection errors
23
+ self.raise_connection_errors = false
24
+ # 2013
25
+ self.yearly_format = '%Y'
26
+ # 2013-01
27
+ self.monthly_format = '%Y-%m'
28
+ # 2013-01-02
29
+ self.weekly_format = '%Y-%m-%d'
30
+ # 2013-01-02
31
+ self.daily_format = '%Y-%m-%d'
32
+ # 2013-01-02 12:00
33
+ self.hourly_format = '%Y-%m-%d %H:00'
34
+ end
35
+
36
+ # Set the Redis connection to use
37
+ #
38
+ # arg - A Redis connection or a Hash of Redis connection options
39
+ #
40
+ # Returns the Redis client
41
+ def redis=(arg)
42
+ if arg.is_a? Redis
43
+ @redis = arg
44
+ else
45
+ @redis = Redis.new(arg)
46
+ end
47
+ end
48
+
49
+ # Returns the Redis connection
50
+ def redis
51
+ @redis ||= Redis.new
52
+ end
53
+
54
+ # Configure options for given Counter. Configures length of given time period
55
+ # and any other options for the Counter
56
+ def counter(field, options = {})
57
+ options.each do |key, value|
58
+ if Period::AVAILABLE_PERIODS.include?(key)
59
+ @periods[field.to_sym] ||= {}
60
+ @periods[field.to_sym][key.to_sym] = Period.new(field, key, value)
61
+ options.delete(key)
62
+ end
63
+ end
64
+
65
+ @counter_options[field.to_sym] = options
66
+ end
67
+
68
+ # Returns a True if a Period is defined for the
69
+ # given period identifier and the period has a length
70
+ # False if not
71
+ def period_defined_for?(key, period)
72
+ @periods.has_key?(key) && @periods[key].has_key?(period)
73
+ end
74
+
75
+ # TODO: rename
76
+ def counter_options(field)
77
+ @counter_options[field.to_sym] ||= {}
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,113 @@
1
+ module Von
2
+ class Counter
3
+ PARENT_REGEX = /:?[^:]+\z/
4
+
5
+ # Initialize a new Counter
6
+ #
7
+ # field - counter field name
8
+ def initialize(field)
9
+ @field = field.to_sym
10
+ end
11
+
12
+ # Returns options specified in config for this Counter
13
+ def options
14
+ @options ||= Von.config.counter_options(@field)
15
+ end
16
+
17
+ # Returns the Redis hash key used for storing counts for this Counter
18
+ def hash_key
19
+ @hash_key ||= "#{Von.config.namespace}:counters:#{@field}"
20
+ end
21
+
22
+ # Increment the total count for this Counter
23
+ # If the key has time periods specified, increment those.
24
+ #
25
+ # Returns the Integer total for the key
26
+ def increment
27
+ total = Von.connection.hincrby(hash_key, 'total', 1)
28
+
29
+ increment_periods
30
+
31
+ total
32
+ end
33
+
34
+ # Increment periods associated with this key
35
+ def increment_periods
36
+ return unless Von.config.periods.has_key?(@field.to_sym)
37
+
38
+ Von.config.periods[@field.to_sym].each do |key, period|
39
+ Von.connection.hincrby(period.hash_key, period.field, 1)
40
+ unless Von.connection.lrange(period.list_key, 0, -1).include?(period.field)
41
+ Von.connection.rpush(period.list_key, period.field)
42
+ end
43
+
44
+ if Von.connection.llen(period.list_key) > period.length
45
+ expired_counter = Von.connection.lpop(period.list_key)
46
+ Von.connection.hdel(period.hash_key, expired_counter)
47
+ end
48
+ end
49
+ end
50
+
51
+ # Increment the Redis count for this Counter.
52
+ # If the key has parents, increment them as well.
53
+ #
54
+ # Returns the Integer total for the key
55
+ def self.increment(field)
56
+ total = Counter.new(field).increment
57
+ parents = field.sub(PARENT_REGEX, '')
58
+
59
+ until parents.empty? do
60
+ Counter.new(parents).increment
61
+ parents.sub!(PARENT_REGEX, '')
62
+ end
63
+
64
+ total
65
+ end
66
+
67
+ # Count the "total" field for this Counter.
68
+ #
69
+ # Returns an Integer count
70
+ def count
71
+ Von.connection.hget(hash_key, 'total')
72
+ end
73
+
74
+ # Count the fields for the given time period for this Counter.
75
+ #
76
+ # Returns an Array of Hashes representing the count
77
+ def count_period(period)
78
+ return unless Von.config.period_defined_for?(@field, period)
79
+
80
+ _counts = []
81
+ _period = Von.config.periods[@field][period]
82
+ now = DateTime.now.beginning_of_hour
83
+
84
+ _period.length.times do
85
+ this_period = now.strftime(_period.format)
86
+ _counts.unshift(this_period)
87
+ now = _period.hours? ? now.ago(3600) : now.send(:"prev_#{_period.time_unit}")
88
+ end
89
+
90
+ keys = Von.connection.hgetall("#{hash_key}:#{period}")
91
+ _counts.map { |date| { date => keys.fetch(date, 0) }}
92
+ end
93
+
94
+ # Lookup the count for this Counter in Redis.
95
+ # If a Period argument is given we lookup the count for
96
+ # all of the possible units (not expired), zeroing ones that
97
+ # aren't set in Redis already.
98
+ #
99
+ # period - A Period to lookup
100
+ #
101
+ # Returns an Integer representing the count or an Array of counts.
102
+ def self.count(field, period = nil)
103
+ counter = Counter.new(field)
104
+
105
+ if period.nil?
106
+ counter.count
107
+ else
108
+ counter.count_period(period.to_sym)
109
+ end
110
+ end
111
+
112
+ end
113
+ end
@@ -0,0 +1,28 @@
1
+ module Von
2
+ module ModelCounter
3
+ extend ::ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def increments_stat(key, options = {})
7
+ increment_method = "increment_stat_#{key.gsub(/:/, '_')}".to_sym
8
+
9
+ define_method increment_method do
10
+ Von.increment(key)
11
+ end
12
+
13
+ case options[:on]
14
+ when :create
15
+ after_create increment_method
16
+ when :save
17
+ after_save increment_method
18
+ when :update
19
+ after_update increment_method
20
+ else
21
+ # default to create
22
+ after_create increment_method
23
+ end
24
+ end
25
+ end
26
+
27
+ end
28
+ end
data/lib/von/period.rb ADDED
@@ -0,0 +1,58 @@
1
+ module Von
2
+ class Period
3
+ AVAILABLE_PERIODS = [ :hourly, :daily, :weekly, :monthly, :yearly ]
4
+
5
+ attr_reader :counter_key
6
+ attr_reader :length
7
+ attr_reader :format
8
+
9
+ # Initialize a Period object
10
+ #
11
+ # counter - the field name for the counter
12
+ # period - the time period one of AVAILABLE_PERIODS
13
+ # length - length of period
14
+ def initialize(counter_key, period, length)
15
+ @counter_key = counter_key
16
+ @period = period
17
+ @length = length
18
+ @format = Von.config.send(:"#{@period}_format")
19
+ end
20
+
21
+ # Returns a Symbol representing the time unit
22
+ # for the current period.
23
+ def time_unit
24
+ @time_unit ||= case @period
25
+ when :hourly
26
+ :hour
27
+ when :daily
28
+ :day
29
+ when :weekly
30
+ :week
31
+ when :monthly
32
+ :month
33
+ when :yearly
34
+ :year
35
+ end
36
+ end
37
+
38
+ # Returns True or False if the period is hourly
39
+ def hours?
40
+ @period == :hourly
41
+ end
42
+
43
+ # Returns the Redis hash key used for storing counts for this Period
44
+ def hash_key
45
+ @hash ||= "#{Von.config.namespace}:counters:#{@counter_key}:#{@period}"
46
+ end
47
+
48
+ # Returns the Redis list key used for storing current "active" counters
49
+ def list_key
50
+ @list ||= "#{Von.config.namespace}:lists:#{@counter_key}:#{@period}"
51
+ end
52
+
53
+ # Returns the Redis field representation used for storing the count value
54
+ def field
55
+ Time.now.strftime(format)
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,3 @@
1
+ module Von
2
+ VERSION = '0.1.0'
3
+ end
data/lib/von.rb ADDED
@@ -0,0 +1,35 @@
1
+ require 'redis'
2
+ require 'active_support/time'
3
+
4
+ require 'von/config'
5
+ require 'von/counter'
6
+ require 'von/period'
7
+ require 'von/version'
8
+
9
+ module Von
10
+ def self.connection
11
+ @connection ||= config.redis
12
+ end
13
+
14
+ def self.config
15
+ Config
16
+ end
17
+
18
+ def self.configure
19
+ yield(config)
20
+ end
21
+
22
+ def self.increment(field)
23
+ Counter.increment(field)
24
+ rescue Redis::BaseError => e
25
+ raise e if config.raise_connection_errors
26
+ end
27
+
28
+ def self.count(field, period = nil)
29
+ Counter.count(field, period)
30
+ rescue Redis::BaseError => e
31
+ raise e if config.raise_connection_errors
32
+ end
33
+
34
+ config.init!
35
+ end
@@ -0,0 +1,41 @@
1
+ require 'test_helper'
2
+
3
+ describe Von::Config do
4
+
5
+ before :each do
6
+ @config = Von::Config
7
+ @config.init!
8
+ end
9
+
10
+ it 'intiializes a config with defaults' do
11
+ @config.namespace.must_equal 'von'
12
+ @config.hourly_format.must_equal '%Y-%m-%d %H:00'
13
+ end
14
+
15
+ it 'initializes a config and overloads it with a block' do
16
+ @config.namespace = 'something'
17
+
18
+ @config.namespace.must_equal 'something'
19
+ end
20
+
21
+ it 'stores counter options per key and retrieves them' do
22
+ options = { :monthly => 3, :total => false }
23
+
24
+ @config.counter 'bar', options
25
+
26
+ @config.namespace.must_equal 'von'
27
+ @config.counter_options('bar').must_equal options
28
+ end
29
+
30
+ it "allows config options to be updated via configure" do
31
+ options = { :monthly => 3, :total => false }
32
+
33
+ Von.configure do |config|
34
+ config.counter 'bar', options
35
+ end
36
+
37
+ Von.config.namespace.must_equal 'von'
38
+ Von.config.counter_options('bar').must_equal options
39
+ end
40
+
41
+ end
@@ -0,0 +1,97 @@
1
+ require 'test_helper'
2
+
3
+ describe Von::Counter do
4
+ Counter = Von::Counter
5
+
6
+ before :each do
7
+ Timecop.freeze(Time.local(2013, 01))
8
+ Von.config.init!
9
+ mock_connection!
10
+ end
11
+
12
+ it "increments the total counter if given a single key" do
13
+ Counter.increment('foo')
14
+
15
+ @store.has_key?('von:counters:foo').must_equal true
16
+ @store['von:counters:foo']['total'].must_equal 1
17
+
18
+ Counter.increment('foo')
19
+ @store['von:counters:foo']['total'].must_equal 2
20
+ end
21
+
22
+ it "increments the total counter for a key and it's parent keys" do
23
+ Counter.increment('foo:bar')
24
+
25
+ @store.has_key?('von:counters:foo').must_equal true
26
+ @store['von:counters:foo']['total'].must_equal 1
27
+ @store.has_key?('von:counters:foo:bar').must_equal true
28
+ @store['von:counters:foo:bar']['total'].must_equal 1
29
+
30
+ Counter.increment('foo:bar')
31
+ @store['von:counters:foo']['total'].must_equal 2
32
+ @store['von:counters:foo:bar']['total'].must_equal 2
33
+ end
34
+
35
+ it "increments a month counter" do
36
+ Von.configure do |config|
37
+ config.counter 'foo', :monthly => 1
38
+ end
39
+
40
+ Counter.increment('foo')
41
+ Counter.increment('foo')
42
+
43
+ @store.has_key?('von:counters:foo').must_equal true
44
+ @store.has_key?('von:counters:foo:monthly').must_equal true
45
+ @store['von:counters:foo']['total'].must_equal 2
46
+ @store['von:counters:foo:monthly']['2013-01'].must_equal 2
47
+ @store['von:lists:foo:monthly'].size.must_equal 1
48
+ end
49
+
50
+ it "expires counters past the limit" do
51
+ Von.configure do |config|
52
+ config.counter 'foo', :monthly => 1
53
+ end
54
+
55
+ Counter.increment('foo')
56
+ Timecop.freeze(Time.local(2013, 02))
57
+ Counter.increment('foo')
58
+
59
+ @store.has_key?('von:counters:foo').must_equal true
60
+ @store.has_key?('von:counters:foo:monthly').must_equal true
61
+ @store['von:counters:foo']['total'].must_equal 2
62
+ @store['von:counters:foo:monthly'].has_key?('2013-02').must_equal true
63
+ @store['von:lists:foo:monthly'].size.must_equal 1
64
+ end
65
+
66
+ it "gets a total count for a counter" do
67
+ Counter.increment('foo')
68
+ Counter.increment('foo')
69
+ Counter.increment('foo')
70
+
71
+ Von.count('foo').must_equal 3
72
+ end
73
+
74
+ it "gets a count for a time period and 0s missing entries" do
75
+ Von.configure do |config|
76
+ config.counter 'foo', :monthly => 1, :hourly => 6
77
+ end
78
+
79
+ Timecop.freeze(Time.local(2013, 02, 01, 05))
80
+ Counter.increment('foo')
81
+ Timecop.freeze(Time.local(2013, 02, 01, 07))
82
+ Counter.increment('foo')
83
+
84
+ Von.count('foo').must_equal 2
85
+
86
+ Von.count('foo', :monthly).must_equal [{"2013-02" => 2}]
87
+ Von.count('foo', :hourly).must_equal [
88
+ {"2013-02-01 02:00"=>0},
89
+ {"2013-02-01 03:00"=>0},
90
+ {"2013-02-01 04:00"=>0},
91
+ {"2013-02-01 05:00"=>1},
92
+ {"2013-02-01 06:00"=>0},
93
+ {"2013-02-01 07:00"=>1}
94
+ ]
95
+ end
96
+
97
+ end
@@ -0,0 +1,65 @@
1
+ describe Von::Period do
2
+ Period = Von::Period
3
+
4
+ before :each do
5
+ @config = Von::Config
6
+ @config.init!
7
+ end
8
+
9
+ it "intiializes given a counter, period, and length" do
10
+ period = Period.new('foo', :monthly, 6)
11
+ period.counter_key.must_equal 'foo'
12
+ period.length.must_equal 6
13
+ period.format.must_equal '%Y-%m'
14
+ end
15
+
16
+ it "checks if the period is an hourly period" do
17
+ Period.new('foo', :hourly, 6).must_be :hours?
18
+ Period.new('foo', :daily, 6).wont_be :hours?
19
+ Period.new('foo', :weekly, 6).wont_be :hours?
20
+ Period.new('foo', :monthly, 6).wont_be :hours?
21
+ Period.new('foo', :yearly, 6).wont_be :hours?
22
+ end
23
+
24
+ it "knows what time unit it is" do
25
+ Period.new('foo', :hourly, 6).time_unit.must_equal :hour
26
+ Period.new('foo', :daily, 6).time_unit.must_equal :day
27
+ Period.new('foo', :weekly, 6).time_unit.must_equal :week
28
+ Period.new('foo', :monthly, 6).time_unit.must_equal :month
29
+ Period.new('foo', :yearly, 6).time_unit.must_equal :year
30
+ end
31
+
32
+ it "pulls a time format from config options" do
33
+ Period.new('foo', :hourly, 6).format.must_equal Von.config.hourly_format
34
+ Period.new('foo', :daily, 6).format.must_equal Von.config.daily_format
35
+ Period.new('foo', :weekly, 6).format.must_equal Von.config.weekly_format
36
+ Period.new('foo', :monthly, 6).format.must_equal Von.config.monthly_format
37
+ Period.new('foo', :yearly, 6).format.must_equal Von.config.yearly_format
38
+ end
39
+
40
+ it "builds a redis hash key string" do
41
+ field = 'foo'
42
+ period = :hourly
43
+ period_obj = Period.new(field, period, 6)
44
+
45
+ period_obj.hash_key.must_equal "#{@config.namespace}:counters:#{field}:#{period}"
46
+ end
47
+
48
+ it "builds a redis list key string" do
49
+ field = 'foo'
50
+ period = :hourly
51
+ period_obj = Period.new(field, period, 6)
52
+
53
+ period_obj.list_key.must_equal "#{@config.namespace}:lists:#{field}:#{period}"
54
+ end
55
+
56
+ it "builds a redis field for the given period and current time" do
57
+ Timecop.freeze(Time.local(2013, 02, 01, 05))
58
+ Period.new('foo', :hourly, 6).field.must_equal '2013-02-01 05:00'
59
+ Period.new('foo', :daily, 6).field.must_equal '2013-02-01'
60
+ Period.new('foo', :weekly, 6).field.must_equal '2013-02-01'
61
+ Period.new('foo', :monthly, 6).field.must_equal '2013-02'
62
+ Period.new('foo', :yearly, 6).field.must_equal '2013'
63
+ end
64
+
65
+ end
@@ -0,0 +1,73 @@
1
+ $LOAD_PATH.unshift(File.expand_path('../../test', __FILE__))
2
+
3
+ require 'rubygems'
4
+ require 'bundler'
5
+ Bundler.setup
6
+
7
+ require 'minitest/autorun'
8
+ require 'minitest/pride'
9
+ require 'mocha'
10
+ require 'timecop'
11
+
12
+ require 'von'
13
+
14
+ module Von
15
+ class TestConnection
16
+ attr_reader :store
17
+
18
+ def initialize
19
+ @store = {}
20
+ end
21
+
22
+ def hincrby(hash, key, counter)
23
+ @store[hash] ||= {}
24
+
25
+ if @store[hash].has_key?(key)
26
+ @store[hash][key] += counter
27
+ else
28
+ @store[hash][key] = counter
29
+ end
30
+
31
+ @store[hash][key]
32
+ end
33
+
34
+ def hget(hash, key)
35
+ @store[hash][key]
36
+ end
37
+
38
+ def hgetall(hash)
39
+ @store.fetch(hash, {})
40
+ end
41
+
42
+ def hdel(hash, key)
43
+ @store[hash].delete(key)
44
+ end
45
+
46
+ def rpush(list, member)
47
+ @store[list] ||= []
48
+ @store[list] << member
49
+ end
50
+
51
+ def lpop(list)
52
+ @store[list].shift
53
+ end
54
+
55
+ def lrange(list, start, stop)
56
+ @store[list] ||= []
57
+ @store[list][start..stop]
58
+ end
59
+
60
+ def llen(list)
61
+ @store[list].length
62
+ end
63
+
64
+ end
65
+ end
66
+
67
+ module MiniTest::Expectations
68
+ def mock_connection!
69
+ connection = Von::TestConnection.new
70
+ @store = connection.store
71
+ Von.expects(:connection).returns(connection).at_least_once
72
+ end
73
+ end
data/test/von_test.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'test_helper'
2
+
3
+ describe Von do
4
+
5
+ before :each do
6
+ Von.config.init!
7
+ end
8
+
9
+ it "increments a counter and counts it" do
10
+ mock_connection!
11
+
12
+ Von.increment('foo')
13
+ Von.increment('foo')
14
+ Von.count('foo').must_equal 2
15
+ end
16
+
17
+ it "raises a Redis connection errors if raise_connection_errors is true" do
18
+ Von.config.raise_connection_errors = true
19
+ Redis.expects(:new).raises(Redis::CannotConnectError)
20
+
21
+ lambda { Von.increment('foo') }.must_raise Redis::CannotConnectError
22
+ end
23
+
24
+ end
data/von.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'von/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "von"
8
+ gem.version = Von::VERSION
9
+ gem.authors = ["blahed"]
10
+ gem.email = ["tdunn13@gmail.com"]
11
+ gem.description = "Von is an opinionated Redis stats tracker."
12
+ gem.summary = "Von is an opinionated Redis stats tracker."
13
+ gem.homepage = ""
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency 'redis', '~> 3.0.2'
21
+ gem.add_dependency 'activesupport', '~> 3.2.11'
22
+
23
+ gem.add_development_dependency 'rake', '~> 10.0.3'
24
+ gem.add_development_dependency 'minitest', '~> 3.0.0'
25
+ gem.add_development_dependency 'mocha', '~> 0.11.4'
26
+ gem.add_development_dependency 'timecop', '~> 0.5.9.1'
27
+ end
metadata ADDED
@@ -0,0 +1,164 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: von
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - blahed
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-01-28 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.2
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.2
30
+ - !ruby/object:Gem::Dependency
31
+ name: activesupport
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 3.2.11
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: 3.2.11
46
+ - !ruby/object:Gem::Dependency
47
+ name: rake
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 10.0.3
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: 10.0.3
62
+ - !ruby/object:Gem::Dependency
63
+ name: minitest
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 3.0.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: 3.0.0
78
+ - !ruby/object:Gem::Dependency
79
+ name: mocha
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ~>
84
+ - !ruby/object:Gem::Version
85
+ version: 0.11.4
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.11.4
94
+ - !ruby/object:Gem::Dependency
95
+ name: timecop
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ~>
100
+ - !ruby/object:Gem::Version
101
+ version: 0.5.9.1
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ~>
108
+ - !ruby/object:Gem::Version
109
+ version: 0.5.9.1
110
+ description: Von is an opinionated Redis stats tracker.
111
+ email:
112
+ - tdunn13@gmail.com
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - .gitignore
118
+ - .travis.yml
119
+ - Gemfile
120
+ - LICENSE.txt
121
+ - README.md
122
+ - Rakefile
123
+ - lib/von.rb
124
+ - lib/von/config.rb
125
+ - lib/von/counter.rb
126
+ - lib/von/model_counter.rb
127
+ - lib/von/period.rb
128
+ - lib/von/version.rb
129
+ - test/config_test.rb
130
+ - test/counter_test.rb
131
+ - test/period_test.rb
132
+ - test/test_helper.rb
133
+ - test/von_test.rb
134
+ - von.gemspec
135
+ homepage: ''
136
+ licenses: []
137
+ post_install_message:
138
+ rdoc_options: []
139
+ require_paths:
140
+ - lib
141
+ required_ruby_version: !ruby/object:Gem::Requirement
142
+ none: false
143
+ requirements:
144
+ - - ! '>='
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ required_rubygems_version: !ruby/object:Gem::Requirement
148
+ none: false
149
+ requirements:
150
+ - - ! '>='
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ requirements: []
154
+ rubyforge_project:
155
+ rubygems_version: 1.8.23
156
+ signing_key:
157
+ specification_version: 3
158
+ summary: Von is an opinionated Redis stats tracker.
159
+ test_files:
160
+ - test/config_test.rb
161
+ - test/counter_test.rb
162
+ - test/period_test.rb
163
+ - test/test_helper.rb
164
+ - test/von_test.rb