remodel 0.1.3 → 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.
- data/VERSION +1 -1
- data/lib/remodel/entity.rb +206 -0
- data/lib/remodel/has_many.rb +67 -0
- data/lib/remodel/has_one.rb +57 -0
- data/lib/remodel/mapper.rb +29 -0
- data/lib/remodel.rb +22 -325
- data/remodel.gemspec +6 -2
- data/test/test_one_to_many.rb +10 -1
- metadata +6 -2
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.4
|
@@ -0,0 +1,206 @@
|
|
1
|
+
module Remodel
|
2
|
+
|
3
|
+
# The superclass of all persistent remodel entities.
|
4
|
+
class Entity
|
5
|
+
attr_accessor :key
|
6
|
+
|
7
|
+
def initialize(attributes = {}, key = nil)
|
8
|
+
@attributes = {}
|
9
|
+
@key = key
|
10
|
+
attributes = self.class.default_values.merge(attributes) if key.nil?
|
11
|
+
attributes.each do |name, value|
|
12
|
+
send("#{name}=", value) if respond_to? "#{name}="
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def id
|
17
|
+
key && key.split(':').last.to_i
|
18
|
+
end
|
19
|
+
|
20
|
+
def save
|
21
|
+
@key = self.class.next_key unless @key
|
22
|
+
Remodel.redis.set(@key, to_json)
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def update(properties)
|
27
|
+
properties.each { |name, value| send("#{name}=", value) }
|
28
|
+
save
|
29
|
+
end
|
30
|
+
|
31
|
+
def reload
|
32
|
+
raise EntityNotSaved unless @key
|
33
|
+
initialize(self.class.parse(self.class.fetch(@key)), @key)
|
34
|
+
instance_variables.each do |var|
|
35
|
+
remove_instance_variable(var) if var =~ /^@association_/
|
36
|
+
end
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def delete
|
41
|
+
raise EntityNotSaved unless @key
|
42
|
+
Remodel.redis.del(@key)
|
43
|
+
end
|
44
|
+
|
45
|
+
def as_json
|
46
|
+
{ :id => id }.merge(@attributes)
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_json
|
50
|
+
JSON.generate(self.class.pack(@attributes))
|
51
|
+
end
|
52
|
+
|
53
|
+
def inspect
|
54
|
+
properties = @attributes.map { |name, value| "#{name}: #{value.inspect}" }.join(', ')
|
55
|
+
"\#<#{self.class.name}(#{id}) #{properties}>"
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.create(attributes = {})
|
59
|
+
new(attributes).save
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.find(key)
|
63
|
+
key = "#{key_prefix}:#{key}" if key.kind_of? Integer
|
64
|
+
restore(key, fetch(key))
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.all
|
68
|
+
keys = Remodel.redis.keys("#{key_prefix}:*").select { |k| k =~ /:[0-9]+$/ }
|
69
|
+
values = keys.empty? ? [] : Remodel.redis.mget(keys)
|
70
|
+
keys.zip(values).map do |key, json|
|
71
|
+
restore(key, json) if json
|
72
|
+
end.compact
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.restore(key, json)
|
76
|
+
new(parse(json), key)
|
77
|
+
end
|
78
|
+
|
79
|
+
protected # --- DSL for subclasses ---
|
80
|
+
|
81
|
+
def self.set_key_prefix(prefix)
|
82
|
+
raise(InvalidKeyPrefix, prefix) unless prefix =~ /^[a-z]+$/
|
83
|
+
@key_prefix = prefix
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.property(name, options = {})
|
87
|
+
name = name.to_sym
|
88
|
+
mapper[name] = Remodel.mapper_for(options[:class])
|
89
|
+
default_values[name] = options[:default] if options.has_key?(:default)
|
90
|
+
define_method(name) { @attributes[name] }
|
91
|
+
define_method("#{name}=") { |value| @attributes[name] = value }
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.has_many(name, options)
|
95
|
+
var = "@association_#{name}".to_sym
|
96
|
+
|
97
|
+
define_method(name) do
|
98
|
+
if instance_variable_defined? var
|
99
|
+
instance_variable_get(var)
|
100
|
+
else
|
101
|
+
clazz = Class[options[:class]]
|
102
|
+
instance_variable_set(var, HasMany.new(self, clazz, "#{key}:#{name}", options[:reverse]))
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.has_one(name, options)
|
108
|
+
var = "@association_#{name}".to_sym
|
109
|
+
|
110
|
+
define_method(name) do
|
111
|
+
if instance_variable_defined? var
|
112
|
+
instance_variable_get(var)
|
113
|
+
else
|
114
|
+
clazz = Class[options[:class]]
|
115
|
+
value_key = Remodel.redis.get("#{key}:#{name}")
|
116
|
+
instance_variable_set(var, clazz.find(value_key)) if value_key
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
define_method("#{name}=") do |value|
|
121
|
+
send("_reverse_association_of_#{name}=", value) if options[:reverse]
|
122
|
+
send("_#{name}=", value)
|
123
|
+
end
|
124
|
+
|
125
|
+
define_method("_#{name}=") do |value|
|
126
|
+
if value
|
127
|
+
instance_variable_set(var, value)
|
128
|
+
Remodel.redis.set("#{key}:#{name}", value.key)
|
129
|
+
else
|
130
|
+
remove_instance_variable(var) if instance_variable_defined? var
|
131
|
+
Remodel.redis.del("#{key}:#{name}")
|
132
|
+
end
|
133
|
+
end; private "_#{name}="
|
134
|
+
|
135
|
+
if options[:reverse]
|
136
|
+
define_method("_reverse_association_of_#{name}=") do |value|
|
137
|
+
if old_value = send(name)
|
138
|
+
association = old_value.send("#{options[:reverse]}")
|
139
|
+
if association.is_a? HasMany
|
140
|
+
association.send("_remove", self)
|
141
|
+
else
|
142
|
+
old_value.send("_#{options[:reverse]}=", nil)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
if value
|
146
|
+
association = value.send("#{options[:reverse]}")
|
147
|
+
if association.is_a? HasMany
|
148
|
+
association.send("_add", self)
|
149
|
+
else
|
150
|
+
value.send("_#{options[:reverse]}=", self)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end; private "_reverse_association_of_#{name}="
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
private # --- Helper methods ---
|
158
|
+
|
159
|
+
def self.fetch(key)
|
160
|
+
Remodel.redis.get(key) || raise(EntityNotFound, "no #{name} with key #{key}")
|
161
|
+
end
|
162
|
+
|
163
|
+
# Each entity has its own sequence to generate unique ids.
|
164
|
+
def self.next_key
|
165
|
+
id = Remodel.redis.incr("#{key_prefix}:seq")
|
166
|
+
"#{key_prefix}:#{id}"
|
167
|
+
end
|
168
|
+
|
169
|
+
# Default key prefix is the first letter of the class name, in lowercase.
|
170
|
+
def self.key_prefix
|
171
|
+
@key_prefix ||= name.split('::').last[0,1].downcase
|
172
|
+
end
|
173
|
+
|
174
|
+
def self.parse(json)
|
175
|
+
unpack(JSON.parse(json))
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.pack(attributes)
|
179
|
+
result = {}
|
180
|
+
attributes.each do |name, value|
|
181
|
+
result[name] = mapper[name].pack(value)
|
182
|
+
end
|
183
|
+
result
|
184
|
+
end
|
185
|
+
|
186
|
+
def self.unpack(attributes)
|
187
|
+
result = {}
|
188
|
+
attributes.each do |name, value|
|
189
|
+
name = name.to_sym
|
190
|
+
result[name] = mapper[name].unpack(value)
|
191
|
+
end
|
192
|
+
result
|
193
|
+
end
|
194
|
+
|
195
|
+
# Lazy init
|
196
|
+
def self.mapper
|
197
|
+
@mapper ||= {}
|
198
|
+
end
|
199
|
+
|
200
|
+
def self.default_values
|
201
|
+
@default_values ||= {}
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
|
206
|
+
end
|
@@ -0,0 +1,67 @@
|
|
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
|
+
Remodel.redis.rpush(@key, entity.key)
|
33
|
+
entity
|
34
|
+
end
|
35
|
+
|
36
|
+
def _remove(entity)
|
37
|
+
delete_if { |x| x.key == entity.key }
|
38
|
+
Remodel.redis.lrem(@key, 0, entity.key)
|
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 _fetch(clazz, key)
|
59
|
+
keys = Remodel.redis.lrange(key, 0, -1)
|
60
|
+
values = keys.empty? ? [] : Remodel.redis.mget(keys)
|
61
|
+
keys.zip(values).map do |key, json|
|
62
|
+
clazz.restore(key, json) if json
|
63
|
+
end.compact
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Remodel
|
2
|
+
|
3
|
+
# Represents the one-end of a many-to-one or one-to-one association.
|
4
|
+
class HasOne
|
5
|
+
def initialize(this, clazz, key, reverse = nil)
|
6
|
+
@this, @clazz, @key, @reverse = this, clazz, key, reverse
|
7
|
+
@value = Remodel.redis.get(@key)
|
8
|
+
end
|
9
|
+
|
10
|
+
def value
|
11
|
+
@value
|
12
|
+
end
|
13
|
+
|
14
|
+
def add(entity)
|
15
|
+
_add_to_reverse_association_of(entity) if @reverse
|
16
|
+
_add(entity)
|
17
|
+
end
|
18
|
+
|
19
|
+
def remove(entity)
|
20
|
+
_remove_from_reverse_association_of(entity) if @reverse
|
21
|
+
_remove(entity)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def _add(entity)
|
27
|
+
@value = entity
|
28
|
+
if entity.nil?
|
29
|
+
Remodel.redis.del(@key)
|
30
|
+
else
|
31
|
+
Remodel.redis.set(@key, entity.key)
|
32
|
+
end
|
33
|
+
entity
|
34
|
+
end
|
35
|
+
|
36
|
+
def _remove(entity)
|
37
|
+
_add(nil)
|
38
|
+
end
|
39
|
+
|
40
|
+
def _add_to_reverse_association_of(entity)
|
41
|
+
if entity.send(@reverse).is_a? HasMany
|
42
|
+
entity.send(@reverse).send(:_add, @this)
|
43
|
+
else
|
44
|
+
entity.send("_#{@reverse}").send., @this)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def _remove_from_reverse_association_of(entity)
|
49
|
+
if entity.send(@reverse).is_a? HasMany
|
50
|
+
entity.send(@reverse).send(:_remove, @this)
|
51
|
+
else
|
52
|
+
entity.send("_#{@reverse}=", nil)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Remodel
|
2
|
+
|
3
|
+
# A mapper converts a given value into a native JSON value —
|
4
|
+
# *nil*, *true*, *false*, *Number*, *String*, *Hash*, *Array* —
|
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
CHANGED
@@ -2,42 +2,31 @@ require 'rubygems'
|
|
2
2
|
require 'redis'
|
3
3
|
require 'date'
|
4
4
|
|
5
|
-
#
|
6
|
-
#
|
7
|
-
# [yajl]: http://github.com/brianmario/yajl-ruby
|
8
|
-
# [json]: http://json.org/
|
5
|
+
# If available, use the superfast YAJL lib to parse JSON.
|
9
6
|
begin
|
10
7
|
require 'yajl/json_gem'
|
11
8
|
rescue LoadError
|
12
9
|
require 'json'
|
13
10
|
end
|
14
11
|
|
15
|
-
|
16
|
-
|
17
|
-
# Define `Boolean` as the superclass of `true` and `false`.
|
12
|
+
# Define `Boolean` -- the missing superclass of `true` and `false`.
|
18
13
|
module Boolean; end
|
19
14
|
true.extend(Boolean)
|
20
15
|
false.extend(Boolean)
|
21
16
|
|
22
|
-
# Find the `Class` object for a given class name, which can be
|
17
|
+
# Find the `Class` object for a given class name, which can be
|
18
|
+
# a `String` or a `Symbol` (or a `Class`).
|
23
19
|
def Class.[](clazz)
|
24
20
|
return clazz if clazz.nil? or clazz.is_a?(Class)
|
25
21
|
clazz.to_s.split('::').inject(Kernel) { |mod, name| mod.const_get(name) }
|
26
22
|
end
|
27
23
|
|
28
|
-
|
29
|
-
|
30
|
-
|
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')
|
31
27
|
|
32
|
-
# By default, we expect to find the redis server on `localhost:6379` —
|
33
|
-
# otherwise you will have to set `Remodel.redis` to a suitably initialized redis client.
|
34
|
-
def self.redis
|
35
|
-
@redis ||= Redis.new
|
36
|
-
end
|
37
28
|
|
38
|
-
|
39
|
-
@redis = redis
|
40
|
-
end
|
29
|
+
module Remodel
|
41
30
|
|
42
31
|
# Custom errors
|
43
32
|
class Error < ::StandardError; end
|
@@ -46,35 +35,23 @@ module Remodel
|
|
46
35
|
class InvalidKeyPrefix < Error; end
|
47
36
|
class InvalidType < Error; end
|
48
37
|
|
49
|
-
|
50
|
-
|
51
|
-
#
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
# Without any arguments, `Mapper.new` returns the identity mapper, which maps every value into itself.
|
56
|
-
# If `clazz` is set, the mapper rejects any value which is not of the given type.
|
57
|
-
class Mapper
|
58
|
-
def initialize(clazz = nil, pack_method = nil, unpack_method = nil)
|
59
|
-
@clazz = clazz
|
60
|
-
@pack_method = pack_method
|
61
|
-
@unpack_method = unpack_method
|
62
|
-
end
|
38
|
+
# By default, the redis server is expected to listen at `localhost:6379`.
|
39
|
+
# Otherwise you will have to set `Remodel.redis` to a suitably initialized
|
40
|
+
# redis client.
|
41
|
+
def self.redis
|
42
|
+
@redis ||= Redis.new
|
43
|
+
end
|
63
44
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
@pack_method ? value.send(@pack_method) : value
|
68
|
-
end
|
45
|
+
def self.redis=(redis)
|
46
|
+
@redis = redis
|
47
|
+
end
|
69
48
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
end
|
49
|
+
# Returns the mapper defined for a given class, or the identity mapper.
|
50
|
+
def self.mapper_for(clazz)
|
51
|
+
mapper_by_class[Class[clazz]]
|
74
52
|
end
|
75
53
|
|
76
|
-
#
|
77
|
-
# If no mapper is defined for a given class, the identity mapper is used.
|
54
|
+
# Define some mappers for common types.
|
78
55
|
def self.mapper_by_class
|
79
56
|
@mapper_by_class ||= Hash.new(Mapper.new).merge(
|
80
57
|
Boolean => Mapper.new(Boolean),
|
@@ -88,284 +65,4 @@ module Remodel
|
|
88
65
|
)
|
89
66
|
end
|
90
67
|
|
91
|
-
|
92
|
-
mapper_by_class[Class[clazz]]
|
93
|
-
end
|
94
|
-
|
95
|
-
#### HasMany
|
96
|
-
|
97
|
-
# Represents the many-end of a many-to-one or many-to-many association.
|
98
|
-
class HasMany < Array
|
99
|
-
def initialize(this, clazz, key, reverse = nil)
|
100
|
-
super _fetch(clazz, key)
|
101
|
-
@this, @clazz, @key, @reverse = this, clazz, key, reverse
|
102
|
-
end
|
103
|
-
|
104
|
-
def create(attributes = {})
|
105
|
-
add(@clazz.create(attributes))
|
106
|
-
end
|
107
|
-
|
108
|
-
def find(id)
|
109
|
-
detect { |x| x.id == id } || raise(EntityNotFound, "no element with id #{id}")
|
110
|
-
end
|
111
|
-
|
112
|
-
def add(entity)
|
113
|
-
_add_to_reverse_association_of(entity) if @reverse
|
114
|
-
_add(entity)
|
115
|
-
end
|
116
|
-
|
117
|
-
def remove(entity)
|
118
|
-
_remove_from_reverse_association_of(entity) if @reverse
|
119
|
-
_remove(entity)
|
120
|
-
end
|
121
|
-
|
122
|
-
private
|
123
|
-
|
124
|
-
def _add(entity)
|
125
|
-
self << entity
|
126
|
-
Remodel.redis.rpush(@key, entity.key)
|
127
|
-
entity
|
128
|
-
end
|
129
|
-
|
130
|
-
def _remove(entity)
|
131
|
-
delete_if { |x| x.key == entity.key }
|
132
|
-
Remodel.redis.lrem(@key, 0, entity.key)
|
133
|
-
entity
|
134
|
-
end
|
135
|
-
|
136
|
-
def _add_to_reverse_association_of(entity)
|
137
|
-
if entity.send(@reverse).is_a? HasMany
|
138
|
-
entity.send(@reverse).send(:_add, @this)
|
139
|
-
else
|
140
|
-
entity.send("_#{@reverse}=", @this)
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
def _remove_from_reverse_association_of(entity)
|
145
|
-
if entity.send(@reverse).is_a? HasMany
|
146
|
-
entity.send(@reverse).send(:_remove, @this)
|
147
|
-
else
|
148
|
-
entity.send("_#{@reverse}=", nil)
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
def _fetch(clazz, key)
|
153
|
-
keys = Remodel.redis.lrange(key, 0, -1)
|
154
|
-
values = keys.empty? ? [] : Remodel.redis.mget(keys)
|
155
|
-
keys.zip(values).map do |key, json|
|
156
|
-
clazz.restore(key, json) if json
|
157
|
-
end.compact
|
158
|
-
end
|
159
|
-
end
|
160
|
-
|
161
|
-
#### Entity
|
162
|
-
|
163
|
-
# The superclass of all persistent remodel entities.
|
164
|
-
class Entity
|
165
|
-
attr_accessor :key
|
166
|
-
|
167
|
-
def initialize(attributes = {}, key = nil)
|
168
|
-
@attributes = {}
|
169
|
-
@key = key
|
170
|
-
attributes = self.class.default_values.merge(attributes) if key.nil?
|
171
|
-
attributes.each do |name, value|
|
172
|
-
send("#{name}=", value) if respond_to? "#{name}="
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
def id
|
177
|
-
key && key.split(':').last.to_i
|
178
|
-
end
|
179
|
-
|
180
|
-
def save
|
181
|
-
@key = self.class.next_key unless @key
|
182
|
-
Remodel.redis.set(@key, to_json)
|
183
|
-
self
|
184
|
-
end
|
185
|
-
|
186
|
-
def update(properties)
|
187
|
-
properties.each { |name, value| send("#{name}=", value) }
|
188
|
-
save
|
189
|
-
end
|
190
|
-
|
191
|
-
def reload
|
192
|
-
raise EntityNotSaved unless @key
|
193
|
-
initialize(self.class.parse(self.class.fetch(@key)), @key)
|
194
|
-
instance_variables.each do |var|
|
195
|
-
remove_instance_variable(var) if var =~ /^@association_/
|
196
|
-
end
|
197
|
-
self
|
198
|
-
end
|
199
|
-
|
200
|
-
def delete
|
201
|
-
raise EntityNotSaved unless @key
|
202
|
-
Remodel.redis.del(@key)
|
203
|
-
end
|
204
|
-
|
205
|
-
def as_json
|
206
|
-
{ :id => id }.merge(@attributes)
|
207
|
-
end
|
208
|
-
|
209
|
-
def to_json
|
210
|
-
JSON.generate(self.class.pack(@attributes))
|
211
|
-
end
|
212
|
-
|
213
|
-
def inspect
|
214
|
-
properties = @attributes.map { |name, value| "#{name}: #{value.inspect}" }.join(', ')
|
215
|
-
"\#<#{self.class.name}(#{id}) #{properties}>"
|
216
|
-
end
|
217
|
-
|
218
|
-
def self.create(attributes = {})
|
219
|
-
new(attributes).save
|
220
|
-
end
|
221
|
-
|
222
|
-
def self.find(key)
|
223
|
-
key = "#{key_prefix}:#{key}" if key.kind_of? Integer
|
224
|
-
restore(key, fetch(key))
|
225
|
-
end
|
226
|
-
|
227
|
-
def self.all
|
228
|
-
keys = Remodel.redis.keys("#{key_prefix}:*").select { |k| k =~ /:[0-9]+$/ }
|
229
|
-
values = keys.empty? ? [] : Remodel.redis.mget(keys)
|
230
|
-
keys.zip(values).map do |key, json|
|
231
|
-
restore(key, json) if json
|
232
|
-
end.compact
|
233
|
-
end
|
234
|
-
|
235
|
-
def self.restore(key, json)
|
236
|
-
new(parse(json), key)
|
237
|
-
end
|
238
|
-
|
239
|
-
#### DSL for subclasses
|
240
|
-
|
241
|
-
protected
|
242
|
-
|
243
|
-
def self.set_key_prefix(prefix)
|
244
|
-
raise(InvalidKeyPrefix, prefix) unless prefix =~ /^[a-z]+$/
|
245
|
-
@key_prefix = prefix
|
246
|
-
end
|
247
|
-
|
248
|
-
def self.property(name, options = {})
|
249
|
-
name = name.to_sym
|
250
|
-
mapper[name] = Remodel.mapper_for(options[:class])
|
251
|
-
default_values[name] = options[:default] if options.has_key?(:default)
|
252
|
-
define_method(name) { @attributes[name] }
|
253
|
-
define_method("#{name}=") { |value| @attributes[name] = value }
|
254
|
-
end
|
255
|
-
|
256
|
-
def self.has_many(name, options)
|
257
|
-
var = "@association_#{name}".to_sym
|
258
|
-
|
259
|
-
define_method(name) do
|
260
|
-
if instance_variable_defined? var
|
261
|
-
instance_variable_get(var)
|
262
|
-
else
|
263
|
-
clazz = Class[options[:class]]
|
264
|
-
instance_variable_set(var, HasMany.new(self, clazz, "#{key}:#{name}", options[:reverse]))
|
265
|
-
end
|
266
|
-
end
|
267
|
-
end
|
268
|
-
|
269
|
-
def self.has_one(name, options)
|
270
|
-
var = "@association_#{name}".to_sym
|
271
|
-
|
272
|
-
define_method(name) do
|
273
|
-
if instance_variable_defined? var
|
274
|
-
instance_variable_get(var)
|
275
|
-
else
|
276
|
-
clazz = Class[options[:class]]
|
277
|
-
value_key = Remodel.redis.get("#{key}:#{name}")
|
278
|
-
instance_variable_set(var, clazz.find(value_key)) if value_key
|
279
|
-
end
|
280
|
-
end
|
281
|
-
|
282
|
-
define_method("#{name}=") do |value|
|
283
|
-
send("_reverse_association_of_#{name}=", value) if options[:reverse]
|
284
|
-
send("_#{name}=", value)
|
285
|
-
end
|
286
|
-
|
287
|
-
define_method("_#{name}=") do |value|
|
288
|
-
if value
|
289
|
-
instance_variable_set(var, value)
|
290
|
-
Remodel.redis.set("#{key}:#{name}", value.key)
|
291
|
-
else
|
292
|
-
remove_instance_variable(var) if instance_variable_defined? var
|
293
|
-
Remodel.redis.del("#{key}:#{name}")
|
294
|
-
end
|
295
|
-
end; private "_#{name}="
|
296
|
-
|
297
|
-
if options[:reverse]
|
298
|
-
define_method("_reverse_association_of_#{name}=") do |value|
|
299
|
-
if value
|
300
|
-
association = value.send("#{options[:reverse]}")
|
301
|
-
if association.is_a? HasMany
|
302
|
-
association.send("_add", self)
|
303
|
-
else
|
304
|
-
value.send("_#{options[:reverse]}=", self)
|
305
|
-
end
|
306
|
-
else
|
307
|
-
if old_value = send(name)
|
308
|
-
association = old_value.send("#{options[:reverse]}")
|
309
|
-
if association.is_a? HasMany
|
310
|
-
association.send("_remove", self)
|
311
|
-
else
|
312
|
-
old_value.send("_#{options[:reverse]}=", nil)
|
313
|
-
end
|
314
|
-
end
|
315
|
-
end
|
316
|
-
end; private "_reverse_association_of_#{name}="
|
317
|
-
end
|
318
|
-
end
|
319
|
-
|
320
|
-
#### Helper methods
|
321
|
-
|
322
|
-
private
|
323
|
-
|
324
|
-
def self.fetch(key)
|
325
|
-
Remodel.redis.get(key) || raise(EntityNotFound, "no #{name} with key #{key}")
|
326
|
-
end
|
327
|
-
|
328
|
-
# Each entity has its own sequence to generate unique ids.
|
329
|
-
def self.next_key
|
330
|
-
id = Remodel.redis.incr("#{key_prefix}:seq")
|
331
|
-
"#{key_prefix}:#{id}"
|
332
|
-
end
|
333
|
-
|
334
|
-
# Default key prefix is the first letter of the class name, in lowercase.
|
335
|
-
def self.key_prefix
|
336
|
-
@key_prefix ||= name.split('::').last[0,1].downcase
|
337
|
-
end
|
338
|
-
|
339
|
-
def self.parse(json)
|
340
|
-
unpack(JSON.parse(json))
|
341
|
-
end
|
342
|
-
|
343
|
-
def self.pack(attributes)
|
344
|
-
result = {}
|
345
|
-
attributes.each do |name, value|
|
346
|
-
result[name] = mapper[name].pack(value)
|
347
|
-
end
|
348
|
-
result
|
349
|
-
end
|
350
|
-
|
351
|
-
def self.unpack(attributes)
|
352
|
-
result = {}
|
353
|
-
attributes.each do |name, value|
|
354
|
-
name = name.to_sym
|
355
|
-
result[name] = mapper[name].unpack(value)
|
356
|
-
end
|
357
|
-
result
|
358
|
-
end
|
359
|
-
|
360
|
-
# Lazy init
|
361
|
-
def self.mapper
|
362
|
-
@mapper ||= {}
|
363
|
-
end
|
364
|
-
|
365
|
-
def self.default_values
|
366
|
-
@default_values ||= {}
|
367
|
-
end
|
368
|
-
|
369
|
-
end
|
370
|
-
|
371
|
-
end
|
68
|
+
end
|
data/remodel.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{remodel}
|
8
|
-
s.version = "0.1.
|
8
|
+
s.version = "0.1.4"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Tim Lossen"]
|
12
|
-
s.date = %q{2010-
|
12
|
+
s.date = %q{2010-07-01}
|
13
13
|
s.default_executable = %q{redis-monitor.rb}
|
14
14
|
s.description = %q{build your domain model in ruby, persist your objects to redis.}
|
15
15
|
s.email = %q{tim@lossen.de}
|
@@ -29,6 +29,10 @@ Gem::Specification.new do |s|
|
|
29
29
|
"docs/remodel.html",
|
30
30
|
"example/book.rb",
|
31
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",
|
32
36
|
"remodel.gemspec",
|
33
37
|
"test/helper.rb",
|
34
38
|
"test/test_entity.rb",
|
data/test/test_one_to_many.rb
CHANGED
@@ -9,7 +9,7 @@ class TestOneToMany < Test::Unit::TestCase
|
|
9
9
|
end
|
10
10
|
|
11
11
|
class Puzzle < Remodel::Entity
|
12
|
-
has_many :pieces, :class => 'TestOneToMany::Piece'
|
12
|
+
has_many :pieces, :class => 'TestOneToMany::Piece', :reverse => 'puzzle'
|
13
13
|
property :topic
|
14
14
|
end
|
15
15
|
|
@@ -48,6 +48,15 @@ class TestOneToMany < Test::Unit::TestCase
|
|
48
48
|
piece = Piece.create
|
49
49
|
piece.puzzle = puzzle
|
50
50
|
assert_equal 1, puzzle.pieces.size
|
51
|
+
assert_equal piece.id, puzzle.pieces.first.id
|
52
|
+
end
|
53
|
+
|
54
|
+
should "remove the entity from the old reverse association" do
|
55
|
+
puzzle = Puzzle.create
|
56
|
+
piece = puzzle.pieces.create
|
57
|
+
new_puzzle = Puzzle.create
|
58
|
+
piece.puzzle = new_puzzle
|
59
|
+
assert_equal [], puzzle.reload.pieces
|
51
60
|
end
|
52
61
|
|
53
62
|
should "be settable to nil" do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: remodel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tim Lossen
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-
|
12
|
+
date: 2010-07-01 00:00:00 +02:00
|
13
13
|
default_executable: redis-monitor.rb
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -33,6 +33,10 @@ files:
|
|
33
33
|
- docs/remodel.html
|
34
34
|
- example/book.rb
|
35
35
|
- lib/remodel.rb
|
36
|
+
- lib/remodel/entity.rb
|
37
|
+
- lib/remodel/has_many.rb
|
38
|
+
- lib/remodel/has_one.rb
|
39
|
+
- lib/remodel/mapper.rb
|
36
40
|
- remodel.gemspec
|
37
41
|
- test/helper.rb
|
38
42
|
- test/test_entity.rb
|