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 +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +44 -0
- data/VERSION +1 -0
- data/lib/tractor/model/base.rb +253 -0
- data/lib/tractor/model/mapper.rb +93 -0
- data/lib/tractor.rb +3 -0
- data/spec/model/base_spec.rb +366 -0
- data/spec/model/mapper_spec.rb +271 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +73 -0
- data/spec/tractor_spec.rb +0 -0
- data/tractor.gemspec +58 -0
- metadata +73 -0
data/.document
ADDED
data/.gitignore
ADDED
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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|