mongoid_max_denormalize 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) Maxime Garcia
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # Mongoid::Max::Denormalize
2
+
3
+ `Mongoid::Max::Denormalize` is a denormalization extension for Mongoid.
4
+
5
+ It was designed for a minimum number of queries to the database.
6
+
7
+ For now, support only Mongoid 3.
8
+
9
+ * Propagate only when needed
10
+ * Take advantage of atomic operations on multi documents of MongoDB
11
+
12
+
13
+
14
+ ## Installation
15
+
16
+ Add the gem to your Gemfile:
17
+
18
+ gem 'mongoid_max_denormalize'
19
+
20
+ Or install with RubyGems:
21
+
22
+ $ gem install mongoid_max_denormalize
23
+
24
+
25
+
26
+ ## Usage
27
+
28
+ ### Basic usage
29
+
30
+ Add `include Mongoid::Max::Denormalize` in your model and also:
31
+
32
+ denormalize relation, field_1, field_2 ... field_n, options
33
+
34
+
35
+ ### One to Many
36
+
37
+ Supported fields: normal Mongoid fields, and methods.
38
+
39
+ Supported options: none.
40
+
41
+ Example :
42
+
43
+ class Post
44
+ include Mongoid::Document
45
+ field :title
46
+ def slug
47
+ title.try(:parameterize)
48
+ end
49
+ has_many :comments
50
+ end
51
+
52
+ class Comment
53
+ include Mongoid::Document
54
+ include Mongoid::Max::Denormalize
55
+ belons_to :post
56
+ denormalize :post, :title, :slug
57
+ end
58
+
59
+ @post = Post.create(:title => "Mush from the Wimp")
60
+ @comment = @post.comments.create
61
+ @comment.post_title #=> "Mush from the Wimp"
62
+ @comment.post_slug #=> "mush-from-the-wimp"
63
+ #
64
+ @post.update_attributes(:title => "All Must Share The Burden")
65
+ @comment.reload # to reload the comment from the DB
66
+ @comment.post_title #=> "All Must Share The Burden"
67
+ @comment.post_slug #=> "all-must-share-the-burden"
68
+
69
+ **Tips :** In your views, do not use `@comment.post` but `@comment.post_id` or `@comment.post_id?`
70
+ to avoid a query that checks/retrieve for the post. We want to avoid it, don't we ?
71
+
72
+ Exemple : Check your logs, you'll see queries for the post :
73
+
74
+ # app/views/comments/_comment.html.erb
75
+ <div class="comment">
76
+ <% if @comment.post %>
77
+ <%= link_to @comment.post_title, @comment.post %>
78
+ <% end %>
79
+ </div>
80
+
81
+ This is better :
82
+
83
+ # app/views/comments/_comment.html.erb
84
+ <div class="comment">
85
+ <% if @comment.post_id? %>
86
+ <%= link_to @comment.post_title, post_path(@comment.post_id, :slug => @comment.post_slug) %>
87
+ <% end %>
88
+ </div>
89
+
90
+
91
+ ### Many to One
92
+
93
+ Supported fields: **only** normal Mongoid fields (no methods)
94
+
95
+ Supported options:
96
+
97
+ * `:count => true` : to keep a count !
98
+
99
+
100
+ Example :
101
+
102
+ class Post
103
+ include Mongoid::Document
104
+ include Mongoid::Max::Denormalize
105
+ has_many :comments
106
+ denormalize :comments, :rating, :stuff, :count => true
107
+ end
108
+
109
+ class Comment
110
+ include Mongoid::Document
111
+ belons_to :post
112
+ field :rating
113
+ field :stuff
114
+ end
115
+
116
+ @post = Post.create(:title => "J'accuse !")
117
+ @comment = @post.comments.create(:rating => 5, :stuff => "A")
118
+ @comment = @post.comments.create(:rating => 3, :stuff => "B")
119
+ @post.reload
120
+ @post.comments_count #=> 2
121
+ @post.comments_rating #=> [5, 3]
122
+ @post.comments_stuff #=> ["A", "B"]
123
+
124
+ You can see that each denormalized field in stored in a separate array. This is wanted.
125
+ An option `:group` will come to allow :
126
+
127
+ @post.comments_fields #=> [{:rating => 5, :stuff => "A"},{:rating => 5, :stuff => "B"}]
128
+
129
+
130
+ ### Many to One
131
+
132
+ To come...
133
+
134
+
135
+
136
+ ## Contributing
137
+
138
+ Contributions and bug reports are welcome.
139
+
140
+ Clone the repository and run `bundle install` to setup the development environment.
141
+
142
+ Provide a case spec according to your changes/needs, taking example on existing ones (in `spec/cases`).
143
+
144
+ To run the specs:
145
+
146
+ bundle exec rspec
147
+
148
+
149
+
150
+ ## Credits
151
+
152
+ * Maxime Garcia [emaxime.com](http://emaxime.com) [@maximegarcia](http://twitter.com/maximegarcia)
153
+
154
+
155
+ [License](https://github.com/maximeg/mongoid_max_denormalize/blob/master/LICENSE)
156
+ \- [Report a bug](https://github.com/maximeg/mongoid_max_denormalize/issues).
157
+
@@ -0,0 +1,52 @@
1
+ # encoding: utf-8
2
+ module Mongoid
3
+ module Max
4
+ module Denormalize
5
+
6
+ class Base
7
+
8
+ attr_accessor :klass, :meta, :inverse_meta, :fields, :options
9
+
10
+ def initialize(klass, meta, inverse_meta, fields, options)
11
+ @klass = klass
12
+ @meta = meta
13
+ @inverse_meta = inverse_meta
14
+ @fields = fields
15
+ @options = options
16
+ end
17
+
18
+
19
+ def relation
20
+ @meta.name
21
+ end
22
+ def inverse_relation
23
+ @meta.inverse
24
+ end
25
+ def inverse_klass
26
+ @meta.klass
27
+ end
28
+
29
+
30
+ def fields_methods
31
+ @fields_methods ||= fields.select do |field|
32
+ meta.klass.fields[field.to_s].nil?
33
+ end
34
+ end
35
+ def fields_only
36
+ @fields_only ||= fields - fields_methods
37
+ end
38
+
39
+ class << self
40
+
41
+ def array_code_for(fields)
42
+ fields.map { |field| ":#{field}" }.join(", ")
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+ end
51
+ end
52
+
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+ module Mongoid
3
+ module Max
4
+ module Denormalize
5
+ class ConfigError < ::StandardError
6
+
7
+ def initialize(summary, klass)
8
+ super("[#{klass}.denormalize] #{summary}")
9
+ end
10
+
11
+ end
12
+ end
13
+ end
14
+ end
15
+
@@ -0,0 +1,218 @@
1
+ # encoding: utf-8
2
+ module Mongoid
3
+ module Max
4
+ module Denormalize
5
+
6
+ class ManyToOne < Base
7
+
8
+ def attach
9
+
10
+ fields.each do |field|
11
+ klass.field "#{relation}_#{field}", type: Array
12
+ end
13
+
14
+ if has_count?
15
+ klass.field "#{relation}_count", type: Integer, default: 0
16
+ end
17
+
18
+ callback_code = <<EOM
19
+ before_save :denormalize_from_#{relation}
20
+
21
+ def denormalize_from_#{relation}
22
+ return unless #{meta.key}_changed?
23
+
24
+ fields = [#{Base.array_code_for(fields)}]
25
+ if #{meta.key}.nil?
26
+ fields.each do |field|
27
+ self.send(:"#{relation}_\#{field}=", nil)
28
+ end
29
+ else
30
+ fields.each do |field|
31
+ self.send(:"#{relation}_\#{field}=", #{relation}.send(field))
32
+ end
33
+ end
34
+
35
+ true
36
+ end
37
+ EOM
38
+ #klass.class_eval callback_code
39
+
40
+ callback_code = <<EOM
41
+ around_save :denormalize_to_#{inverse_relation}
42
+
43
+ def denormalize_to_#{inverse_relation}
44
+ return if !changed? && !new_record?
45
+ was_new = new_record?
46
+ was_added = false
47
+ was_removed = false
48
+
49
+ fields = [#{Base.array_code_for(fields_only)}]
50
+
51
+ remote_id = send(:#{inverse_meta.key})
52
+
53
+ to_rem = {}
54
+ to_add = {}
55
+ if #{inverse_meta.key}_changed?
56
+ changed_fields = fields
57
+ if !#{inverse_meta.key}.nil? && !#{inverse_meta.key}_was.nil?
58
+ was_added = true
59
+ changed_fields.each do |field|
60
+ to_add[:"#{relation}_\#{field}"] = send(field)
61
+ end
62
+ denormalize_to_#{inverse_relation}_old
63
+ else
64
+ if #{inverse_meta.key}_was.nil?
65
+ was_added = true
66
+ changed_fields.each do |field|
67
+ to_add[:"#{relation}_\#{field}"] = send(:"\#{field}_changed?") ? send(field) : send(:"\#{field}_was")
68
+ end
69
+ else
70
+ was_removed = true
71
+ remote_id = send(:#{inverse_meta.key}_was)
72
+ changed_fields.each do |field|
73
+ to_rem[:"#{relation}_\#{field}"] = send(:"\#{field}_changed?") ? send(:"\#{field}_was") : send(field)
74
+ end
75
+ end
76
+ end
77
+ else
78
+ changed_fields = fields & changed.map(&:to_sym)
79
+ changed_fields.each do |field|
80
+ to_rem[:"#{relation}_\#{field}"] = send(:"\#{field}_was")
81
+ to_add[:"#{relation}_\#{field}"] = send(field)
82
+ end
83
+ end
84
+
85
+ yield
86
+ return if changed_fields.empty?
87
+
88
+ to_update = { "$set" => {}, "$inc" => {} }
89
+ to_push = {}
90
+ to_get = {}
91
+
92
+ to_rem_fields = to_rem.reject {|k,v| v.nil?}.keys
93
+ to_add_fields = to_add.reject {|k,v| v.nil?}.keys
94
+
95
+ # Those to add only
96
+ (to_add_only_fields = to_add_fields - to_rem_fields).each do |field|
97
+ to_push[field] = to_add[field]
98
+ end
99
+
100
+ to_set_fields = (to_add_fields + to_rem_fields - to_add_only_fields).uniq
101
+
102
+ to_get.merge! Hash[to_set_fields.map{ |f| [f, 1] }] unless to_set_fields.empty?
103
+
104
+ obj = #{klass}.collection.find("$query" => {:_id => remote_id}, "$only" => to_get).first unless to_get.empty?
105
+
106
+ to_set_fields.each do |field|
107
+ array = obj[field.to_s] || []
108
+
109
+ if to_rem_fields.include? field
110
+ (i = array.index(to_rem[field])) and array.delete_at(i)
111
+ end
112
+ if to_add_fields.include? field
113
+ array << to_add[field]
114
+ end
115
+
116
+ to_update["$set"][field] = array
117
+ end
118
+
119
+
120
+ to_update["$inc"][:#{relation}_count] = 1 if #{has_count?} && (was_new || was_added)
121
+ to_update["$inc"][:#{relation}_count] = -1 if #{has_count?} && (was_removed)
122
+
123
+ #{klass}.collection.find(:_id => remote_id).update_all(to_update) unless to_update.empty?
124
+
125
+
126
+ #{klass}.collection.find(:_id => remote_id).update_all({"$push" => to_push}) unless to_push.empty?
127
+ end
128
+
129
+ def denormalize_to_#{inverse_relation}_old
130
+ fields = [#{Base.array_code_for(fields_only)}]
131
+
132
+ remote_id = send(:#{inverse_meta.key}_was)
133
+
134
+ to_rem = {}
135
+ fields.each do |field|
136
+ to_rem[:"#{relation}_\#{field}"] = send(:"\#{field}_was")
137
+ end
138
+
139
+ to_update = { "$set" => {}, "$inc" => {} }
140
+ to_get = {}
141
+
142
+ to_rem_fields = to_rem.reject {|k,v| v.nil?}.keys
143
+
144
+ to_set_fields = to_rem_fields
145
+
146
+ to_get.merge! Hash[to_set_fields.map{ |f| [f, 1] }] unless to_set_fields.empty?
147
+
148
+ obj = #{klass}.collection.find("$query" => {:_id => remote_id}, "$only" => to_get).first unless to_get.empty?
149
+
150
+ to_set_fields.each do |field|
151
+ array = obj[field.to_s] || []
152
+
153
+ if to_rem_fields.include? field
154
+ (i = array.index(to_rem[field])) and array.delete_at(i)
155
+ end
156
+
157
+ to_update["$set"][field] = array
158
+ end
159
+
160
+ to_update["$inc"][:#{relation}_count] = -1 if #{has_count?}
161
+
162
+ #{klass}.collection.find(:_id => remote_id).update_all(to_update) unless to_update.empty?
163
+ end
164
+
165
+
166
+
167
+ around_destroy :denormalize_to_#{inverse_relation}_destroy
168
+
169
+ def denormalize_to_#{inverse_relation}_destroy
170
+ fields = [#{Base.array_code_for(fields)}]
171
+
172
+ remote_id = send(:#{inverse_meta.key})
173
+
174
+ to_rem = {}
175
+ fields.each do |field|
176
+ to_rem[:"#{relation}_\#{field}"] = send(field)
177
+ end
178
+
179
+ yield
180
+
181
+ to_update = { "$set" => {}, "$inc" => {} }
182
+ to_push = {}
183
+ to_get = {}
184
+
185
+ to_rem_fields = to_rem.reject {|k,v| v.nil?}.keys
186
+
187
+ to_get.merge! Hash[to_rem_fields.map{ |f| [f, 1] }]
188
+ obj = #{klass}.collection.find("$query" => {:_id => remote_id}, "$only" => to_get).first unless to_get.empty?
189
+
190
+ to_rem_fields.each do |field|
191
+ array = obj[field.to_s] || []
192
+
193
+ if to_rem_fields.include? field
194
+ (i = array.index(to_rem[field])) and array.delete_at(i)
195
+ end
196
+
197
+ to_update["$set"][field] = array
198
+ end
199
+
200
+ to_update["$inc"][:#{relation}_count] = -1 if #{has_count?}
201
+
202
+ #{klass}.collection.find(:_id => remote_id).update_all(to_update) unless to_update.empty?
203
+ end
204
+ EOM
205
+ meta.klass.class_eval callback_code
206
+ puts callback_code
207
+ end
208
+
209
+ def has_count?
210
+ !options[:count].nil?
211
+ end
212
+
213
+ end
214
+
215
+ end
216
+ end
217
+ end
218
+
@@ -0,0 +1,86 @@
1
+ # encoding: utf-8
2
+ module Mongoid
3
+ module Max
4
+ module Denormalize
5
+
6
+ class OneToMany < Base
7
+
8
+ def attach
9
+
10
+ fields.each do |field|
11
+ field_meta = meta.klass.fields[field.to_s]
12
+ klass.field "#{relation}_#{field}", type: field_meta.try(:type)
13
+ end
14
+
15
+ callback_code = <<EOM
16
+ before_save :denormalize_from_#{relation}
17
+
18
+ def denormalize_from_#{relation}
19
+ return unless #{meta.key}_changed?
20
+
21
+ fields = [#{Base.array_code_for(fields)}]
22
+ if #{meta.key}.nil?
23
+ fields.each do |field|
24
+ self.send(:"#{relation}_\#{field}=", nil)
25
+ end
26
+ else
27
+ fields.each do |field|
28
+ self.send(:"#{relation}_\#{field}=", #{relation}.send(field))
29
+ end
30
+ end
31
+
32
+ true
33
+ end
34
+ EOM
35
+ klass.class_eval callback_code
36
+
37
+ callback_code = <<EOM
38
+ around_save :denormalize_to_#{inverse_relation}
39
+
40
+ def denormalize_to_#{inverse_relation}
41
+ return unless changed?
42
+
43
+ fields = [#{Base.array_code_for(fields)}]
44
+ fields_only = [#{Base.array_code_for(fields_only)}]
45
+ methods = [#{Base.array_code_for(fields_methods)}]
46
+ changed_fields = fields_only & changed.map(&:to_sym)
47
+
48
+ yield
49
+
50
+ return if changed_fields.count == 0
51
+
52
+ to_set = {}
53
+ (changed_fields + methods).each do |field|
54
+ to_set[:"#{relation}_\#{field}"] = send(field)
55
+ end
56
+
57
+ #{inverse_relation}.update to_set
58
+ end
59
+
60
+ around_destroy :denormalize_to_#{inverse_relation}_destroy
61
+
62
+ def denormalize_to_#{inverse_relation}_destroy
63
+ fields = [#{Base.array_code_for(fields)}]
64
+
65
+ yield
66
+
67
+ to_set = {}
68
+ fields.each do |field|
69
+ to_set[:"#{relation}_\#{field}"] = nil
70
+ end
71
+
72
+ #{inverse_relation}.update to_set
73
+ end
74
+ EOM
75
+ meta.klass.class_eval callback_code
76
+
77
+
78
+
79
+ end
80
+
81
+ end
82
+
83
+ end
84
+ end
85
+ end
86
+
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+ module Mongoid
3
+ module Max
4
+ module Denormalize
5
+ module Version
6
+
7
+ STRING = '0.0.1'
8
+
9
+ end
10
+ end
11
+ end
12
+ end
13
+
@@ -0,0 +1,52 @@
1
+ # encoding: utf-8
2
+ module Mongoid
3
+ module Max
4
+ module Denormalize
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+
9
+ end
10
+
11
+ module ClassMethods
12
+
13
+ def denormalize(relation, *fields)
14
+ options = fields.extract_options!
15
+
16
+ #TODO make all real fields if fields.empty?
17
+
18
+ # puts "#relation : #{relation.inspect}"
19
+ # puts "#fields : #{fields.inspect}"
20
+ # puts "#options : #{options.inspect}"
21
+
22
+ meta = self.relations[relation.to_s]
23
+ raise ConfigError.new("Unknown relation :#{relation}", self) if meta.nil?
24
+ # puts "# meta : #{meta.inspect}"
25
+
26
+ inverse_meta = meta.klass.relations[meta.inverse.to_s]
27
+ raise ConfigError.new("Unknown inverse relation for :#{relation}", self) if inverse_meta.nil?
28
+ # puts "#inverse_meta : #{inverse_meta.inspect}"
29
+
30
+ methods = []
31
+ fields.each do |field|
32
+ unless meta.klass.instance_methods.include? field
33
+ raise ConfigError.new("Unknown field or method :#{field} in :#{relation}", self)
34
+ end
35
+ end
36
+
37
+ if meta.relation == Mongoid::Relations::Referenced::In && inverse_meta.relation == Mongoid::Relations::Referenced::Many
38
+ OneToMany.new(self, meta, inverse_meta, fields, options).attach
39
+ elsif meta.relation == Mongoid::Relations::Referenced::Many && inverse_meta.relation == Mongoid::Relations::Referenced::In
40
+ ManyToOne.new(self, meta, inverse_meta, fields, options).attach
41
+ else
42
+ raise ConfigError.new("Relation not supported :#{relation}", self)
43
+ end
44
+
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+ end
51
+ end
52
+
@@ -0,0 +1,11 @@
1
+ # encoding: utf-8
2
+
3
+ require 'active_support/core_ext'
4
+
5
+ require 'mongoid/max/denormalize'
6
+ require 'mongoid/max/denormalize/config_error'
7
+
8
+ require 'mongoid/max/denormalize/base'
9
+ require 'mongoid/max/denormalize/one_to_many'
10
+ require 'mongoid/max/denormalize/many_to_one'
11
+
@@ -0,0 +1,82 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ #
5
+ # The case
6
+ #
7
+ class Contact
8
+ include Mongoid::Document
9
+
10
+ field :name, type: String
11
+
12
+ has_many :addresses
13
+ end
14
+
15
+ class Person < Contact
16
+ end
17
+
18
+ class Address
19
+ include Mongoid::Document
20
+ include Mongoid::Max::Denormalize
21
+
22
+ belongs_to :contact
23
+
24
+ denormalize :contact, :name
25
+ end
26
+
27
+ #
28
+ # The specs
29
+ #
30
+ describe "Case: a contact and his addresses" do
31
+
32
+ before do
33
+ @person = Person.create!(name: "John Doe")
34
+ end
35
+
36
+ context "when nothing" do
37
+ context "considering the person" do
38
+ subject { @person }
39
+
40
+ its(:addresses) { should be_empty }
41
+ end
42
+ end
43
+
44
+ context "when adding a first address" do
45
+ before do
46
+ @address = @person.addresses.create!
47
+ end
48
+
49
+ context "considering the person" do
50
+ subject { @person }
51
+
52
+ its(:addresses) { should have(1).address }
53
+ end
54
+
55
+ context "considering the address" do
56
+ subject { @address }
57
+
58
+ its(:contact_name) { should eq @person.name }
59
+
60
+ context "when changing the person name" do
61
+ before do
62
+ @person.name = "John Doe Jr."
63
+ @person.save!
64
+ @address.reload
65
+ end
66
+
67
+ its(:contact_name) { should eq @person.name }
68
+ end
69
+
70
+ context "when destroying the person" do
71
+ before do
72
+ @person.destroy
73
+ @address.reload
74
+ end
75
+
76
+ its(:contact_name) { should be_nil }
77
+ end
78
+ end
79
+ end
80
+
81
+ end
82
+
@@ -0,0 +1,158 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ #
5
+ # The case
6
+ #
7
+ class Post
8
+ include Mongoid::Document
9
+
10
+ field :title, type: String
11
+
12
+ def slug
13
+ title.try(:parameterize)
14
+ end
15
+
16
+ has_many :comments
17
+ end
18
+
19
+ class Comment
20
+ include Mongoid::Document
21
+ include Mongoid::Max::Denormalize
22
+
23
+ belongs_to :post
24
+
25
+ denormalize :post, :title, :slug
26
+ end
27
+
28
+ #
29
+ # The specs
30
+ #
31
+ describe "Case: a post and his comments" do
32
+
33
+ before do
34
+ @post = Post.create!(title: "A good title !")
35
+ end
36
+
37
+ context "when nothing" do
38
+ context "considering the post" do
39
+ subject { @post }
40
+
41
+ its(:comments) { should be_empty }
42
+ end
43
+ end
44
+
45
+ comments_number = 2
46
+ context "when adding #{comments_number} comments" do
47
+ before do
48
+ @comments = []
49
+ comments_number.times do
50
+ @comments << @post.comments.create!
51
+ end
52
+ end
53
+
54
+ context "considering the post" do
55
+ subject { @post }
56
+
57
+ its(:comments) { should have(comments_number).comment }
58
+ end
59
+
60
+ (0..comments_number-1).each do |i|
61
+ context "considering the comment #{i}" do
62
+ subject { @comments[i] }
63
+
64
+ its(:post_title) { should eq @post.title }
65
+ its(:post_slug) { should eq @post.slug }
66
+ end
67
+ end
68
+
69
+ context "when changing the post title" do
70
+ before do
71
+ @post.title = "A new title !"
72
+ @post.save!
73
+ @comments.each(&:reload)
74
+ end
75
+
76
+ (0..comments_number-1).each do |i|
77
+ context "considering the comment #{i}" do
78
+ subject { @comments[i] }
79
+
80
+ its(:post_title) { should eq @post.title }
81
+ its(:post_slug) { should eq @post.slug }
82
+ end
83
+ end
84
+ end
85
+
86
+ context "when destroying the post" do
87
+ before do
88
+ @post.destroy
89
+ @comments.each(&:reload)
90
+ end
91
+
92
+ (0..comments_number-1).each do |i|
93
+ context "considering the comment #{i}" do
94
+ subject { @comments[i] }
95
+
96
+ its(:post_title) { should be_nil }
97
+ its(:post_slug) { should be_nil }
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ context "when creating a comment without associating it" do
104
+ before do
105
+ @comment_only = Comment.create!
106
+ @post.reload
107
+ end
108
+
109
+ subject { @comment_only }
110
+
111
+ its(:post_title) { should be_nil }
112
+ its(:post_slug) { should be_nil }
113
+
114
+ context "when associating it" do
115
+ before do
116
+ @comment_only.post = @post
117
+ @comment_only.save!
118
+ end
119
+
120
+ its(:post_title) { should eq @post.title }
121
+ its(:post_slug) { should eq @post.slug }
122
+ end
123
+
124
+ context "when associating it (2nd way)" do
125
+ before do
126
+ @post.comments << @comment_only
127
+ @comment_only.reload
128
+ end
129
+
130
+ its(:post_title) { should eq @post.title }
131
+ its(:post_slug) { should eq @post.slug }
132
+ end
133
+ end
134
+
135
+ context "when has a comment" do
136
+ before do
137
+ @comment = @post.comments.create!
138
+ end
139
+
140
+ subject { @comment }
141
+
142
+ its(:post_title) { should eq @post.title }
143
+ its(:post_slug) { should eq @post.slug }
144
+
145
+ context "when associating the comment to another post" do
146
+ before do
147
+ @other_post = Post.create!(title: "Another title.")
148
+ @comment.post = @other_post
149
+ @comment.save
150
+ end
151
+
152
+ its(:post_title) { should eq @other_post.title }
153
+ its(:post_slug) { should eq @other_post.slug }
154
+ end
155
+ end
156
+
157
+ end
158
+
@@ -0,0 +1,202 @@
1
+ # encoding: utf-8
2
+ require 'spec_helper'
3
+
4
+ #
5
+ # The case
6
+ #
7
+ class Rating
8
+ include Mongoid::Document
9
+
10
+ field :note, type: Integer
11
+ field :comment, type: String
12
+
13
+ belongs_to :song
14
+ end
15
+
16
+ class Song
17
+ include Mongoid::Document
18
+ include Mongoid::Max::Denormalize
19
+
20
+ has_many :ratings
21
+
22
+ denormalize :ratings, :note, :comment, count: true, mean: [:note]
23
+ end
24
+
25
+
26
+ #
27
+ # The specs
28
+ #
29
+ describe "Case: a song and his ratings" do
30
+
31
+ before do
32
+ @song = Song.create!
33
+ end
34
+
35
+ context "when nothing" do
36
+ context "considering the song" do
37
+ subject { @song }
38
+
39
+ it "should not have ratings" do
40
+ @song.ratings.should be_empty
41
+ end
42
+ end
43
+ end
44
+
45
+ context "when adding a first rating (=5)" do
46
+ before do
47
+ @rating = @song.ratings.create!(note: 5, comment: "Good!")
48
+ @song.reload
49
+ end
50
+
51
+ context "considering the song" do
52
+ subject { @song }
53
+
54
+ its(:ratings) { should have(1).rating }
55
+ its(:ratings_count) { should eq 1 }
56
+ its(:ratings_note) { should eq [5] }
57
+ its(:ratings_comment) { should eq ["Good!"] }
58
+
59
+ # its(:ratings_mean) { should eq 5 }
60
+
61
+ context "when modifing the first rating (=4)" do
62
+ before do
63
+ @rating.note = 4
64
+ @rating.save!
65
+ @song.reload
66
+ end
67
+
68
+ its(:ratings) { should have(1).rating }
69
+ its(:ratings_count) { should eq 1 }
70
+ its(:ratings_note) { should eq [4] }
71
+ its(:ratings_comment) { should eq ["Good!"] }
72
+ end
73
+
74
+ context "when adding an other rating (=5)" do
75
+ before do
76
+ @other_rating = @song.ratings.create!(note: 5, comment: "Another good!")
77
+ @song.reload
78
+ end
79
+
80
+ its(:ratings) { should have(2).rating }
81
+ its(:ratings_count) { should eq 2 }
82
+ its(:ratings_note) { should eq [5, 5] }
83
+ its(:ratings_comment) { should eq ["Good!", "Another good!"] }
84
+
85
+ context "when modifing the first rating (=4)" do
86
+ before do
87
+ @rating.note = 4
88
+ @rating.save!
89
+ @song.reload
90
+ end
91
+
92
+ its(:ratings_count) { should eq 2 }
93
+ its(:ratings_note) { should eq [5, 4] }
94
+ its(:ratings_comment) { should eq ["Good!", "Another good!"] }
95
+ end
96
+
97
+ context "when modifing the other rating (=4)" do
98
+ before do
99
+ @other_rating.note = 4
100
+ @other_rating.save!
101
+ @song.reload
102
+ end
103
+
104
+ its(:ratings_count) { should eq 2 }
105
+ its(:ratings_note) { should eq [5, 4] }
106
+ its(:ratings_comment) { should eq ["Good!", "Another good!"] }
107
+
108
+ context "when modifing again the other rating (=3)" do
109
+ before do
110
+ @other_rating.note = 3
111
+ @other_rating.comment = "Another good, again!"
112
+ @other_rating.save!
113
+ @song.reload
114
+ end
115
+
116
+ its(:ratings_count) { should eq 2 }
117
+ its(:ratings_note) { should eq [5, 3] }
118
+ its(:ratings_comment) { should eq ["Good!", "Another good, again!"] }
119
+ end
120
+ end
121
+
122
+ context "when destroying the other rating" do
123
+ before do
124
+ @other_rating.destroy
125
+ @song.reload
126
+ end
127
+
128
+ its(:ratings_count) { should eq 1 }
129
+ its(:ratings) { should have(1).rating }
130
+ its(:ratings_note) { should eq [5] }
131
+ its(:ratings_comment) { should eq ["Good!"] }
132
+ end
133
+ end
134
+
135
+ context "when creating a rating (=1) without associating it" do
136
+ before do
137
+ @rating_only = Rating.create!(note: 1, comment: "Bad")
138
+ @song.reload
139
+ end
140
+
141
+ its(:ratings) { should have(1).rating }
142
+ its(:ratings_count) { should eq 1 }
143
+ its(:ratings_note) { should eq [5] }
144
+ its(:ratings_comment) { should eq ["Good!"] }
145
+
146
+ context "when associating it" do
147
+ before do
148
+ @rating_only.song = @song
149
+ @rating_only.save!
150
+ @song.reload
151
+ end
152
+
153
+ its(:ratings) { should have(2).rating }
154
+ its(:ratings_count) { should eq 2 }
155
+ its(:ratings_note) { should eq [5, 1] }
156
+ its(:ratings_comment) { should eq ["Good!", "Bad"] }
157
+ end
158
+
159
+ context "when associating it (2nd way)" do
160
+ before do
161
+ @song.ratings << @rating_only
162
+ @song.reload
163
+ end
164
+
165
+ its(:ratings) { should have(2).rating }
166
+ its(:ratings_count) { should eq 2 }
167
+ its(:ratings_note) { should eq [5, 1] }
168
+ its(:ratings_comment) { should eq ["Good!", "Bad"] }
169
+ end
170
+ end
171
+
172
+ context "when associating to another song" do
173
+ before do
174
+ @other_song = Song.create!
175
+ @rating.song = @other_song
176
+ @rating.save!
177
+ @song.reload
178
+ end
179
+
180
+ its(:ratings) { should have(0).rating }
181
+ its(:ratings_count) { should eq 0 }
182
+ its(:ratings_note) { should eq [] }
183
+ its(:ratings_comment) { should eq [] }
184
+
185
+ context "considering the other song" do
186
+ before do
187
+ @other_song.reload
188
+ end
189
+
190
+ subject { @other_song }
191
+
192
+ its(:ratings) { should have(1).rating }
193
+ its(:ratings_count) { should eq 1 }
194
+ its(:ratings_note) { should eq [5] }
195
+ its(:ratings_comment) { should eq ["Good!"] }
196
+ end
197
+ end
198
+ end
199
+ end
200
+
201
+ end
202
+
@@ -0,0 +1,4 @@
1
+ # encoding: utf-8
2
+
3
+ require 'spec_helper'
4
+
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+ require 'rubygems'
3
+ require 'rspec'
4
+ require 'mongoid'
5
+ require 'mongoid_max_denormalize'
6
+ require 'logger'
7
+
8
+ Mongoid.configure do |config|
9
+ config.connect_to('mongoid_max_denormalize_test')
10
+ end
11
+
12
+ Mongoid.logger = Logger.new("log/mongoid-test.log")
13
+ Mongoid.logger.level = Logger::DEBUG
14
+ Moped.logger = Logger.new("log/moped-test.log")
15
+ Moped.logger.level = Logger::DEBUG
16
+
17
+ RSpec.configure do |config|
18
+
19
+ # Drop all collections and clear the identity map before each spec.
20
+ config.before(:each) do
21
+ Mongoid.purge!
22
+ Mongoid::IdentityMap.clear
23
+ end
24
+
25
+ end
26
+
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongoid_max_denormalize
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Maxime Garcia
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-06-14 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mongoid
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 3.0.0.rc
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 3.0.0.rc
30
+ - !ruby/object:Gem::Dependency
31
+ name: activesupport
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '3.1'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '3.1'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '2.9'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: '2.9'
62
+ - !ruby/object:Gem::Dependency
63
+ name: guard-rspec
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: ! ' Mongoid::Max::Denormalize is a denormalization extension for Mongoid.
79
+
80
+ '
81
+ email:
82
+ - maxime.garcia@maxbusiness.fr
83
+ executables: []
84
+ extensions: []
85
+ extra_rdoc_files: []
86
+ files:
87
+ - README.md
88
+ - LICENSE
89
+ - lib/mongoid_max_denormalize.rb
90
+ - lib/mongoid/max/denormalize/one_to_many.rb
91
+ - lib/mongoid/max/denormalize/config_error.rb
92
+ - lib/mongoid/max/denormalize/version.rb
93
+ - lib/mongoid/max/denormalize/many_to_one.rb
94
+ - lib/mongoid/max/denormalize/base.rb
95
+ - lib/mongoid/max/denormalize.rb
96
+ - spec/lib/mongoid_max_denormalize_spec.rb
97
+ - spec/cases/contact_and_addresses_spec.rb
98
+ - spec/cases/post_and_comments_spec.rb
99
+ - spec/cases/song_and_notes_spec.rb
100
+ - spec/spec_helper.rb
101
+ homepage: http://github.com/maximeg/mongoid_max_denormalize
102
+ licenses: []
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ! '>='
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ none: false
115
+ requirements:
116
+ - - ! '>='
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ requirements: []
120
+ rubyforge_project:
121
+ rubygems_version: 1.8.24
122
+ signing_key:
123
+ specification_version: 3
124
+ summary: MaxMapper, polyvalent ORM for Rails
125
+ test_files:
126
+ - spec/lib/mongoid_max_denormalize_spec.rb
127
+ - spec/cases/contact_and_addresses_spec.rb
128
+ - spec/cases/post_and_comments_spec.rb
129
+ - spec/cases/song_and_notes_spec.rb
130
+ - spec/spec_helper.rb