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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/LICENSE +1 -1
- data/README.md +21 -3
- data/lib/mongoid/sleeping_king_studios.rb +11 -1
- data/lib/mongoid/sleeping_king_studios/concern.rb +69 -0
- data/lib/mongoid/sleeping_king_studios/concern/metadata.rb +61 -0
- data/lib/mongoid/sleeping_king_studios/ext/mongoid/document.rb +15 -0
- data/lib/mongoid/sleeping_king_studios/has_tree.rb +119 -68
- data/lib/mongoid/sleeping_king_studios/has_tree/cache_ancestry.rb +177 -113
- data/lib/mongoid/sleeping_king_studios/has_tree/cache_ancestry/metadata.rb +82 -0
- data/lib/mongoid/sleeping_king_studios/has_tree/children/metadata.rb +40 -0
- data/lib/mongoid/sleeping_king_studios/has_tree/errors.rb +6 -5
- data/lib/mongoid/sleeping_king_studios/has_tree/metadata.rb +64 -0
- data/lib/mongoid/sleeping_king_studios/has_tree/parent/metadata.rb +40 -0
- data/lib/mongoid/sleeping_king_studios/sluggable.rb +125 -46
- data/lib/mongoid/sleeping_king_studios/sluggable/metadata.rb +42 -0
- data/lib/mongoid/sleeping_king_studios/version.rb +2 -1
- metadata +32 -37
@@ -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
|
-
#
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
#
|
69
|
-
#
|
70
|
-
#
|
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
|
-
#
|
126
|
-
#
|
127
|
-
#
|
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
|
-
|
12
|
-
|
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
|
-
|
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
|