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 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 ezras excellent
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(b:3) title: "Moby Dick", year: 1851>
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(c:4) title: "Ishmael">
71
+ => #<Chapter(shelf, 1) title: "Ishmael">
67
72
  >> chapter.book
68
- => #<Book(b:3) title: "Moby Dick", year: 1851>
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
- still pretty alpha &mdash; play around at your own risk :)
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), baby!
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 = "a minimal ORM (object-redis-mapper)"
9
- gem.description = "build your domain model in ruby, persist your objects to redis."
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.4
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
@@ -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.split(':').last.to_i
17
+ key && key.match(/\d+/)[0].to_i
18
18
  end
19
19
 
20
20
  def save
21
- @key = self.class.next_key unless @key
22
- Remodel.redis.set(@key, to_json)
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
- initialize(self.class.parse(self.class.fetch(@key)), @key)
34
- instance_variables.each do |var|
35
- remove_instance_variable(var) if var =~ /^@association_/
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.del(@key)
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(@attributes)
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 = @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
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.find(key)
63
- key = "#{key_prefix}:#{key}" if key.kind_of? Integer
64
- restore(key, fetch(key))
63
+ def self.create(context, attributes = {})
64
+ new(context, attributes).save
65
65
  end
66
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
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
- default_values[name] = options[:default] if options.has_key?(:default)
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
- var = "@association_#{name}".to_sym
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}:#{name}", options[:reverse]))
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
- var = "@association_#{name}".to_sym
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.get("#{key}:#{name}")
116
- instance_variable_set(var, clazz.find(value_key)) if value_key
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.set("#{key}:#{name}", value.key)
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.del("#{key}:#{name}")
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 self.fetch(key)
160
- Remodel.redis.get(key) || raise(EntityNotFound, "no #{name} with key #{key}")
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 self.next_key
165
- id = Remodel.redis.incr("#{key_prefix}:seq")
166
- "#{key_prefix}:#{id}"
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.default_values
201
- @default_values ||= {}
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
@@ -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
- Remodel.redis.rpush(@key, entity.key)
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
- Remodel.redis.lrem(@key, 0, entity.key)
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 _fetch(clazz, key)
59
- keys = Remodel.redis.lrange(key, 0, -1)
60
- values = keys.empty? ? [] : Remodel.redis.mget(keys)
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
@@ -9,5 +9,5 @@ class Test::Unit::TestCase
9
9
  def redis
10
10
  Remodel.redis
11
11
  end
12
-
12
+
13
13
  end