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 +1 -1
- data/lib/tractor/model/base.rb +54 -4
- data/performance/dirty.rb +59 -0
- data/performance/test.rb +2 -0
- data/spec/model/base_spec.rb +410 -307
- data/spec/spec_helper.rb +5 -0
- data/tractor.gemspec +3 -2
- metadata +6 -5
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.5.0
|
data/lib/tractor/model/base.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
data/spec/model/base_spec.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
2
|
|
3
|
-
describe Tractor
|
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
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
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
|
-
|
74
|
-
|
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
|
-
|
80
|
-
|
80
|
+
describe "#association" do
|
81
|
+
attr_reader :game, :player1, :player2
|
81
82
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
89
|
+
game.save
|
90
|
+
player1.save
|
91
|
+
player2.save
|
92
|
+
end
|
92
93
|
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
-
|
123
|
-
|
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
|
-
|
126
|
-
|
127
|
-
|
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
|
-
|
132
|
-
|
133
|
-
|
132
|
+
describe "index" do
|
133
|
+
it "removes newline characters from index key"
|
134
|
+
end
|
134
135
|
|
135
|
-
|
136
|
-
|
136
|
+
describe ".attributes" do
|
137
|
+
attr_reader :sorted_attributes
|
137
138
|
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
148
|
-
|
149
|
-
|
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
|
-
|
152
|
-
|
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
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
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
|
-
|
166
|
-
|
167
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
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
|
-
|
181
|
-
|
182
|
-
end
|
181
|
+
Tractor::Model::Game.all.size.should == 1
|
182
|
+
end
|
183
183
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
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
|
-
|
193
|
-
|
194
|
-
|
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
|
-
|
197
|
-
|
205
|
+
MonkeyClient.create({ :id => 'a1a', :evil => true, :birthday => "Dec 3" })
|
206
|
+
BananaClient.create({ :id => 'a1a', :name => "delicious" })
|
198
207
|
|
199
|
-
|
200
|
-
|
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
|
-
|
204
|
-
|
205
|
-
|
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
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
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
|
-
|
216
|
-
|
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
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
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
|
-
|
227
|
-
|
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
|
-
|
232
|
-
|
295
|
+
describe ".create" do
|
296
|
+
it "allows you to specify which attributes should be unique"
|
233
297
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
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
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
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
|
-
|
249
|
-
|
250
|
-
|
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
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
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
|
-
|
258
|
-
|
259
|
-
|
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
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
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
|
-
|
270
|
-
|
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
|
-
|
275
|
-
|
276
|
-
|
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
|
-
|
279
|
-
|
280
|
-
|
342
|
+
redis_sammich = Sammich.find_by_id('1')
|
343
|
+
redis_sammich.product.should == "Cold Cut Trio"
|
344
|
+
end
|
281
345
|
|
282
|
-
|
283
|
-
|
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
|
-
|
288
|
-
|
351
|
+
describe "#update" do
|
352
|
+
attr_reader :sammich
|
289
353
|
|
290
|
-
|
291
|
-
|
292
|
-
|
354
|
+
before do
|
355
|
+
@sammich = Sammich.create({ :id => '1', :weight => "medium", :product => "Turkey Avocado" })
|
356
|
+
end
|
293
357
|
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
358
|
+
it "updates the item from redis" do
|
359
|
+
@sammich.update( {:weight => "heavy"} )
|
360
|
+
@sammich.weight.should == "heavy"
|
361
|
+
end
|
298
362
|
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
363
|
+
it "does not update the id" do
|
364
|
+
@sammich.update( {:id => "111111"} )
|
365
|
+
@sammich.id.should == "1"
|
366
|
+
end
|
303
367
|
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
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
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
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
|
-
|
320
|
-
|
383
|
+
it "raises if object has not been saved yet"
|
384
|
+
end
|
321
385
|
|
322
|
-
|
323
|
-
|
386
|
+
describe "#destroy" do
|
387
|
+
attr_reader :cheese, :balogna
|
324
388
|
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
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
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
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
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
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
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
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
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
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
|
-
|
417
|
+
bocci_ball.players.ids.should == []
|
418
|
+
end
|
354
419
|
end
|
355
|
-
end
|
356
420
|
|
357
|
-
|
358
|
-
|
421
|
+
describe ".find" do
|
422
|
+
attr_reader :cheese, :balogna
|
359
423
|
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
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
|
-
|
367
|
-
|
368
|
-
|
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
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
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
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
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
|
-
|
387
|
-
|
388
|
-
|
450
|
+
it "returns empty array if no options are given" do
|
451
|
+
Sammich.find({}).should == []
|
452
|
+
end
|
389
453
|
|
390
|
-
|
391
|
-
|
392
|
-
|
454
|
+
it "returns empty array if nothing matches the given options" do
|
455
|
+
Sammich.find( {:weight => "light" } ).should == []
|
456
|
+
end
|
393
457
|
|
394
|
-
|
395
|
-
|
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
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
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
|
-
|
407
|
-
|
408
|
-
|
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
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
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
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
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
|
-
|
421
|
-
|
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
|
426
|
-
|
427
|
-
|
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
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
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
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.
|
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-
|
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:
|
4
|
+
hash: 11
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
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-
|
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
|