remodel 0.1.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/.gitignore +2 -0
- data/LICENSE +20 -0
- data/README.md +103 -0
- data/Rakefile +27 -0
- data/VERSION +1 -0
- data/bin/redis-monitor.rb +25 -0
- data/example/book.rb +13 -0
- data/lib/remodel.rb +279 -0
- data/remodel.gemspec +64 -0
- data/test/helper.rb +13 -0
- data/test/test_entity.rb +220 -0
- data/test/test_many_to_many.rb +53 -0
- data/test/test_many_to_one.rb +88 -0
- data/test/test_mappers.rb +64 -0
- data/test/test_one_to_many.rb +87 -0
- data/test/test_one_to_one.rb +57 -0
- metadata +77 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Tim Lossen -- http://tim.lossen.de
|
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.md
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# remodel your storage layer
|
2
|
+
|
3
|
+
use [redis](http://github.com/antirez/redis) instead of mysql to store your application data.
|
4
|
+
|
5
|
+
remodel (= redis model) is an ActiveRecord-like mapping layer which offers familiar syntax
|
6
|
+
like `has_many`, `has_one` etc. to build your domain model in ruby.
|
7
|
+
|
8
|
+
|
9
|
+
## why redis?
|
10
|
+
|
11
|
+
redis offers in-memory read and write performance — on the order of 10K to 100K
|
12
|
+
operations per second, comparable to [memcached](http://memcached.org/) — plus asynchronous
|
13
|
+
persistence to disk. for example, on my macbook (2 ghz):
|
14
|
+
|
15
|
+
$ redis-benchmark -d 100 -r 10000 -q
|
16
|
+
SET: 13864.27 requests per second
|
17
|
+
GET: 18152.17 requests per second
|
18
|
+
INCR: 17006.80 requests per second
|
19
|
+
LPUSH: 17243.99 requests per second
|
20
|
+
LPOP: 18706.54 requests per second
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
## how to get started
|
25
|
+
|
26
|
+
1. install [redis](http://github.com/antirez/redis) and ezras excellent
|
27
|
+
[redis-rb](http://github.com/ezmobius/redis-rb) ruby client:
|
28
|
+
|
29
|
+
$ brew install redis
|
30
|
+
$ gem install redis
|
31
|
+
|
32
|
+
2. install the super-fast [yajl](http://github.com/lloyd/yajl) json parser
|
33
|
+
plus ruby bindings:
|
34
|
+
|
35
|
+
$ brew install yajl
|
36
|
+
$ gem install yajl-ruby
|
37
|
+
|
38
|
+
3. start redis:
|
39
|
+
|
40
|
+
$ redis-server
|
41
|
+
|
42
|
+
4. now the tests should run successfully:
|
43
|
+
|
44
|
+
$ rake
|
45
|
+
Started
|
46
|
+
.................................................................
|
47
|
+
Finished in 0.063041 seconds.
|
48
|
+
65 tests, 103 assertions, 0 failures, 0 errors
|
49
|
+
|
50
|
+
|
51
|
+
## example
|
52
|
+
|
53
|
+
define your domain model [like this](http://github.com/tlossen/remodel/blob/master/example/book.rb):
|
54
|
+
|
55
|
+
class Book < Remodel::Entity
|
56
|
+
has_many :chapters, :class => 'Chapter', :reverse => :book
|
57
|
+
property :title, :class => 'String'
|
58
|
+
property :year, :class => 'Integer'
|
59
|
+
end
|
60
|
+
|
61
|
+
class Chapter < Remodel::Entity
|
62
|
+
has_one :book, :class => Book, :reverse => :chapters
|
63
|
+
property :title, :class => String
|
64
|
+
end
|
65
|
+
|
66
|
+
now you can do:
|
67
|
+
|
68
|
+
>> require 'example/book'
|
69
|
+
=> true
|
70
|
+
>> book = Book.create :title => 'Moby Dick', :year => 1851
|
71
|
+
=> #<Book(b:3) title: "Moby Dick", year: 1851>
|
72
|
+
>> chapter = book.chapters.create :title => 'Ishmael'
|
73
|
+
=> #<Chapter(c:4) title: "Ishmael">
|
74
|
+
>> chapter.book
|
75
|
+
=> #<Book(b:3) title: "Moby Dick", year: 1851>
|
76
|
+
|
77
|
+
|
78
|
+
## inspired by
|
79
|
+
|
80
|
+
* [how to redis](http://www.paperplanes.de/2009/10/30/how_to_redis.html)
|
81
|
+
— good overview of different mapping options by [mattmatt](http://github.com/mattmatt).
|
82
|
+
* [hurl](http://github.com/defunkt/hurl) — basically i started with
|
83
|
+
defunkts [Hurl::Model](http://github.com/defunkt/hurl/blob/master/models/model.rb).
|
84
|
+
* [ohm](http://github.com/soveran/ohm) — object-hash mapping for redis.
|
85
|
+
somewhat similar, but instead of serializing to json, stores each attribute under a separate key.
|
86
|
+
|
87
|
+
|
88
|
+
## todo
|
89
|
+
|
90
|
+
* documentation ([rocco](http://github.com/rtomayko/rocco))
|
91
|
+
* benchmarks
|
92
|
+
* `delete`
|
93
|
+
* `find_by`
|
94
|
+
|
95
|
+
|
96
|
+
## status
|
97
|
+
|
98
|
+
alpha. play around at your own risk :)
|
99
|
+
|
100
|
+
|
101
|
+
## license
|
102
|
+
|
103
|
+
[MIT](http://github.com/tlossen/remodel/raw/master/LICENSE), baby!
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "remodel"
|
8
|
+
gem.summary = "a minimal ORM (object-redis-mapper)"
|
9
|
+
gem.description = "build your domain model in ruby, persist your objects to redis."
|
10
|
+
gem.email = "tim@lossen.de"
|
11
|
+
gem.homepage = "http://github.com/tlossen/remodel"
|
12
|
+
gem.authors = ["Tim Lossen"]
|
13
|
+
gem.files.include('lib/**/*.rb')
|
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 'rake/testtask'
|
21
|
+
Rake::TestTask.new(:test) do |test|
|
22
|
+
test.libs << 'lib' << 'test'
|
23
|
+
test.pattern = 'test/**/test_*.rb'
|
24
|
+
test.verbose = true
|
25
|
+
end
|
26
|
+
|
27
|
+
task :default => :test
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,25 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# http://gist.github.com/267149 (mathias meyer)
|
4
|
+
|
5
|
+
require 'socket'
|
6
|
+
host = ARGV[0] || 'localhost'
|
7
|
+
port = ARGV[1] || '6379'
|
8
|
+
|
9
|
+
trap(:INT) {
|
10
|
+
exit
|
11
|
+
}
|
12
|
+
|
13
|
+
puts "Connecting to #{host}:#{port}"
|
14
|
+
begin
|
15
|
+
sock = TCPSocket.new(host, port)
|
16
|
+
sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
|
17
|
+
|
18
|
+
sock.write("monitor\r\n")
|
19
|
+
|
20
|
+
while line = sock.gets
|
21
|
+
puts line
|
22
|
+
end
|
23
|
+
rescue Errno::ECONNREFUSED
|
24
|
+
puts "Connection refused"
|
25
|
+
end
|
data/example/book.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/../lib/remodel"
|
2
|
+
|
3
|
+
class Book < Remodel::Entity
|
4
|
+
has_many :chapters, :class => 'Chapter', :reverse => :book
|
5
|
+
property :title, :class => 'String'
|
6
|
+
property :year, :class => 'Integer'
|
7
|
+
end
|
8
|
+
|
9
|
+
class Chapter < Remodel::Entity
|
10
|
+
has_one :book, :class => Book, :reverse => :chapters
|
11
|
+
property :title, :class => String
|
12
|
+
end
|
13
|
+
|
data/lib/remodel.rb
ADDED
@@ -0,0 +1,279 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'redis'
|
3
|
+
require 'yajl'
|
4
|
+
|
5
|
+
module Remodel
|
6
|
+
|
7
|
+
class Error < ::StandardError; end
|
8
|
+
class EntityNotFound < Error; end
|
9
|
+
class EntityNotSaved < Error; end
|
10
|
+
class InvalidKeyPrefix < Error; end
|
11
|
+
class InvalidType < Error; end
|
12
|
+
|
13
|
+
class Mapper
|
14
|
+
def initialize(clazz = nil, pack_method = nil, unpack_method = nil)
|
15
|
+
@clazz = clazz
|
16
|
+
@pack_method = pack_method
|
17
|
+
@unpack_method = unpack_method
|
18
|
+
end
|
19
|
+
|
20
|
+
def pack(value)
|
21
|
+
return nil if value.nil?
|
22
|
+
raise(InvalidType, "#{value.inspect} is not a #{@clazz}") if @clazz && !value.is_a?(@clazz)
|
23
|
+
@pack_method ? value.send(@pack_method) : value
|
24
|
+
end
|
25
|
+
|
26
|
+
def unpack(value)
|
27
|
+
return nil if value.nil?
|
28
|
+
@unpack_method ? @clazz.send(@unpack_method, value) : value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class HasMany < Array
|
33
|
+
def initialize(this, clazz, key, reverse = nil)
|
34
|
+
super fetch(clazz, key)
|
35
|
+
@this, @clazz, @key, @reverse = this, clazz, key, reverse
|
36
|
+
end
|
37
|
+
|
38
|
+
def create(attributes = {})
|
39
|
+
add(@clazz.create(attributes))
|
40
|
+
end
|
41
|
+
|
42
|
+
def add(entity)
|
43
|
+
add_to_reverse_association_of(entity) if @reverse
|
44
|
+
_add(entity)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def _add(entity)
|
50
|
+
self << entity
|
51
|
+
Remodel.redis.rpush(@key, entity.key)
|
52
|
+
entity
|
53
|
+
end
|
54
|
+
|
55
|
+
def _remove(entity)
|
56
|
+
delete_if { |x| x.key = entity.key }
|
57
|
+
Remodel.redis.lrem(@key, 0, entity.key)
|
58
|
+
end
|
59
|
+
|
60
|
+
def add_to_reverse_association_of(entity)
|
61
|
+
if entity.send(@reverse).is_a? HasMany
|
62
|
+
entity.send(@reverse).send(:_add, @this)
|
63
|
+
else
|
64
|
+
entity.send("_#{@reverse}=", @this)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def fetch(clazz, key)
|
69
|
+
keys = Remodel.redis.lrange(key, 0, -1)
|
70
|
+
values = keys.empty? ? [] : Remodel.redis.mget(keys)
|
71
|
+
keys.zip(values).map do |key, json|
|
72
|
+
clazz.restore(key, json) if json
|
73
|
+
end.compact
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class Entity
|
78
|
+
attr_accessor :key
|
79
|
+
|
80
|
+
def initialize(attributes = {}, key = nil)
|
81
|
+
@attributes = {}
|
82
|
+
@key = key
|
83
|
+
attributes.each do |name, value|
|
84
|
+
send("#{name}=", value) if respond_to? "#{name}="
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def save
|
89
|
+
@key = self.class.next_key unless @key
|
90
|
+
Remodel.redis.set(@key, to_json)
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
def reload
|
95
|
+
raise EntityNotSaved unless @key
|
96
|
+
initialize(self.class.parse(self.class.fetch(@key)), @key)
|
97
|
+
instance_variables.each do |var|
|
98
|
+
remove_instance_variable(var) if var =~ /^@association_/
|
99
|
+
end
|
100
|
+
self
|
101
|
+
end
|
102
|
+
|
103
|
+
def to_json
|
104
|
+
Yajl::Encoder.encode(self.class.pack(@attributes))
|
105
|
+
end
|
106
|
+
|
107
|
+
def inspect
|
108
|
+
properties = @attributes.map { |name, value| "#{name}: #{value.inspect}" }.join(', ')
|
109
|
+
"\#<#{self.class.name}(#{key}) #{properties}>"
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.create(attributes = {})
|
113
|
+
new(attributes).save
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.find(key)
|
117
|
+
restore(key, fetch(key))
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.restore(key, json)
|
121
|
+
new(parse(json), key)
|
122
|
+
end
|
123
|
+
|
124
|
+
protected
|
125
|
+
|
126
|
+
def self.set_key_prefix(prefix)
|
127
|
+
raise(InvalidKeyPrefix, prefix) unless prefix =~ /^[a-z]+$/
|
128
|
+
@key_prefix = prefix
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.property(name, options = {})
|
132
|
+
name = name.to_sym
|
133
|
+
mapper[name] = Remodel.mapper_for(options[:class])
|
134
|
+
define_method(name) { @attributes[name] }
|
135
|
+
define_method("#{name}=") { |value| @attributes[name] = value }
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.has_many(name, options)
|
139
|
+
var = "@association_#{name}".to_sym
|
140
|
+
|
141
|
+
define_method(name) do
|
142
|
+
if instance_variable_defined? var
|
143
|
+
instance_variable_get(var)
|
144
|
+
else
|
145
|
+
clazz = Remodel.find_class(options[:class])
|
146
|
+
instance_variable_set(var, HasMany.new(self, clazz, "#{key}:#{name}", options[:reverse]))
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def self.has_one(name, options)
|
152
|
+
var = "@association_#{name}".to_sym
|
153
|
+
|
154
|
+
define_method(name) do
|
155
|
+
if instance_variable_defined? var
|
156
|
+
instance_variable_get(var)
|
157
|
+
else
|
158
|
+
clazz = Remodel.find_class(options[:class])
|
159
|
+
value_key = Remodel.redis.get("#{key}:#{name}")
|
160
|
+
instance_variable_set(var, clazz.find(value_key)) if value_key
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
define_method("#{name}=") do |value|
|
165
|
+
send("_reverse_association_of_#{name}=", value) if options[:reverse]
|
166
|
+
send("_#{name}=", value)
|
167
|
+
end
|
168
|
+
|
169
|
+
define_method("_#{name}=") do |value|
|
170
|
+
if value
|
171
|
+
instance_variable_set(var, value)
|
172
|
+
Remodel.redis.set("#{key}:#{name}", value.key)
|
173
|
+
else
|
174
|
+
remove_instance_variable(var) if instance_variable_defined? var
|
175
|
+
Remodel.redis.del("#{key}:#{name}")
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
private "_#{name}="
|
180
|
+
|
181
|
+
if options[:reverse]
|
182
|
+
define_method("_reverse_association_of_#{name}=") do |value|
|
183
|
+
if value
|
184
|
+
association = value.send("#{options[:reverse]}")
|
185
|
+
if association.is_a? HasMany
|
186
|
+
association.send("_add", self)
|
187
|
+
else
|
188
|
+
value.send("_#{options[:reverse]}=", self)
|
189
|
+
end
|
190
|
+
else
|
191
|
+
if old_value = send(name)
|
192
|
+
association = old_value.send("#{options[:reverse]}")
|
193
|
+
if association.is_a? HasMany
|
194
|
+
association.send("_remove", self)
|
195
|
+
else
|
196
|
+
old_value.send("_#{options[:reverse]}=", nil)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
private "_reverse_association_of_#{name}="
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
private
|
207
|
+
|
208
|
+
def self.fetch(key)
|
209
|
+
Remodel.redis.get(key) || raise(EntityNotFound, "no #{name} with key #{key}")
|
210
|
+
end
|
211
|
+
|
212
|
+
def self.next_key
|
213
|
+
counter = Remodel.redis.incr("#{key_prefix}:seq")
|
214
|
+
"#{key_prefix}:#{counter}"
|
215
|
+
end
|
216
|
+
|
217
|
+
def self.key_prefix
|
218
|
+
@key_prefix ||= name.split('::').last[0,1].downcase
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.parse(json)
|
222
|
+
unpack(Yajl::Parser.parse(json))
|
223
|
+
end
|
224
|
+
|
225
|
+
def self.pack(attributes)
|
226
|
+
result = {}
|
227
|
+
attributes.each do |name, value|
|
228
|
+
result[name] = mapper[name].pack(value)
|
229
|
+
end
|
230
|
+
result
|
231
|
+
end
|
232
|
+
|
233
|
+
def self.unpack(attributes)
|
234
|
+
result = {}
|
235
|
+
attributes.each do |name, value|
|
236
|
+
name = name.to_sym
|
237
|
+
result[name] = mapper[name].unpack(value)
|
238
|
+
end
|
239
|
+
result
|
240
|
+
end
|
241
|
+
|
242
|
+
def self.mapper
|
243
|
+
@mapper ||= {}
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def self.redis=(redis)
|
248
|
+
@redis = redis
|
249
|
+
end
|
250
|
+
|
251
|
+
def self.redis
|
252
|
+
@redis ||= Redis.new
|
253
|
+
end
|
254
|
+
|
255
|
+
private
|
256
|
+
|
257
|
+
# converts String, Symbol or Class into Class
|
258
|
+
def self.find_class(clazz)
|
259
|
+
return nil unless clazz
|
260
|
+
clazz.to_s.split('::').inject(Kernel) { |mod, name| mod.const_get(name) }
|
261
|
+
end
|
262
|
+
|
263
|
+
def self.mapper_for(clazz)
|
264
|
+
mapper_by_class[find_class(clazz)]
|
265
|
+
end
|
266
|
+
|
267
|
+
def self.mapper_by_class
|
268
|
+
@mapper_by_class ||= Hash.new(Mapper.new).merge(
|
269
|
+
String => Mapper.new(String),
|
270
|
+
Integer => Mapper.new(Integer),
|
271
|
+
Float => Mapper.new(Float),
|
272
|
+
Array => Mapper.new(Array),
|
273
|
+
Hash => Mapper.new(Hash),
|
274
|
+
Date => Mapper.new(Date, :to_s, :parse),
|
275
|
+
Time => Mapper.new(Time, :to_i, :at)
|
276
|
+
)
|
277
|
+
end
|
278
|
+
|
279
|
+
end
|
data/remodel.gemspec
ADDED
@@ -0,0 +1,64 @@
|
|
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{remodel}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Tim Lossen"]
|
12
|
+
s.date = %q{2010-03-23}
|
13
|
+
s.default_executable = %q{redis-monitor.rb}
|
14
|
+
s.description = %q{build your domain model in ruby, persist your objects to redis.}
|
15
|
+
s.email = %q{tim@lossen.de}
|
16
|
+
s.executables = ["redis-monitor.rb"]
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"LICENSE",
|
19
|
+
"README.md"
|
20
|
+
]
|
21
|
+
s.files = [
|
22
|
+
".gitignore",
|
23
|
+
"LICENSE",
|
24
|
+
"README.md",
|
25
|
+
"Rakefile",
|
26
|
+
"VERSION",
|
27
|
+
"bin/redis-monitor.rb",
|
28
|
+
"example/book.rb",
|
29
|
+
"lib/remodel.rb",
|
30
|
+
"remodel.gemspec",
|
31
|
+
"test/helper.rb",
|
32
|
+
"test/test_entity.rb",
|
33
|
+
"test/test_many_to_many.rb",
|
34
|
+
"test/test_many_to_one.rb",
|
35
|
+
"test/test_mappers.rb",
|
36
|
+
"test/test_one_to_many.rb",
|
37
|
+
"test/test_one_to_one.rb"
|
38
|
+
]
|
39
|
+
s.homepage = %q{http://github.com/tlossen/remodel}
|
40
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
41
|
+
s.require_paths = ["lib"]
|
42
|
+
s.rubygems_version = %q{1.3.5}
|
43
|
+
s.summary = %q{a minimal ORM (object-redis-mapper)}
|
44
|
+
s.test_files = [
|
45
|
+
"test/helper.rb",
|
46
|
+
"test/test_entity.rb",
|
47
|
+
"test/test_many_to_many.rb",
|
48
|
+
"test/test_many_to_one.rb",
|
49
|
+
"test/test_mappers.rb",
|
50
|
+
"test/test_one_to_many.rb",
|
51
|
+
"test/test_one_to_one.rb"
|
52
|
+
]
|
53
|
+
|
54
|
+
if s.respond_to? :specification_version then
|
55
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
56
|
+
s.specification_version = 3
|
57
|
+
|
58
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
59
|
+
else
|
60
|
+
end
|
61
|
+
else
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
data/test/helper.rb
ADDED
data/test/test_entity.rb
ADDED
@@ -0,0 +1,220 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class Foo < Remodel::Entity
|
4
|
+
property :x
|
5
|
+
property :y
|
6
|
+
end
|
7
|
+
|
8
|
+
class Bar < Remodel::Entity; end
|
9
|
+
|
10
|
+
class TestEntity < Test::Unit::TestCase
|
11
|
+
|
12
|
+
context "new" do
|
13
|
+
should "set properties" do
|
14
|
+
foo = Foo.new :x => 1, :y => 2
|
15
|
+
assert 1, foo.x
|
16
|
+
assert 2, foo.y
|
17
|
+
end
|
18
|
+
|
19
|
+
should "ignore undefined properties" do
|
20
|
+
foo = Foo.new :z => 3
|
21
|
+
assert foo.instance_eval { !@attributes.key? :z }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "reload" do
|
26
|
+
setup do
|
27
|
+
@foo = Foo.create :x => 'hello', :y => true
|
28
|
+
end
|
29
|
+
|
30
|
+
should "reload all properties" do
|
31
|
+
redis.set @foo.key, %q({"x":23,"y":"adios"})
|
32
|
+
@foo.reload
|
33
|
+
assert_equal 23, @foo.x
|
34
|
+
assert_equal 'adios', @foo.y
|
35
|
+
end
|
36
|
+
|
37
|
+
should "keep the key" do
|
38
|
+
key = @foo.key
|
39
|
+
@foo.reload
|
40
|
+
assert_equal key, @foo.key
|
41
|
+
end
|
42
|
+
|
43
|
+
should "stay the same object" do
|
44
|
+
id = @foo.object_id
|
45
|
+
@foo.reload
|
46
|
+
assert_equal id, @foo.object_id
|
47
|
+
end
|
48
|
+
|
49
|
+
should "raise EntityNotFound if the entity does not exist any more" do
|
50
|
+
redis.del @foo.key
|
51
|
+
assert_raise(Remodel::EntityNotFound) { @foo.reload }
|
52
|
+
end
|
53
|
+
|
54
|
+
should "raise EntityNotSaved if the entity was never saved" do
|
55
|
+
assert_raise(Remodel::EntityNotSaved) { Foo.new.reload }
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context "create" do
|
60
|
+
setup do
|
61
|
+
redis.flushdb
|
62
|
+
end
|
63
|
+
|
64
|
+
should "work without attributes" do
|
65
|
+
foo = Foo.create
|
66
|
+
assert foo.is_a?(Foo)
|
67
|
+
end
|
68
|
+
|
69
|
+
should "give the entity a key based on the class name" do
|
70
|
+
assert_equal 'f:1', Foo.create.key
|
71
|
+
assert_equal 'b:1', Bar.create.key
|
72
|
+
assert_equal 'b:2', Bar.create.key
|
73
|
+
end
|
74
|
+
|
75
|
+
should "store the entity under its key" do
|
76
|
+
foo = Foo.create :x => 'hello', :y => false
|
77
|
+
assert redis.exists(foo.key)
|
78
|
+
end
|
79
|
+
|
80
|
+
should "store all properties" do
|
81
|
+
foo = Foo.create :x => 'hello', :y => false
|
82
|
+
foo.reload
|
83
|
+
assert_equal 'hello', foo.x
|
84
|
+
assert_equal false, foo.y
|
85
|
+
end
|
86
|
+
|
87
|
+
should "not store the key as a property" do
|
88
|
+
foo = Foo.create :x => 'hello', :y => false
|
89
|
+
assert !(/f:1/ =~ redis.get(foo.key))
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context "save" do
|
94
|
+
setup do
|
95
|
+
redis.flushdb
|
96
|
+
end
|
97
|
+
|
98
|
+
should "give the entity a key, if necessary" do
|
99
|
+
foo = Foo.new.save
|
100
|
+
assert foo.key
|
101
|
+
end
|
102
|
+
|
103
|
+
should "store the entity under its key" do
|
104
|
+
foo = Foo.new :x => 'hello', :y => false
|
105
|
+
foo.save
|
106
|
+
assert redis.exists(foo.key)
|
107
|
+
end
|
108
|
+
|
109
|
+
should "store all properties" do
|
110
|
+
foo = Foo.new :x => 'hello', :y => false
|
111
|
+
foo.save
|
112
|
+
foo.reload
|
113
|
+
assert_equal 'hello', foo.x
|
114
|
+
assert_equal false, foo.y
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
context "#set_key_prefix" do
|
119
|
+
should "use the given key prefix" do
|
120
|
+
class Custom < Remodel::Entity; set_key_prefix 'my'; end
|
121
|
+
assert_match /^my:\d+$/, Custom.create.key
|
122
|
+
end
|
123
|
+
|
124
|
+
should "ensure that the prefix is letters only" do
|
125
|
+
assert_raise(Remodel::InvalidKeyPrefix) do
|
126
|
+
class InvalidPrefix < Remodel::Entity; set_key_prefix '666'; end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
context "find" do
|
132
|
+
setup do
|
133
|
+
redis.flushdb
|
134
|
+
@foo = Foo.create :x => 'hello', :y => 123
|
135
|
+
end
|
136
|
+
|
137
|
+
should "load an entity from redis" do
|
138
|
+
foo = Foo.find(@foo.key)
|
139
|
+
assert_equal foo.x, @foo.x
|
140
|
+
assert_equal foo.y, @foo.y
|
141
|
+
end
|
142
|
+
|
143
|
+
should "raise EntityNotFound if the key does not exist" do
|
144
|
+
assert_raise(Remodel::EntityNotFound) { Foo.find(23) }
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context "properties" do
|
149
|
+
should "have property x" do
|
150
|
+
foo = Foo.new
|
151
|
+
foo.x = 23
|
152
|
+
assert_equal 23, foo.x
|
153
|
+
foo.x += 10
|
154
|
+
assert_equal 33, foo.x
|
155
|
+
end
|
156
|
+
|
157
|
+
should "not have property z" do
|
158
|
+
foo = Foo.new
|
159
|
+
assert_raise(NoMethodError) { foo.z }
|
160
|
+
assert_raise(NoMethodError) { foo.z = 42 }
|
161
|
+
end
|
162
|
+
|
163
|
+
context "types" do
|
164
|
+
should "work with nil" do
|
165
|
+
foo = Foo.create :x => nil
|
166
|
+
assert_equal nil, foo.reload.x
|
167
|
+
end
|
168
|
+
|
169
|
+
should "work with booleans" do
|
170
|
+
foo = Foo.create :x => false
|
171
|
+
assert_equal false, foo.reload.x
|
172
|
+
end
|
173
|
+
|
174
|
+
should "work with integers" do
|
175
|
+
foo = Foo.create :x => -42
|
176
|
+
assert_equal -42, foo.reload.x
|
177
|
+
end
|
178
|
+
|
179
|
+
should "work with floats" do
|
180
|
+
foo = Foo.create :x => 3.141
|
181
|
+
assert_equal 3.141, foo.reload.x
|
182
|
+
end
|
183
|
+
|
184
|
+
should "work with strings" do
|
185
|
+
foo = Foo.create :x => 'hello'
|
186
|
+
assert_equal 'hello', foo.reload.x
|
187
|
+
end
|
188
|
+
|
189
|
+
should "work with lists" do
|
190
|
+
foo = Foo.create :x => [1, 2, 3]
|
191
|
+
assert_equal [1, 2, 3], foo.reload.x
|
192
|
+
end
|
193
|
+
|
194
|
+
should "work with hashes" do
|
195
|
+
hash = { 'a' => 17, 'b' => 'test' }
|
196
|
+
foo = Foo.create :x => hash
|
197
|
+
assert_equal hash, foo.reload.x
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
context "json" do
|
203
|
+
should "serialize to json" do
|
204
|
+
foo = Foo.new :x => 42, :y => true
|
205
|
+
assert_match /"x":42/, foo.to_json
|
206
|
+
assert_match /"y":true/, foo.to_json
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
context "restore" do
|
211
|
+
should "restore an entity from json" do
|
212
|
+
before = Foo.create :x => 42, :y => true
|
213
|
+
after = Foo.restore(before.key, before.to_json)
|
214
|
+
assert_equal before.key, after.key
|
215
|
+
assert_equal before.x, after.x
|
216
|
+
assert_equal before.y, after.y
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestManyToMany < Test::Unit::TestCase
|
4
|
+
|
5
|
+
class Person < Remodel::Entity
|
6
|
+
has_many :groups, :class => 'TestManyToMany::Group', :reverse => 'members'
|
7
|
+
property :name
|
8
|
+
end
|
9
|
+
|
10
|
+
class Group < Remodel::Entity
|
11
|
+
has_many :members, :class => 'TestManyToMany::Person', :reverse => 'groups'
|
12
|
+
property :name
|
13
|
+
end
|
14
|
+
|
15
|
+
context "both associations" do
|
16
|
+
should "be empty by default" do
|
17
|
+
assert_equal [], Person.new.groups
|
18
|
+
assert_equal [], Group.new.members
|
19
|
+
end
|
20
|
+
|
21
|
+
context "create" do
|
22
|
+
should "add a new group to both associations" do
|
23
|
+
tim = Person.create :name => 'tim'
|
24
|
+
rugb = tim.groups.create :name => 'rug-b'
|
25
|
+
assert_equal [tim], rugb.members
|
26
|
+
end
|
27
|
+
|
28
|
+
should "add a new person to both associations" do
|
29
|
+
rugb = Group.create :name => 'rug-b'
|
30
|
+
tim = rugb.members.create :name => 'tim'
|
31
|
+
assert_equal [rugb], tim.groups
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "add" do
|
36
|
+
setup do
|
37
|
+
@tim = Person.create :name => 'tim'
|
38
|
+
@rugb = Group.create :name => 'rug-b'
|
39
|
+
end
|
40
|
+
|
41
|
+
should "add a new group to both associations" do
|
42
|
+
@tim.groups.add(@rugb)
|
43
|
+
assert_equal [@tim], @rugb.members
|
44
|
+
end
|
45
|
+
|
46
|
+
should "add a new person to both associations" do
|
47
|
+
@rugb.members.add(@tim)
|
48
|
+
assert_equal [@rugb], @tim.groups
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestManyToOne < Test::Unit::TestCase
|
4
|
+
|
5
|
+
class Puzzle < Remodel::Entity
|
6
|
+
has_many :pieces, :class => 'TestManyToOne::Piece', :reverse => 'puzzle'
|
7
|
+
property :topic
|
8
|
+
end
|
9
|
+
|
10
|
+
class Piece < Remodel::Entity
|
11
|
+
has_one :puzzle, :class => 'TestManyToOne::Puzzle'
|
12
|
+
property :color
|
13
|
+
end
|
14
|
+
|
15
|
+
context "has_many" do
|
16
|
+
context "association" do
|
17
|
+
should "exist" do
|
18
|
+
assert Puzzle.create.respond_to?(:pieces)
|
19
|
+
end
|
20
|
+
|
21
|
+
should "return an empty list by default" do
|
22
|
+
assert_equal [], Puzzle.create.pieces
|
23
|
+
end
|
24
|
+
|
25
|
+
should "return any existing children" do
|
26
|
+
puzzle = Puzzle.create
|
27
|
+
redis.rpush "#{puzzle.key}:pieces", Piece.create(:color => 'red').key
|
28
|
+
redis.rpush "#{puzzle.key}:pieces", Piece.create(:color => 'blue').key
|
29
|
+
assert_equal 2, puzzle.pieces.size
|
30
|
+
assert_equal Piece, puzzle.pieces[0].class
|
31
|
+
assert_equal 'red', puzzle.pieces[0].color
|
32
|
+
end
|
33
|
+
|
34
|
+
context "create" do
|
35
|
+
should "have a create method" do
|
36
|
+
assert Puzzle.create.pieces.respond_to?(:create)
|
37
|
+
end
|
38
|
+
|
39
|
+
should "work without attributes" do
|
40
|
+
puzzle = Puzzle.create
|
41
|
+
piece = puzzle.pieces.create
|
42
|
+
assert piece.is_a?(Piece)
|
43
|
+
end
|
44
|
+
|
45
|
+
should "create and store a new child" do
|
46
|
+
puzzle = Puzzle.create
|
47
|
+
puzzle.pieces.create :color => 'green'
|
48
|
+
assert_equal 1, puzzle.pieces.size
|
49
|
+
puzzle.reload
|
50
|
+
assert_equal 1, puzzle.pieces.size
|
51
|
+
assert_equal Piece, puzzle.pieces[0].class
|
52
|
+
assert_equal 'green', puzzle.pieces[0].color
|
53
|
+
end
|
54
|
+
|
55
|
+
should "associate the created child with self" do
|
56
|
+
puzzle = Puzzle.create :topic => 'provence'
|
57
|
+
piece = puzzle.pieces.create :color => 'green'
|
58
|
+
assert_equal 'provence', piece.puzzle.topic
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context "add" do
|
63
|
+
should "add the given entity to the association" do
|
64
|
+
puzzle = Puzzle.create
|
65
|
+
piece = Piece.create :color => 'white'
|
66
|
+
puzzle.pieces.add piece
|
67
|
+
assert_equal 1, puzzle.pieces.size
|
68
|
+
puzzle.reload
|
69
|
+
assert_equal 1, puzzle.pieces.size
|
70
|
+
assert_equal Piece, puzzle.pieces[0].class
|
71
|
+
assert_equal 'white', puzzle.pieces[0].color
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
context "reload" do
|
79
|
+
should "reset has_many associations" do
|
80
|
+
puzzle = Puzzle.create
|
81
|
+
piece = puzzle.pieces.create :color => 'black'
|
82
|
+
redis.del "#{puzzle.key}:pieces"
|
83
|
+
puzzle.reload
|
84
|
+
assert_equal [], puzzle.pieces
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class Item < Remodel::Entity
|
4
|
+
property :string, :class => String
|
5
|
+
property :integer, :class => Integer
|
6
|
+
property :float, :class => Float
|
7
|
+
property :array, :class => Array
|
8
|
+
property :hash, :class => Hash
|
9
|
+
property :time, :class => Time
|
10
|
+
property :date, :class => Date
|
11
|
+
end
|
12
|
+
|
13
|
+
class TestMappers < Test::Unit::TestCase
|
14
|
+
|
15
|
+
context "create" do
|
16
|
+
setup do
|
17
|
+
@item = Item.create :time => Time.at(1234567890), :date => Date.parse("1972-06-16")
|
18
|
+
end
|
19
|
+
|
20
|
+
should "store unmapped values" do
|
21
|
+
assert_equal Time, @item.instance_eval { @attributes[:time].class }
|
22
|
+
assert_equal Date, @item.instance_eval { @attributes[:date].class }
|
23
|
+
end
|
24
|
+
|
25
|
+
should "not change mapped values" do
|
26
|
+
assert_equal Time.at(1234567890), @item.time
|
27
|
+
assert_equal Date.parse("1972-06-16"), @item.date
|
28
|
+
end
|
29
|
+
|
30
|
+
should "not change mapped values after reload" do
|
31
|
+
@item.reload
|
32
|
+
assert_equal Time.at(1234567890), @item.time
|
33
|
+
assert_equal Date.parse("1972-06-16"), @item.date
|
34
|
+
end
|
35
|
+
|
36
|
+
should "serialize mapped values correctly" do
|
37
|
+
json = redis.get(@item.key)
|
38
|
+
assert_match /1234567890/, json
|
39
|
+
assert_match /"1972-06-16"/, json
|
40
|
+
end
|
41
|
+
|
42
|
+
should "handle nil values" do
|
43
|
+
item = Item.create
|
44
|
+
assert_nil item.string
|
45
|
+
assert_nil item.integer
|
46
|
+
assert_nil item.float
|
47
|
+
assert_nil item.array
|
48
|
+
assert_nil item.hash
|
49
|
+
assert_nil item.time
|
50
|
+
assert_nil item.date
|
51
|
+
end
|
52
|
+
|
53
|
+
should "reject invalid types" do
|
54
|
+
assert_raise(Remodel::InvalidType) { Item.create :string => true }
|
55
|
+
assert_raise(Remodel::InvalidType) { Item.create :integer => 33.5 }
|
56
|
+
assert_raise(Remodel::InvalidType) { Item.create :float => 5 }
|
57
|
+
assert_raise(Remodel::InvalidType) { Item.create :array => {} }
|
58
|
+
assert_raise(Remodel::InvalidType) { Item.create :hash => [] }
|
59
|
+
assert_raise(Remodel::InvalidType) { Item.create :time => Date.new }
|
60
|
+
assert_raise(Remodel::InvalidType) { Item.create :date => Time.now }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
|
4
|
+
class TestOneToMany < Test::Unit::TestCase
|
5
|
+
|
6
|
+
class Piece < Remodel::Entity
|
7
|
+
has_one :puzzle, :class => 'TestOneToMany::Puzzle', :reverse => 'pieces'
|
8
|
+
property :color
|
9
|
+
end
|
10
|
+
|
11
|
+
class Puzzle < Remodel::Entity
|
12
|
+
has_many :pieces, :class => 'TestOneToMany::Piece'
|
13
|
+
property :topic
|
14
|
+
end
|
15
|
+
|
16
|
+
context "has_one" do
|
17
|
+
context "association getter" do
|
18
|
+
should "exist" do
|
19
|
+
assert Piece.create.respond_to?(:puzzle)
|
20
|
+
end
|
21
|
+
|
22
|
+
should "return nil by default" do
|
23
|
+
assert_nil Piece.create.puzzle
|
24
|
+
end
|
25
|
+
|
26
|
+
should "return the associated entity" do
|
27
|
+
puzzle = Puzzle.create :topic => 'animals'
|
28
|
+
piece = Piece.create
|
29
|
+
redis.set("#{piece.key}:puzzle", puzzle.key)
|
30
|
+
assert_equal 'animals', piece.puzzle.topic
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "association setter" do
|
35
|
+
should "exist" do
|
36
|
+
assert Piece.create.respond_to?(:'puzzle=')
|
37
|
+
end
|
38
|
+
|
39
|
+
should "store the key of the associated entity" do
|
40
|
+
puzzle = Puzzle.create
|
41
|
+
piece = Piece.create
|
42
|
+
piece.puzzle = puzzle
|
43
|
+
assert_equal puzzle.key, redis.get("#{piece.key}:puzzle")
|
44
|
+
end
|
45
|
+
|
46
|
+
should "add the entity to the reverse association" do
|
47
|
+
puzzle = Puzzle.create
|
48
|
+
piece = Piece.create
|
49
|
+
piece.puzzle = puzzle
|
50
|
+
assert_equal 1, puzzle.pieces.size
|
51
|
+
end
|
52
|
+
|
53
|
+
should "be settable to nil" do
|
54
|
+
piece = Piece.create
|
55
|
+
piece.puzzle = nil
|
56
|
+
assert_nil piece.puzzle
|
57
|
+
end
|
58
|
+
|
59
|
+
should "remove the key if set to nil" do
|
60
|
+
piece = Piece.create
|
61
|
+
piece.puzzle = Puzzle.create
|
62
|
+
piece.puzzle = nil
|
63
|
+
assert_nil redis.get("#{piece.key}:puzzle")
|
64
|
+
end
|
65
|
+
|
66
|
+
should "remove the entity from the reverse association if set to nil" do
|
67
|
+
puzzle = Puzzle.create
|
68
|
+
piece = Piece.create
|
69
|
+
piece.puzzle = puzzle
|
70
|
+
piece.puzzle = nil
|
71
|
+
puzzle.reload
|
72
|
+
assert_equal 0, puzzle.pieces.size
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
context "reload" do
|
78
|
+
should "reset has_one associations" do
|
79
|
+
piece = Piece.create :color => 'black'
|
80
|
+
piece.puzzle = Puzzle.create
|
81
|
+
redis.del "#{piece.key}:puzzle"
|
82
|
+
piece.reload
|
83
|
+
assert_nil piece.puzzle
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestOneToOne < Test::Unit::TestCase
|
4
|
+
|
5
|
+
class Man < Remodel::Entity
|
6
|
+
has_one :wife, :class => 'TestOneToOne::Woman', :reverse => 'husband'
|
7
|
+
property :name
|
8
|
+
end
|
9
|
+
|
10
|
+
class Woman < Remodel::Entity
|
11
|
+
has_one :husband, :class => 'TestOneToOne::Man', :reverse => 'wife'
|
12
|
+
property :name
|
13
|
+
end
|
14
|
+
|
15
|
+
context "both associations" do
|
16
|
+
should "be nil by default" do
|
17
|
+
assert_equal nil, Man.new.wife
|
18
|
+
assert_equal nil, Woman.new.husband
|
19
|
+
end
|
20
|
+
|
21
|
+
context "setter" do
|
22
|
+
setup do
|
23
|
+
@bill = Man.create :name => 'Bill'
|
24
|
+
@mary = Woman.create :name => 'Mary'
|
25
|
+
end
|
26
|
+
|
27
|
+
context "non-nil value" do
|
28
|
+
should "also set husband" do
|
29
|
+
@bill.wife = @mary
|
30
|
+
assert_equal @bill, @mary.husband
|
31
|
+
end
|
32
|
+
|
33
|
+
should "also set wife" do
|
34
|
+
@mary.husband = @bill
|
35
|
+
assert_equal @mary, @bill.wife
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "nil value" do
|
40
|
+
setup do
|
41
|
+
@bill.wife = @mary
|
42
|
+
end
|
43
|
+
|
44
|
+
should "also clear husband" do
|
45
|
+
@bill.wife = nil
|
46
|
+
assert_equal nil, @mary.husband
|
47
|
+
end
|
48
|
+
|
49
|
+
should "also clear wife" do
|
50
|
+
@mary.husband = nil
|
51
|
+
assert_equal nil, @bill.wife
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: remodel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tim Lossen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-03-23 00:00:00 +01:00
|
13
|
+
default_executable: redis-monitor.rb
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: build your domain model in ruby, persist your objects to redis.
|
17
|
+
email: tim@lossen.de
|
18
|
+
executables:
|
19
|
+
- redis-monitor.rb
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README.md
|
25
|
+
files:
|
26
|
+
- .gitignore
|
27
|
+
- LICENSE
|
28
|
+
- README.md
|
29
|
+
- Rakefile
|
30
|
+
- VERSION
|
31
|
+
- bin/redis-monitor.rb
|
32
|
+
- example/book.rb
|
33
|
+
- lib/remodel.rb
|
34
|
+
- remodel.gemspec
|
35
|
+
- test/helper.rb
|
36
|
+
- test/test_entity.rb
|
37
|
+
- test/test_many_to_many.rb
|
38
|
+
- test/test_many_to_one.rb
|
39
|
+
- test/test_mappers.rb
|
40
|
+
- test/test_one_to_many.rb
|
41
|
+
- test/test_one_to_one.rb
|
42
|
+
has_rdoc: true
|
43
|
+
homepage: http://github.com/tlossen/remodel
|
44
|
+
licenses: []
|
45
|
+
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options:
|
48
|
+
- --charset=UTF-8
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: "0"
|
56
|
+
version:
|
57
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: "0"
|
62
|
+
version:
|
63
|
+
requirements: []
|
64
|
+
|
65
|
+
rubyforge_project:
|
66
|
+
rubygems_version: 1.3.5
|
67
|
+
signing_key:
|
68
|
+
specification_version: 3
|
69
|
+
summary: a minimal ORM (object-redis-mapper)
|
70
|
+
test_files:
|
71
|
+
- test/helper.rb
|
72
|
+
- test/test_entity.rb
|
73
|
+
- test/test_many_to_many.rb
|
74
|
+
- test/test_many_to_one.rb
|
75
|
+
- test/test_mappers.rb
|
76
|
+
- test/test_one_to_many.rb
|
77
|
+
- test/test_one_to_one.rb
|