tractor 0.4.9 → 0.5.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/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.8
1
+ 0.5.0
@@ -1,7 +1,6 @@
1
1
  require 'base64'
2
2
 
3
3
  module Tractor
4
-
5
4
  class MissingIdError < StandardError; end
6
5
  class DuplicateKeyError < StandardError; end
7
6
  class MissingIndexError < StandardError; end
@@ -40,7 +39,7 @@ module Tractor
40
39
  end
41
40
 
42
41
  def count
43
- ids.size
42
+ Tractor.redis.scard(key)
44
43
  end
45
44
 
46
45
  def all
@@ -77,7 +76,32 @@ module Tractor
77
76
  end
78
77
 
79
78
  module Model
79
+ module Dirty
80
+ def self.key
81
+ "Tractor::Model::Dirty:all"
82
+ end
83
+
84
+ def mark
85
+ Tractor.redis.sadd Dirty.key, "#{self.class},#{id}"
86
+ end
87
+
88
+ def self.all
89
+ Tractor.redis.smembers(Dirty.key).each do |k|
90
+ klass, id = k.split(',')
91
+ data = {:id => id, :klass => klass, :data => eval(klass).find_by_id(id).send(:attribute_store)}
92
+ begin
93
+ yield data
94
+ Tractor.redis.srem(Dirty.key, k)
95
+ rescue Exception => e
96
+ # Something went wrong, this should be handled on the callers end, but this prevents us from
97
+ # removing the dirty record from our list while continuing to process other dirty items.
98
+ end
99
+ end
100
+ end
101
+ end
102
+
80
103
  class Base
104
+ include Dirty
81
105
  def initialize(attributes={})
82
106
  @attribute_store = {}
83
107
  @association_store = {}
@@ -93,6 +117,8 @@ module Tractor
93
117
  Tractor.redis.sadd "#{self.class}:all", self.id
94
118
  add_to_indices
95
119
  add_to_associations
120
+ mark
121
+ self.class.run_callbacks(:after_save, self)
96
122
 
97
123
  return self
98
124
  end
@@ -102,6 +128,7 @@ module Tractor
102
128
  remove_from_associations
103
129
  Tractor.redis.srem("#{self.class}:all", self.id)
104
130
  Tractor.redis.del "#{self.class}:#{self.id}"
131
+ self.class.run_callbacks(:after_destroy, self)
105
132
  end
106
133
 
107
134
  def update(attributes = {})
@@ -147,12 +174,31 @@ module Tractor
147
174
  end
148
175
 
149
176
  class << self
150
- attr_reader :attributes, :associations, :indices
177
+ attr_reader :attributes, :associations, :indices, :callbacks
178
+
179
+ def after_create(name)
180
+ callbacks[:after_create] << name
181
+ end
182
+
183
+ def after_destroy(name)
184
+ callbacks[:after_destroy] << name
185
+ end
186
+
187
+ def after_save(name)
188
+ callbacks[:after_save] << name
189
+ end
190
+
191
+ def run_callbacks(type, obj)
192
+ callbacks[type].each do |cb|
193
+ obj.send(cb)
194
+ end
195
+ end
151
196
 
152
197
  def create(attributes={})
153
198
  raise DuplicateKeyError, "Duplicate value for #{self} 'id'" if Tractor.redis.sismember("#{self}:all", attributes[:id])
154
199
  m = new(attributes)
155
200
  m.save
201
+ run_callbacks(:after_create, m)
156
202
  m
157
203
  end
158
204
 
@@ -215,7 +261,7 @@ module Tractor
215
261
  end
216
262
 
217
263
  def count
218
- ids.size
264
+ Tractor.redis.scard("#{self}:all")
219
265
  end
220
266
 
221
267
  def all
@@ -259,6 +305,10 @@ module Tractor
259
305
  def indices
260
306
  @indices ||= []
261
307
  end
308
+
309
+ def callbacks
310
+ @callbacks ||= Hash.new {|h, k| h[k] = [] }
311
+ end
262
312
  end
263
313
 
264
314
  private
@@ -0,0 +1,59 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'rubygems'
4
+ require 'tractor'
5
+ require 'faker'
6
+
7
+ Tractor.connectdb
8
+ Tractor.flushdb
9
+
10
+ class Person < Tractor::Model::Base
11
+ attribute :id
12
+ attribute :first_name, :index => true
13
+ attribute :last_name, :index => true
14
+ attribute :awesome, :type => :boolean, :index => true
15
+ attribute :company_id, :index => true
16
+ end
17
+
18
+ class Company < Tractor::Model::Base
19
+ association :people, Person
20
+
21
+ attribute :id
22
+ attribute :name
23
+ end
24
+
25
+ 10.times do |i|
26
+ Company.create(:id => i+1, :name => Faker::Company.name)
27
+ end
28
+
29
+ puts "Companies created successfully"
30
+
31
+ 10.times do |i|
32
+ Person.create(
33
+ :id => i+1,
34
+ :first_name => Faker::Name.first_name,
35
+ :last_name => Faker::Name.last_name,
36
+ :awesome => rand(2)==0,
37
+ :company_id => rand(Company.count)+1)
38
+ end
39
+
40
+ puts "Employees created successfully"
41
+
42
+ puts "# of dirty objects: #{Tractor.redis.scard("Tractor::Model::Dirty:all")}"
43
+
44
+ 20.times do
45
+ obj = Tractor.redis.spop("Tractor::Model::Dirty:all")
46
+ puts "Background syncinng dirty object: #{obj}"
47
+ end
48
+
49
+ puts "# of dirty objects: #{Tractor.redis.scard("Tractor::Model::Dirty:all")}"
50
+
51
+ puts "updating company with id 1"
52
+
53
+ c = Company.find_by_id(1)
54
+ puts "Company name: #{c.name}"
55
+ c.name = "Jelly Copter Inc."
56
+ c.save
57
+ puts "New Company name: #{c.name}"
58
+
59
+ puts "# of dirty objects: #{Tractor.redis.scard("Tractor::Model::Dirty:all")}"
data/performance/test.rb CHANGED
@@ -27,6 +27,8 @@ end
27
27
  Company.create(:id => i+1, :name => Faker::Company.name)
