remodel 0.1.4 → 0.3.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/README.md +19 -10
- data/Rakefile +2 -2
- data/VERSION +1 -1
- data/example/book.rb +1 -0
- data/lib/remodel/entity.rb +57 -45
- data/lib/remodel/has_many.rb +12 -8
- data/lib/remodel.rb +2 -1
- data/test/helper.rb +1 -1
- data/test/test_entity.rb +47 -96
- data/test/test_entity_defaults.rb +55 -0
- data/test/test_entity_delete.rb +61 -0
- data/test/test_many_to_many.rb +7 -7
- data/test/test_many_to_one.rb +28 -15
- data/test/test_mappers.rb +11 -11
- data/test/test_one_to_many.rb +22 -22
- data/test/test_one_to_one.rb +4 -4
- metadata +22 -12
- data/.gitignore +0 -2
- data/lib/remodel/has_one.rb +0 -57
- data/remodel.gemspec +0 -72
data/README.md
CHANGED
@@ -5,6 +5,10 @@ use [redis](http://github.com/antirez/redis) instead of mysql to store your appl
|
|
5
5
|
remodel (= redis model) is an ActiveRecord-like mapping layer which offers familiar syntax
|
6
6
|
like `has_many`, `has_one` etc. to build your domain model in ruby.
|
7
7
|
|
8
|
+
entities are serialized to json and stored as fields in a redis hash. using different hashes
|
9
|
+
(called 'contexts' in remodel), you can easily separate data belonging to multiple users,
|
10
|
+
for example.
|
11
|
+
|
8
12
|
|
9
13
|
## why redis?
|
10
14
|
|
@@ -23,7 +27,7 @@ persistence to disk. for example, on my macbook (2 ghz):
|
|
23
27
|
|
24
28
|
## how to get started
|
25
29
|
|
26
|
-
1. install [redis](http://github.com/antirez/redis) and
|
30
|
+
1. install [redis](http://github.com/antirez/redis) and the
|
27
31
|
[redis-rb](http://github.com/ezmobius/redis-rb) ruby client:
|
28
32
|
|
29
33
|
$ brew install redis
|
@@ -49,6 +53,7 @@ define your domain model [like this](http://github.com/tlossen/remodel/blob/mast
|
|
49
53
|
has_many :chapters, :class => 'Chapter', :reverse => :book
|
50
54
|
property :title, :class => 'String'
|
51
55
|
property :year, :class => 'Integer'
|
56
|
+
property :author, :class => 'String', :default => '(anonymous)'
|
52
57
|
end
|
53
58
|
|
54
59
|
class Chapter < Remodel::Entity
|
@@ -58,15 +63,20 @@ define your domain model [like this](http://github.com/tlossen/remodel/blob/mast
|
|
58
63
|
|
59
64
|
now you can do:
|
60
65
|
|
61
|
-
>> require 'example/book'
|
66
|
+
>> require './example/book'
|
62
67
|
=> true
|
63
|
-
>> book = Book.create :title => 'Moby Dick', :year => 1851
|
64
|
-
=> #<Book(
|
68
|
+
>> book = Book.create 'shelf', :title => 'Moby Dick', :year => 1851
|
69
|
+
=> #<Book(shelf, 1) title: "Moby Dick", year: 1851, author: "(anonymous)">
|
65
70
|
>> chapter = book.chapters.create :title => 'Ishmael'
|
66
|
-
=> #<Chapter(
|
71
|
+
=> #<Chapter(shelf, 1) title: "Ishmael">
|
67
72
|
>> chapter.book
|
68
|
-
=> #<Book(
|
73
|
+
=> #<Book(shelf, 1) title: "Moby Dick", year: 1851, author: "(anonymous)">
|
74
|
+
|
75
|
+
all entities have been created in the redis hash 'shelf' we have used as context:
|
69
76
|
|
77
|
+
>> Remodel.redis.hgetall 'shelf'
|
78
|
+
=> {"b"=>"1", "b1"=>"{\"title\":\"Moby Dick\",\"year\":1851}", "c"=>"1",
|
79
|
+
"c1"=>"{\"title\":\"Ishmael\"}", "c1_book"=>"b1", "b1_chapters"=>"[\"c1\"]"}
|
70
80
|
|
71
81
|
## inspired by
|
72
82
|
|
@@ -81,16 +91,15 @@ somewhat similar, but instead of serializing to json, stores each attribute unde
|
|
81
91
|
## todo
|
82
92
|
|
83
93
|
* better docs
|
84
|
-
* `find_by`
|
85
94
|
* make serializer (json, messagepack, marshal ...) configurable
|
86
|
-
* benchmarks
|
87
95
|
|
88
96
|
|
89
97
|
## status
|
90
98
|
|
91
|
-
|
99
|
+
it has some rough edges, but i have successfully been using remodel in production since summer 2010.
|
100
|
+
|
92
101
|
|
93
102
|
|
94
103
|
## license
|
95
104
|
|
96
|
-
[MIT](http://github.com/tlossen/remodel/raw/master/LICENSE)
|
105
|
+
[MIT](http://github.com/tlossen/remodel/raw/master/LICENSE)
|
data/Rakefile
CHANGED
@@ -5,8 +5,8 @@ begin
|
|
5
5
|
require 'jeweler'
|
6
6
|
Jeweler::Tasks.new do |gem|
|
7
7
|
gem.name = "remodel"
|
8
|
-
gem.summary = "
|
9
|
-
gem.description = "
|
8
|
+
gem.summary = "remodel variant which uses hashes"
|
9
|
+
gem.description = "persist your objects to redis hashes."
|
10
10
|
gem.email = "tim@lossen.de"
|
11
11
|
gem.homepage = "http://github.com/tlossen/remodel"
|
12
12
|
gem.authors = ["Tim Lossen"]
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.3.0
|
data/example/book.rb
CHANGED
@@ -4,6 +4,7 @@ class Book < Remodel::Entity
|
|
4
4
|
has_many :chapters, :class => 'Chapter', :reverse => :book
|
5
5
|
property :title, :class => 'String'
|
6
6
|
property :year, :class => 'Integer'
|
7
|
+
property :author, :class => 'String', :default => '(anonymous)'
|
7
8
|
end
|
8
9
|
|
9
10
|
class Chapter < Remodel::Entity
|
data/lib/remodel/entity.rb
CHANGED
@@ -2,24 +2,24 @@ module Remodel
|
|
2
2
|
|
3
3
|
# The superclass of all persistent remodel entities.
|
4
4
|
class Entity
|
5
|
-
attr_accessor :key
|
5
|
+
attr_accessor :context, :key
|
6
6
|
|
7
|
-
def initialize(attributes = {}, key = nil)
|
7
|
+
def initialize(context, attributes = {}, key = nil)
|
8
|
+
@context = context
|
8
9
|
@attributes = {}
|
9
10
|
@key = key
|
10
|
-
attributes = self.class.default_values.merge(attributes) if key.nil?
|
11
11
|
attributes.each do |name, value|
|
12
12
|
send("#{name}=", value) if respond_to? "#{name}="
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
16
|
def id
|
17
|
-
key && key.
|
17
|
+
key && key.match(/\d+/)[0].to_i
|
18
18
|
end
|
19
19
|
|
20
20
|
def save
|
21
|
-
@key =
|
22
|
-
Remodel.redis.
|
21
|
+
@key = next_key unless @key
|
22
|
+
Remodel.redis.hset(@context, @key, to_json)
|
23
23
|
self
|
24
24
|
end
|
25
25
|
|
@@ -30,20 +30,25 @@ module Remodel
|
|
30
30
|
|
31
31
|
def reload
|
32
32
|
raise EntityNotSaved unless @key
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
attributes = self.class.parse(self.class.fetch(@context, @key))
|
34
|
+
initialize(@context, attributes, @key)
|
35
|
+
self.class.associations.each do |name|
|
36
|
+
var = "@#{name}".to_sym
|
37
|
+
remove_instance_variable(var) if instance_variable_defined? var
|
36
38
|
end
|
37
39
|
self
|
38
40
|
end
|
39
41
|
|
40
42
|
def delete
|
41
43
|
raise EntityNotSaved unless @key
|
42
|
-
Remodel.redis.
|
44
|
+
Remodel.redis.hdel(@context, @key)
|
45
|
+
self.class.associations.each do |name|
|
46
|
+
Remodel.redis.hdel(@context, "#{@key}_#{name}")
|
47
|
+
end
|
43
48
|
end
|
44
49
|
|
45
50
|
def as_json
|
46
|
-
{ :id => id }.merge(
|
51
|
+
{ :id => id }.merge(attributes)
|
47
52
|
end
|
48
53
|
|
49
54
|
def to_json
|
@@ -51,29 +56,21 @@ module Remodel
|
|
51
56
|
end
|
52
57
|
|
53
58
|
def inspect
|
54
|
-
properties =
|
55
|
-
"\#<#{self.class.name}(#{id}) #{properties}>"
|
56
|
-
end
|
57
|
-
|
58
|
-
def self.create(attributes = {})
|
59
|
-
new(attributes).save
|
59
|
+
properties = attributes.map { |name, value| "#{name}: #{value.inspect}" }.join(', ')
|
60
|
+
"\#<#{self.class.name}(#{context}, #{id}) #{properties}>"
|
60
61
|
end
|
61
62
|
|
62
|
-
def self.
|
63
|
-
|
64
|
-
restore(key, fetch(key))
|
63
|
+
def self.create(context, attributes = {})
|
64
|
+
new(context, attributes).save
|
65
65
|
end
|
66
66
|
|
67
|
-
def self.
|
68
|
-
|
69
|
-
|
70
|
-
keys.zip(values).map do |key, json|
|
71
|
-
restore(key, json) if json
|
72
|
-
end.compact
|
67
|
+
def self.find(context, key)
|
68
|
+
key = "#{key_prefix}#{key}" if key.kind_of? Integer
|
69
|
+
restore(context, key, fetch(context, key))
|
73
70
|
end
|
74
71
|
|
75
|
-
def self.restore(key, json)
|
76
|
-
new(parse(json), key)
|
72
|
+
def self.restore(context, key, json)
|
73
|
+
new(context, parse(json), key)
|
77
74
|
end
|
78
75
|
|
79
76
|
protected # --- DSL for subclasses ---
|
@@ -86,34 +83,37 @@ module Remodel
|
|
86
83
|
def self.property(name, options = {})
|
87
84
|
name = name.to_sym
|
88
85
|
mapper[name] = Remodel.mapper_for(options[:class])
|
89
|
-
|
90
|
-
define_method(name) { @attributes[name] }
|
86
|
+
default_value = options[:default]
|
87
|
+
define_method(name) { @attributes[name].nil? ? self.class.copy_of(default_value) : @attributes[name] }
|
91
88
|
define_method("#{name}=") { |value| @attributes[name] = value }
|
92
89
|
end
|
93
90
|
|
94
91
|
def self.has_many(name, options)
|
95
|
-
|
92
|
+
associations.push(name)
|
93
|
+
var = "@#{name}".to_sym
|
96
94
|
|
97
95
|
define_method(name) do
|
98
96
|
if instance_variable_defined? var
|
99
97
|
instance_variable_get(var)
|
100
98
|
else
|
101
99
|
clazz = Class[options[:class]]
|
102
|
-
instance_variable_set(var, HasMany.new(self, clazz, "#{key}
|
100
|
+
instance_variable_set(var, HasMany.new(self, clazz, "#{key}_#{name}", options[:reverse]))
|
103
101
|
end
|
104
102
|
end
|
105
103
|
end
|
106
104
|
|
107
105
|
def self.has_one(name, options)
|
108
|
-
|
106
|
+
associations.push(name)
|
107
|
+
var = "@#{name}".to_sym
|
109
108
|
|
110
109
|
define_method(name) do
|
111
110
|
if instance_variable_defined? var
|
112
111
|
instance_variable_get(var)
|
113
112
|
else
|
114
113
|
clazz = Class[options[:class]]
|
115
|
-
value_key = Remodel.redis.
|
116
|
-
|
114
|
+
value_key = Remodel.redis.hget(self.context, "#{key}_#{name}")
|
115
|
+
value = value_key && clazz.find(self.context, value_key) rescue nil
|
116
|
+
instance_variable_set(var, value)
|
117
117
|
end
|
118
118
|
end
|
119
119
|
|
@@ -125,10 +125,10 @@ module Remodel
|
|
125
125
|
define_method("_#{name}=") do |value|
|
126
126
|
if value
|
127
127
|
instance_variable_set(var, value)
|
128
|
-
Remodel.redis.
|
128
|
+
Remodel.redis.hset(self.context, "#{key}_#{name}", value.key)
|
129
129
|
else
|
130
130
|
remove_instance_variable(var) if instance_variable_defined? var
|
131
|
-
Remodel.redis.
|
131
|
+
Remodel.redis.hdel(self.context, "#{key}_#{name}")
|
132
132
|
end
|
133
133
|
end; private "_#{name}="
|
134
134
|
|
@@ -156,14 +156,22 @@ module Remodel
|
|
156
156
|
|
157
157
|
private # --- Helper methods ---
|
158
158
|
|
159
|
-
def
|
160
|
-
|
159
|
+
def attributes
|
160
|
+
result = {}
|
161
|
+
self.class.mapper.keys.each do |name|
|
162
|
+
result[name] = send(name)
|
163
|
+
end
|
164
|
+
result
|
165
|
+
end
|
166
|
+
|
167
|
+
def self.fetch(context, key)
|
168
|
+
Remodel.redis.hget(context, key) || raise(EntityNotFound, "no #{name} with key #{key} in context #{context}")
|
161
169
|
end
|
162
170
|
|
163
171
|
# Each entity has its own sequence to generate unique ids.
|
164
|
-
def
|
165
|
-
id = Remodel.redis.
|
166
|
-
"#{key_prefix}
|
172
|
+
def next_key
|
173
|
+
id = Remodel.redis.hincrby(@context, "#{self.class.key_prefix}", 1)
|
174
|
+
"#{self.class.key_prefix}#{id}"
|
167
175
|
end
|
168
176
|
|
169
177
|
# Default key prefix is the first letter of the class name, in lowercase.
|
@@ -187,7 +195,7 @@ module Remodel
|
|
187
195
|
result = {}
|
188
196
|
attributes.each do |name, value|
|
189
197
|
name = name.to_sym
|
190
|
-
result[name] = mapper[name].unpack(value)
|
198
|
+
result[name] = mapper[name].unpack(value) if mapper[name]
|
191
199
|
end
|
192
200
|
result
|
193
201
|
end
|
@@ -197,8 +205,12 @@ module Remodel
|
|
197
205
|
@mapper ||= {}
|
198
206
|
end
|
199
207
|
|
200
|
-
def self.
|
201
|
-
@
|
208
|
+
def self.associations
|
209
|
+
@associations ||= []
|
210
|
+
end
|
211
|
+
|
212
|
+
def self.copy_of(value)
|
213
|
+
value.is_a?(Array) || value.is_a?(Hash) ? value.dup : value
|
202
214
|
end
|
203
215
|
|
204
216
|
end
|
data/lib/remodel/has_many.rb
CHANGED
@@ -3,12 +3,12 @@ module Remodel
|
|
3
3
|
# Represents the many-end of a many-to-one or many-to-many association.
|
4
4
|
class HasMany < Array
|
5
5
|
def initialize(this, clazz, key, reverse = nil)
|
6
|
-
super _fetch(clazz, key)
|
6
|
+
super _fetch(clazz, this.context, key)
|
7
7
|
@this, @clazz, @key, @reverse = this, clazz, key, reverse
|
8
8
|
end
|
9
9
|
|
10
10
|
def create(attributes = {})
|
11
|
-
add(@clazz.create(attributes))
|
11
|
+
add(@clazz.create(@this.context, attributes))
|
12
12
|
end
|
13
13
|
|
14
14
|
def find(id)
|
@@ -29,13 +29,13 @@ module Remodel
|
|
29
29
|
|
30
30
|
def _add(entity)
|
31
31
|
self << entity
|
32
|
-
|
32
|
+
_store
|
33
33
|
entity
|
34
34
|
end
|
35
35
|
|
36
36
|
def _remove(entity)
|
37
37
|
delete_if { |x| x.key == entity.key }
|
38
|
-
|
38
|
+
_store
|
39
39
|
entity
|
40
40
|
end
|
41
41
|
|
@@ -55,11 +55,15 @@ module Remodel
|
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
|
-
def
|
59
|
-
|
60
|
-
|
58
|
+
def _store
|
59
|
+
Remodel.redis.hset(@this.context, @key, JSON.generate(self.map(&:key)))
|
60
|
+
end
|
61
|
+
|
62
|
+
def _fetch(clazz, context, key)
|
63
|
+
keys = JSON.parse(Remodel.redis.hget(context, key) || '[]').uniq
|
64
|
+
values = keys.empty? ? [] : Remodel.redis.hmget(context, *keys)
|
61
65
|
keys.zip(values).map do |key, json|
|
62
|
-
clazz.restore(key, json) if json
|
66
|
+
clazz.restore(context, key, json) if json
|
63
67
|
end.compact
|
64
68
|
end
|
65
69
|
end
|
data/lib/remodel.rb
CHANGED
@@ -34,6 +34,7 @@ module Remodel
|
|
34
34
|
class EntityNotSaved < Error; end
|
35
35
|
class InvalidKeyPrefix < Error; end
|
36
36
|
class InvalidType < Error; end
|
37
|
+
class MissingContext < Error; end
|
37
38
|
|
38
39
|
# By default, the redis server is expected to listen at `localhost:6379`.
|
39
40
|
# Otherwise you will have to set `Remodel.redis` to a suitably initialized
|
@@ -45,7 +46,7 @@ module Remodel
|
|
45
46
|
def self.redis=(redis)
|
46
47
|
@redis = redis
|
47
48
|
end
|
48
|
-
|
49
|
+
|
49
50
|
# Returns the mapper defined for a given class, or the identity mapper.
|
50
51
|
def self.mapper_for(clazz)
|
51
52
|
mapper_by_class[Class[clazz]]
|
data/test/helper.rb
CHANGED