trackoid 0.1.1 → 0.1.2

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/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.1
1
+ 0.1.2
@@ -1,26 +1,74 @@
1
+ # encoding: utf-8
1
2
  module Mongoid #:nodoc:
2
- module Tracking
3
-
3
+ module Tracking #:nodoc:
4
4
  module Aggregates
5
5
  # This module includes aggregate data extensions to Trackoid instances
6
6
  def self.included(base)
7
7
  base.class_eval do
8
8
  extend ClassMethods
9
- include InstanceMethods
10
9
 
11
10
  class_inheritable_accessor :aggregate_fields, :aggregate_klass
12
- self.aggregate_fields = []
11
+ self.aggregate_fields = {}
13
12
  self.aggregate_klass = nil
14
- # delegate :aggregate_fields, :aggregate_klass, :to => "self.class"
13
+ delegate :aggregate_fields,
14
+ :aggregate_klass,
15
+ :aggregated?,
16
+ :to => "self.class"
15
17
  end
16
18
  end
17
19
 
18
20
  module ClassMethods
19
-
21
+ # Defines an aggregate token to an already tracked model. It defines
22
+ # a new mongoid model named after the original model.
23
+ #
24
+ # Example:
25
+ #
26
+ # <tt>class Page</tt>
27
+ # <tt> include Mongoid::Document</tt>
28
+ # <tt> include Mongoid::Document</tt>
29
+ # <tt> track :visits</tt>
30
+ # <tt> aggregate :browsers do |b|</tt>
31
+ # <tt> b.split(" ").first</tt>
32
+ # <tt> end</tt>
33
+ # <tt>end</tt>
34
+ #
35
+ # A new model is defined as <tt>class PageAggregates</tt>
36
+ #
37
+ # This model has the following structure:
38
+ #
39
+ # <tt>belongs_to :page</tt>
40
+ # <tt>field :ns, :type => String</tt>
41
+ # <tt>field :key, :type => String</tt>
42
+ # <tt>index [:ns, :key], :unique => true</tt>
43
+ # <tt>track :[original_parent_tracking_data]</tt>
44
+ # <tt>track :...</tt>
45
+ #
46
+ # :ns is the "namespace". It's the name you put along the
47
+ # "aggregate :browsers" in the original model definition.
48
+ #
49
+ # :key is your aggregation key. This is the value you are required to
50
+ # return in the "aggregate" block.
51
+ #
52
+ # With the above structure, you can always query aggregates directly
53
+ # using Mongoid this way:
54
+ #
55
+ # <tt>TestModelAggregates.where(:ns => "browsers", :key => "explorer").first</tt>
56
+ #
57
+ # But you are encouraged to use Trackoid methods whenever possible.
58
+ #
20
59
  def aggregate(name, &block)
60
+ raise Errors::AggregationAlreadyDefined.new(self.name, name) if
61
+ aggregate_fields.has_key? name
62
+
21
63
  define_aggregate_model if aggregate_klass.nil?
22
- has_many name.to_sym, :class_name => aggregate_klass.to_s
64
+ has_many_related internal_accessor_name(name), :class_name => aggregate_klass.to_s
23
65
  add_aggregate_field(name, block)
66
+ create_aggregation_accessors(name)
67
+ end
68
+
69
+ # Return true if this model has aggregated data.
70
+ def aggregated?
71
+ !aggregate_klass.nil?
24
72
  end
25
73
 
26
74
  protected
@@ -30,37 +78,62 @@ module Mongoid #:nodoc:
30
78
  str.camelize
31
79
  end
32
80
 
81
+ def internal_accessor_name(name)
82
+ (name.to_s + "_accessor").to_sym
83
+ end
84
+
85
+ # Defines the aggregation model. It checks for class name conflicts
33
86
  def define_aggregate_model
34
87
  raise Errors::ClassAlreadyDefined.new(internal_aggregates_name) if foreign_class_defined
88
+ parent_name = self.name.underscore
35
89
  define_klass do
36
90
  include Mongoid::Document
37
91
  include Mongoid::Tracking
