remodel 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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