mongoid-sleeping_king_studios 0.5.0 → 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,7 @@
1
1
  # lib/mongoid/sleeping_king_studios/has_tree/cache_ancestry.rb
2
2
 
3
3
  require 'mongoid/sleeping_king_studios'
4
+ require 'mongoid/sleeping_king_studios/concern'
4
5
  require 'mongoid/sleeping_king_studios/has_tree/errors'
5
6
 
6
7
  module Mongoid::SleepingKingStudios
@@ -21,6 +22,142 @@ module Mongoid::SleepingKingStudios
21
22
  # @since 0.5.0
22
23
  module CacheAncestry
23
24
  extend ActiveSupport::Concern
25
+ extend Mongoid::SleepingKingStudios::Concern
26
+
27
+ # @api private
28
+ #
29
+ # Sets up the ancestry caching concern.
30
+ #
31
+ # @param [Class] base The base class into which the concern is mixed in.
32
+ # @param [Hash] options The options for the relation.
33
+ #
34
+ # @since 0.6.0
35
+ def self.apply base, metadata
36
+ name = :has_tree_cache_ancestry
37
+ validate_options name, metadata.properties
38
+
39
+ define_fields base, metadata
40
+ define_accessors base, metadata
41
+ define_helpers base, metadata
42
+ end # class method apply
43
+
44
+ # @api private
45
+ #
46
+ # Overwrites the parent id setter to update the ancestor ids field,
47
+ # and defines the ancestors and descendents methods.
48
+ #
49
+ # @param [Class] base The base class into which the concern is mixed in.
50
+ # @param [Metadata] metadata The metadata for the relation.
51
+ #
52
+ # @since 0.6.0
53
+ def self.define_accessors base, metadata
54
+ parent_id_writer = base.instance_method metadata.parent_foreign_key_writer
55
+
56
+ base.re_define_method metadata.parent_foreign_key_writer do |value|
57
+ old_ancestor_ids = send(metadata.foreign_key).dup
58
+
59
+ parent_id_writer.bind(self).call value
60
+ new_ancestor_ids = send(metadata.parent_name) ?
61
+ send(metadata.parent_name).send(metadata.foreign_key) + [send(metadata.parent_name).id] :
62
+ []
63
+
64
+ descendents.each do |descendent|
65
+ ary = descendent.send(metadata.foreign_key).dup
66
+ ary[0..old_ancestor_ids.count] = new_ancestor_ids + [id]
67
+ descendent.update_attributes metadata.foreign_key => ary
68
+ end # each
69
+
70
+ send :"#{metadata.foreign_key}=", new_ancestor_ids
71
+ end # method
72
+
73
+ base.send :define_method, metadata.relation_name do
74
+ begin
75
+ self.class.find(send(metadata.foreign_key))
76
+ rescue Mongoid::Errors::DocumentNotFound, Mongoid::Errors::InvalidFind
77
+ raise Mongoid::SleepingKingStudios::HasTree::Errors::MissingAncestor.new metadata.relation_name, send(metadata.foreign_key)
78
+ end # begin-rescue
79
+ end # method
80
+
81
+ base.send :define_method, :descendents do
82
+ criteria = self.class.all
83
+
84
+ send(metadata.foreign_key).each_with_index do |ancestor_id, index|
85
+ criteria = criteria.where(:"#{metadata.foreign_key}.#{index}" => ancestor_id)
86
+ end # each
87
+
88
+ criteria.where(:"#{metadata.foreign_key}.#{send(metadata.foreign_key).count}" => id)
89
+ end # method descendents
90
+ end # class method define_fields
91
+
92
+ # @api private
93
+ #
94
+ # Defines the foreign key field on the base class.
95
+ #
96
+ # @param [Class] base The base class into which the concern is mixed in.
97
+ # @param [Metadata] metadata The metadata for the relation.
98
+ #
99
+ # @since 0.6.0
100
+ def self.define_fields base, metadata
101
+ base.send :field, metadata.foreign_key, :type => Array, :default => []
102
+ end # class method define_fields
103
+
104
+ # @api private
105
+ #
106
+ # Defines the rebuild and validate ancestry helper methods.
107
+ #
108
+ # @param [Class] base The base class into which the concern is mixed in.
109
+ # @param [Metadata] metadata The metadata for the relation.
110
+ #
111
+ # @since 0.6.0
112
+ def self.define_helpers base, metadata
113
+ base.send :define_method, :rebuild_ancestry! do
114
+ begin
115
+ ary, object = [], self
116
+ while object.send(metadata.parent_name)
117
+ ary.unshift object.send(metadata.parent_name).id
118
+ object = object.send(metadata.parent_name)
119
+ end # while
120
+ self.send :"#{metadata.foreign_key}=", ary
121
+ rescue Mongoid::Errors::DocumentNotFound
122
+ raise Mongoid::SleepingKingStudios::HasTree::Errors::MissingAncestor.new "#{relation_name}", object.send(metadata.parent_foreign_key)
123
+ end # begin-rescue
124
+ end # method rebuild_ancestry!
125
+
126
+ base.send :define_method, :validate_ancestry! do
127
+ return if send(metadata.foreign_key).empty?
128
+
129
+ ancestors = []
130
+ send(metadata.foreign_key).each_with_index do |ancestor_id, index|
131
+ begin
132
+ ancestor = self.class.find(ancestor_id)
133
+ ancestors << ancestor
134
+
135
+ if index > 0 && ancestor.send(metadata.parent_foreign_key) != send(metadata.foreign_key)[index - 1]
136
+ # If the ancestor's parent is not the same as the previous
137
+ # ancestor.
138
+ raise Mongoid::SleepingKingStudios::HasTree::Errors::UnexpectedAncestor.new "#{metadata.relation_name}", ancestor.send(metadata.parent_foreign_key), send(metadata.foreign_key)[index - 1]
139
+ end # if
140
+ rescue Mongoid::Errors::InvalidFind, Mongoid::Errors::DocumentNotFound
141
+ # If the ancestor id is nil, or the ancestor does not exist.
142
+ raise Mongoid::SleepingKingStudios::HasTree::Errors::MissingAncestor.new "#{metadata.relation_name}", ancestor_id
143
+ end # begin-rescue
144
+ end # each with index
145
+ end # method validate_ancestry!
146
+ end # class method define_helpers
147
+
148
+ # Get the valid options allowed with this concern.
149
+ #
150
+ # @return [ Array<Symbol> ] The valid options.
151
+ #
152
+ # @since 0.5.1
153
+ def self.valid_options
154
+ %i(
155
+ children_name # Internal; do not set directly.
156
+ foreign_key
157
+ parent_name # Internal; do not set directly.
158
+ relation_name
159
+ ) # end Array
160
+ end # class method valid_options
24
161
 