38
- field :name, :type => String, :default => "Dummy Text"
39
- # belongs_to :
92
+
93
+ # Make the relation to the original class
94
+ belongs_to_related parent_name.to_sym, :class_name => parent_name.camelize
95
+
96
+ # Internal fields to track aggregation token and keys
97
+ field :ns, :type => String
98
+ field :key, :type => String
99
+ index [[:ns, Mongo::ASCENDING], [:key, Mongo::ASCENDING]], :unique => true, :background => true
100
+
101
+ # Include parent tracking data.
102
+ parent_name.camelize.constantize.tracked_fields.each {|track_field| track track_field }
40
103
  end
41
104
  self.aggregate_klass = internal_aggregates_name.constantize
42
105
  end
43
-
106
+
107
+ # Returns true if there is a class defined with the same name as our
108
+ # aggregate class.
44
109
  def foreign_class_defined
45
110
  Object.const_defined?(internal_aggregates_name.to_sym)
46
111
  end
47
112
 
113
+ # Adds the aggregate field to the array of aggregated fields.
48
114
  def add_aggregate_field(name, block)
49
- aggregate_fields << { name => block }
115
+ aggregate_fields[name] = block
50
116
  end
51
117
 
118
+ # Defines the aggregation external class. This class is named after
119
+ # the original class model but with "Aggregates" appended.
120
+ # Example: TestModel ==> TestModelAggregates
52
121
  def define_klass(&block)
53
- # klass = Class.new Object, &block
54
- klass = Object.const_set internal_aggregates_name, Class.new
122
+ klass = Object.const_set(internal_aggregates_name, Class.new)
55
123
  klass.class_eval(&block)
56
124
  end
57
125
 
58
- end
59
-
60
- module InstanceMethods
61
- def aggregated?
62
- !self.class.aggregate_klass.nil?
126
+ def create_aggregation_accessors(name)
127
+ # Aggregation accessors in the model acts like a named scope
128
+ define_method(name) do |*args|
129
+ TrackerAggregates.new(self, name, args)
130
+ end
131
+
132
+ define_method("#{name}=") do
133
+ raise NoMethodError
134
+ end
63
135
  end
136
+
64
137
  end
65
138
 
66
139
  end
@@ -79,9 +152,10 @@ module Mongoid #:nodoc:
79
152
  # end
80
153
  #
81
154
  #
82
- # self.visits.inc("Google engine")
83
- #
84
- #
155
+ # self.visits(agg).inc
156
+ # self.visits.today
157
+ # self.visits.browsers.today
158
+ #
85
159
  # users
86
160
  # { _id: 32334333, name:"pepe", visits_data:{} }
87
161
  #
@@ -11,6 +11,15 @@ module Mongoid #:nodoc
11
11
  end
12
12
  end
13
13
 
14
+ class AggregationAlreadyDefined < RuntimeError
15
+ def initialize(klass, token)
16
+ @token = token
17
+ end
18
+ def message
19
+ "Aggregation '#{@token}' already defined for model #{klass}"
20
+ end
21
+ end
22
+
14
23
  class ModelNotSaved < RuntimeError; end
15
24
 
16
25
  class NotMongoid < RuntimeError; end
@@ -3,19 +3,41 @@ module Mongoid #:nodoc:
3
3
  module Tracking
4
4
  # This internal class handles all interaction for a track field.
5
5
  class Tracker
6
- def initialize(owner, field)
6
+
7
+ def initialize(owner, field, aggregate_data)
7
8
  @owner, @for = owner, field
8
- @data = @owner.read_attribute(@for) || {}
9
+ @data = @owner.read_attribute(@for)
10
+
11
+ # The following is needed if the "field" Mongoid definition for our
12
+ # internal tracking field does not include option ":default => {}"
13
+ if @data.nil?
14
+ @owner.write_attribute(@for, {})
15
+ @data = @owner.read_attribute(@for)
16
+ end
17
+
18
+ @aggregate_data = aggregate_data
9
19
  end
10
20
 
11
21
  # Update methods
12
22
  def add(how_much = 1, date = DateTime.now)
13
23
  raise Errors::ModelNotSaved, "Can't update a new record" if @owner.new_record?
14
-
15
24
  update_data(data_for(date) + how_much, date)
25
+
16
26
  @owner.collection.update( @owner._selector,
17
27
  { (how_much > 0 ? "$inc" : "$dec") => update_hash(how_much.abs, date) },
18
28
  :upsert => true)
