tractor 0.2.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/.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