tractor 0.4.9 → 0.5.0

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