29
+
30
+ return unless @owner.aggregated?
31
+
32
+ @owner.aggregate_fields.each do |(k,v)|
33
+ token = v.call(@aggregate_data)
34
+ fk = @owner.class.name.to_s.foreign_key.to_sym
35
+ selector = { fk => @owner.id, :ns => k, :key => token }
36
+
37
+ @owner.aggregate_klass.collection.update( selector,
38
+ { (how_much > 0 ? "$inc" : "$dec") => update_hash(how_much.abs, date) },
39
+ :upsert => true)
40
+ end
19
41
  end
20
42
 
21
43
  def inc(date = DateTime.now)
@@ -28,13 +50,13 @@ module Mongoid #:nodoc:
28
50
 
29
51
  def set(how_much, date = DateTime.now)
30
52
  raise Errors::ModelNotSaved, "Can't update a new record" if @owner.new_record?
31
-
32
53
  update_data(how_much, date)
33
54
  @owner.collection.update( @owner._selector,
34
55
  { "$set" => update_hash(how_much, date) },
35
56
  :upsert => true)
36
57
  end
37
58
 
59
+
38
60
  # Access methods
39
61
  def today
40
62
  data_for(Date.today)
@@ -46,7 +68,6 @@ module Mongoid #:nodoc:
46
68
 
47
69
  def last_days(how_much = 7)
48
70
  return [today] unless how_much > 0
49
-
50
71
  date, values = DateTime.now, []
51
72
  (date - how_much.abs + 1).step(date) {|d| values << data_for(d) }
52
73
  values
@@ -61,11 +82,13 @@ module Mongoid #:nodoc:
61
82
  # Private methods
62
83
  private
63
84
  def data_for(date)
64
- return 0 if @data[date.year.to_s].nil?
65
- return 0 if @data[date.year.to_s][date.month.to_s].nil?
85
+ return 0 if @data.nil? ||
86
+ @data[date.year.to_s].nil? ||
87
+ @data[date.year.to_s][date.month.to_s].nil?
66
88
  @data[date.year.to_s][date.month.to_s][date.day.to_s] || 0
67
89
  end
68
90
 
91
+ # TODO: It should be a better way of updating a hash. :-)
69
92
  def update_data(value, date)
70
93
  if @data[date.year.to_s]
71
94
  if @data[date.year.to_s][date.month.to_s]
@@ -94,7 +117,6 @@ module Mongoid #:nodoc:
94
117
  }
95
118
  end
96
119
 
97
-
98
120
  # WARNING: This is +only+ for debugging pourposes (rspec y tal)
99
121
  def _original_hash
100
122
  @data
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+ module Mongoid #:nodoc:
3
+ module Tracking
4
+ # This internal class handles all interaction of an aggregation token.
5
+ class TrackerAggregates
6
+
7
+ def initialize(owner, token, key_selector)
8
+ @owner, @token = owner, token
9
+ @key = key_selector
10
+
11
+ @accessor = @owner.class.send(:internal_accessor_name, @token)
12
+ @selector = {:ns => @token}
13
+ @selector.merge!(:key => @key.first) if @key.first
14
+ end
15
+
16
+ def method_missing(name, *args, &block)
17
+ @criteria ||= @owner.send(@accessor).where(@selector)
18
+
19
+ # Delegate all missing methods to the underlying Mongoid Criteria
20
+ return @criteria.send(name) unless @owner.tracked_fields.member?(name)
21
+
22
+ # Otherwise, it's a track method, so process it
23
+ @criteria.collect {|x| x.send(name, *args, &block) }
24
+ end
25
+
26
+ end
27
+
28
+ end
29
+ end
@@ -20,13 +20,14 @@ module Mongoid #:nodoc:
20
20
 
21
21
  module ClassMethods
22
22
  # Adds analytics tracking for +name+. Adds a +'name'_data+ mongoid
23
- # field as a Hash for tracking this information. Additionaly, makes
24
- this field hidden, so that the user can not mangle with the original
25
- # field. This is necessary so that Mongoid does not "dirty" the field
26
- # potentially overwriting the original data.
23
+ # field as a Hash for tracking this information. Additionaly, hiddes
24
+ the field, so that the user can not mangle with the original one.
25
+ # This is necessary so that Mongoid does not "dirty" the field
26
+ # potentially overwriting the original data.
27
27
  def track(name)
