boffin 0.2.0 → 0.3.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/CHANGELOG.md +10 -2
- data/README.md +52 -18
- data/lib/boffin/hit.rb +17 -6
- data/lib/boffin/trackable.rb +2 -2
- data/lib/boffin/tracker.rb +5 -3
- data/lib/boffin/utils.rb +5 -1
- data/lib/boffin/version.rb +1 -1
- data/spec/boffin/hit_spec.rb +13 -3
- data/spec/boffin/trackable_spec.rb +2 -2
- data/spec/boffin/tracker_spec.rb +31 -31
- metadata +12 -12
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,15 @@
|
|
1
|
-
0.
|
1
|
+
**0.3.0**
|
2
|
+
|
3
|
+
* `Hit` can now accept a custom increment, this allows values such as cents to
|
4
|
+
be tracked. (Justin Giancola)
|
5
|
+
* Unique qualities are now passed in as a member in an options hash for
|
6
|
+
`Tracker#hit`, this deprecates the unique qualities argument and allows for
|
7
|
+
other options such as `:increment` to be passed as well. (Justin Giancola)
|
8
|
+
|
9
|
+
**0.2.0**
|
2
10
|
|
3
11
|
* Support for Ruby 1.8.7 thanks to Justin Giancola
|
4
12
|
|
5
|
-
0.1.0
|
13
|
+
**0.1.0**
|
6
14
|
|
7
15
|
* Initial public release
|
data/README.md
CHANGED
@@ -51,7 +51,7 @@ Most of Boffin's default configuration options are quite reasonable, but they
|
|
51
51
|
are easy to change if required:
|
52
52
|
|
53
53
|
```ruby
|
54
|
-
Boffin.
|
54
|
+
Boffin.config do |c|
|
55
55
|
c.redis = MyApp.redis # Redis.connect by default
|
56
56
|
c.namespace = "tracking:#{MyApp.env}" # Redis key namespace
|
57
57
|
c.hours_window_secs = 3.days # Time to maintain hourly interval data
|
@@ -128,7 +128,7 @@ identify hits from particular users or sessions:
|
|
128
128
|
```ruby
|
129
129
|
get '/listings/:id' do
|
130
130
|
@listing = Listing[params[:id]]
|
131
|
-
@listing.hit(:views, [current_user, session[:id]])
|
131
|
+
@listing.hit(:views, unique: [current_user, session[:id]])
|
132
132
|
haml :'listings/show'
|
133
133
|
end
|
134
134
|
```
|
@@ -144,7 +144,7 @@ we want to hit an instance, so let's create a helper:
|
|
144
144
|
```ruby
|
145
145
|
helpers do
|
146
146
|
def hit(trackable, type)
|
147
|
-
trackable.hit(type, [current_user, session[:id]])
|
147
|
+
trackable.hit(type, unique: [current_user, session[:id]])
|
148
148
|
end
|
149
149
|
end
|
150
150
|
```
|
@@ -156,7 +156,7 @@ applicable to a Rails application as well:
|
|
156
156
|
class ApplicationController < ActionController::Base
|
157
157
|
protected
|
158
158
|
def hit(trackable, type)
|
159
|
-
trackable.hit(type, [current_user, session[:session_id]])
|
159
|
+
trackable.hit(type, unique: [current_user, session[:session_id]])
|
160
160
|
end
|
161
161
|
end
|
162
162
|
```
|
@@ -238,14 +238,14 @@ track your friends' favourite and least favourite colours:
|
|
238
238
|
```ruby
|
239
239
|
@tracker = Boffin::Tracker.new(:colours, [:faves, :unfaves])
|
240
240
|
|
241
|
-
@tracker.hit(:faves, 'red', ['lena'])
|
242
|
-
@tracker.hit(:unfaves, 'blue', ['lena'])
|
243
|
-
@tracker.hit(:faves, 'green', ['soren'])
|
244
|
-
@tracker.hit(:unfaves, 'red', ['soren'])
|
245
|
-
@tracker.hit(:faves, 'green', ['jens'])
|
246
|
-
@tracker.hit(:unfaves, 'yellow', ['jens'])
|
241
|
+
@tracker.hit(:faves, 'red', unique: ['lena'])
|
242
|
+
@tracker.hit(:unfaves, 'blue', unique: ['lena'])
|
243
|
+
@tracker.hit(:faves, 'green', unique: ['soren'])
|
244
|
+
@tracker.hit(:unfaves, 'red', unique: ['soren'])
|
245
|
+
@tracker.hit(:faves, 'green', unique: ['jens'])
|
246
|
+
@tracker.hit(:unfaves, 'yellow', unique: ['jens'])
|
247
247
|
|
248
|
-
@tracker.top(:faves,
|
248
|
+
@tracker.top(:faves, days: 1)
|
249
249
|
```
|
250
250
|
|
251
251
|
Or, perhaps you'd like to clone Twitter? Using Boffin, all the work is
|
@@ -263,7 +263,7 @@ end
|
|
263
263
|
post '/tweets' do
|
264
264
|
@tweet = Tweet.create(params[:tweet])
|
265
265
|
if @tweet.valid?
|
266
|
-
@tweet.words.each { WordsTracker.hit(:tweets, word) }
|
266
|
+
@tweet.words.each { |word| WordsTracker.hit(:tweets, word) }
|
267
267
|
redirect to("/tweets/#{@tweet.id}")
|
268
268
|
else
|
269
269
|
haml :'tweets/form'
|
@@ -277,18 +277,52 @@ end
|
|
277
277
|
```
|
278
278
|
_*This is a joke._
|
279
279
|
|
280
|
-
|
281
|
-
|
280
|
+
|
281
|
+
Custom increments
|
282
|
+
-----------------
|
283
|
+
|
284
|
+
For some applications you might want to track something beyond simple hits.
|
285
|
+
To accomodate this you can specify a custom increment to any hit you record.
|
286
|
+
For example, if you run an ecommerce site it might be nice to know which
|
287
|
+
products are your bestsellers:
|
288
|
+
|
289
|
+
```ruby
|
290
|
+
class Product < ActiveRecord::Base
|
291
|
+
Boffin.track(self, [:sales])
|
292
|
+
end
|
293
|
+
|
294
|
+
class Order < ActiveRecord::Base
|
295
|
+
after_create :track_sales
|
296
|
+
|
297
|
+
private
|
298
|
+
|
299
|
+
def track_sales
|
300
|
+
line_items.each do |line_item|
|
301
|
+
product = line_item.product
|
302
|
+
amount = product.amount.cents * line_item.quantity
|
303
|
+
product.hit :sales, increment: amount
|
304
|
+
end
|
305
|
+
end
|
306
|
+
end
|
307
|
+
```
|
308
|
+
|
309
|
+
Then, when you want to check on your sales over the last day:
|
310
|
+
|
311
|
+
```ruby
|
312
|
+
Product.top_ids(:sales, hours: 24, counts: true)
|
313
|
+
```
|
314
|
+
|
315
|
+
The Future™
|
316
|
+
-----------------
|
282
317
|
|
283
318
|
* Ability to hit multiple instances in one command
|
284
319
|
* Ability to get hit-count range for an instance
|
285
320
|
* Some nice examples with pretty things
|
286
|
-
* ORM adapters for niceness and tighter integration
|
321
|
+
* Maybe ORM adapters for niceness and tighter integration
|
287
322
|
* Examples of how to turn IDs back into instances
|
288
323
|
* Reporting DSL thingy
|
289
|
-
* Web framework integration (helpers for tracking hits)
|
290
|
-
* Ability to
|
291
|
-
* Ability to unhit an instance (if a model instance is destroyed for example)
|
324
|
+
* Web framework integration (helpers for tracking hits, console type ditty.)
|
325
|
+
* Ability to union on unique hits and raw hits
|
292
326
|
|
293
327
|
FAQ
|
294
328
|
---
|
data/lib/boffin/hit.rb
CHANGED
@@ -12,10 +12,21 @@ module Boffin
|
|
12
12
|
# @param [Object] instance
|
13
13
|
# The instance that is being hit, any object that responds to
|
14
14
|
# `#to_member`, `#id`, or `#to_s`
|
15
|
-
# @param [
|
16
|
-
#
|
17
|
-
# identifier for hit uniqueness
|
18
|
-
|
15
|
+
# @param [Hash] options
|
16
|
+
# @option options [Array] :unique ([]) An array of which the first
|
17
|
+
# object is used to generate a session identifier for hit uniqueness
|
18
|
+
# @option options [Fixnum] :increment (1) The hit increment
|
19
|
+
def initialize(tracker, type, instance, opts = {})
|
20
|
+
if opts.is_a?(Array)
|
21
|
+
warn "This constructor is deprecated and will soon be unavailable\n" \
|
22
|
+
"please create Hits with: \n" \
|
23
|
+
"Hit.new(@tracker, :type, @instance, :unique => [1,2,3,4])\n"
|
24
|
+
uniquenesses = opts
|
25
|
+
@increment = 1
|
26
|
+
else
|
27
|
+
uniquenesses = opts.delete(:unique) || []
|
28
|
+
@increment = opts.delete(:increment) || 1
|
29
|
+
end
|
19
30
|
@now = Time.now
|
20
31
|
@sessid = Utils.uniquenesses_as_session_identifier(uniquenesses)
|
21
32
|
@type = type
|
@@ -54,7 +65,7 @@ module Boffin
|
|
54
65
|
# `true` if this hit is unique, `false` if it has been made before by the
|
55
66
|
# same session identifer.
|
56
67
|
def track_hit
|
57
|
-
redis.
|
68
|
+
redis.incrby(keyspace.hit_count(@type, @instance), @increment)
|
58
69
|
redis.zincrby(keyspace.hits(@type, @instance), 1, @sessid) == '1'
|
59
70
|
end
|
60
71
|
|
@@ -75,7 +86,7 @@ module Boffin
|
|
75
86
|
# Changes keyspace scope to keys under .uniq
|
76
87
|
def set_window_interval(interval, uniq = false)
|
77
88
|
key = keyspace(uniq).hits_time_window(@type, interval, @now)
|
78
|
-
redis.zincrby(key,
|
89
|
+
redis.zincrby(key, @increment, @member)
|
79
90
|
redis.expire(key, @tracker.config.send("#{interval}_window_secs"))
|
80
91
|
end
|
81
92
|
|
data/lib/boffin/trackable.rb
CHANGED
data/lib/boffin/tracker.rb
CHANGED
@@ -25,14 +25,16 @@ module Boffin
|
|
25
25
|
|
26
26
|
# @param [Symbol] hit_type
|
27
27
|
# @param [#as_member, #id, #to_s] instance
|
28
|
-
# @param [
|
28
|
+
# @param [Hash] options
|
29
|
+
# @option options [Array] :unique ([]) uniquenesses
|
30
|
+
# @option options [Fixnum] :increment (1) hit increment
|
29
31
|
# @return [Hit]
|
30
32
|
# @raise Boffin::UndefinedHitTypeError
|
31
33
|
# Raised if a list of hit types is available and the provided hit type is
|
32
34
|
# not in the list.
|
33
|
-
def hit(hit_type, instance,
|
35
|
+
def hit(hit_type, instance, opts = {})
|
34
36
|
validate_hit_type(hit_type)
|
35
|
-
Hit.new(self, hit_type, instance,
|
37
|
+
Hit.new(self, hit_type, instance, opts)
|
36
38
|
end
|
37
39
|
|
38
40
|
# @param [Symbol] hit_type
|
data/lib/boffin/utils.rb
CHANGED
@@ -54,7 +54,11 @@ module Boffin
|
|
54
54
|
# NOTE: this feels like a hack. I'm sure there is a more elegant way
|
55
55
|
# to determine whether the :id method is the built in Object#id but
|
56
56
|
# I can't think of it
|
57
|
-
|
57
|
+
if RUBY_VERSION < "1.9"
|
58
|
+
obj.respond_to?(:id) and obj.id != obj.object_id
|
59
|
+
else
|
60
|
+
obj.respond_to?(:id)
|
61
|
+
end
|
58
62
|
end
|
59
63
|
|
60
64
|
# Pulls time interval information from a hash of options.
|
data/lib/boffin/version.rb
CHANGED
data/spec/boffin/hit_spec.rb
CHANGED
@@ -14,7 +14,7 @@ describe Boffin::Hit, '::new' do
|
|
14
14
|
end
|
15
15
|
|
16
16
|
it 'stores hit data under the appropriate keys' do
|
17
|
-
Boffin::Hit.new(@tracker, :tests, @ditty, [nil, @user])
|
17
|
+
Boffin::Hit.new(@tracker, :tests, @ditty, :unique => [nil, @user])
|
18
18
|
[:hours, :days, :months].each do |interval|
|
19
19
|
@tracker.top(:tests, interval => 1, :counts => true).
|
20
20
|
should == [['1', 1]]
|
@@ -26,8 +26,8 @@ describe Boffin::Hit, '::new' do
|
|
26
26
|
end
|
27
27
|
|
28
28
|
it 'does not store data under unique keys if the hit is not unique' do
|
29
|
-
Boffin::Hit.new(@tracker, :tests, @ditty, [nil, @user])
|
30
|
-
Boffin::Hit.new(@tracker, :tests, @ditty, [nil, @user])
|
29
|
+
Boffin::Hit.new(@tracker, :tests, @ditty, :unique => [nil, @user])
|
30
|
+
Boffin::Hit.new(@tracker, :tests, @ditty, :unique => [nil, @user])
|
31
31
|
[:hours, :days, :months].each do |interval|
|
32
32
|
@tracker.top(:tests, interval => 1, :counts => true).
|
33
33
|
should == [['1', 2]]
|
@@ -39,4 +39,14 @@ describe Boffin::Hit, '::new' do
|
|
39
39
|
@tracker.uhit_count(:tests, @ditty).should == 1
|
40
40
|
end
|
41
41
|
|
42
|
+
it 'allows arbitrary hit increments' do
|
43
|
+
Boffin::Hit.new(@tracker, :tests, @ditty, :unique => [nil, @user], :increment => 5)
|
44
|
+
Boffin::Hit.new(@tracker, :tests, @ditty, :unique => [nil, @user], :increment => 5)
|
45
|
+
[:hours, :days, :months].each do |interval|
|
46
|
+
@tracker.top(:tests, interval => 1, :counts => true).
|
47
|
+
should == [['1', 10]]
|
48
|
+
@tracker.top(:tests, interval => 1, :counts => true, :unique => true).
|
49
|
+
should == [['1', 5]]
|
50
|
+
end
|
51
|
+
end
|
42
52
|
end
|
@@ -4,8 +4,8 @@ describe Boffin::Trackable do
|
|
4
4
|
before :all do
|
5
5
|
SpecHelper.flush_keyspace!
|
6
6
|
@mock = MockTrackableInjected.new(1)
|
7
|
-
@mock.hit(:views, ['sess.1'])
|
8
|
-
@mock.hit(:views, ['sess.1'])
|
7
|
+
@mock.hit(:views, :unique => ['sess.1'])
|
8
|
+
@mock.hit(:views, :unique => ['sess.1'])
|
9
9
|
end
|
10
10
|
|
11
11
|
it 'can be included' do
|
data/spec/boffin/tracker_spec.rb
CHANGED
@@ -14,43 +14,43 @@ describe Boffin::Tracker do
|
|
14
14
|
|
15
15
|
Timecop.freeze(@date - 2) do
|
16
16
|
@tracker.hit(:views, @instance3)
|
17
|
-
@tracker.hit(:likes, @instance3, [@user1])
|
18
|
-
@tracker.hit(:views, @instance3, ['sess.1'])
|
19
|
-
@tracker.hit(:views, @instance3, ['sess.2'])
|
20
|
-
@tracker.hit(:views, @instance3, [@user2])
|
21
|
-
@tracker.hit(:likes, @instance1, [nil, nil])
|
22
|
-
@tracker.hit(:views, @instance3, ['sess.4'])
|
23
|
-
@tracker.hit(:views, @instance3, [@user1])
|
24
|
-
@tracker.hit(:views, @instance2, [@user2])
|
25
|
-
@tracker.hit(:views, @instance3, [@user2])
|
17
|
+
@tracker.hit(:likes, @instance3, :unique => [@user1])
|
18
|
+
@tracker.hit(:views, @instance3, :unique => ['sess.1'])
|
19
|
+
@tracker.hit(:views, @instance3, :unique => ['sess.2'])
|
20
|
+
@tracker.hit(:views, @instance3, :unique => [@user2])
|
21
|
+
@tracker.hit(:likes, @instance1, :unique => [nil, nil])
|
22
|
+
@tracker.hit(:views, @instance3, :unique => ['sess.4'])
|
23
|
+
@tracker.hit(:views, @instance3, :unique => [@user1])
|
24
|
+
@tracker.hit(:views, @instance2, :unique => [@user2])
|
25
|
+
@tracker.hit(:views, @instance3, :unique => [@user2])
|
26
26
|
@tracker.hit(:likes, @instance3)
|
27
|
-
@tracker.hit(:views, @instance3, ['sess.1'])
|
27
|
+
@tracker.hit(:views, @instance3, :unique => ['sess.1'])
|
28
28
|
@tracker.hit(:views, @instance3)
|
29
|
-
@tracker.hit(:likes, @instance3, [@user1])
|
30
|
-
@tracker.hit(:views, @instance3, ['sess.1'])
|
29
|
+
@tracker.hit(:likes, @instance3, :unique => [@user1])
|
30
|
+
@tracker.hit(:views, @instance3, :unique => ['sess.1'], :increment => 2)
|
31
31
|
end
|
32
32
|
|
33
33
|
Timecop.freeze(@date - 1) do
|
34
34
|
@tracker.hit(:views, @instance1)
|
35
|
-
@tracker.hit(:likes, @instance2, [@user1])
|
36
|
-
@tracker.hit(:views, @instance2, ['sess.4'])
|
37
|
-
@tracker.hit(:views, @instance2, [nil, @user1])
|
38
|
-
@tracker.hit(:views, @instance2, ['sess.3'])
|
39
|
-
@tracker.hit(:views, @instance1, ['sess.3'])
|
40
|
-
@tracker.hit(:views, @instance1, [@user1])
|
41
|
-
@tracker.hit(:views, @instance2, ['sess.2'])
|
42
|
-
@tracker.hit(:views, @instance1, [@user1])
|
43
|
-
@tracker.hit(:views, @instance1, [@user2])
|
44
|
-
end
|
45
|
-
|
46
|
-
@tracker.hit(:views, @instance3, ['sess.2'])
|
47
|
-
@tracker.hit(:views, @instance2, [@user2])
|
35
|
+
@tracker.hit(:likes, @instance2, :unique => [@user1])
|
36
|
+
@tracker.hit(:views, @instance2, :unique => ['sess.4'])
|
37
|
+
@tracker.hit(:views, @instance2, :unique => [nil, @user1])
|
38
|
+
@tracker.hit(:views, @instance2, :unique => ['sess.3'])
|
39
|
+
@tracker.hit(:views, @instance1, :unique => ['sess.3'])
|
40
|
+
@tracker.hit(:views, @instance1, :unique => [@user1])
|
41
|
+
@tracker.hit(:views, @instance2, :unique => ['sess.2'])
|
42
|
+
@tracker.hit(:views, @instance1, :unique => [@user1])
|
43
|
+
@tracker.hit(:views, @instance1, :unique => [@user2])
|
44
|
+
end
|
45
|
+
|
46
|
+
@tracker.hit(:views, @instance3, :unique => ['sess.2'])
|
47
|
+
@tracker.hit(:views, @instance2, :unique => [@user2])
|
48
48
|
@tracker.hit(:likes, @instance2)
|
49
|
-
@tracker.hit(:views, @instance2, [@user1])
|
50
|
-
@tracker.hit(:views, @instance1, ['sess.4'])
|
51
|
-
@tracker.hit(:views, @instance3, ['sess.3'])
|
52
|
-
@tracker.hit(:views, @instance1, [@user1])
|
53
|
-
@tracker.hit(:views, @instance1, [@user2])
|
49
|
+
@tracker.hit(:views, @instance2, :unique => [@user1])
|
50
|
+
@tracker.hit(:views, @instance1, :unique => ['sess.4'])
|
51
|
+
@tracker.hit(:views, @instance3, :unique => ['sess.3'])
|
52
|
+
@tracker.hit(:views, @instance1, :unique => [@user1])
|
53
|
+
@tracker.hit(:views, @instance1, :unique => [@user2])
|
54
54
|
end
|
55
55
|
|
56
56
|
describe '#hit' do
|
@@ -134,7 +134,7 @@ describe Boffin::Tracker do
|
|
134
134
|
it 'returns ids and counts when passed { counts: true } as an option' do
|
135
135
|
ids = @tracker.top(:views, :days => 3, :counts => true)
|
136
136
|
ids.should == [
|
137
|
-
['300',
|
137
|
+
['300', 13],
|
138
138
|
['100', 8],
|
139
139
|
['200', 7]
|
140
140
|
]
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: boffin
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2011-09-
|
12
|
+
date: 2011-09-12 00:00:00.000000000Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: redis
|
16
|
-
requirement: &
|
16
|
+
requirement: &70276273568860 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '2.2'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70276273568860
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rspec
|
27
|
-
requirement: &
|
27
|
+
requirement: &70276273568280 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ~>
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '2.6'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70276273568280
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: timecop
|
38
|
-
requirement: &
|
38
|
+
requirement: &70276273567900 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - ! '>='
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: '0'
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70276273567900
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: bundler
|
49
|
-
requirement: &
|
49
|
+
requirement: &70276273567340 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - ! '>='
|
@@ -54,7 +54,7 @@ dependencies:
|
|
54
54
|
version: 1.0.14
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70276273567340
|
58
58
|
description: ! 'Boffin is a library for tracking hits to things in your Ruby application.
|
59
59
|
Things
|
60
60
|
|
@@ -111,7 +111,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
111
111
|
version: '0'
|
112
112
|
segments:
|
113
113
|
- 0
|
114
|
-
hash:
|
114
|
+
hash: -2879744131493119363
|
115
115
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
116
|
none: false
|
117
117
|
requirements:
|
@@ -120,7 +120,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
120
120
|
version: '0'
|
121
121
|
segments:
|
122
122
|
- 0
|
123
|
-
hash:
|
123
|
+
hash: -2879744131493119363
|
124
124
|
requirements: []
|
125
125
|
rubyforge_project: boffin
|
126
126
|
rubygems_version: 1.8.6
|