25
162
  # @!attribute [r] ancestor_ids
26
163
  # Stores the ids of the object's ancestors, starting from the root
@@ -31,120 +168,47 @@ module Mongoid::SleepingKingStudios
31
168
  #
32
169
  # @since 0.5.0
33
170
 
34
- # Class methods added to the base class via #extend.
35
- module ClassMethods
36
- # @overload cache_ancestry()
37
- # Adds the :ancestry_id field, the #ancestors and #descendents
38
- # scopes, and redefines #parent_id= to update the :ancestry_id
39
- # field on the object and its descendents.
40
- #
41
- # Do not call this method directly; rather, add a
42
- # :cache_ancestry => true options to the call to ::has_tree.
43
- def cache_ancestry **options
44
- parent_name = options[:children][:inverse_of]
45
- children_name = options[:parent][:inverse_of]
46
-
47
- field :ancestor_ids, :type => Array, :default => []
48
-
49
- alias_method :"set_parent_id", :"#{parent_name}_id="
50
- private :set_parent_id
51
- re_define_method "#{parent_name}_id=" do |value|
52
- old_ancestor_ids = ancestor_ids.dup
53
-
54
- set_parent_id value
55
- new_ancestor_ids = parent ? parent.ancestor_ids + [parent.id] : []
56
-
57
- descendents.each do |descendent|
58
- ary = descendent.ancestor_ids.dup
59
- ary[0..old_ancestor_ids.count] = new_ancestor_ids + [id]
60
- descendent.update_attributes :ancestor_ids => ary
61
- end # each
62
-
63
- self.send :ancestor_ids=, new_ancestor_ids
64
- end # method #{parent_name}_id=
65
- end # class method cache_ancestry
66
- end # module ClassMethods
67
-
68
- # Returns an array of the current object's ancestors, from the root
69
- # object to the current parent. If the object has no parent, returns an
70
- # empty array. If an error is raised, consider calling #rebuild_ancestry!
71
- #
72
- # @raise [Mongoid::SleepingKingStudios::HasTree::Errors::MissingAncestor]
73
- # If one or more of the ancestors is not found in the datastore (the id
74
- # is wrong, the object is not persisted, there is a nil value in
75
- # ancestor_ids, and so on).
76
- #
77
- # @return [Array] The objects' ancestors
78
- def ancestors
79
- self.class.find(ancestor_ids)
80
- rescue Mongoid::Errors::DocumentNotFound, Mongoid::Errors::InvalidFind
81
- raise Mongoid::SleepingKingStudios::HasTree::Errors::MissingAncestor.new ancestor_ids
82
- end # method ancestors
83
-
84
- # Returns a scope for all of the descendents of the current object, i.e.
85
- # all objects that have the current object as an ancestor.
86
- #
87
- # @return [Mongoid::Criteria] The criteria for finding the descendents.
88
- def descendents
89
- criteria = self.class.all
90
-
91
- ancestor_ids.each_with_index do |ancestor_id, index|
92
- criteria = criteria.where(:"ancestor_ids.#{index}" => ancestor_id)
93
- end # each
94
-
95
- criteria.where(:"ancestor_ids.#{ancestor_ids.count}" => id)
96
- end # scope descendents
97
-
98
- # Travels up the tree using the #parent method and saves the ancestors to
99
- # the :ancestor_ids field. This overwrites the value of :ancestor_ids on
100
- # the current object, but not on any of its ancestors.
101
- #
102
- # @raise [Mongoid::SleepingKingStudios::HasTree::Errors::MissingAncestor]
103
- # If the current object or an ancestor has an invalid #parent.
104
- def rebuild_ancestry!
105
- ary, object = [], self
106
- while object.parent
107
- ary.unshift object.parent.id
108
- object = object.parent
109
- end # while
110
- self.send :ancestor_ids=, ary
111
- rescue Mongoid::Errors::DocumentNotFound
112
- raise Mongoid::SleepingKingStudios::HasTree::Errors::MissingAncestor.new object.parent_id
113
- end # method rebuild_ancestry!
114
-
115
- # Travels up the tree using the :ancestor_ids and ensures that each
116
- # ancestor exists and is persisted to the database, and that the
117
- # object's parent correctly matches the last value in its own
118
- # :ancestor_ids field.
119
- #
120
- # @raise [Mongoid::SleepingKingStudios::HasTree::Errors::MissingAncestor]
121
- # If any of the ancestors is not found in the datastore (the id is
122
- # wrong, the object is not persisted, there is a nil value in
123
- # ancestor_ids, and so on).
171
+ # @!method ancestors
172
+ # Returns an array of the current object's ancestors, from the root
173
+ # object to the current parent. If the object has no parent, returns an
174
+ # empty array. If an error is raised, consider calling
175
+ # #rebuild_ancestry!
176
+ #
177
+ # @raise [Mongoid::SleepingKingStudios::HasTree::Errors::MissingAncestor]
178
+ # If one or more of the ancestors is not found in the datastore (the
179
+ # id is wrong, the object is not persisted, there is a nil value in
180
+ # ancestor_ids, and so on).
181
+ #
182
+ # @return [Array] The objects' ancestors
183
+
184
+ # @!method descendents
185
+ # Returns a scope for all of the descendents of the current object,
186
+ # i.e. all objects that have the current object as an ancestor.
187
+ #
188
+ # @return [Mongoid::Criteria] The criteria for finding the descendents.
189
+
190
+ # @!method rebuild_ancestry!
191
+ # Travels up the tree using the #parent method and saves the ancestors
192
+ # to the :ancestor_ids field. This overwrites the value of
193
+ # :ancestor_ids on the current object, but not on any of its ancestors.
194
+ #
195
+ # @raise [Mongoid::SleepingKingStudios::HasTree::Errors::MissingAncestor]
196
+ # If the current object or an ancestor has an invalid #parent.
197
+
198
+ # @!method validate_ancestry!
199
+ # Travels up the tree using the :ancestor_ids and ensures that each
200
+ # ancestor exists and is persisted to the database, and that the
201
+ # object's parent correctly matches the last value in its own
202
+ # :ancestor_ids field.
203
+ #
204
+ # @raise [Mongoid::SleepingKingStudios::HasTree::Errors::MissingAncestor]
205
+ # If any of the ancestors is not found in the datastore (the id is
206
+ # wrong, the object is not persisted, there is a nil value in
207
+ # ancestor_ids, and so on).
124
208
  #