28
28
  end
29
29
 
30
+ puts "Companies created successfully"
31
+
30
32
  10000.times do |i|
31
33
  Person.create(
32
34
  :id => i+1,
@@ -1,6 +1,6 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
2
 
3
- describe Tractor::Model::Base do
3
+ describe Tractor do
4
4
  attr_reader :redis
5
5
  before do
6
6
  module Tractor
@@ -11,426 +11,529 @@ describe Tractor::Model::Base do
11
11
  attribute :wins_loses
12
12
  attribute :game_id
13
13
  end
14
-
14
+
15
15
  class Game < Tractor::Model::Base
16
16
  attribute :id
17
17
  attribute :board
18
18
  attribute :flying_object
19
19
  attribute :score, :type => :integer, :index => true
20
-
20
+
21
21
  association :players, Tractor::Model::Player
22
22
  end
23
23
  end
24
24
  end
25
25
  end
26
-
26
+
27
27
  after do
28
28
  Tractor.redis.flushdb
29
29
  end
30
30
 
31
- describe ".attribute" do
32
- it "inserts the values into the attributes class instance variable" do
33
- Tractor::Model::Game.attributes.should include(:board)
34
- end
35
-
36
- it "allows you to specify what type the value should be when it comes out of the tractor" do
37
- Tractor::Model::Game.attributes[:score][:type].should == :integer
38
- end
31
+ describe Tractor::Model::Base do
32
+ describe ".attribute" do
33
+ it "inserts the values into the attributes class instance variable" do
34
+ Tractor::Model::Game.attributes.should include(:board)
35
+ end
39
36
 
40
- it "creates a set method for each attribute" do
41
- game = Tractor::Model::Game.new(:board => "fancy")
42
- game.send(:attribute_store)[:board].should == "fancy"
43
- end
37
+ it "allows you to specify what type the value should be when it comes out of the tractor" do
38
+ Tractor::Model::Game.attributes[:score][:type].should == :integer
39
+ end
44
40
 
45
- it "creates a get method for each attribute" do
46
- game = Tractor::Model::Game.new(:board => "schmancy")
47
- game.board.should == "schmancy"
48
- end
41
+ it "creates a set method for each attribute" do
42
+ game = Tractor::Model::Game.new(:board => "fancy")
43
+ game.send(:attribute_store)[:board].should == "fancy"
44
+ end
49
45
 
50
- describe "when attribute is a boolean" do
51
- it "returns a boolean" do
52
- expensive_sammich = Sammich.new(:expensive => true)
53
- expensive_sammich.expensive.should == true
54
- expensive_sammich.expensive.should be_a(TrueClass)
46
+ it "creates a get method for each attribute" do
47
+ game = Tractor::Model::Game.new(:board => "schmancy")
48
+ game.board.should == "schmancy"
55
49
  end
56
- end
57
50
 
58
- describe "when attribute is a integer" do
59
- it "returns an integer" do
60
- game = Tractor::Model::Game.new(:score => 1222)
61
- game.score.should == 1222
62
- game.score.should be_a(Fixnum)
51
+ describe "when attribute is a boolean" do
52
+ it "returns a boolean" do
53
+ expensive_sammich = Sammich.new(:expensive => true)
54
+ expensive_sammich.expensive.should == true
55
+ expensive_sammich.expensive.should be_a(TrueClass)
56
+ end
63
57
  end
64
- end
65
58
 
66
- describe "when attribute is an index" do
67
- before do
68
- class Zombo < Tractor::Model::Base
69
- attribute :anything, :index => true
59
+ describe "when attribute is a integer" do
60
+ it "returns an integer" do
61
+ game = Tractor::Model::Game.new(:score => 1222)
62
+ game.score.should == 1222
63
+ game.score.should be_a(Fixnum)
70
64
  end
71
65
  end
66
+
67
+ describe "when attribute is an index" do
68
+ before do
69
+ class Zombo < Tractor::Model::Base
70
+ attribute :anything, :index => true
71
+ end
72
+ end
72
73
 
73
- it "returns creates an index for the attribute" do
74
- Zombo.indices.should == [:anything]
74
+ it "returns creates an index for the attribute" do
75
+ Zombo.indices.should == [:anything]
76
+ end
75
77
  end
76
78
  end
77
- end
78
79
 
79
- describe "#association" do
80
- attr_reader :game, :player1, :player2
80
+ describe "#association" do
81
+ attr_reader :game, :player1, :player2
81
82
 
82
- before do
83
- @game = Tractor::Model::Game.new({ :id => 'g1' })
84
- Tractor::Model::Game.create({ :id => 'g2' })
85
- @player1 = Tractor::Model::Player.new({ :id => 'p1', :name => "delicious", :game_id => "g1" })
86
- @player2 = Tractor::Model::Player.new({ :id => 'p2', :name => "gross", :game_id => "g2" })
83
+ before do
84
+ @game = Tractor::Model::Game.new({ :id => 'g1' })
85
+ Tractor::Model::Game.create({ :id => 'g2' })
86
+ @player1 = Tractor::Model::Player.new({ :id => 'p1', :name => "delicious", :game_id => "g1" })
87
+ @player2 = Tractor::Model::Player.new({ :id => 'p2', :name => "gross", :game_id => "g2" })
87
88
 
88
- game.save
89
- player1.save
90
- player2.save
91
- end
89
+ game.save
90
+ player1.save
91
+ player2.save
92
+ end
92
93
 
93
- it "adds a method with the given name to the instance" do # "Monkey:a1a:SET_NAME"
94
- game.players.should be_a(Tractor::Association)
95
- end
94
+ it "adds a method with the given name to the instance" do # "Monkey:a1a:SET_NAME"
95
+ game.players.should be_a(Tractor::Association)
96
+ end
96
97
 
97
- it "adds a push method for the set on an instance of the class" do
98
- game.players.push player2
99
- redis.smembers('Tractor::Model::Game:g1:players').should == ['p1', 'p2']
100
- end
98
+ it "adds a push method for the set on an instance of the class" do
99
+ game.players.push player2
100
+ redis.smembers('Tractor::Model::Game:g1:players').should == ['p1', 'p2']
101
+ end
101
102
 
102
- it "adds an ids method for the set that returns all ids in it" do
103
- game.players.ids.should == [player1.id]
104
- end
103
+ it "adds an ids method for the set that returns all ids in it" do
104
+ game.players.ids.should == [player1.id]
105
+ end
105
106
 
106
- it "adds a count method for the set that returns the size of the association" do
107
- game.players.count.should == 1
108
- end
107
+ it "adds a count method for the set that returns the size of the association" do
108
+ game.players.count.should == 1
109
+ end
109
110
 
110
- it "automatically adds items to association when they are created" do
111
- bocci_ball = Tractor::Model::Game.create({ :id => "bocci_ball" })
112
- Tractor::Model::Player.create({ :id => "tobias", :name => "deciduous", :game_id => "bocci_ball" })
113
- bocci_ball.players.ids.should == ["tobias"]
114
- end
111
+ it "automatically adds items to association when they are created" do
112
+ bocci_ball = Tractor::Model::Game.create({ :id => "bocci_ball" })
113
+ Tractor::Model::Player.create({ :id => "tobias", :name => "deciduous", :game_id => "bocci_ball" })
114
+ bocci_ball.players.ids.should == ["tobias"]
115
+ end
115
116
 
116
- it "adds an all method for the association to return the items in it" do
117
- player1_from_game = game.players.all[0]
118
- player1_from_game.name.should == player1.name
119
- player1_from_game.id.should == player1.id
120
- end
117
+ it "adds an all method for the association to return the items in it" do
118
+ player1_from_game = game.players.all[0]
119
+ player1_from_game.name.should == player1.name
120
+ player1_from_game.id.should == player1.id
121
+ end
121
122
 
122
- it "requires the object being added to have been saved to the database before adding it to the association"
123
- end
123
+ it "requires the object being added to have been saved to the database before adding it to the association"
124
+ end
124
125
 
125
- describe ".indices" do
126
- it "returns all indices on a class" do
127
- Sammich.indices.should == [:product, :weight]
126
+ describe ".indices" do
127
+ it "returns all indices on a class" do
128
+ Sammich.indices.should == [:product, :weight]
129
+ end
128
130
  end
129
- end
130
131
 
131
- describe "index" do
132
- it "removes newline characters from index key"
133
- end
132
+ describe "index" do
133
+ it "removes newline characters from index key"
134
+ end
134
135
 
135
- describe ".attributes" do
136
- attr_reader :sorted_attributes
136
+ describe ".attributes" do
137
+ attr_reader :sorted_attributes
137
138
 
138
- before do
139
- @sorted_attributes = Tractor::Model::Game.attributes.keys.sort{|x,y| x.to_s <=> y.to_s}
140
- end
139
+ before do
140
+ @sorted_attributes = Tractor::Model::Game.attributes.keys.sort{|x,y| x.to_s <=> y.to_s}
141
+ end
141
142
 
142
- it "returns all attributes that have been added to this class" do
143
- sorted_attributes.size.should == 4
144
- sorted_attributes.should == [:board, :flying_object, :id, :score]
145
- end
143
+ it "returns all attributes that have been added to this class" do
144
+ sorted_attributes.size.should == 4
145
+ sorted_attributes.should == [:board, :flying_object, :id, :score]
146
+ end
146
147
 
147
- it "allows different attributes to be specified for different child classes" do
148
- Tractor::Model::Game.attributes.size.should == 4
149
- Tractor::Model::Player.attributes.size.should == 4
148
+ it "allows different attributes to be specified for different child classes" do
149
+ Tractor::Model::Game.attributes.size.should == 4
150
+ Tractor::Model::Player.attributes.size.should == 4
150
151
 
151
- Tractor::Model::Game.attributes.keys.should_not include(:name)
152
- Tractor::Model::Player.attributes.keys.should_not include(:flying_object)
152
+ Tractor::Model::Game.attributes.keys.should_not include(:name)
153
+ Tractor::Model::Player.attributes.keys.should_not include(:flying_object)
154
+ end
153
155
  end
154
- end
155
156
 
156
- describe "#save" do
157
- it "raises if id is nil or empty" do
158
- game = Tractor::Model::Game.new
159
- game.id = nil
160
- lambda { game.save }.should raise_error(Tractor::MissingIdError, "Probably wanna set an id")
161
- game.id = ''
162
- lambda { game.save }.should raise_error(Tractor::MissingIdError, "Probably wanna set an id")
163
- end
157
+ describe "#save" do
158
+ it "raises if id is nil or empty" do
159
+ game = Tractor::Model::Game.new
160
+ game.id = nil
161
+ lambda { game.save }.should raise_error(Tractor::MissingIdError, "Probably wanna set an id")
162
+ game.id = ''
163
+ lambda { game.save }.should raise_error(Tractor::MissingIdError, "Probably wanna set an id")
164
+ end
164
165
 
165
- it "should write attributes to redis" do
166
- game = Tractor::Model::Game.new({:id => '1', :board => "large", :flying_object => "disc"})
167
- game.save
166
+ it "should write attributes to redis" do
167
+ game = Tractor::Model::Game.new({:id => '1', :board => "large", :flying_object => "disc"})
168
+ game.save
168
169
 
169
- redis_game_attributes = Tractor.redis.mapped_hmget("Tractor::Model::Game:1", "id", "board", "flying_object")
170
- redis_game_attributes["id"].should == "1"
171
- redis_game_attributes["board"].should == "large"
172
- redis_game_attributes["flying_object"].should == "disc"
173
- end
170
+ redis_game_attributes = Tractor.redis.mapped_hmget("Tractor::Model::Game:1", "id", "board", "flying_object")
171
+ redis_game_attributes["id"].should == "1"
172
+ redis_game_attributes["board"].should == "large"
173
+ redis_game_attributes["flying_object"].should == "disc"
174
+ end
174
175
 
175
- it "appends the new object to the Game set" do
176
- Tractor::Model::Game.all.size.should == 0
177
- game = Tractor::Model::Game.new({ :id => '1', :board => "small" })
178
- game.save
176
+ it "appends the new object to the Game set" do
177
+ Tractor::Model::Game.all.size.should == 0
178
+ game = Tractor::Model::Game.new({ :id => '1', :board => "small" })
179
+ game.save
179
180
 
180
- Tractor::Model::Game.all.size.should == 1
181
- end
182
- end
181
+ Tractor::Model::Game.all.size.should == 1
182
+ end
183
183
 
184
- describe ".all" do
185
- it "every object that is created for this class will be in this set" do
186
- MonkeyClient.all.size.should == 0
187
- MonkeyClient.create({ :id => 'a1a', :evil => true, :birthday => "Dec 3" })
188
- MonkeyClient.create({ :id => 'b1b', :evil => false, :birthday => "Dec 4" })
189
- MonkeyClient.all.size.should == 2
184
+ it "puts the record in the dirty set to be persisted to the databse the next time it polls" do
185
+ Tractor::Model::Game.all.size.should == 0
186
+ game = Tractor::Model::Game.new({ :id => '1', :board => "small" })
187
+ game.save
188
+
189
+ Tractor.redis.smembers("Tractor::Model::Dirty:all").should include("Tractor::Model::Game,1")
190
+ end
190
191
  end
191
192
 
192
- it "each class only tracks their own" do
193
- MonkeyClient.all.size.should == 0
194
- BananaClient.all.size.should == 0
193
+ describe ".all" do
194
+ it "every object that is created for this class will be in this set" do
195
+ MonkeyClient.all.size.should == 0
196
+ MonkeyClient.create({ :id => 'a1a', :evil => true, :birthday => "Dec 3" })
197
+ MonkeyClient.create({ :id => 'b1b', :evil => false, :birthday => "Dec 4" })
198
+ MonkeyClient.all.size.should == 2
199
+ end
200
+
201
+ it "each class only tracks their own" do
202
+ MonkeyClient.all.size.should == 0
203
+ BananaClient.all.size.should == 0
195
204
 
196
- MonkeyClient.create({ :id => 'a1a', :evil => true, :birthday => "Dec 3" })
197
- BananaClient.create({ :id => 'a1a', :name => "delicious" })
205
+ MonkeyClient.create({ :id => 'a1a', :evil => true, :birthday => "Dec 3" })
206
+ BananaClient.create({ :id => 'a1a', :name => "delicious" })
198
207
 
199
- MonkeyClient.all.size.should == 1
200
- BananaClient.all.size.should == 1
208
+ MonkeyClient.all.size.should == 1
209
+ BananaClient.all.size.should == 1
210
+ end
211
+
212
+ it "returns the entire instance of a given object" do
213
+ MonkeyClient.create({ :id => 'a1a', :evil => true, :birthday => "Dec 3" })
214
+ MonkeyClient.all[0].birthday.should == "Dec 3"
215
+ end
201
216
  end
217
+
218
+ describe ".ids" do
219
+ before do
220
+ Sammich.create({ :id => 's1', :weight => "medium", :product => "Turkey Avocado" })
221
+ Sammich.create({ :id => 's2', :weight => "medium", :product => "Reuben Sammich" })
222
+ end
202
223
 
203
- it "returns the entire instance of a given object" do
204
- MonkeyClient.create({ :id => 'a1a', :evil => true, :birthday => "Dec 3" })
205
- MonkeyClient.all[0].birthday.should == "Dec 3"
224
+ it "returns all the ids for a given class" do
225
+ Sammich.ids.should == ['s1', 's2']
226
+ end
206
227
  end
207
- end
208
228
 
209
- describe ".ids" do
210
- before do
211
- Sammich.create({ :id => 's1', :weight => "medium", :product => "Turkey Avocado" })
212
- Sammich.create({ :id => 's2', :weight => "medium", :product => "Reuben Sammich" })
229
+ describe ".count" do
230
+ before do
231
+ Sammich.create({ :id => 's1', :weight => "medium", :product => "Turkey Avocado" })
232
+ Sammich.create({ :id => 's2', :weight => "medium", :product => "Reuben Sammich" })
233
+ end
234
+
235
+ it "returns the count of all items of a given class" do
236
+ Sammich.count.should == 2
237
+ end
213
238
  end
214
239
 
215
- it "returns all the ids for a given class" do
216
- Sammich.ids.should == ['s1', 's2']
240
+ describe "after_create" do
241
+ before do
242
+ class Callbackinator
243
+ after_create :reverse_name
244
+
245
+ def reverse_name
246
+ self.name = self.name.reverse
247
+ self.save
248
+ end
249
+ end
250
+ end
251
+
252
+ it "adds a method to be called after an instance is created" do
253
+ obj = Callbackinator.create(:name => "asdf", :id => 1)
254
+ obj.name.should == "fdsa"
255
+ end
217
256
  end
218
- end
219
-
220
- describe ".count" do
221
- before do
222
- Sammich.create({ :id => 's1', :weight => "medium", :product => "Turkey Avocado" })
223
- Sammich.create({ :id => 's2', :weight => "medium", :product => "Reuben Sammich" })
257
+
258
+ describe "after_save" do
259
+ before do
260
+ class Callbackinator
261
+ after_save :duplicate_name
262
+
263
+ def duplicate_name
264
+ self.name = self.name*2
265
+ end
266
+ end
267
+ end
268
+
269
+ it "adds a method to be called after an instance is created" do
270
+ obj = Callbackinator.new(:name => "asdf", :id => 1)
271
+ obj.save
272
+ obj.name.should == "asdfasdf"
273
+ end
224
274
  end
225
275
 
226
- it "returns the count of all items of a given class" do
227
- Sammich.count.should == 2
276
+ describe "after_destroy" do
277
+ before do
278
+ class Callbackinator
279
+ after_destroy :put_something_in_redis
280
+
281
+ def put_something_in_redis
282
+ Tractor.redis["something_in_redis"] = "thanks!"
283
+ end
284
+ end
285
+ end
286
+
287
+ it "adds a method to be called after an instance is destroyed" do
288
+ obj = Callbackinator.create(:name => "asdf", :id => 1)
289
+ Tractor.redis["something_in_redis"].should be_nil
290
+ obj.destroy
291
+ Tractor.redis["something_in_redis"].should == "thanks!"
292
+ end
228
293
  end
229
- end
230
294
 
231
- describe "#create" do
232
- it "allows you to specify which attributes should be unique"
295
+ describe ".create" do
296
+ it "allows you to specify which attributes should be unique"
233
297
 
234
- it "raises exception if the id exists" do
235
- MonkeyClient.create({ :id => 'a1a', :evil => true, :birthday => "Dec 3" })
236
- lambda do
237
- MonkeyClient.create({ :id => 'a1a', :evil => false, :birthday => "Jan 4" })
238
- end.should raise_error(Tractor::DuplicateKeyError, "Duplicate value for MonkeyClient 'id'")
239
- end
298
+ it "raises exception if the id exists" do
299
+ MonkeyClient.create({ :id => 'a1a', :evil => true, :birthday => "Dec 3" })
300
+ lambda do
301
+ MonkeyClient.create({ :id => 'a1a', :evil => false, :birthday => "Jan 4" })
302
+ end.should raise_error(Tractor::DuplicateKeyError, "Duplicate value for MonkeyClient 'id'")
303
+ end
240
304
 
241
- it "should write attributes to redis" do
242
- sammich = Sammich.create({ :id => '1', :product => "Veggie Sammich" })
243
- redis_sammich = Tractor.redis.mapped_hmget("Sammich:1", "id", "product")
244
- redis_sammich["id"].should == "1"
245
- redis_sammich["product"].should == "Veggie Sammich"
246
- end
305
+ it "should write attributes to redis" do
306
+ sammich = Sammich.create({ :id => '1', :product => "Veggie Sammich" })
307
+ redis_sammich = Tractor.redis.mapped_hmget("Sammich:1", "id", "product")
308
+ redis_sammich["id"].should == "1"
309
+ redis_sammich["product"].should == "Veggie Sammich"
310
+ end
247
311
 
248
- it "populates all the indices that are specified on the class" do
249
- Sammich.create({ :id => '1', :weight => "heavy", :product => "Ham Sammich" })
250
- Sammich.create({ :id => '2', :weight => "heavy", :product => "Tuna Sammich" })
312
+ it "populates all the indices that are specified on the class" do
313
+ Sammich.create({ :id => '1', :weight => "heavy", :product => "Ham Sammich" })
314
+ Sammich.create({ :id => '2', :weight => "heavy", :product => "Tuna Sammich" })
251
315
 
252
- redis.smembers("Sammich:product:SGFtIFNhbW1pY2g=").should include('1')
253
- redis.smembers("Sammich:product:VHVuYSBTYW1taWNo").should include('2')
254
- redis.smembers("Sammich:weight:aGVhdnk=").should == ['1', '2']
255
- end
316
+ redis.smembers("Sammich:product:SGFtIFNhbW1pY2g=").should include('1')
317
+ redis.smembers("Sammich:product:VHVuYSBTYW1taWNo").should include('2')
318
+ redis.smembers("Sammich:weight:aGVhdnk=").should == ['1', '2']
319
+ end
256
320
 
257
- it "returns the instance that has been created" do
258
- sammich = Sammich.create({ :id => '1', :weight => "heavy", :product => "Tuna Melt" })
259
- sammich.weight.should == "heavy"
321
+ it "returns the instance that has been created" do
322
+ sammich = Sammich.create({ :id => '1', :weight => "heavy", :product => "Tuna Melt" })
323
+ sammich.weight.should == "heavy"
324
+ end
260
325
  end
261
- end
262
326
 
263
- describe ".exists?" do
264
- it "returns true if the object with the given id exists" do
265
- MonkeyClient.create({ :id => 'a1a', :evil => true, :birthday => "Dec 3" })
266
- MonkeyClient.exists?('a1a').should be_true
267
- end
327
+ describe ".exists?" do
328
+ it "returns true if the object with the given id exists" do
329
+ MonkeyClient.create({ :id => 'a1a', :evil => true, :birthday => "Dec 3" })
330
+ MonkeyClient.exists?('a1a').should be_true
331
+ end
268
332
 
269
- it "returns false if the object with the given id does not exist" do
270
- MonkeyClient.exists?('a1a').should be_false
333
+ it "returns false if the object with the given id does not exist" do
334
+ MonkeyClient.exists?('a1a').should be_false
335
+ end
271
336
  end
272
- end
273
337
 
274
- describe ".find_by_id" do
275
- it "takes an id and returns the object from redis" do
276
- sammich = Sammich.create({ :id => '1', :product => "Cold Cut Trio" })
338
+ describe ".find_by_id" do
339
+ it "takes an id and returns the object from redis" do
340
+ sammich = Sammich.create({ :id => '1', :product => "Cold Cut Trio" })
277
341
 
278
- redis_sammich = Sammich.find_by_id('1')
279
- redis_sammich.product.should == "Cold Cut Trio"
280
- end
342
+ redis_sammich = Sammich.find_by_id('1')
343
+ redis_sammich.product.should == "Cold Cut Trio"
344
+ end
281
345
 
282
- it "returns nil if the keys do not exist in redis" do
283
- Sammich.find_by_id('1').should be_nil
346
+ it "returns nil if the keys do not exist in redis" do
347
+ Sammich.find_by_id('1').should be_nil
348
+ end
284
349
  end
285
- end
286
350
 
287
- describe "#update" do
288
- attr_reader :sammich
351
+ describe "#update" do
352
+ attr_reader :sammich
289
353
 
290
- before do
291
- @sammich = Sammich.create({ :id => '1', :weight => "medium", :product => "Turkey Avocado" })
292
- end
354
+ before do
355
+ @sammich = Sammich.create({ :id => '1', :weight => "medium", :product => "Turkey Avocado" })
356
+ end
293
357
 
294
- it "updates the item from redis" do
295
- @sammich.update( {:weight => "heavy"} )
296
- @sammich.weight.should == "heavy"
297
- end
358
+ it "updates the item from redis" do
359
+ @sammich.update( {:weight => "heavy"} )
360
+ @sammich.weight.should == "heavy"
361
+ end
298
362
 
299
- it "does not update the id" do
300
- @sammich.update( {:id => "111111"} )
301
- @sammich.id.should == "1"
302
- end
363
+ it "does not update the id" do
364
+ @sammich.update( {:id => "111111"} )
365
+ @sammich.id.should == "1"
366
+ end
303
367
 
304
- it "only changes attributes passed in" do
305
- @sammich.update( {:weight => "light"} )
306
- @sammich.id.should == "1"
307
- @sammich.weight.should == "light"
308
- @sammich.product.should == "Turkey Avocado"
309
- end
368
+ it "only changes attributes passed in" do
369
+ @sammich.update( {:weight => "light"} )
370
+ @sammich.id.should == "1"
371
+ @sammich.weight.should == "light"
372
+ @sammich.product.should == "Turkey Avocado"
373
+ end
310
374
 
311
- it "updates all the indices associated with this object" do
312
- Sammich.find( {:weight => "light"} ).should be_empty
313
- Sammich.find( {:weight => "medium"} ).should_not be_empty
314
- sammich.update( {:weight => "light"} )
315
- Sammich.find( {:weight => "light"} ).should_not be_empty
316
- Sammich.find( {:weight => "medium"} ).should be_empty
317
- end
375
+ it "updates all the indices associated with this object" do
376
+ Sammich.find( {:weight => "light"} ).should be_empty
377
+ Sammich.find( {:weight => "medium"} ).should_not be_empty
378
+ sammich.update( {:weight => "light"} )
379
+ Sammich.find( {:weight => "light"} ).should_not be_empty
380
+ Sammich.find( {:weight => "medium"} ).should be_empty
381
+ end
318
382
 
319
- it "raises if object has not been saved yet"
320
- end
383
+ it "raises if object has not been saved yet"
384
+ end
321
385
 
322
- describe "#destroy" do
323
- attr_reader :cheese, :balogna
386
+ describe "#destroy" do
387
+ attr_reader :cheese, :balogna
324
388
 
325
- before do
326
- @cheese = Sammich.create({ :id => '1', :weight => "medium", :product => "Cheese Sammich" })
327
- @balogna = Sammich.create({ :id => '2', :weight => "medium", :product => "Balogna Sammich" })
328
- end
389
+ before do
390
+ @cheese = Sammich.create({ :id => '1', :weight => "medium", :product => "Cheese Sammich" })
391
+ @balogna = Sammich.create({ :id => '2', :weight => "medium", :product => "Balogna Sammich" })
392
+ end
329
393
 
330
- it "removes the item from redis" do
331
- @cheese.destroy
332
- Sammich.find_by_id(cheese.id).should be_nil
333
- end
394
+ it "removes the item from redis" do
395
+ @cheese.destroy
396
+ Sammich.find_by_id(cheese.id).should be_nil
397
+ end
334
398
 
335
- it "removes the id from the all index" do
336
- Sammich.all.map{|t| t.id }.should == ["1", "2"]
337
- cheese.destroy
338
- Sammich.all.map{|t| t.id }.should == ["2"]
339
- end
399
+ it "removes the id from the all index" do
400
+ Sammich.all.map{|t| t.id }.should == ["1", "2"]
401
+ cheese.destroy
402
+ Sammich.all.map{|t| t.id }.should == ["2"]
403
+ end
340
404
 
341
- it "removes the id from all of it's other indices" do
342
- Sammich.find({ :weight => "medium" }).size.should == 2
343
- cheese.destroy
344
- Sammich.find({ :weight => "medium" }).size.should == 1
345
- end
405
+ it "removes the id from all of it's other indices" do
406
+ Sammich.find({ :weight => "medium" }).size.should == 2
407
+ cheese.destroy
408
+ Sammich.find({ :weight => "medium" }).size.should == 1
409
+ end
346
410
 
347
- it "removes the id from all of the associations that it may be in" do
348
- bocci_ball = Tractor::Model::Game.create({ :id => "bocci_ball" })
349
- tobias = Tractor::Model::Player.create({ :id => "tobias", :name => "deciduous", :game_id => "bocci_ball" })
350
- bocci_ball.players.ids.should == ["tobias"]
351
- tobias.destroy
411
+ it "removes the id from all of the associations that it may be in" do
412
+ bocci_ball = Tractor::Model::Game.create({ :id => "bocci_ball" })
413
+ tobias = Tractor::Model::Player.create({ :id => "tobias", :name => "deciduous", :game_id => "bocci_ball" })
414
+ bocci_ball.players.ids.should == ["tobias"]
415
+ tobias.destroy
352
416
 
353
- bocci_ball.players.ids.should == []
417
+ bocci_ball.players.ids.should == []
418
+ end
354
419
  end
355
- end
356
420
 
357
- describe ".find" do
358
- attr_reader :cheese, :balogna
421
+ describe ".find" do
422
+ attr_reader :cheese, :balogna
359
423
 
360
- before do
361
- @cheese = Sammich.create({ :id => '1', :weight => "medium", :product => "Cheese Sammich" })
362
- @balogna = Sammich.create({ :id => '2', :weight => "medium", :product => "Balogna Sammich" })
363
- @meat = Sammich.create({ :id => '3', :weight => "heavy", :product => "Meat & More Sammich" })
364
- end
424
+ before do
425
+ @cheese = Sammich.create({ :id => '1', :weight => "medium", :product => "Cheese Sammich" })
426
+ @balogna = Sammich.create({ :id => '2', :weight => "medium", :product => "Balogna Sammich" })
427
+ @meat = Sammich.create({ :id => '3', :weight => "heavy", :product => "Meat & More Sammich" })
428
+ end
365
429
 
366
- context "when searching on 1 attribute" do
367
- it "returns all matching products" do
368
- redis_cheese, redis_balogna = Sammich.find( {:weight => "medium" } )
430
+ context "when searching on 1 attribute" do
431
+ it "returns all matching products" do
432
+ redis_cheese, redis_balogna = Sammich.find( {:weight => "medium" } )
369
433
 
370
- redis_cheese.id.should == cheese.id
371
- redis_cheese.product.should == cheese.product
372
- redis_balogna.id.should == balogna.id
373
- redis_balogna.product.should == balogna.product
434
+ redis_cheese.id.should == cheese.id
435
+ redis_cheese.product.should == cheese.product
436
+ redis_balogna.id.should == balogna.id
437
+ redis_balogna.product.should == balogna.product
438
+ end
374
439
  end
375
- end
376
440
 
377
- context "when searching on multiple attribute" do
378
- it "returns the intersection of all matching objects" do
379
- sammiches = Sammich.find( {:weight => "medium", :product => "Cheese Sammich" } )
380
- sammiches.size.should == 1
381
- sammiches[0].id.should == "1"
382
- sammiches[0].product.should == "Cheese Sammich"
441
+ context "when searching on multiple attribute" do
442
+ it "returns the intersection of all matching objects" do
443
+ sammiches = Sammich.find( {:weight => "medium", :product => "Cheese Sammich" } )
444
+ sammiches.size.should == 1
445
+ sammiches[0].id.should == "1"
446
+ sammiches[0].product.should == "Cheese Sammich"
447
+ end
383
448
  end
384
- end
385
449
 
386
- it "returns empty array if no options are given" do
387
- Sammich.find({}).should == []
388
- end
450
+ it "returns empty array if no options are given" do
451
+ Sammich.find({}).should == []
452
+ end
389
453
 
390
- it "returns empty array if nothing matches the given options" do
391
- Sammich.find( {:weight => "light" } ).should == []
392
- end
454
+ it "returns empty array if nothing matches the given options" do
455
+ Sammich.find( {:weight => "light" } ).should == []
456
+ end
393
457
 
394
- it "returns all matching objects if multiple values are given for an attribute" do
395
- Sammich.find( {:weight => ["medium", "heavy"]} ).map(&:id).sort.should == ['1','2','3']
458
+ it "returns all matching objects if multiple values are given for an attribute" do
459
+ Sammich.find( {:weight => ["medium", "heavy"]} ).map(&:id).sort.should == ['1','2','3']
460
+ end
396
461
  end
397
- end
398
462
 
399
- describe ".find_by_attribute" do
400
- it "raises if index does not exist for given key" do
401
- lambda do
402
- Sammich.find_by_attribute(:expensive, true)
403
- end.should raise_error(Tractor::MissingIndexError, "No index on 'expensive'")
404
- end
463
+ describe ".find_by_attribute" do
464
+ it "raises if index does not exist for given key" do
465
+ lambda do
466
+ Sammich.find_by_attribute(:expensive, true)
467
+ end.should raise_error(Tractor::MissingIndexError, "No index on 'expensive'")
468
+ end
405
469
 
406
- it "takes an index name and value and finds all matching objects" do
407
- meat_supreme = Sammich.create({ :id => '1', :weight => "heavy", :product => "Meat Supreme" })
408
- bacon_with_bacon = Sammich.create({ :id => '2', :weight => "heavy", :product => "Bacon with extra Bacon" })
470
+ it "takes an index name and value and finds all matching objects" do
471
+ meat_supreme = Sammich.create({ :id => '1', :weight => "heavy", :product => "Meat Supreme" })
472
+ bacon_with_bacon = Sammich.create({ :id => '2', :weight => "heavy", :product => "Bacon with extra Bacon" })
409
473
 
410
- redis_meat_supreme, redis_bacon_with_bacon = Sammich.find_by_attribute(:weight, "heavy")
411
- redis_meat_supreme.id.should == meat_supreme.id
412
- redis_meat_supreme.weight.should == meat_supreme.weight
413
- redis_meat_supreme.product.should == meat_supreme.product
474
+ redis_meat_supreme, redis_bacon_with_bacon = Sammich.find_by_attribute(:weight, "heavy")
475
+ redis_meat_supreme.id.should == meat_supreme.id
476
+ redis_meat_supreme.weight.should == meat_supreme.weight
477
+ redis_meat_supreme.product.should == meat_supreme.product
414
478
 
415
- redis_bacon_with_bacon.id.should == bacon_with_bacon.id
416
- redis_bacon_with_bacon.weight.should == bacon_with_bacon.weight
417
- redis_bacon_with_bacon.product.should == bacon_with_bacon.product
418
- end
479
+ redis_bacon_with_bacon.id.should == bacon_with_bacon.id
480
+ redis_bacon_with_bacon.weight.should == bacon_with_bacon.weight
481
+ redis_bacon_with_bacon.product.should == bacon_with_bacon.product
482
+ end
419
483
 
420
- it "returns nil if nothing matches" do
421
- Sammich.find_by_attribute(:weight, "heavy").should == []
484
+ it "returns nil if nothing matches" do
485
+ Sammich.find_by_attribute(:weight, "heavy").should == []
486
+ end
487
+ end
488
+
489
+ describe ".to_h" do
490
+ it "returns the attributes for a mapped object in a hash" do
491
+ chicken = Sammich.create({ :id => '1', :weight => "heavy", :product => "Chicken" })
492
+
493
+ chicken = Sammich.find_by_id('1')
494
+ hashed_attributes = chicken.to_h
495
+ hashed_attributes[:id].should == "1"
496
+ hashed_attributes[:weight].should == "heavy"
497
+ hashed_attributes[:product].should == "Chicken"
498
+ end
422
499
  end
423
500
  end
424
501
 
425
- describe ".to_h" do
426
- it "returns the attributes for a mapped object in a hash" do
427
- chicken = Sammich.create({ :id => '1', :weight => "heavy", :product => "Chicken" })
502
+ describe Tractor::Model::Dirty do
503
+ describe ".objectify" do
504
+ before do
505
+ chicken = Sammich.create({ :id => '1', :weight => "heavy", :product => "Chicken" })
506
+ lettuce = Sammich.create({ :id => '2', :weight => "light", :product => "Lettuce" })
507
+ Tractor.redis.scard("Tractor::Model::Dirty:all").should == 2
508
+ end
428
509
 
429
- chicken = Sammich.find_by_id('1')
430
- hashed_attributes = chicken.to_h
431
- hashed_attributes[:id].should == "1"
432
- hashed_attributes[:weight].should == "heavy"
433
- hashed_attributes[:product].should == "Chicken"
510
+ it "iterates over all dirty records and returns important information and removes the record" do
511
+ result = Tractor::Model::Dirty.all do |obj|
512
+ if obj[:id] == '1'
513
+ obj[:id].should == '1'
514
+ obj[:klass].should == 'Sammich'
515
+ obj[:data][:weight].should == 'heavy'
516
+ else
517
+ obj[:id].should == '2'
518
+ obj[:klass].should == 'Sammich'
519
+ obj[:data][:weight].should == 'light'
520
+ end
521
+ end
522
+
523
+ Tractor.redis.scard("Tractor::Model::Dirty:all").should == 0
524
+ end
525
+ it "does not remove the record from the dirty list if something goes wrong" do
526
+ result = Tractor::Model::Dirty.all do |obj|
527
+ if obj[:id] == '1'
528
+ raise 'wuh-oh'
529
+ else
530
+ # persist to the database or something else cool
531
+ end
532
+ end
533
+
534
+ Tractor.redis.smembers("Tractor::Model::Dirty:all").should == ['Sammich,1']
535
+ Tractor.redis.scard("Tractor::Model::Dirty:all").should == 1
536
+ end
434
537
  end
435
538
  end
436
539
  end
data/spec/spec_helper.rb CHANGED
@@ -13,6 +13,11 @@ Spec::Runner.configure do |config|
13
13
  end
14
14
  end
15
15
 
16
+ class Callbackinator < Tractor::Model::Base
17
+ attribute :id
18
+ attribute :name
19
+ end
20
+
16
21
  class Sammich < Tractor::Model::Base
17
22
  attribute :id
18
23
  attribute :product
data/tractor.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{tractor}
8
- s.version = "0.4.9"
8
+ s.version = "0.5.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Shane Wolf"]
12
- s.date = %q{2011-01-06}
12
+ s.date = %q{2011-01-07}
13
13
  s.description = %q{Very simple object mappings for ruby objects}
14
14
  s.email = %q{shanewolf@gmail.com}
15
15
  s.extra_rdoc_files = [
@@ -25,6 +25,7 @@ Gem::Specification.new do |s|
25
25
  "lib/tractor.rb",
26
26
  "lib/tractor/model/base.rb",
27
27
  "lib/tractor/model/mapper.rb",
28
+ "performance/dirty.rb",
28
29
  "performance/test.rb",
29
30
  "spec/model/base_spec.rb",
30
31
  "spec/model/mapper_spec.rb",
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tractor
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 11
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 4
9
- - 9
10
- version: 0.4.9
8
+ - 5
9
+ - 0
10
+ version: 0.5.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Shane Wolf
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-01-06 00:00:00 -08:00
18
+ date: 2011-01-07 00:00:00 -08:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -52,6 +52,7 @@ files:
52
52
  - lib/tractor.rb
53
53
  - lib/tractor/model/base.rb
54
54
  - lib/tractor/model/mapper.rb
55
+ - performance/dirty.rb
55
56
  - performance/test.rb
56
57
  - spec/model/base_spec.rb
57
58
  - spec/model/mapper_spec.rb