28
28
  set_tracking_field(name)
29
29
  create_tracking_accessors(name)
30
+ update_aggregates(name) if aggregated?
30
31
  end
31
32
 
32
33
  protected
@@ -38,10 +39,10 @@ module Mongoid #:nodoc:
38
39
  # Configures the internal fields for tracking. Additionally also creates
39
40
  # an index for the internal tracking field.
40
41
  def set_tracking_field(name)
41
- field internal_track_name(name), :type => Hash, :default => {}
42
- # Shoul we make an index for this field?
42
+ field internal_track_name(name), :type => Hash # , :default => {}
43
+ # Should we make an index for this field?
43
44
  index internal_track_name(name)
44
- tracked_fields << internal_track_name(name)
45
+ tracked_fields << name.to_sym
45
46
  end
46
47
 
47
48
  # Creates the tracking field accessor and also disables the original
@@ -49,8 +50,8 @@ module Mongoid #:nodoc:
49
50
  # Mongoid fields ensures they doesn't get dirty, so Mongoid does not
50
51
  # overwrite old data.
51
52
  def create_tracking_accessors(name)
52
- define_method("#{name}") do
53
- Tracker.new(self, "#{name}_data".to_sym)
53
+ define_method("#{name}") do |*aggr|
54
+ Tracker.new(self, "#{name}_data".to_sym, aggr)
54
55
  end
55
56
 
56
57
  # Should we just "undef" this methods?
@@ -62,6 +63,17 @@ module Mongoid #:nodoc:
62
63
  define_method("#{name}_data=") do
63
64
  raise NoMethodError
64
65
  end
66
+
67
+ # I think it's important to override also the #{name}_changed? so
68
+ # as to be sure Mongoid never mark this field as dirty.
69
+ define_method("#{name}_changed?") do
70
+ false
71
+ end
72
+ end
73
+
74
+ # Updates the aggregated class for it to include a new tracking field
75
+ def update_aggregates(name)
76
+ aggregate_klass.track name
65
77
  end
66
78
 
67
79
  end
data/lib/trackoid.rb CHANGED
@@ -5,6 +5,7 @@ gem "mongoid", ">= 1.9.0"
5
5
  require 'trackoid/errors'
6
6
  require 'trackoid/tracker'
7
7
  require 'trackoid/aggregates'
8
+ require 'trackoid/tracker_aggregates'
8
9
  require 'trackoid/tracking'
9
10
 
10
11
  module Mongoid #:nodoc:
@@ -5,11 +5,15 @@ class TestModel
5
5
  include Mongoid::Tracking
6
6
 
7
7
  field :name # Dummy field
8
+
9
+ # Note that references to "track" and "aggregate" in this test are mixed
10
+ # for testing pourposes. Trackoid does not make any difference in the
11
+ # declaration order of tracking fields and aggregate tokens.
8
12
  track :visits
13
+ aggregate :browsers do; "Mozilla".downcase; end
9
14
 
10
- aggregate :browsers do
11
- "Mozilla"
12
- end
15
+ track :uniques
16
+ aggregate :referers do; "GoogleBot".downcase; end
13
17
  end
14
18
 
15
19
  class SecondTestModel
@@ -20,7 +24,7 @@ class SecondTestModel
20
24
  track :visits
21
25
 
22
26
  aggregate :browsers do
23
- "Chrome"
27
+ "Chrome".downcase
24
28
  end
25
29
  end
26
30
 
@@ -39,30 +43,61 @@ describe Mongoid::Tracking::Aggregates do
39
43
  end
40
44
 
41
45
  it "should create a has_many relationship in the original model" do
42
- @mock.class.method_defined?(:browsers).should be_true
46
+ # Note that due to ActiveSupport "class_inheritable_accessor" this method
47
+ # is available both as class method and instance method.
48
+ @mock.class.method_defined?(:browsers_accessor).should be_true
43
49
  end
44
50
 
45
- it "should have the aggregates klass in a instance var" do
46
- @mock.aggregate_klass == TestModelAggregates
51
+ it "should have the aggregates klass in a class/instance var" do
52
+ # Note that due to ActiveSupport "class_inheritable_accessor" this method
53
+ # is available both as class method and instance method.
54
+ @mock.class.aggregate_klass == TestModelAggregates
47
55
  end
48
56
 
