mongoid_max_denormalize 0.0.1

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/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