125
- # @raise [Mongoid::SleepingKingStudios::HasTree::Errors::UnexpectedAncestor]
126
- # If there is a mismatch between an object's #parent and the last value
127
- # in the object's :ancestor_ids field.
128
- def validate_ancestry!
129
- return if ancestor_ids.empty?
130
-
131
- ancestors = []
132
- ancestor_ids.each_with_index do |ancestor_id, index|
133
- begin
134
- ancestor = self.class.find(ancestor_id)
135
- ancestors << ancestor
136
-
137
- if index > 0 && ancestor.parent_id != ancestor_ids[index - 1]
138
- # If the ancestor's parent is not the same as the previous
139
- # ancestor.
140
- raise Mongoid::SleepingKingStudios::HasTree::Errors::UnexpectedAncestor.new ancestor.parent_id, ancestor_ids[index - 1]
141
- end # if
142
- rescue Mongoid::Errors::InvalidFind, Mongoid::Errors::DocumentNotFound
143
- # If the ancestor id is nil, or the ancestor does not exist.
144
- raise Mongoid::SleepingKingStudios::HasTree::Errors::MissingAncestor.new ancestor_id
145
- end # begin-rescue
146
- end # each
147
- end # method validate_ancestry!
209
+ # @raise [Mongoid::SleepingKingStudios::HasTree::Errors::UnexpectedAncestor]
210
+ # If there is a mismatch between an object's #parent and the last
211
+ # value in the object's :ancestor_ids field.
148
212
  end # module
