remodel-h 0.1.4

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.
@@ -0,0 +1,71 @@
1
+ module Remodel
2
+
3
+ # Represents the many-end of a many-to-one or many-to-many association.
4
+ class HasMany < Array
5
+ def initialize(this, clazz, key, reverse = nil)
6
+ super _fetch(clazz, key)
7
+ @this, @clazz, @key, @reverse = this, clazz, key, reverse
8
+ end
9
+
10
+ def create(attributes = {})
11
+ add(@clazz.create(attributes))
12
+ end
13
+
14
+ def find(id)
15
+ detect { |x| x.id == id } || raise(EntityNotFound, "no element with id #{id}")
16
+ end
17
+
18
+ def add(entity)
19
+ _add_to_reverse_association_of(entity) if @reverse
20
+ _add(entity)
21
+ end
22
+
23
+ def remove(entity)
24
+ _remove_from_reverse_association_of(entity) if @reverse
25
+ _remove(entity)
26
+ end
27
+
28
+ private
29
+
30
+ def _add(entity)
31
+ self << entity
32
+ _store
33
+ entity
34
+ end
35
+
36
+ def _remove(entity)
37
+ delete_if { |x| x.key == entity.key }
38
+ _store
39
+ entity
40
+ end
41
+
42
+ def _add_to_reverse_association_of(entity)
43
+ if entity.send(@reverse).is_a? HasMany
44
+ entity.send(@reverse).send(:_add, @this)
45
+ else
46
+ entity.send("_#{@reverse}=", @this)
47
+ end
48
+ end
49
+
50
+ def _remove_from_reverse_association_of(entity)
51
+ if entity.send(@reverse).is_a? HasMany
52
+ entity.send(@reverse).send(:_remove, @this)
53
+ else
54
+ entity.send("_#{@reverse}=", nil)
55
+ end
56
+ end
57
+
58
+ def _store
59
+ Remodel.redis.hset(Remodel.context, @key, JSON.generate(self.map(&:key)))
60
+ end
61
+
62
+ def _fetch(clazz, key)
63
+ keys = JSON.parse(Remodel.redis.hget(Remodel.context, key) || '[]')
64
+ values = keys.empty? ? [] : Remodel.redis.hmget(Remodel.context, *keys)
65
+ keys.zip(values).map do |key, json|
66
+ clazz.restore(key, json) if json
67
+ end.compact
68
+ end
69
+ end
70
+
71
+ end
@@ -0,0 +1,29 @@
1
+ module Remodel
2
+
3
+ # A mapper converts a given value into a native JSON value &mdash;
4
+ # *nil*, *true*, *false*, *Number*, *String*, *Hash*, *Array* &mdash;
5
+ # via `pack`, and back again via `unpack`.
6
+ #
7
+ # Without any arguments, `Mapper.new` returns the identity mapper, which
8
+ # maps every value to itself. If `clazz` is set, the mapper rejects any
9
+ # value which is not of the given type.
10
+ class Mapper
11
+ def initialize(clazz = nil, pack_method = nil, unpack_method = nil)
12
+ @clazz = clazz
13
+ @pack_method = pack_method
14
+ @unpack_method = unpack_method
15
+ end
16
+
17
+ def pack(value)
18
+ return nil if value.nil?
19
+ raise(InvalidType, "#{value.inspect} is not a #{@clazz}") if @clazz && !value.is_a?(@clazz)
20
+ @pack_method ? value.send(@pack_method) : value
21
+ end
22
+
23
+ def unpack(value)
24
+ return nil if value.nil?
25
+ @unpack_method ? @clazz.send(@unpack_method, value) : value
26
+ end
27
+ end
28
+
29
+ end
data/lib/remodel.rb ADDED
@@ -0,0 +1,77 @@
1
+ require 'rubygems'
2
+ require 'redis'
3
+ require 'date'
4
+
5
+ # If available, use the superfast YAJL lib to parse JSON.
6
+ begin
7
+ require 'yajl/json_gem'
8
+ rescue LoadError
9
+ require 'json'
10
+ end
11
+
12
+ # Define `Boolean` -- the missing superclass of `true` and `false`.
13
+ module Boolean; end
14
+ true.extend(Boolean)
15
+ false.extend(Boolean)
16
+
17
+ # Find the `Class` object for a given class name, which can be
18
+ # a `String` or a `Symbol` (or a `Class`).
19
+ def Class.[](clazz)
20
+ return clazz if clazz.nil? or clazz.is_a?(Class)
21
+ clazz.to_s.split('::').inject(Kernel) { |mod, name| mod.const_get(name) }
22
+ end
23
+
24
+ require File.join(File.dirname(__FILE__), 'remodel', 'mapper')
25
+ require File.join(File.dirname(__FILE__), 'remodel', 'has_many')
26
+ require File.join(File.dirname(__FILE__), 'remodel', 'entity')
27
+
28
+
29
+ module Remodel
30
+
31
+ # Custom errors
32
+ class Error < ::StandardError; end
33
+ class EntityNotFound < Error; end
34
+ class EntityNotSaved < Error; end
35
+ class InvalidKeyPrefix < Error; end
36
+ class InvalidType < Error; end
37
+ class MissingContext < Error; end
38
+
39
+ # By default, the redis server is expected to listen at `localhost:6379`.
40
+ # Otherwise you will have to set `Remodel.redis` to a suitably initialized
41
+ # redis client.
42
+ def self.redis
43
+ @redis ||= Redis.new
44
+ end
45
+
46
+ def self.redis=(redis)
47
+ @redis = redis
48
+ end
49
+
50
+ def self.context
51
+ Thread.current[:remodel_context] || raise(MissingContext)
52
+ end
53
+
54
+ def self.context=(context)
55
+ Thread.current[:remodel_context] = context
56
+ end
57
+
58
+ # Returns the mapper defined for a given class, or the identity mapper.
59
+ def self.mapper_for(clazz)
60
+ mapper_by_class[Class[clazz]]
61
+ end
62
+
63
+ # Define some mappers for common types.
64
+ def self.mapper_by_class
65
+ @mapper_by_class ||= Hash.new(Mapper.new).merge(
66
+ Boolean => Mapper.new(Boolean),
67
+ String => Mapper.new(String),
68
+ Integer => Mapper.new(Integer),
69
+ Float => Mapper.new(Float),
70
+ Array => Mapper.new(Array),
71
+ Hash => Mapper.new(Hash),
72
+ Date => Mapper.new(Date, :to_s, :parse),
73
+ Time => Mapper.new(Time, :to_i, :at)
74
+ )
75
+ end
76
+
77
+ end
data/remodel-h.gemspec ADDED
@@ -0,0 +1,72 @@
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-h}
8
+ s.version = "0.1.4"
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-07-20}
13
+ s.default_executable = %q{redis-monitor.rb}
14
+ s.description = %q{persist your objects to redis hashes.}
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
+ "docs/docco.css",
29
+ "docs/remodel.html",
30
+ "example/book.rb",
31
+ "lib/remodel.rb",
32
+ "lib/remodel/entity.rb",
33
+ "lib/remodel/has_many.rb",
34
+ "lib/remodel/mapper.rb",
35
+ "remodel-h.gemspec",
36
+ "remodel.gemspec",
37
+ "test/helper.rb",
38
+ "test/test_entity.rb",
39
+ "test/test_many_to_many.rb",
40
+ "test/test_many_to_one.rb",
41
+ "test/test_mappers.rb",
42
+ "test/test_monkeypatches.rb",
43
+ "test/test_one_to_many.rb",
44
+ "test/test_one_to_one.rb"
45
+ ]
46
+ s.homepage = %q{http://github.com/tlossen/remodel}
47
+ s.rdoc_options = ["--charset=UTF-8"]
48
+ s.require_paths = ["lib"]
49
+ s.rubygems_version = %q{1.3.5}
50
+ s.summary = %q{remodel variant which uses hashes}
51
+ s.test_files = [
52
+ "test/helper.rb",
53
+ "test/test_entity.rb",
54
+ "test/test_many_to_many.rb",
55
+ "test/test_many_to_one.rb",
56
+ "test/test_mappers.rb",
57
+ "test/test_monkeypatches.rb",
58
+ "test/test_one_to_many.rb",
59
+ "test/test_one_to_one.rb"
60
+ ]
61
+
62
+ if s.respond_to? :specification_version then
63
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
64
+ s.specification_version = 3
65
+
66
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
67
+ else
68
+ end
69
+ else
70
+ end
71
+ end
72
+
data/remodel.gemspec ADDED
@@ -0,0 +1,72 @@
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.4"
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-07-01}
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
+ "docs/docco.css",
29
+ "docs/remodel.html",
30
+ "example/book.rb",
31
+ "lib/remodel.rb",
32
+ "lib/remodel/entity.rb",
33
+ "lib/remodel/has_many.rb",
34
+ "lib/remodel/has_one.rb",
35
+ "lib/remodel/mapper.rb",
36
+ "remodel.gemspec",
37
+ "test/helper.rb",
38
+ "test/test_entity.rb",
39
+ "test/test_many_to_many.rb",
40
+ "test/test_many_to_one.rb",
41
+ "test/test_mappers.rb",
42
+ "test/test_monkeypatches.rb",
43
+ "test/test_one_to_many.rb",
44
+ "test/test_one_to_one.rb"
45
+ ]
46
+ s.homepage = %q{http://github.com/tlossen/remodel}
47
+ s.rdoc_options = ["--charset=UTF-8"]
48
+ s.require_paths = ["lib"]
49
+ s.rubygems_version = %q{1.3.5}
50
+ s.summary = %q{a minimal ORM (object-redis-mapper)}
51
+ s.test_files = [
52
+ "test/helper.rb",
53
+ "test/test_entity.rb",
54
+ "test/test_many_to_many.rb",
55
+ "test/test_many_to_one.rb",
56
+ "test/test_mappers.rb",
57
+ "test/test_monkeypatches.rb",
58
+ "test/test_one_to_many.rb",
59
+ "test/test_one_to_one.rb"
60
+ ]
61
+
62
+ if s.respond_to? :specification_version then
63
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
64
+ s.specification_version = 3
65
+
66
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
67
+ else
68
+ end
69
+ else
70
+ end
71
+ end
72
+
data/test/helper.rb ADDED
@@ -0,0 +1,19 @@
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
+ def context
14
+ Remodel.context
15
+ end
16
+
17
+ end
18
+
19
+ Remodel.context = 'foo'
@@ -0,0 +1,313 @@
1
+ require 'helper'
2
+
3
+ class Foo < Remodel::Entity
4
+ property :x
5
+ property :y
6
+ end
7
+
8
+ class Bar < Remodel::Entity
9
+ property :d, :default => 123
10
+ end
11
+
12
+ class TestEntity < Test::Unit::TestCase
13
+
14
+ context "new" do
15
+ should "set properties" do
16
+ foo = Foo.new :x => 1, :y => 2
17
+ assert 1, foo.x
18
+ assert 2, foo.y
19
+ end
20
+
21
+ should "ignore undefined properties" do
22
+ foo = Foo.new :z => 3
23
+ assert foo.instance_eval { !@attributes.key? :z }
24
+ end
25
+
26
+ should "not set the key" do
27
+ foo = Foo.new :x => 23
28
+ assert_equal nil, foo.key
29
+ end
30
+
31
+ should "not set the id" do
32
+ foo = Foo.new :x => 23
33
+ assert_equal nil, foo.id
34
+ end
35
+
36
+ should "use default values for missing properties" do
37
+ bar = Bar.new
38
+ assert_equal 123, bar.d
39
+ end
40
+
41
+ should "not use default values for given properties" do
42
+ bar = Bar.new :d => 'cool'
43
+ assert_equal 'cool', bar.d
44
+ end
45
+ end
46
+
47
+ context "create" do
48
+ setup do
49
+ redis.flushdb
50
+ end
51
+
52
+ should "work without attributes" do
53
+ foo = Foo.create
54
+ assert foo.is_a?(Foo)
55
+ end
56
+
57
+ should "give the entity a key based on the class name" do
58
+ assert_equal 'f:1', Foo.create.key
59
+ assert_equal 'b:1', Bar.create.key
60
+ assert_equal 'b:2', Bar.create.key
61
+ end
62
+
63
+ should "give the entity an id which is unique per entity class" do
64
+ assert_equal 1, Foo.create.id
65
+ assert_equal 1, Bar.create.id
66
+ assert_equal 2, Bar.create.id
67
+ end
68
+
69
+ should "store the entity under its key" do
70
+ foo = Foo.create :x => 'hello', :y => false
71
+ assert redis.hexists(context, foo.key)
72
+ end
73
+
74
+ should "store all properties" do
75
+ foo = Foo.create :x => 'hello', :y => false
76
+ foo.reload
77
+ assert_equal 'hello', foo.x
78
+ assert_equal false, foo.y
79
+ end
80
+
81
+ should "not store the key as a property" do
82
+ foo = Foo.create :x => 'hello', :y => false
83
+ assert !(/f:1/ =~ redis.hget(context, foo.key))
84
+ end
85
+
86
+ should "use default values for missing properties" do
87
+ bar = Bar.create
88
+ assert_equal 123, bar.d
89
+ end
90
+
91
+ should "not use default values for given properties" do
92
+ bar = Bar.create :d => 'cool'
93
+ assert_equal 'cool', bar.d
94
+ end
95
+ end
96
+
97
+ context "save" do
98
+ setup do
99
+ redis.flushdb
100
+ end
101
+
102
+ should "give the entity a key, if necessary" do
103
+ foo = Foo.new.save
104
+ assert foo.key
105
+ end
106
+
107
+ should "store the entity under its key" do
108
+ foo = Foo.new :x => 'hello', :y => false
109
+ foo.save
110
+ assert redis.hexists(context, foo.key)
111
+ end
112
+
113
+ should "store all properties" do
114
+ foo = Foo.new :x => 'hello', :y => false
115
+ foo.save
116
+ foo.reload
117
+ assert_equal 'hello', foo.x
118
+ assert_equal false, foo.y
119
+ end
120
+ end
121
+
122
+ context "reload" do
123
+ setup do
124
+ @foo = Foo.create :x => 'hello', :y => true
125
+ end
126
+
127
+ should "reload all properties" do
128
+ redis.hset context, @foo.key, %q({"x":23,"y":"adios"})
129
+ @foo.reload
130
+ assert_equal 23, @foo.x
131
+ assert_equal 'adios', @foo.y
132
+ end
133
+
134
+ should "keep the key" do
135
+ key = @foo.key
136
+ @foo.reload
137
+ assert_equal key, @foo.key
138
+ end
139
+
140
+ should "stay the same object" do
141
+ id = @foo.object_id
142
+ @foo.reload
143
+ assert_equal id, @foo.object_id
144
+ end
145
+
146
+ should "raise EntityNotFound if the entity does not exist any more" do
147
+ redis.hdel context, @foo.key
148
+ assert_raise(Remodel::EntityNotFound) { @foo.reload }
149
+ end
150
+
151
+ should "raise EntityNotSaved if the entity was never saved" do
152
+ assert_raise(Remodel::EntityNotSaved) { Foo.new.reload }
153
+ end
154
+ end
155
+
156
+ context "update" do
157
+ setup do
158
+ redis.flushdb
159
+ @foo = Foo.create :x => 'Tim', :y => true
160
+ end
161
+
162
+ should "set the given properties" do
163
+ @foo.update(:x => 12, :y => 'Jan')
164
+ assert_equal 12, @foo.x
165
+ assert_equal 'Jan', @foo.y
166
+ end
167
+
168
+ should "save the entity" do
169
+ @foo.update(:x => 12, :y => 'Jan')
170
+ @foo.reload
171
+ assert_equal 12, @foo.x
172
+ assert_equal 'Jan', @foo.y
173
+ end
174
+ end
175
+
176
+ context "delete" do
177
+ setup do
178
+ redis.flushdb
179
+ @foo = Foo.create :x => 'Tim', :y => true
180
+ end
181
+
182
+ should "delete the given entity" do
183
+ @foo.delete
184
+ assert_nil redis.hget(context, @foo.key)
185
+ end
186
+
187
+ should "ensure that the entity is persistent" do
188
+ assert_raise(Remodel::EntityNotSaved) { Foo.new.delete }
189
+ end
190
+ end
191
+
192
+ context "to_json" do
193
+ should "serialize to json" do
194
+ foo = Foo.new :x => 42, :y => true
195
+ assert_match /"x":42/, foo.to_json
196
+ assert_match /"y":true/, foo.to_json
197
+ end
198
+ end
199
+
200
+ context "as_json" do
201
+ should "serialize into a hash" do
202
+ foo = Foo.create :x => 42, :y => true
203
+ expected = { :id => foo.id, :x => 42, :y => true }
204
+ assert_equal expected, foo.as_json
205
+ end
206
+ end
207
+
208
+ context "#set_key_prefix" do
209
+ should "use the given key prefix" do
210
+ class Custom < Remodel::Entity; set_key_prefix 'my'; end
211
+ assert_match /^my:\d+$/, Custom.create.key
212
+ end
213
+
214
+ should "ensure that the prefix is letters only" do
215
+ assert_raise(Remodel::InvalidKeyPrefix) do
216
+ class InvalidPrefix < Remodel::Entity; set_key_prefix '666'; end
217
+ end
218
+ end
219
+ end
220
+
221
+ context "#find" do
222
+ setup do
223
+ redis.flushdb
224
+ @foo = Foo.create :x => 'hello', :y => 123
225
+ Foo.create :x => 'hallo', :y => 124
226
+ end
227
+
228
+ should "find and load an entity by key" do
229
+ foo = Foo.find(@foo.key)
230
+ assert_equal foo.x, @foo.x
231
+ assert_equal foo.y, @foo.y
232
+ end
233
+
234
+ should "find and load an entity by id" do
235
+ foo = Foo.find(@foo.id)
236
+ assert_equal foo.x, @foo.x
237
+ assert_equal foo.y, @foo.y
238
+ end
239
+
240
+ should "reject a key which does not exist" do
241
+ assert_raise(Remodel::EntityNotFound) { Foo.find('x:66') }
242
+ end
243
+
244
+ should "reject an id which does not exist" do
245
+ assert_raise(Remodel::EntityNotFound) { Foo.find(66) }
246
+ end
247
+ end
248
+
249
+ context "properties" do
250
+ should "have property x" do
251
+ foo = Foo.new
252
+ foo.x = 23
253
+ assert_equal 23, foo.x
254
+ foo.x += 10
255
+ assert_equal 33, foo.x
256
+ end
257
+
258
+ should "not have property z" do
259
+ foo = Foo.new
260
+ assert_raise(NoMethodError) { foo.z }
261
+ assert_raise(NoMethodError) { foo.z = 42 }
262
+ end
263
+
264
+ context "types" do
265
+ should "work with nil" do
266
+ foo = Foo.create :x => nil
267
+ assert_equal nil, foo.reload.x
268
+ end
269
+
270
+ should "work with booleans" do
271
+ foo = Foo.create :x => false
272
+ assert_equal false, foo.reload.x
273
+ end
274
+
275
+ should "work with integers" do
276
+ foo = Foo.create :x => -42
277
+ assert_equal -42, foo.reload.x
278
+ end
279
+
280
+ should "work with floats" do
281
+ foo = Foo.create :x => 3.141
282
+ assert_equal 3.141, foo.reload.x
283
+ end
284
+
285
+ should "work with strings" do
286
+ foo = Foo.create :x => 'hello'
287
+ assert_equal 'hello', foo.reload.x
288
+ end
289
+
290
+ should "work with lists" do
291
+ foo = Foo.create :x => [1, 2, 3]
292
+ assert_equal [1, 2, 3], foo.reload.x
293
+ end
294
+
295
+ should "work with hashes" do
296
+ hash = { 'a' => 17, 'b' => 'test' }
297
+ foo = Foo.create :x => hash
298
+ assert_equal hash, foo.reload.x
299
+ end
300
+ end
301
+ end
302
+
303
+ context "#restore" do
304
+ should "restore an entity from json" do
305
+ before = Foo.create :x => 42, :y => true
306
+ after = Foo.restore(before.key, before.to_json)
307
+ assert_equal before.key, after.key
308
+ assert_equal before.x, after.x
309
+ assert_equal before.y, after.y
310
+ end
311
+ end
312
+
313
+ end