49
- it "should create an array in the class with all aggregate fields" do
50
- @mock.class.aggregate_fields.map(&:keys).flatten.should == [ :browsers ]
57
+ it "should create a hash in the class with all aggregate fields" do
58
+ # Note that due to ActiveSupport "class_inheritable_accessor" this method
59
+ # is available both as class method and instance method.
60
+ @mock.class.aggregate_fields.keys.to_set.should == [ :browsers, :referers ].to_set
51
61
  end
52
62
 
53
63
  it "should create an array in the class with all aggregate fields even when monkey patching" do
54
64
  class TestModel
55
- aggregate :referers do
56
- "(none)"
65
+ aggregate :quarters do
66
+ "Q1"
57
67
  end
58
68
  end
59
- @mock.class.aggregate_fields.map(&:keys).flatten.should == [ :browsers, :referers ]
69
+ @mock.class.aggregate_fields.keys.to_set.should == [ :browsers, :referers, :quarters ].to_set
70
+ end
71
+
72
+ it "the aggregated class should have the same tracking fields as the parent class" do
73
+ TestModelAggregates.tracked_fields.should == TestModel.tracked_fields
74
+ end
75
+
76
+ it "should raise error if we try to add an aggregation token twice" do
77
+ lambda {
78
+ class TestModel
79
+ aggregate :referers do
80
+ "(none)"
81
+ end
82
+ end
83
+ }.should raise_error Mongoid::Errors::AggregationAlreadyDefined
84
+ end
85
+
86
+ it "should have Mongoid accessors defined" do
87
+ tm = TestModel.create(:name => "Dummy")
88
+ tm.send(tm.class.send(:internal_accessor_name, "browsers")).class.should == Mongoid::Criteria
89
+ tm.send(tm.class.send(:internal_accessor_name, "referers")).class.should == Mongoid::Criteria
90
+ tm.send(tm.class.send(:internal_accessor_name, "quarters")).class.should == Mongoid::Criteria
60
91
  end
61
92
 
62
93
  it "should indicate this is an aggregated traking object with aggregated?" do
63
94
  @mock.aggregated?.should be_true
64
95
  end
65
96
 
97
+ it "should indicate this is an aggregated class with aggregated?" do
98
+ @mock.class.aggregated?.should be_true
99
+ end
100
+
66
101
  it "should raise error if already defined class with the same aggregated klass name" do
67
102
  lambda {
68
103
  class MockTestAggregates
@@ -115,4 +150,55 @@ describe Mongoid::Tracking::Aggregates do
115
150
  }.should raise_error Mongoid::Errors::ClassAlreadyDefined
116
151
  end
117
152
 
153
+ describe "when tracking a model with aggregation data" do
154
+
155
+ before(:all) do
156
+ TestModel.delete_all
157
+ TestModel.create(:name => "test")
158
+ @object_id = TestModel.first.id
159
+ end
160
+
161
+ before do
162
+ @mock = TestModel.find(@object_id)
163
+ end
164
+
165
+ it "calling an aggregation scope should return the appropiate class" do
166
+ @mock.browsers.class.should == Mongoid::Tracking::TrackerAggregates
167
+ end
168
+
169
+ it "should increment visits for all aggregated instances" do
170
+ @mock.visits("Aggregate data").inc
171
+ @mock.browsers.count.should == 1
172
+ @mock.referers.count.should == 1
173
+ @mock.quarters.count.should == 1
174
+ end
175
+
176
+ it "should increment visits for specific aggregation keys" do
177
+ @mock.browsers("mozilla").size.should == 1
178
+ @mock.referers("googlebot").size.should == 1
179
+ @mock.quarters("Q1").size.should == 1
180
+ end
181
+
182
+ it "should NOT increment visits for different aggregation keys" do
183
+ @mock.browsers("internet explorer").size.should == 0
184
+ @mock.referers("yahoo slurp").size.should == 0
185
+ @mock.quarters("Q2").size.should == 0
186
+ end
187
+
188
+ it "should have 1 in visits" do
189
+ tt = @mock.browsers.visits.collect {|v| v.last_days(7)}
190
+ tt.should == [[0, 0, 0, 0, 0, 0, 1]]
191
+ end
192
+
193
+
194
+ end
195
+
196
+
197
+ # it "should print methods && their classes to stdout" do
198
+ # TestModel.methods.sort.each {|x|
199
+ # puts "#{x}"
200
+ # }
201
+ # should be_true
202
+ # end
203
+
118
204
  end