149
213
  end # module
150
214
  end # module
@@ -0,0 +1,82 @@
1
+ # lib/mongoid/sleeping_king_studios/has_tree/cache_ancestry/metadata.rb
2
+
3
+ require 'mongoid/sleeping_king_studios/concern/metadata'
4
+
5
+ module Mongoid::SleepingKingStudios
6
+ module HasTree
7
+ module CacheAncestry
8
+ # Stores information about a HasTree::CacheAncestry concern.
9
+ class Metadata < Mongoid::SleepingKingStudios::Concern::Metadata
10
+ # The name of the children relation for the tree.
11
+ #
12
+ # @return [Symbol] The relation name.
13
+ def children_name
14
+ self[:children_name] || :children
15
+ end # method parent_name
16
+
17
+ # @return [Boolean] True if a custom children relation name is set;
18
+ # otherwise false.
19
+ def children_name?
20
+ !!self[:children_name]
21
+ end # method children_name?
22
+
23
+ # The name of the field used to store ancestor references. If no
24
+ # foreign key is set, uses the relation name and the field name to
25
+ # generate a key.
26
+ #
27
+ # @return [Symbol] The field name.
28
+ def foreign_key
29
+ self[:foreign_key] || :"#{relation_name.to_s.singularize}_ids"
30
+ end # method foreign_key
31
+
32
+ # The name of the field used to store the tree's parent relation.
33
+ #
34
+ # @return [Symbol] The method name.
35
+ def parent_foreign_key
36
+ :"#{parent_name}_id"
37
+ end # method parent_foreign_key
38
+
39
+ # The writer for the tree's parent relation id.
40
+ #
41
+ # @return [Symbol] The method name.
42
+ def parent_foreign_key_writer
43
+ :"#{parent_name}_id="
44
+ end # method parent_foreign_key_writer
45
+
46
+ # The name of the parent relation for the tree.
47
+ #
48
+ # @return [Symbol] The relation name.
49
+ def parent_name
50
+ self[:parent_name] || :parent
51
+ end # method parent_name
52
+
53
+ # @return [Boolean] True if a custom children relation name is set;
54
+ # otherwise false.
55
+ def parent_name?
56
+ !!self[:parent_name]
57
+ end # method parent_name?
58
+
59
+ # The writer for the tree's parent relation.
60
+ #
61
+ # @return [Symbol] The method name.
62
+ def parent_writer
63
+ :"#{parent_name}="
64
+ end # method parent_name
65
+
66
+ # The name of the tree's ancestors method. If no relation name is set,
67
+ # defaults to :ancestors.
68
+ #
69
+ # @return [Symbol] The relation name.
70
+ def relation_name
71
+ fetch(:relation_name, :ancestors)
72
+ end # method relation_name
73
+
74
+ # @return [Boolean] True if a custom relation name is set; otherwise
75
+ # false.
76
+ def relation_name?
77
+ !!self[:relation_name]
78
+ end # method relation_name?
79
+ end # class
80
+ end # module
81
+ end # module
82
+ end # module
@@ -0,0 +1,40 @@
1
+ # lib/mongoid/sleeping_king_studios/has_tree/children/metadata.rb
2
+
3
+ require 'mongoid/sleeping_king_studios/concern/metadata'
4
+
5
+ module Mongoid::SleepingKingStudios
6
+ module HasTree
7
+ module Children
8
+ # Stores information about a HasTree concern's children relation.
9
+ class Metadata < Mongoid::SleepingKingStudios::Concern::Metadata
10
+ # The name of the tree's parent relation. If no relation name is set,
11
+ # defaults to :parent.
12
+ #
13
+ # @return [Symbol] The relation name.
14
+ def inverse_of
15
+ fetch(:inverse_of, :parent)
16
+ end # method inverse_of
17
+
18
+ # @return [Boolean] True if a custom inverse relation name is set;
19
+ # otherwise false.
20
+ def inverse_of?
21
+ !!self[:inverse_of]
22
+ end # method inverse_of?
23
+
24
+ # The name of the tree's children relation. If no relation name is set,
25
+ # defaults to :children.
26
+ #
27
+ # @return [Symbol] The relation name.
28
+ def relation_name
29
+ fetch(:relation_name, :children)
30
+ end # method relation_name
31
+
32
+ # @return [Boolean] True if a custom relation name is set; otherwise
33
+ # false.
34
+ def relation_name?
35
+ !!self[:relation_name]
36
+ end # method relation_name?
37
+ end # class
38
+ end # module
39
+ end # module
40
+ end # module
@@ -6,17 +6,18 @@ module Mongoid::SleepingKingStudios
6
6
  module HasTree
7
7
  module Errors
8
8
  class MissingAncestor < Mongoid::SleepingKingStudios::Error
9
- def initialize ancestor_id
9
+ def initialize relation_name, ancestor_id
10
10
  message = Array === ancestor_id ?
11
- 'ancestors with ids' :
12
- 'ancestor with id'
11
+ "#{relation_name.to_s.pluralize} with ids" :
12
+ "#{relation_name.to_s.singularize} with id"
13
13
  super "unable to find #{message} #{ancestor_id.inspect}"
14
14
  end # constructor
15
15
  end # class
16
16
 
17
17
  class UnexpectedAncestor < Mongoid::SleepingKingStudios::Error
18
- def initialize expected_id, received_id
19
- super "expected ancestor with id #{expected_id.inspect}, but received id #{received_id.inspect}"
18
+ def initialize relation_name, expected_id, received_id
19
+ message = "expected #{relation_name.to_s.singularize} with id"
20
+ super "#{message} #{expected_id.inspect}, but received id #{received_id.inspect}"
20
21
  end # constructor
21
22
  end # class
22
23
  end # module Errors