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 ADDED
@@ -0,0 +1,2 @@
1
+ dump.rdb
2
+ pkg/*
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
+ &mdash; good overview of different mapping options by [mattmatt](http://github.com/mattmatt).
82
+ * [hurl](http://github.com/defunkt/hurl) &mdash; 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) &mdash; 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
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ require File.dirname(__FILE__) + '/../lib/remodel.rb'
6
+
7
+ class Test::Unit::TestCase
8
+
9
+ def redis
10
+ Remodel.redis
11
+ end
12
+
13
+ end
@@ -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