data/spec/spec_helper.rb CHANGED
@@ -14,8 +14,9 @@ Mongoid.configure do |config|
14
14
  name = "trackoid_test"
15
15
  host = "localhost"
16
16
  port = "27017"
17
- # config.master = Mongo::Connection.new(host, port, :logger => Logger.new(STDOUT)).db(name)
17
+ # config.master = Mongo::Connection.new(host, port, :logger => Logger.new(STDOUT)).db(name)
18
18
  config.master = Mongo::Connection.new.db(name)
19
+ config.use_object_ids = true
19
20
  end
20
21
 
21
22
  Spec::Runner.configure do |config|
@@ -49,7 +49,13 @@ describe Mongoid::Tracking do
49
49
  end
50
50
 
51
51
  it "should create a method for accesing the stats" do
52
- @mock.respond_to?(:visits).should == true
52
+ @mock.respond_to?(:visits).should be_true
53
+ end
54
+
55
+ it "should respond 'false' to field_changed? method" do
56
+ # Ok, this test is not very relevant since it will return false even
57
+ # if Trackid does not override it.
58
+ @mock.visits_changed?.should be_false
53
59
  end
54
60
 
55
61
  it "should create a method for accesing the stats of the proper class" do
@@ -57,14 +63,14 @@ describe Mongoid::Tracking do
57
63
  end
58
64
 
59
65
  it "should create an array in the class with all tracking fields" do
60
- @mock.class.tracked_fields.should == [ :visits_data ]
66
+ @mock.class.tracked_fields.should == [ :visits ]
61
67
  end
62
68
 
63
69
  it "should create an array in the class with all tracking fields even when monkey patching" do
64
70
  class Test
65
71
  track :something_else
66
72
  end
67
- @mock.class.tracked_fields.should == [ :visits_data, :something_else_data ]
73
+ @mock.class.tracked_fields.should == [ :visits, :something_else ]
68
74
  end
69
75
 
70
76
  it "should not update stats when new record" do
@@ -211,8 +217,10 @@ describe Mongoid::Tracking do
211
217
  end
212
218
  tm = TestModel.first
213
219
  tm.something.today.should == 0
220
+ tm.something.inc
221
+ tm.something.today.should == 1
214
222
  end
215
-
223
+
216
224
  end
217
225
 
218
226
  end
data/trackoid.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{trackoid}
8
- s.version = "0.1.1"
8
+ s.version = "0.1.2"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Jose Miguel Perez"]
12
- s.date = %q{2010-06-04}
12
+ s.date = %q{2010-06-06}
13
13
  s.description = %q{Trackoid uses an embeddable approach to track analytics data using the poweful features of MongoDB for scalability}
14
14
  s.email = %q{josemiguel@perezruiz.com}
15
15
  s.extra_rdoc_files = [
@@ -27,6 +27,7 @@ Gem::Specification.new do |s|
27
27
  "lib/trackoid/aggregates.rb",
28
28
  "lib/trackoid/errors.rb",
29
29
  "lib/trackoid/tracker.rb",
30
+ "lib/trackoid/tracker_aggregates.rb",
30
31
  "lib/trackoid/tracking.rb",
31
32
  "spec/aggregates_spec.rb",
32
33
  "spec/spec.opts",
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trackoid
3
3
  version: !ruby/object:Gem::Version
4
- hash: 25
4
+ hash: 31
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
8
  - 1
9
- - 1
10
- version: 0.1.1
9
+ - 2
10
+ version: 0.1.2
11
11
  platform: ruby
12
12
  authors:
13
13
  - Jose Miguel Perez
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-06-04 00:00:00 +02:00
18
+ date: 2010-06-06 00:00:00 +02:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -54,6 +54,7 @@ files:
54
54
  - lib/trackoid/aggregates.rb
55
55
  - lib/trackoid/errors.rb
56
56
  - lib/trackoid/tracker.rb
57
+ - lib/trackoid/tracker_aggregates.rb
57
58
  - lib/trackoid/tracking.rb
58
59
  - spec/aggregates_spec.rb
59
60
  - spec/spec.opts