trackoid 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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