remodel 0.4.2 → 0.5.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
@@ -51,14 +51,13 @@ persistence to disk. for example, on my macbook (2 ghz):
51
51
  define your domain model [like this](http://github.com/tlossen/remodel/blob/master/example/book.rb):
52
52
 
53
53
  class Book < Remodel::Entity
54
- has_many :chapters, :class => 'Chapter', :reverse => :book
54
+ has_many :chapters, :class => 'Chapter'
55
55
  property :title, :short => 't', :class => 'String'
56
56
  property :year, :class => 'Integer'
57
57
  property :author, :class => 'String', :default => '(anonymous)'
58
58
  end
59
59
 
60
60
  class Chapter < Remodel::Entity
61
- has_one :book, :class => Book, :reverse => :chapters
62
61
  property :title, :class => String
63
62
  end
64
63
 
@@ -72,8 +71,6 @@ now you can do:
72
71
  => #<Book(shelf, 1) title: "Moby Dick", year: 1851, author: "(anonymous)">
73
72
  >> chapter = book.chapters.create :title => 'Ishmael'
74
73
  => #<Chapter(shelf, 1) title: "Ishmael">
75
- >> chapter.book
76
- => #<Book(shelf, 1) title: "Moby Dick", year: 1851, author: "(anonymous)">
77
74
 
78
75
  all entities have been created in the redis hash 'shelf' we have used as context:
79
76
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.2
1
+ 0.5.0
data/example/book.rb CHANGED
@@ -1,14 +1,13 @@
1
1
  require File.dirname(__FILE__) + "/../lib/remodel"
2
2
 
3
3
  class Book < Remodel::Entity
4
- has_many :chapters, :class => 'Chapter', :reverse => :book
4
+ has_many :chapters, :class => 'Chapter'
5
5
  property :title, :short => 't', :class => 'String'
6
6
  property :year, :class => 'Integer'
7
7
  property :author, :class => 'String', :default => '(anonymous)'
8
8
  end
9
9
 
10
10
  class Chapter < Remodel::Entity
11
- has_one :book, :class => Book, :reverse => :chapters
12
11
  property :title, :class => String
13
12
  end
14
13
 
@@ -82,7 +82,7 @@ module Remodel
82
82
 
83
83
  def self.property(name, options = {})
84
84
  name = name.to_sym
85
- mapper[name] = Remodel.mapper_for(options[:class])
85
+ _mapper[name] = Remodel.mapper_for(options[:class])
86
86
  define_shortname(name, options[:short])
87
87
  default_value = options[:default]
88
88
  define_method(name) { @attributes[name].nil? ? self.class.copy_of(default_value) : @attributes[name] }
@@ -90,68 +90,44 @@ module Remodel
90
90
  end
91
91
 
92
92
  def self.has_many(name, options)
93
- associations.push(name)
93
+ _associations.push(name)
94
94
  var = "@#{name}".to_sym
95
+ shortname = options[:short] || name
95
96
 
96
97
  define_method(name) do
97
98
  if instance_variable_defined? var
98
99
  instance_variable_get(var)
99
100
  else
100
101
  clazz = Class[options[:class]]
101
- instance_variable_set(var, HasMany.new(self, clazz, "#{key}_#{name}", options[:reverse]))
102
+ instance_variable_set(var, HasMany.new(self, clazz, "#{key}_#{shortname}"))
102
103
  end
103
104
  end
104
105
  end
105
106
 
106
107
  def self.has_one(name, options)
107
- associations.push(name)
108
+ _associations.push(name)
108
109
  var = "@#{name}".to_sym
110
+ shortname = options[:short] || name
109
111
 
110
112
  define_method(name) do
111
113
  if instance_variable_defined? var
112
114
  instance_variable_get(var)
113
115
  else
114
116
  clazz = Class[options[:class]]
115
- value_key = self.context.hget("#{key}_#{name}")
117
+ value_key = self.context.hget("#{key}_#{shortname}")
116
118
  value = value_key && clazz.find(self.context, value_key) rescue nil
117
119
  instance_variable_set(var, value)
118
120
  end
119
121
  end
120
122
 
121
123
  define_method("#{name}=") do |value|
122
- send("_reverse_association_of_#{name}=", value) if options[:reverse]
123
- send("_#{name}=", value)
124
- end
125
-
126
- define_method("_#{name}=") do |value|
127
124
  if value
128
125
  instance_variable_set(var, value)
129
- self.context.hset("#{key}_#{name}", value.key)
126
+ self.context.hset("#{key}_#{shortname}", value.key)
130
127
  else
131
128
  remove_instance_variable(var) if instance_variable_defined? var
132
- self.context.hdel("#{key}_#{name}")
129
+ self.context.hdel("#{key}_#{shortname}")
133
130
  end
134
- end; private "_#{name}="
135
-
136
- if options[:reverse]
137
- define_method("_reverse_association_of_#{name}=") do |value|
138
- if old_value = send(name)
139
- association = old_value.send("#{options[:reverse]}")
140
- if association.is_a? HasMany
141
- association.send("_remove", self)
142
- else
143
- old_value.send("_#{options[:reverse]}=", nil)
144
- end
145
- end
146
- if value
147
- association = value.send("#{options[:reverse]}")
148
- if association.is_a? HasMany
149
- association.send("_add", self)
150
- else
151
- value.send("_#{options[:reverse]}=", self)
152
- end
153
- end
154
- end; private "_reverse_association_of_#{name}="
155
131
  end
156
132
  end
157
133
 
@@ -211,24 +187,42 @@ module Remodel
211
187
  def self.define_shortname(name, short)
212
188
  return unless short
213
189
  short = short.to_sym
214
- shortname[name] = short
215
- fullname[short] = name
190
+ _shortname[name] = short
191
+ _fullname[short] = name
216
192
  end
217
193
 
218
- # class instance variables (lazy init)
194
+ # class instance variables:
195
+ # lazy init + recursive lookup in superclasses
196
+
219
197
  def self.mapper
198
+ self == Entity || superclass == Entity ? _mapper : superclass.mapper.merge(_mapper)
199
+ end
200
+
201
+ def self._mapper
220
202
  @mapper ||= {}
221
203
  end
222
204
 
223
205
  def self.shortname
206
+ self == Entity || superclass == Entity ? _shortname : superclass.shortname.merge(_shortname)
207
+ end
208
+
209
+ def self._shortname
224
210
  @shortname ||= {}
225
211
  end
226
212
 
227
213
  def self.fullname
214
+ self == Entity || superclass == Entity ? _fullname : superclass.fullname.merge(_fullname)
215
+ end
216
+
217
+ def self._fullname
228
218
  @fullname ||= {}
229
219
  end
230
220
 
231
221
  def self.associations
222
+ self == Entity || superclass == Entity ? _associations : superclass.associations + _associations
223
+ end
224
+
225
+ def self._associations
232
226
  @associations ||= []
233
227
  end
234
228
 
@@ -2,9 +2,9 @@ module Remodel
2
2
 
3
3
  # Represents the many-end of a many-to-one or many-to-many association.
4
4
  class HasMany < Array
5
- def initialize(this, clazz, key, reverse = nil)
5
+ def initialize(this, clazz, key)
6
6
  super _fetch(clazz, this.context, key)
7
- @this, @clazz, @key, @reverse = this, clazz, key, reverse
7
+ @this, @clazz, @key = this, clazz, key
8
8
  end
9
9
 
10
10
  def create(attributes = {})
@@ -16,51 +16,25 @@ module Remodel
16
16
  end
17
17
 
18
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
19
  self << entity
32
20
  _store
33
21
  entity
34
22
  end
35
23
 
36
- def _remove(entity)
24
+ def remove(entity)
37
25
  delete_if { |x| x.key == entity.key }
38
26
  _store
39
27
  entity
40
28
  end
41
29
 
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
30
+ private
57
31
 
58
32
  def _store
59
- @this.context.hset(@key, JSON.generate(self.map(&:key)))
33
+ @this.context.hset(@key, self.map(&:key).join(' '))
60
34
  end
61
35
 
62
36
  def _fetch(clazz, context, key)
63
- keys = JSON.parse(context.hget(key) || '[]').uniq
37
+ keys = (context.hget(key) || '').split.uniq
64
38
  values = keys.empty? ? [] : context.hmget(*keys)
65
39
  keys.zip(values).map do |key, json|
66
40
  clazz.restore(context, key, json) if json
@@ -0,0 +1,105 @@
1
+ require 'helper'
2
+ require 'json'
3
+
4
+ class TestHasMany < Test::Unit::TestCase
5
+
6
+ class Puzzle < Remodel::Entity
7
+ has_many :pieces, :class => 'TestHasMany::Piece'
8
+ property :topic
9
+ end
10
+
11
+ class Piece < Remodel::Entity
12
+ property :color
13
+ end
14
+
15
+ context "association" do
16
+ should "exist" do
17
+ assert Puzzle.create(context).respond_to?(:pieces)
18
+ end
19
+
20
+ should "return an empty list by default" do
21
+ assert_equal [], Puzzle.create(context).pieces
22
+ end
23
+
24
+ should "return any existing children" do
25
+ puzzle = Puzzle.create(context)
26
+ red_piece = Piece.create(context, :color => 'red')
27
+ blue_piece = Piece.create(context, :color => 'blue')
28
+ redis.hset(context.key, "#{puzzle.key}_pieces", "#{red_piece.key} #{blue_piece.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
+ should "not return any child multiple times" do
35
+ puzzle = Puzzle.create(context)
36
+ red_piece = Piece.create(context, :color => 'red')
37
+ redis.hset(context.key, "#{puzzle.key}_pieces", "#{red_piece.key} #{red_piece.key}")
38
+ assert_equal 1, puzzle.pieces.size
39
+ assert_equal Piece, puzzle.pieces[0].class
40
+ assert_equal 'red', puzzle.pieces[0].color
41
+ end
42
+
43
+ context "create" do
44
+ should "have a create method" do
45
+ assert Puzzle.create(context).pieces.respond_to?(:create)
46
+ end
47
+
48
+ should "work without attributes" do
49
+ puzzle = Puzzle.create(context)
50
+ piece = puzzle.pieces.create
51
+ assert piece.is_a?(Piece)
52
+ end
53
+
54
+ should "create and store a new child" do
55
+ puzzle = Puzzle.create(context)
56
+ puzzle.pieces.create :color => 'green'
57
+ assert_equal 1, puzzle.pieces.size
58
+ puzzle.reload
59
+ assert_equal 1, puzzle.pieces.size
60
+ assert_equal Piece, puzzle.pieces[0].class
61
+ assert_equal 'green', puzzle.pieces[0].color
62
+ end
63
+ end
64
+
65
+ context "add" do
66
+ should "add the given entity to the association" do
67
+ puzzle = Puzzle.create(context)
68
+ piece = Piece.create(context, :color => 'white')
69
+ puzzle.pieces.add piece
70
+ assert_equal 1, puzzle.pieces.size
71
+ puzzle.reload
72
+ assert_equal 1, puzzle.pieces.size
73
+ assert_equal Piece, puzzle.pieces[0].class
74
+ assert_equal 'white', puzzle.pieces[0].color
75
+ end
76
+ end
77
+
78
+ context "find" do
79
+ setup do
80
+ @puzzle = Puzzle.create(context)
81
+ 5.times { @puzzle.pieces.create :color => 'blue' }
82
+ end
83
+
84
+ should "find the element with the given id" do
85
+ piece = @puzzle.pieces[2]
86
+ assert_equal piece, @puzzle.pieces.find(piece.id)
87
+ end
88
+
89
+ should "raise an exception if no element with the given id exists" do
90
+ assert_raises(Remodel::EntityNotFound) { @puzzle.pieces.find(-1) }
91
+ end
92
+ end
93
+ end
94
+
95
+ context "reload" do
96
+ should "reset has_many associations" do
97
+ puzzle = Puzzle.create(context)
98
+ piece = puzzle.pieces.create :color => 'black'
99
+ redis.hdel(context.key, "#{puzzle.key}_pieces")
100
+ puzzle.reload
101
+ assert_equal [], puzzle.pieces
102
+ end
103
+ end
104
+
105
+ end
@@ -0,0 +1,68 @@
1
+ require 'helper'
2
+
3
+
4
+ class TestHasOne < Test::Unit::TestCase
5
+
6
+ class Piece < Remodel::Entity
7
+ has_one :puzzle, :class => 'TestHasOne::Puzzle'
8
+ property :color
9
+ end
10
+
11
+ class Puzzle < Remodel::Entity
12
+ property :topic
13
+ end
14
+
15
+ context "association getter" do
16
+ should "exist" do
17
+ assert Piece.create(context).respond_to?(:puzzle)
18
+ end
19
+
20
+ should "return nil by default" do
21
+ assert_nil Piece.create(context).puzzle
22
+ end
23
+
24
+ should "return the associated entity" do
25
+ puzzle = Puzzle.create(context, :topic => 'animals')
26
+ piece = Piece.create(context)
27
+ redis.hset(context.key, "#{piece.key}_puzzle", puzzle.key)
28
+ assert_equal 'animals', piece.puzzle.topic
29
+ end
30
+ end
31
+
32
+ context "association setter" do
33
+ should "exist" do
34
+ assert Piece.create(context).respond_to?(:'puzzle=')
35
+ end
36
+
37
+ should "store the key of the associated entity" do
38
+ puzzle = Puzzle.create(context)
39
+ piece = Piece.create(context)
40
+ piece.puzzle = puzzle
41
+ assert_equal puzzle.key, redis.hget(context.key, "#{piece.key}_puzzle")
42
+ end
43
+
44
+ should "be settable to nil" do
45
+ piece = Piece.create(context)
46
+ piece.puzzle = nil
47
+ assert_nil piece.puzzle
48
+ end
49
+
50
+ should "remove the key if set to nil" do
51
+ piece = Piece.create(context)
52
+ piece.puzzle = Puzzle.create(context)
53
+ piece.puzzle = nil
54
+ assert_nil redis.hget(piece.context, "#{piece.key}_puzzle")
55
+ end
56
+ end
57
+
58
+ context "reload" do
59
+ should "reset has_one associations" do
60
+ piece = Piece.create(context, :color => 'black')
61
+ piece.puzzle = Puzzle.create(context)
62
+ redis.hdel(context.key, "#{piece.key}_puzzle")
63
+ piece.reload
64
+ assert_nil piece.puzzle
65
+ end
66
+ end
67
+
68
+ end
@@ -0,0 +1,65 @@
1
+ require 'helper'
2
+
3
+ class TestInheritance < Test::Unit::TestCase
4
+
5
+ class Foo < Remodel::Entity
6
+ property :test
7
+ end
8
+
9
+ class Bar < Remodel::Entity
10
+ property :test
11
+ end
12
+
13
+ class Person < Remodel::Entity
14
+ property :name, :short => 'n'
15
+ has_one :foo, :class => Foo
16
+ has_many :foos, :class => Foo
17
+ end
18
+
19
+ class Admin < Person
20
+ property :password, :short => 'p'
21
+ has_one :bar, :class => Bar
22
+ has_many :bars, :class => Bar
23
+ end
24
+
25
+ context "a subclass of another entity" do
26
+ setup do
27
+ @admin = Admin.create(context, :name => 'peter', :password => 'secret')
28
+ end
29
+
30
+ should "inherit properties" do
31
+ @admin.reload
32
+ assert_equal 'peter', @admin.name
33
+ assert_equal 'secret', @admin.password
34
+ end
35
+
36
+ should "inherit has_one associations" do
37
+ @admin.foo = Foo.create(context, :test => 'foo')
38
+ @admin.bar = Bar.create(context, :test => 'bar')
39
+ @admin.reload
40
+ assert_equal 'foo', @admin.foo.test
41
+ assert_equal 'bar', @admin.bar.test
42
+ end
43
+
44
+ should "inherit has_many associations" do
45
+ @admin.foos.create(:test => 'foo')
46
+ @admin.bars.create(:test => 'bar')
47
+ @admin.reload
48
+ assert_equal 'foo', @admin.foos[0].test
49
+ assert_equal 'bar', @admin.bars[0].test
50
+ end
51
+
52
+ should "be usable as superclass" do
53
+ person = Person.find(context, @admin.key)
54
+ assert_equal 'peter', person.name
55
+ assert_raise(NoMethodError) { person.password }
56
+ end
57
+
58
+ should "use property shortnames in redis" do
59
+ json = redis.hget(context.key, @admin.key)
60
+ assert_match /"n":/, json
61
+ assert_match /"p":/, json
62
+ end
63
+ end
64
+
65
+ end
@@ -0,0 +1,73 @@
1
+ require 'helper'
2
+
3
+ class TestShortnames < Test::Unit::TestCase
4
+
5
+ class Foo < Remodel::Entity; end
6
+
7
+ class Bar < Remodel::Entity
8
+ property :test, :short => 'z'
9
+ has_one :foo, :short => 'y', :class => Foo
10
+ has_many :foos, :short => 'x', :class => Foo
11
+ end
12
+
13
+ context "property shortnames" do
14
+ setup do
15
+ @bar = Bar.create(context, :test => 42)
16
+ end
17
+
18
+ should "be used when storing properties" do
19
+ serialized = redis.hget(context.key, @bar.key)
20
+ assert !serialized.match(/test/)
21
+ assert serialized.match(/z/)
22
+ end
23
+
24
+ should "work in roundtrip" do
25
+ @bar.reload
26
+ assert_equal 42, @bar.test
27
+ end
28
+
29
+ should "not be used in as_json" do
30
+ assert !@bar.as_json.has_key?(:z)
31
+ assert @bar.as_json.has_key?(:test)
32
+ end
33
+
34
+ should "not be used in inspect" do
35
+ assert !@bar.inspect.match(/z/)
36
+ assert @bar.inspect.match(/test/)
37
+ end
38
+ end
39
+
40
+ context "has_one shortnames" do
41
+ setup do
42
+ @bar = Bar.create(context, :test => 42)
43
+ @bar.foo = Foo.create(context)
44
+ end
45
+
46
+ should "be used when storing" do
47
+ assert_not_nil redis.hget(context.key, "#{@bar.key}_y")
48
+ end
49
+
50
+ should "work in roundtrip" do
51
+ @bar.reload
52
+ assert_not_nil @bar.foo
53
+ end
54
+ end
55
+
56
+ context "has_many shortnames" do
57
+ setup do
58
+ @bar = Bar.create(context, :test => 42)
59
+ @bar.foos.create
60
+ @bar.foos.create
61
+ end
62
+
63
+ should "be used when storing" do
64
+ assert_not_nil redis.hget(context.key, "#{@bar.key}_x")
65
+ end
66
+
67
+ should "work in roundtrip" do
68
+ @bar.reload
69
+ assert_equal 2, @bar.foos.size
70
+ end
71
+ end
72
+
73
+ end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 4
8
- - 2
9
- version: 0.4.2
7
+ - 5
8
+ - 0
9
+ version: 0.5.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Tim Lossen
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-08-11 00:00:00 +02:00
17
+ date: 2011-08-16 00:00:00 +02:00
18
18
  default_executable:
19
19
  dependencies: []
20
20
 
@@ -32,8 +32,6 @@ files:
32
32
  - README.md
33
33
  - Rakefile
34
34
  - VERSION
35
- - docs/docco.css
36
- - docs/remodel.html
37
35
  - example/book.rb
38
36
  - lib/remodel.rb
39
37
  - lib/remodel/caching_context.rb
@@ -46,14 +44,13 @@ files:
46
44
  - test/test_entity.rb
47
45
  - test/test_entity_defaults.rb
48
46
  - test/test_entity_delete.rb
49
- - test/test_entity_shortnames.rb
50
- - test/test_many_to_many.rb
51
- - test/test_many_to_one.rb
47
+ - test/test_has_many.rb
48
+ - test/test_has_one.rb
49
+ - test/test_inheritance.rb
52
50
  - test/test_mappers.rb
53
51
  - test/test_monkeypatches.rb
54
- - test/test_one_to_many.rb
55
- - test/test_one_to_one.rb
56
52
  - test/test_remodel.rb
53
+ - test/test_shortnames.rb
57
54
  has_rdoc: true
58
55
  homepage: http://github.com/tlossen/remodel
59
56
  licenses: []
@@ -92,11 +89,10 @@ test_files:
92
89
  - test/test_entity.rb
93
90
  - test/test_entity_defaults.rb
94
91
  - test/test_entity_delete.rb
95
- - test/test_entity_shortnames.rb
96
- - test/test_many_to_many.rb
97
- - test/test_many_to_one.rb
92
+ - test/test_has_many.rb
93
+ - test/test_has_one.rb
94
+ - test/test_inheritance.rb
98
95
  - test/test_mappers.rb
99
96
  - test/test_monkeypatches.rb
100
- - test/test_one_to_many.rb
101
- - test/test_one_to_one.rb
102
97
  - test/test_remodel.rb
98
+ - test/test_shortnames.rb