tractor 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Shane Wolf
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,17 @@
1
+ = tractor
2
+
3
+ Description goes here.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
13
+ * Send me a pull request. Bonus points for topic branches.
14
+
15
+ == Copyright
16
+
17
+ Copyright (c) 2010 Shane Wolf. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,44 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "tractor"
8
+ gem.summary = "Very simple object mapping for ruby objects"
9
+ gem.description = "Very simple object mappings for ruby objects"
10
+ gem.email = "shanewolf@gmail.com"
11
+ gem.homepage = "http://github.com/gizm0duck/tractor"
12
+ gem.authors = ["Shane Wolf"]
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
18
+ end
19
+
20
+ require 'spec/rake/spectask'
21
+ Spec::Rake::SpecTask.new(:spec) do |spec|
22
+ spec.libs << 'lib' << 'spec'
23
+ spec.spec_files = FileList['spec/**/*_spec.rb']
24
+ end
25
+
26
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
27
+ spec.libs << 'lib' << 'spec'
28
+ spec.pattern = 'spec/**/*_spec.rb'
29
+ spec.rcov = true
30
+ end
31
+
32
+ task :spec => :check_dependencies
33
+
34
+ task :default => :spec
35
+
36
+ require 'rake/rdoctask'
37
+ Rake::RDocTask.new do |rdoc|
38
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
39
+
40
+ rdoc.rdoc_dir = 'rdoc'
41
+ rdoc.title = "tractor #{version}"
42
+ rdoc.rdoc_files.include('README*')
43
+ rdoc.rdoc_files.include('lib/**/*.rb')
44
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -0,0 +1,253 @@
1
+ require 'base64'
2
+
3
+ module Tractor
4
+
5
+ class << self
6
+ attr_reader :redis
7
+
8
+ # important options are port, host and db
9
+ def connectdb(options={})
10
+ if options.nil?
11
+ @redis = Redis.new(options)
12
+ else
13
+ @redis = Redis.new(:db => 1)
14
+ end
15
+ end
16
+
17
+ def flushdb
18
+ @redis.flushdb
19
+ end
20
+ end
21
+
22
+ class Set
23
+ include Enumerable
24
+
25
+ attr_accessor :key, :klass
26
+
27
+ def initialize(key, klass)
28
+ self.klass = klass
29
+ self.key = key
30
+ end
31
+
32
+ def push(val)
33
+ Tractor.redis.sadd key, val.id
34
+ end
35
+
36
+ def all
37
+ ids = Tractor.redis.smembers(key)
38
+ ids.inject([]){ |a, id| a << klass.find_by_id(id); a }
39
+ end
40
+ end
41
+
42
+ class Index
43
+ include Enumerable
44
+ attr_reader :klass, :name, :value
45
+
46
+ def initialize(klass, name, value)
47
+ @klass = klass
48
+ @name = name
49
+ @value = value
50
+ end
51
+
52
+ def insert(id)
53
+ Tractor.redis.sadd(key, id) unless Tractor.redis.smembers(key).include?(id)
54
+ end
55
+
56
+ def delete(id)
57
+ Tractor.redis.srem(key, id)
58
+ end
59
+
60
+ def self.key_for(klass, name, value)
61
+ i = self.new(klass, name, value)
62
+ i.key
63
+ end
64
+
65
+ def key
66
+ encoded_value = "#{Base64.encode64(value.to_s)}".gsub("\n", "")
67
+ "#{klass}:#{name}:#{encoded_value}"
68
+ end
69
+ end
70
+
71
+ module Model
72
+ class Base
73
+ def initialize(attributes={})
74
+ @attribute_store = {}
75
+ @association_store = {}
76
+
77
+ attributes.each do |k,v|
78
+ send("#{k}=", v)
79
+ end
80
+ end
81
+
82
+ def save
83
+ raise "Probably wanna set an id" if self.id.nil? || self.id.to_s.empty?
84
+ key_base = "#{self.class}:#{self.id}"
85
+ #raise "Duplicate value for #{self.class} 'id'" if Tractor.redis.keys("#{key_base}:*").any?
86
+
87
+ scoped_attributes = attribute_store.inject({}) do |h, (attr_name, value)|
88
+ h["#{key_base}:#{attr_name}"] = value
89
+ h
90
+ end
91
+ Tractor.redis.mset scoped_attributes
92
+ Tractor.redis.sadd "#{self.class}:all", self.id
93
+ add_to_indices
94
+
95
+ return self
96
+ end
97
+
98
+ def destroy
99
+ keys = Tractor.redis.keys("#{self.class}:#{self.id}:*")
100
+ delete_from_indices(keys.map{|k| k.split(":").last })
101
+ Tractor.redis.srem("#{self.class}:all", self.id)
102
+ keys.each { |k| Tractor.redis.del k }
103
+ end
104
+
105
+ def update(attributes = {})
106
+ attributes.delete(:id)
107
+ delete_from_indices(attributes)
108
+ attributes.each{ |k,v| self.send("#{k}=", v) }
109
+ save
110
+ end
111
+
112
+ def add_to_indices
113
+ self.class.indices.each do |name|
114
+ index = Index.new(self.class, name, send(name))
115
+ index.insert(self.id)
116
+ end
117
+ end
118
+
119
+ def delete_from_indices(attributes)
120
+ attributes.each do |name, value|
121
+ if self.class.indices.include?(name.to_sym)
122
+ index = Index.new(self.class, name, self.send(name))
123
+ index.delete(self.id)
124
+ end
125
+ end
126
+ end
127
+
128
+ def to_h
129
+ self.class.attributes.keys.inject({}) do |h, attribute|
130
+ h[attribute.to_sym] = self.send(attribute)
131
+ h
132
+ end
133
+ end
134
+
135
+ class << self
136
+ attr_reader :attributes, :associations, :indices
137
+
138
+ def create(attributes={})
139
+ m = new(attributes)
140
+ m.save
141
+ m
142
+ end
143
+
144
+ def find_by_id(id)
145
+ keys = Tractor.redis.keys("#{self}:#{id}:*")
146
+ return nil if keys.empty?
147
+
148
+ scoped_attributes = Tractor.redis.mapped_mget(*keys)
149
+ unscoped_attributes = scoped_attributes.inject({}) do |h, (key, value)|
150
+
151
+ name = key.split(":").last
152
+ type = attributes[name.to_sym][:type]
153
+ if type == :integer
154
+ value = value.to_i
155
+ elsif type == :boolean
156
+ value = value.to_s.match(/(true|1)$/i) != nil
157
+ end
158
+ h[name] = value
159
+ h
160
+ end
161
+ self.new(unscoped_attributes)
162
+ end
163
+
164
+ # use method missing to do craziness, or define a find_by on each index (BETTER)
165
+ def find_by_attribute(name, value)
166
+ raise "No index on '#{name}'" unless indices.include?(name)
167
+
168
+ ids = Tractor.redis.smembers(Index.key_for(self, name, value))
169
+ ids.map do |id|
170
+ find_by_id(id)
171
+ end
172
+ end
173
+
174
+ def find(options = {})
175
+ return [] if options.empty?
176
+ sets = options.map do |name, value|
177
+ Index.key_for(self, name, value)
178
+ end
179
+ ids = Tractor.redis.sinter(*sets)
180
+ ids.map do |id|
181
+ find_by_id(id)
182
+ end
183
+ end
184
+
185
+ def attribute(name, options={})
186
+ options[:map] = name unless options[:map]
187
+ attributes[name] = options
188
+ setter(name, options[:type])
189
+ getter(name, options[:type])
190
+ end
191
+
192
+ def index(name)
193
+ indices << name unless indices.include?(name)
194
+ end
195
+
196
+ def association(name, klass)
197
+ associations[name] = name
198
+
199
+ define_method(name) do
200
+ @association_store[name] = Set.new("#{self.class}:#{self.id}:#{name}", klass)
201
+ end
202
+ end
203
+
204
+ def all
205
+ ids = Tractor.redis.smembers("#{self}:all")
206
+ ids.inject([]){ |a, id| a << find_by_id(id); a }
207
+ end
208
+
209
+ ###
210
+ # Minions
211
+ ###
212
+
213
+ def getter(name, type)
214
+ define_method(name) do
215
+ value = @attribute_store[name]
216
+ if type == :integer
217
+ value.to_i
218
+ elsif type == :boolean
219
+ value.to_s.match(/(true|1)$/i) != nil
220
+ else
221
+ value
222
+ end
223
+ end
224
+ end
225
+
226
+ def setter(name, type)
227
+ define_method(:"#{name}=") do |value|
228
+ if type == :boolean
229
+ value = value.to_s
230
+ end
231
+ @attribute_store[name] = value
232
+ end
233
+ end
234
+
235
+ def attributes
236
+ @attributes ||= {}
237
+ end
238
+
239
+ def associations
240
+ @associations ||= {}
241
+ end
242
+
243
+ def indices
244
+ @indices ||= []
245
+ end
246
+ end
247
+
248
+ private
249
+
250
+ attr_reader :attribute_store, :association_store
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,93 @@
1
+ module Tractor
2
+ module Model
3
+ class Mapper < Tractor::Model::Base
4
+ class << self
5
+ attr_reader :dependencies
6
+
7
+ def depends_on(klass, options = {})
8
+ dependencies[klass] = options
9
+
10
+ # set index by default on items that have a depends on
11
+ #set_redis_index(klass, options[:key_name])
12
+ end
13
+
14
+ def representation_for(server_instance)
15
+ if find_from_instance(server_instance)
16
+ update_from_instance(server_instance)
17
+ else
18
+ create_from_instance(server_instance)
19
+ end
20
+ end
21
+
22
+ def find_from_instance(server_instance)
23
+ self.find_by_id(server_instance.id)
24
+ end
25
+
26
+ def create_from_instance(server_instance)
27
+ hydrate_attributes(server_instance) do |attributes|
28
+ self.create(attributes)
29
+ end
30
+ end
31
+
32
+ def update_from_instance(server_instance)
33
+ existing_record = find_from_instance(server_instance)
34
+ raise "Cannot update an object that doesn't exist." unless existing_record
35
+
36
+ hydrate_attributes(server_instance) do |attributes|
37
+ existing_record.update(attributes)
38
+ end
39
+ end
40
+
41
+ def remove(server_id)
42
+ obj_to_destroy = self.find_by_id(server_id)
43
+ return false if obj_to_destroy.nil?
44
+ obj_to_destroy.destroy
45
+ end
46
+
47
+ def hydrate_attributes(server_instance)
48
+ attributes = attribute_mapper(server_instance)
49
+ ensure_dependencies_met(server_instance)
50
+ yield attributes
51
+ end
52
+
53
+ def attribute_mapper(server_instance)
54
+ attributes = {}
55
+ self.attributes.each do |name, options|
56
+ server_value = server_instance.respond_to?(options[:map]) ? server_instance.send(options[:map]) : nil
57
+ attributes[name] = server_value
58
+ end
59
+ attributes
60
+ end
61
+
62
+ def dependency_met_for?(server_instance, klass)
63
+ !!klass.find_by_id(server_instance.send(dependencies[klass][:key_name]))
64
+ end
65
+
66
+ def dependencies_met?(server_instance)
67
+ dependencies.each do |klass, options|
68
+ return false unless dependency_met_for?(server_instance, klass)
69
+ end
70
+ return true
71
+ end
72
+
73
+ def ensure_dependencies_met(server_instance)
74
+ return if dependencies_met?(server_instance)
75
+
76
+ dependencies.each do |klass, options|
77
+ if klass.find_by_id(server_instance.send(options[:key_name])).nil?
78
+ server_instances = server_instance.send(options[:method_name])
79
+ server_instances = server_instances.is_a?(Array) ? server_instances : [server_instances]
80
+ server_instances.each do |obj|
81
+ klass.create_from_instance(obj)
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ def dependencies
88
+ @dependencies ||= {}
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
data/lib/tractor.rb ADDED
@@ -0,0 +1,3 @@
1
+ require 'redis'
2
+ require "tractor/model/base"
3
+ require "tractor/model/mapper"
@@ -0,0 +1,366 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Tractor::Model::Base do
4
+ attr_reader :redis
5
+ before do
6
+ class Player < Tractor::Model::Base
7
+ attribute :id
8
+ attribute :name
9
+ attribute :wins_loses
10
+ end
11
+
12
+ class Game < Tractor::Model::Base
13
+ attribute :id
14
+ attribute :board
15
+ attribute :flying_object
16
+ attribute :score, :type => :integer
17
+
18
+ association :players, Player
19
+ end
20
+ end
21
+
22
+ describe ".attribute" do
23
+ it "inserts the values into the attributes class instance variable" do
24
+ Game.attributes.should include(:board)
25
+ end
26
+
27
+ it "allows you to specify what type the value should be when it comes out of the tractor" do
28
+ Game.attributes[:score][:type].should == :integer
29
+ end
30
+
31
+ it "creates a set method for each attribute" do
32
+ game = Game.new(:board => "fancy")
33
+ game.send(:attribute_store)[:board].should == "fancy"
34
+ end
35
+
36
+ it "creates a get method for each attribute" do
37
+ game = Game.new(:board => "schmancy")
38
+ game.board.should == "schmancy"
39
+ end
40
+
41
+ describe "when attribute is a boolean" do
42
+ it "returns a boolean" do
43
+ expensive_sammich = Sammich.new(:expensive => true)
44
+ expensive_sammich.expensive.should == true
45
+ expensive_sammich.expensive.should be_a(TrueClass)
46
+ end
47
+ end
48
+
49
+ describe "when attribute is a integer" do
50
+ it "returns an integer" do
51
+ game = Game.new(:score => 1222)
52
+ game.score.should == 1222
53
+ game.score.should be_a(Fixnum)
54
+ end
55
+ end
56
+ end
57
+
58
+ describe "#association" do
59
+ attr_reader :game, :player1, :player2
60
+
61
+ before do
62
+ @game = Game.new({ :id => 'g1' })
63
+ @player1 = Player.new({ :id => 'p1', :name => "delicious" })
64
+ @player2 = Player.new({ :id => 'p2', :name => "gross" })
65
+
66
+ game.save
67
+ player1.save
68
+ player2.save
69
+ end
70
+
71
+ it "adds a set with the given name to the instance" do # "Monkey:a1a:SET_NAME"
72
+ Game.associations.keys.should include(:players)
73
+ end
74
+
75
+ it "adds a push method for the set on an instance of the class" do
76
+ game.players.push player1
77
+ redis.smembers('Game:g1:players').should == ['p1']
78
+ end
79
+
80
+ it "adds an all method for the association to return the items in it" do
81
+ game.players.all.should == []
82
+ game.players.push player1
83
+ game.players.push player2
84
+ player1_from_game = game.players.all[0]
85
+ player2_from_game = game.players.all[1]
86
+
87
+ player1_from_game.name.should == player1.name
88
+ player1_from_game.id.should == player1.id
89
+ player2_from_game.name.should == player2.name
90
+ player2_from_game.id.should == player2.id
91
+ end
92
+
93
+ it "requires the object being added to have been saved to the database before adding it to the set"
94
+ end
95
+
96
+ describe ".associations" do
97
+ it "returns all association that have been added to this class" do
98
+ Game.associations.keys.should == [:players]
99
+ end
100
+ end
101
+
102
+ describe ".indices" do
103
+ it "returns all indices on a class" do
104
+ Sammich.indices.should == [:product, :weight]
105
+ end
106
+ end
107
+
108
+ describe "index" do
109
+ it "removes newline characters from index key"
110
+ end
111
+
112
+ describe ".attributes" do
113
+ attr_reader :sorted_attributes
114
+
115
+ before do
116
+ @sorted_attributes = Game.attributes.keys.sort{|x,y| x.to_s <=> y.to_s}
117
+ end
118
+
119
+ it "returns all attributes that have been added to this class" do
120
+ sorted_attributes.size.should == 4
121
+ sorted_attributes.should == [:board, :flying_object, :id, :score]
122
+ end
123
+
124
+ it "allows different attributes to be specified for different child classes" do
125
+ Game.attributes.size.should == 4
126
+ Player.attributes.size.should == 3
127
+
128
+ Game.attributes.keys.should_not include(:name)
129
+ Player.attributes.keys.should_not include(:flying_object)
130
+ end
131
+ end
132
+
133
+ describe "#save" do
134
+ it "raises if id is nil or empty" do
135
+ game = Game.new
136
+ game.id = nil
137
+ lambda { game.save }.should raise_error("Probably wanna set an id")
138
+ game.id = ''
139
+ lambda { game.save }.should raise_error("Probably wanna set an id")
140
+ end
141
+
142
+ it "should write attributes to redis" do
143
+ game = Game.new({:id => '1', :board => "large", :flying_object => "disc"})
144
+ game.save
145
+
146
+ redis["Game:1:id"].should == "1"
147
+ redis["Game:1:board"].should == "large"
148
+ redis["Game:1:flying_object"].should == "disc"
149
+ end
150
+
151
+ it "appends the new object to the Game set" do
152
+ Game.all.size.should == 0
153
+ game = Game.new({ :id => '1', :board => "small" })
154
+ game.save
155
+
156
+ Game.all.size.should == 1
157
+ end
158
+ end
159
+
160
+ describe ".all" do
161
+ it "every object that is created for this class will be in this set" do
162
+ MonkeyClient.all.size.should == 0
163
+ MonkeyClient.create({ :id => 'a1a', :evil => true, :birthday => "Dec 3" })
164
+ MonkeyClient.create({ :id => 'b1b', :evil => false, :birthday => "Dec 4" })
165
+ MonkeyClient.all.size.should == 2
166
+ end
167
+
168
+ it "each class only tracks their own" do
169
+ MonkeyClient.all.size.should == 0
170
+ BananaClient.all.size.should == 0
171
+
172
+ MonkeyClient.create({ :id => 'a1a', :evil => true, :birthday => "Dec 3" })
173
+ BananaClient.create({ :id => 'a1a', :name => "delicious" })
174
+
175
+ MonkeyClient.all.size.should == 1
176
+ BananaClient.all.size.should == 1
177
+ end
178
+
179
+ it "returns the entire instance of a given object" do
180
+ MonkeyClient.create({ :id => 'a1a', :evil => true, :birthday => "Dec 3" })
181
+ MonkeyClient.all[0].birthday.should == "Dec 3"
182
+ end
183
+ end
184
+
185
+ describe "#create" do
186
+ it "allows you to specify which attributes should be unique"
187
+ # it "raises exception if the id exists" do
188
+ # MonkeyClient.create({ :id => 'a1a', :evil => true, :birthday => "Dec 3" })
189
+ # lambda do
190
+ # MonkeyClient.create({ :id => 'a1a', :evil => false, :birthday => "Jan 4" })
191
+ # end.should raise_error("Duplicate value for MonkeyClient 'id'")
192
+ # end
193
+
194
+ it "should write attributes to redis" do
195
+ sammich = Sammich.create({ :id => '1', :product => "Veggie Sammich" })
196
+
197
+ redis["Sammich:1:id"].should == "1"
198
+ redis["Sammich:1:product"].should == "Veggie Sammich"
199
+ end
200
+
201
+ it "populates all the indices that are specified on the class" do
202
+ Sammich.create({ :id => '1', :weight => "heavy", :product => "Ham Sammich" })
203
+ Sammich.create({ :id => '2', :weight => "heavy", :product => "Tuna Sammich" })
204
+
205
+ redis.smembers("Sammich:product:SGFtIFNhbW1pY2g=").should include('1')
206
+ redis.smembers("Sammich:product:VHVuYSBTYW1taWNo").should include('2')
207
+ redis.smembers("Sammich:weight:aGVhdnk=").should == ['1', '2']
208
+ end
209
+
210
+ it "returns the instance that has been created" do
211
+ sammich = Sammich.create({ :id => '1', :weight => "heavy", :product => "Tuna Melt" })
212
+ sammich.weight.should == "heavy"
213
+ end
214
+ end
215
+
216
+ describe ".find_by_id" do
217
+ it "takes an id and returns the object from redis" do
218
+ sammich = Sammich.create({ :id => '1', :product => "Cold Cut Trio" })
219
+
220
+ redis_sammich = Sammich.find_by_id('1')
221
+ redis_sammich.product.should == "Cold Cut Trio"
222
+ end
223
+
224
+ it "returns nil if the keys do not exist in redis" do
225
+ Sammich.find_by_id('1').should be_nil
226
+ end
227
+ end
228
+
229
+ describe "#update" do
230
+ attr_reader :sammich
231
+
232
+ before do
233
+ @sammich = Sammich.create({ :id => '1', :weight => "medium", :product => "Turkey Avocado" })
234
+ end
235
+
236
+ it "updates the item from redis" do
237
+ @sammich.update( {:weight => "heavy"} )
238
+ @sammich.weight.should == "heavy"
239
+ end
240
+
241
+ it "does not update the id" do
242
+ @sammich.update( {:id => "111111"} )
243
+ @sammich.id.should == "1"
244
+ end
245
+
246
+ it "only changes attributes passed in" do
247
+ @sammich.update( {:weight => "light"} )
248
+ @sammich.id.should == "1"
249
+ @sammich.weight.should == "light"
250
+ @sammich.product.should == "Turkey Avocado"
251
+ end
252
+
253
+ it "updates all the indices associated with this object" do
254
+ Sammich.find( {:weight => "light"} ).should be_empty
255
+ Sammich.find( {:weight => "medium"} ).should_not be_empty
256
+ sammich.update( {:weight => "light"} )
257
+ Sammich.find( {:weight => "light"} ).should_not be_empty
258
+ Sammich.find( {:weight => "medium"} ).should be_empty
259
+ end
260
+
261
+ it "raises if object has not been saved yet"
262
+ end
263
+
264
+ describe "#destroy" do
265
+ attr_reader :cheese, :balogna
266
+
267
+ before do
268
+ @cheese = Sammich.create({ :id => '1', :weight => "medium", :product => "Cheese Sammich" })
269
+ @balogna = Sammich.create({ :id => '2', :weight => "medium", :product => "Balogna Sammich" })
270
+ end
271
+
272
+ it "removes the item from redis" do
273
+ @cheese.destroy
274
+ Sammich.find_by_id(cheese.id).should be_nil
275
+ end
276
+
277
+ it "removes the id from the all index" do
278
+ Sammich.all.map{|t| t.id }.should == ["1", "2"]
279
+ cheese.destroy
280
+ Sammich.all.map{|t| t.id }.should == ["2"]
281
+ end
282
+
283
+ it "removes the id from all of it's other indices" do
284
+ Sammich.find({ :weight => "medium" }).size.should == 2
285
+ cheese.destroy
286
+ Sammich.find({ :weight => "medium" }).size.should == 1
287
+ end
288
+
289
+ it "removes the id from all of the associations that it may be in"
290
+ end
291
+
292
+ describe ".find" do
293
+ attr_reader :cheese, :balogna
294
+
295
+ before do
296
+ @cheese = Sammich.create({ :id => '1', :weight => "medium", :product => "Cheese Sammich" })
297
+ @balogna = Sammich.create({ :id => '2', :weight => "medium", :product => "Balogna Sammich" })
298
+ end
299
+
300
+ context "when searching on 1 attribute" do
301
+ it "returns all matching products" do
302
+ redis_cheese, redis_balogna = Sammich.find( {:weight => "medium" } )
303
+
304
+ redis_cheese.id.should == cheese.id
305
+ redis_cheese.product.should == cheese.product
306
+ redis_balogna.id.should == balogna.id
307
+ redis_balogna.product.should == balogna.product
308
+ end
309
+ end
310
+
311
+ context "when searching on multiple attribute" do
312
+ it "returns the intersection of all matching objects" do
313
+ sammiches = Sammich.find( {:weight => "medium", :product => "Cheese Sammich" } )
314
+ sammiches.size.should == 1
315
+ sammiches[0].id.should == "1"
316
+ sammiches[0].product.should == "Cheese Sammich"
317
+ end
318
+ end
319
+
320
+ it "returns empty array if no options are given" do
321
+ Sammich.find({}).should == []
322
+ end
323
+
324
+ it "returns empty array if nothing matches the given options" do
325
+ Sammich.find( {:weight => "light" } ).should == []
326
+ end
327
+ end
328
+
329
+ describe ".find_by_attribute" do
330
+ it "raises if index does not exist for given key" do
331
+ lambda do
332
+ Sammich.find_by_attribute(:expensive, true)
333
+ end.should raise_error("No index on 'expensive'")
334
+ end
335
+
336
+ it "takes an index name and value and finds all matching objects" do
337
+ meat_supreme = Sammich.create({ :id => '1', :weight => "heavy", :product => "Meat Supreme" })
338
+ bacon_with_bacon = Sammich.create({ :id => '2', :weight => "heavy", :product => "Bacon with extra Bacon" })
339
+
340
+ redis_meat_supreme, redis_bacon_with_bacon = Sammich.find_by_attribute(:weight, "heavy")
341
+ redis_meat_supreme.id.should == meat_supreme.id
342
+ redis_meat_supreme.weight.should == meat_supreme.weight
343
+ redis_meat_supreme.product.should == meat_supreme.product
344
+
345
+ redis_bacon_with_bacon.id.should == bacon_with_bacon.id
346
+ redis_bacon_with_bacon.weight.should == bacon_with_bacon.weight
347
+ redis_bacon_with_bacon.product.should == bacon_with_bacon.product
348
+ end
349
+
350
+ it "returns nil if nothing matches" do
351
+ Sammich.find_by_attribute(:weight, "heavy").should == []
352
+ end
353
+ end
354
+
355
+ describe ".to_h" do
356
+ it "returns the attributes for a mapped object in a hash" do
357
+ chicken = Sammich.create({ :id => '1', :weight => "heavy", :product => "Chicken" })
358
+
359
+ chicken = Sammich.find_by_id('1')
360
+ hashed_attributes = chicken.to_h
361
+ hashed_attributes[:id].should == "1"
362
+ hashed_attributes[:weight].should == "heavy"
363
+ hashed_attributes[:product].should == "Chicken"
364
+ end
365
+ end
366
+ end
@@ -0,0 +1,271 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Tractor::Model::Mapper do
4
+ before do
5
+ class RedisTrain < Tractor::Model::Mapper
6
+ attribute :id
7
+ attribute :name
8
+ attribute :num_cars, :type => :integer, :map => :number_of_cars
9
+ index :num_cars
10
+ end
11
+
12
+ class Train
13
+ attr_accessor :number_of_cars, :id
14
+
15
+ def initialize(number_of_cars, id)
16
+ @id = id
17
+ @number_of_cars = number_of_cars
18
+ end
19
+ end
20
+
21
+ class FamousTrain < Train
22
+ attr_accessor :name
23
+ def initialize(name, number_of_cars, id)
24
+ @id = id; @number_of_cars = number_of_cars; @name = name
25
+ end
26
+ end
27
+ end
28
+
29
+ describe ".attribute_mapper" do
30
+ describe "when all of the mapped attributes are methods on the server object" do
31
+ it "should include all attributes with their proper values" do
32
+ train = FamousTrain.new('Wabash Cannonball', 7, '5309')
33
+ redis_train_attributes = RedisTrain.attribute_mapper(train)
34
+
35
+ redis_train_attributes.keys.size.should == 3
36
+ redis_train_attributes[:id].should == "5309"
37
+ redis_train_attributes[:num_cars].should == 7
38
+ redis_train_attributes[:name].should == "Wabash Cannonball"
39
+ end
40
+ end
41
+
42
+ describe "when some of the mapped attributes do not exist on the server object" do
43
+ it "should leave those mappings out of the attributes for the client object" do
44
+ train = Train.new(9, '5309')
45
+ redis_train_attributes = RedisTrain.attribute_mapper(train)
46
+
47
+ redis_train_attributes.keys.size.should == 3
48
+ redis_train_attributes[:id].should == "5309"
49
+ redis_train_attributes[:num_cars].should == 9
50
+ redis_train_attributes[:name].should be_nil
51
+ end
52
+ end
53
+ end
54
+
55
+ describe "find_from_instance" do
56
+ it "returns nil if record does not exist in redis" do
57
+ monkey_1 = Monkey.new("Dec. 3, 1981", true, 'a1a')
58
+ monkey_client_1 = MonkeyClient.find_from_instance(monkey_1)
59
+ monkey_client_1.should be_nil
60
+ end
61
+
62
+ it "returns the object from redis if it exists" do
63
+ monkey_1 = Monkey.new("Dec. 3, 1981", true, 'a1a')
64
+ monkey_2 = Monkey.new("Dec. 4, 1981", false, 'b1b')
65
+
66
+ MonkeyClient.create_from_instance(monkey_1)
67
+ MonkeyClient.create_from_instance(monkey_2)
68
+ MonkeyClient.all.size.should == 2
69
+
70
+ redis_monkey_1 = MonkeyClient.find_from_instance(monkey_1)
71
+ redis_monkey_2 = MonkeyClient.find_from_instance(monkey_2)
72
+
73
+ redis_monkey_1.birthday.should == "Dec. 3, 1981"
74
+ redis_monkey_2.birthday.should == "Dec. 4, 1981"
75
+
76
+ redis_monkey_1.evil.should == true
77
+ redis_monkey_2.evil.should == false
78
+ end
79
+ end
80
+
81
+ describe "create_from_instance" do
82
+ it "ensures dependencies are met"
83
+ it "writes the client representation out to redis with proper object types" do
84
+ monkey = Monkey.new("Dec. 3, 1981", true, 'a1a')
85
+ redis_monkey = MonkeyClient.create_from_instance(monkey)
86
+
87
+ redis_monkey = MonkeyClient.all.first
88
+ redis_monkey.birthday.should == "Dec. 3, 1981"
89
+ redis_monkey.evil.should == true
90
+ redis_monkey.id.should == "a1a"
91
+ end
92
+ end
93
+
94
+ describe "update_from_instance" do
95
+ it "ensures dependencies are met"
96
+ it "finds an existing record based on id and updates the attributes accordingly" do
97
+ monkey = Monkey.new("Dec. 3, 1981", true, 'a1a')
98
+ redis_monkey = MonkeyClient.create_from_instance(monkey)
99
+
100
+ MonkeyClient.all.size.should == 1
101
+ redis_monkey = MonkeyClient.all.first
102
+ redis_id = redis_monkey.id
103
+ redis_monkey.id.should == monkey.id
104
+
105
+ monkey.birthdate = "Dec. 2, 1981"
106
+ monkey.evil_monkey = false
107
+
108
+ MonkeyClient.update_from_instance(monkey)
109
+ MonkeyClient.all.size.should == 1
110
+ redis_monkey = MonkeyClient.find_by_id(redis_id)
111
+
112
+ redis_monkey.birthday.should == "Dec. 2, 1981"
113
+ redis_monkey.evil.should == false
114
+ end
115
+
116
+ it "raises if record does not exist" do
117
+ MonkeyClient.all.should be_empty
118
+ monkey = Monkey.new("Dec. 3, 1981", true, 'a1a')
119
+
120
+ lambda do
121
+ redis_monkey = MonkeyClient.update_from_instance(monkey)
122
+ end.should raise_error("Cannot update an object that doesn't exist.")
123
+ end
124
+ end
125
+
126
+ describe ".remove" do
127
+ it "removes the client representation with the given id" do
128
+ monkey = Monkey.new("Dec. 3, 1981", true, 'a1a')
129
+ redis_monkey = MonkeyClient.create_from_instance(monkey)
130
+
131
+ MonkeyClient.find_from_instance(monkey).should_not be_nil
132
+ MonkeyClient.remove(monkey.id)
133
+ MonkeyClient.find_from_instance(monkey).should be_nil
134
+ end
135
+
136
+ it "returns false if the client representation with the given id does not exist" do
137
+ monkey = Monkey.new("Dec. 3, 1981", true, 'a1a')
138
+
139
+ MonkeyClient.remove(monkey.id).should be_false
140
+ MonkeyClient.find_from_instance(monkey).should be_nil
141
+ end
142
+ end
143
+
144
+ describe ".representation_for" do
145
+ attr_reader :monkey
146
+
147
+ before do
148
+ @monkey = Monkey.new("Dec. 3, 1981", true, 'aabc1')
149
+ end
150
+
151
+ context "when the object does NOT exist in the cache" do
152
+ before do
153
+ MonkeyClient.all.should be_empty
154
+ end
155
+
156
+ it "inserts the object and returns it" do
157
+ monkey_client = MonkeyClient.representation_for(monkey)
158
+ monkey_client.class.should == MonkeyClient
159
+ monkey_client.birthday.should == "Dec. 3, 1981"
160
+ monkey_client.evil.should == true
161
+ end
162
+ end
163
+
164
+ context "when the object exists in the cache" do
165
+ before do
166
+ monkey_client = MonkeyClient.create_from_instance(monkey)
167
+ MonkeyClient.find_by_id(monkey.id).should_not be_nil
168
+ monkey_client.birthday.should == monkey.birthdate
169
+ MonkeyClient.all.size.should == 1
170
+ end
171
+
172
+ it "updates the values and returns the object" do
173
+ monkey.birthdate = "Nov. 27, 1942"
174
+ monkey_client = MonkeyClient.representation_for(monkey)
175
+ monkey_client.birthday.should == "Nov. 27, 1942"
176
+ MonkeyClient.all.size.should == 1
177
+ end
178
+ end
179
+ end
180
+
181
+ describe "dependencies" do
182
+ it "returns a list of all the dependencies for this class" do
183
+ dependencies = SlugClient.dependencies
184
+ dependencies.keys.should == [BananaClient]
185
+ dependencies[BananaClient][:key_name].should == :banana_id
186
+ dependencies[BananaClient][:method_name].should == :banana
187
+ end
188
+ end
189
+
190
+ describe ".dependency_met_for?" do
191
+ attr_reader :slug
192
+ before do
193
+ @slug = Slug.new('slug_1', "banana_1")
194
+ end
195
+
196
+ context "when dependency is met" do
197
+ before do
198
+ banana = BananaClient.create_from_instance(Banana.new("banana_1", "yellow"))
199
+ BananaClient.find_by_id(banana.id).should_not be_nil
200
+ end
201
+
202
+ it "returns true" do
203
+ SlugClient.dependency_met_for?(slug, BananaClient).should be_true
204
+ end
205
+ end
206
+
207
+ context "when dependency is NOT met" do
208
+ before do
209
+ MonkeyClient.find_by_id(slug.banana_id).should be_nil
210
+ end
211
+
212
+ it "returns false" do
213
+ SlugClient.dependency_met_for?(slug, BananaClient).should be_false
214
+ end
215
+ end
216
+ end
217
+
218
+ describe ".dependencies_met?" do
219
+ context "when all dependencies are met" do
220
+ before do
221
+ SlugClient.stub!(:dependency_met_for?).and_return(true)
222
+ end
223
+
224
+ it "returns true" do
225
+ SlugClient.dependencies_met?(BananaClient).should be_true
226
+ end
227
+ end
228
+
229
+ context "when any dependency is not met" do
230
+ before do
231
+ SlugClient.stub!(:dependency_met_for?).and_return(false)
232
+ end
233
+
234
+ it "returns false" do
235
+ SlugClient.dependencies_met?(BananaClient).should be_false
236
+ end
237
+ end
238
+ end
239
+
240
+ describe ".ensure_dependencies_met" do
241
+ attr_reader :banana, :slug
242
+ describe "when the dependencies are met" do
243
+ before do
244
+ SlugClient.stub!(:dependencies_met?).and_return(true)
245
+ @banana = Banana.new('banana1', 'yellowish')
246
+ @slug = Slug.new('slug1', 'banana1')
247
+ end
248
+
249
+ it "Does not create any objects" do
250
+ BananaClient.all.should be_empty
251
+ SlugClient.ensure_dependencies_met(banana)
252
+ BananaClient.all.should be_empty
253
+ end
254
+ end
255
+
256
+ describe "when the dependencies are NOT met" do
257
+ before do
258
+ @banana = Banana.new('banana1', 'yellowish')
259
+ @slug = Slug.new('slug1', 'banana1')
260
+
261
+ SlugClient.dependencies_met?(slug).should be_false
262
+ end
263
+
264
+ it "creates the dependent objects" do
265
+ BananaClient.all.should be_empty
266
+ SlugClient.ensure_dependencies_met(slug)
267
+ BananaClient.all.should_not be_empty
268
+ end
269
+ end
270
+ end
271
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --color
@@ -0,0 +1,73 @@
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 'spec'
6
+ require 'spec/autorun'
7
+ require 'redis'
8
+
9
+ Spec::Runner.configure do |config|
10
+ config.before(:each) do
11
+ @redis = Tractor.connectdb
12
+ Tractor.flushdb
13
+ end
14
+ end
15
+
16
+ class Sammich < Tractor::Model::Base
17
+ attribute :id
18
+ attribute :product
19
+ attribute :weight
20
+ attribute :expensive, :type => :boolean
21
+ index :product
22
+ index :weight
23
+ end
24
+
25
+ class BananaClient < Tractor::Model::Mapper
26
+ attribute :id
27
+ attribute :name
28
+ end
29
+
30
+ class Banana
31
+ attr_accessor :id, :type
32
+ def initialize(id, type)
33
+ @id = id; @type = type;
34
+ end
35
+ end
36
+
37
+ class MonkeyClient < Tractor::Model::Mapper
38
+ attribute :id
39
+ attribute :birthday, :map => :birthdate
40
+ attribute :evil, :type => :boolean, :map => :evil_monkey #[:evil_monkey, :boolean]
41
+ index :evil
42
+
43
+ association :bananas, BananaClient
44
+ end
45
+
46
+ class Monkey
47
+ attr_accessor :birthdate, :evil_monkey, :id
48
+
49
+ def initialize(birthdate, evil_monkey, id)
50
+ @id = id
51
+ @birthdate = birthdate
52
+ @evil_monkey = evil_monkey
53
+ end
54
+ end
55
+
56
+ class SlugClient < Tractor::Model::Mapper
57
+ attribute :id
58
+ attribute :banana_id
59
+
60
+ depends_on BananaClient, :key_name => :banana_id, :method_name => :banana
61
+ end
62
+
63
+ class Slug
64
+ attr_accessor :id, :banana_id
65
+
66
+ def initialize(id, banana_id)
67
+ @id = id; @banana_id = banana_id
68
+ end
69
+
70
+ def banana
71
+ Banana.new(banana_id, "yellow")
72
+ end
73
+ end
File without changes
data/tractor.gemspec ADDED
@@ -0,0 +1,58 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{tractor}
8
+ s.version = "0.2.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Shane Wolf"]
12
+ s.date = %q{2010-02-12}
13
+ s.description = %q{Very simple object mappings for ruby objects}
14
+ s.email = %q{shanewolf@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "lib/tractor.rb",
27
+ "lib/tractor/model/base.rb",
28
+ "lib/tractor/model/mapper.rb",
29
+ "spec/model/base_spec.rb",
30
+ "spec/model/mapper_spec.rb",
31
+ "spec/spec.opts",
32
+ "spec/spec_helper.rb",
33
+ "spec/tractor_spec.rb",
34
+ "tractor.gemspec"
35
+ ]
36
+ s.homepage = %q{http://github.com/gizm0duck/tractor}
37
+ s.rdoc_options = ["--charset=UTF-8"]
38
+ s.require_paths = ["lib"]
39
+ s.rubygems_version = %q{1.3.5}
40
+ s.summary = %q{Very simple object mapping for ruby objects}
41
+ s.test_files = [
42
+ "spec/model/base_spec.rb",
43
+ "spec/model/mapper_spec.rb",
44
+ "spec/spec_helper.rb",
45
+ "spec/tractor_spec.rb"
46
+ ]
47
+
48
+ if s.respond_to? :specification_version then
49
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
50
+ s.specification_version = 3
51
+
52
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
53
+ else
54
+ end
55
+ else
56
+ end
57
+ end
58
+
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tractor
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Shane Wolf
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-02-12 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: Very simple object mappings for ruby objects
17
+ email: shanewolf@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - LICENSE
24
+ - README.rdoc
25
+ files:
26
+ - .document
27
+ - .gitignore
28
+ - LICENSE
29
+ - README.rdoc
30
+ - Rakefile
31
+ - VERSION
32
+ - lib/tractor.rb
33
+ - lib/tractor/model/base.rb
34
+ - lib/tractor/model/mapper.rb
35
+ - spec/model/base_spec.rb
36
+ - spec/model/mapper_spec.rb
37
+ - spec/spec.opts
38
+ - spec/spec_helper.rb
39
+ - spec/tractor_spec.rb
40
+ - tractor.gemspec
41
+ has_rdoc: true
42
+ homepage: http://github.com/gizm0duck/tractor
43
+ licenses: []
44
+
45
+ post_install_message:
46
+ rdoc_options:
47
+ - --charset=UTF-8
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: "0"
55
+ version:
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: "0"
61
+ version:
62
+ requirements: []
63
+
64
+ rubyforge_project:
65
+ rubygems_version: 1.3.5
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: Very simple object mapping for ruby objects
69
+ test_files:
70
+ - spec/model/base_spec.rb
71
+ - spec/model/mapper_spec.rb
72
+ - spec/spec_helper.rb
73
+ - spec/tractor_spec.rb