ancestry 3.0.7 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +82 -31
- data/ancestry.gemspec +5 -7
- data/lib/ancestry.rb +0 -1
- data/lib/ancestry/class_methods.rb +22 -18
- data/lib/ancestry/has_ancestry.rb +14 -22
- data/lib/ancestry/instance_methods.rb +21 -113
- data/lib/ancestry/materialized_path.rb +100 -17
- data/lib/ancestry/version.rb +1 -1
- metadata +10 -38
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9455bbdda28f2f0084ff4b1e96af32ca5d6b3efe40ae062c51f75b3fee84fb30
|
4
|
+
data.tar.gz: 5b06653e17bbe2a238c4d49d9147dd79f2156c33432298290b5b17da190ce203
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4cb481c338a78cd5c294a40ccc43ccbad27232cc0a646b4827fd2446ad3d3fd626410f4a35d3b9361b367c2474ba229113aa191ba7799ddcc7930b1d29eab106
|
7
|
+
data.tar.gz: 6876e974e00c0ee4c5bb4ea4facbfb9ce12fe04eebfab2bc4623d6a74c9591781d3945b4999437054a81fb1ea82a656822c679e38c434541173ee7d192d664dc
|
data/README.md
CHANGED
@@ -4,12 +4,12 @@
|
|
4
4
|
|
5
5
|
Ancestry is a gem that allows the records of a Ruby on Rails
|
6
6
|
ActiveRecord model to be organised as a tree structure (or hierarchy). It uses
|
7
|
-
a single database column,
|
8
|
-
relations (ancestors, parent, root, children, siblings, descendants) and all
|
9
|
-
of them
|
7
|
+
a single database column, employing the materialised path pattern. It exposes all the standard tree structure
|
8
|
+
relations (ancestors, parent, root, children, siblings, descendants) and allows all
|
9
|
+
of them to be fetched in a single SQL query. Additional features are STI
|
10
10
|
support, scopes, depth caching, depth constraints, easy migration from older
|
11
11
|
gems, integrity checking, integrity restoration, arrangement of
|
12
|
-
(sub)
|
12
|
+
(sub)trees into hashes, and various strategies for dealing with orphaned
|
13
13
|
records.
|
14
14
|
|
15
15
|
NOTE:
|
@@ -55,7 +55,7 @@ $ rake db:migrate
|
|
55
55
|
|
56
56
|
|
57
57
|
## Add ancestry to your model
|
58
|
-
* Add to
|
58
|
+
* Add to app/models/[model.rb]:
|
59
59
|
|
60
60
|
```ruby
|
61
61
|
# app/models/[model.rb]
|
@@ -69,9 +69,9 @@ Your model is now a tree!
|
|
69
69
|
|
70
70
|
# Using acts_as_tree instead of has_ancestry
|
71
71
|
|
72
|
-
In version 1.2.0 the **acts_as_tree** method was **renamed to has_ancestry**
|
72
|
+
In version 1.2.0, the **acts_as_tree** method was **renamed to has_ancestry**
|
73
73
|
in order to allow usage of both the acts_as_tree gem and the ancestry gem in a
|
74
|
-
single application.
|
74
|
+
single application. The `acts_as_tree` method will continue to be supported in the future.
|
75
75
|
|
76
76
|
# Organising records into a tree
|
77
77
|
|
@@ -107,13 +107,13 @@ To navigate an Ancestry model, use the following instance methods:
|
|
107
107
|
|`ancestors?` |true if the record has ancestors (aka not a root node)|
|
108
108
|
|`ancestor_ids` |ancestor ids of the record|
|
109
109
|
|`path` |path of the record, starting with the root and ending with self|
|
110
|
-
|`path_ids` |a list the path ids, starting with the root id and ending with the node's own id|
|
110
|
+
|`path_ids` |a list of the path ids, starting with the root id and ending with the node's own id|
|
111
111
|
|`children` |direct children of the record|
|
112
112
|
|`child_ids` |direct children's ids|
|
113
113
|
|`has_parent?` <br/> `ancestors?` |true if the record has a parent, false otherwise|
|
114
114
|
|`has_children?` <br/> `children?` |true if the record has any children, false otherwise|
|
115
115
|
|`is_childless?` <br/> `childless?` |true is the record has no children, false otherwise|
|
116
|
-
|`siblings` |siblings of the record, the record itself
|
116
|
+
|`siblings` |siblings of the record, including the record itself*|
|
117
117
|
|`sibling_ids` |sibling ids|
|
118
118
|
|`has_siblings?` <br/> `siblings?` |true if the record's parent has more than one child|
|
119
119
|
|`is_only_child?` <br/> `only_child?` |true if the record is the only child of its parent|
|
@@ -123,7 +123,7 @@ To navigate an Ancestry model, use the following instance methods:
|
|
123
123
|
|`indirect_ids` |indirect children's ids of the record|
|
124
124
|
|`subtree` |the model on descendants and itself|
|
125
125
|
|`subtree_ids` |a list of all ids in the record's subtree|
|
126
|
-
|`depth` |the depth of the node
|
126
|
+
|`depth` |the depth of the node (root nodes are at depth 0)|
|
127
127
|
|
128
128
|
\* If the record is a root, other root records are considered siblings
|
129
129
|
\* Siblings returns the record itself
|
@@ -139,6 +139,57 @@ There are also instance methods to determine the relationship between 2 nodes:
|
|
139
139
|
|`descendant_of?(node)` | node is one of this record's ancestors|
|
140
140
|
|`indirect_of?(node)` | node is one of this record's ancestors but not a parent|
|
141
141
|
|
142
|
+
## Visual guide for navigation
|
143
|
+
|
144
|
+
In all examples the node with the large border is the reference node, the node
|
145
|
+
from which the navigation method is invoked. The yellow nodes are the nodes
|
146
|
+
returned by the method.
|
147
|
+
|
148
|
+
<table>
|
149
|
+
<tr>
|
150
|
+
<td>
|
151
|
+
<p align="center">parent</p>
|
152
|
+
<img src="img/parent.png" alt="parent"/>
|
153
|
+
</td>
|
154
|
+
<td>
|
155
|
+
<p align="center">root</p>
|
156
|
+
<img src="img/root.png" alt="root"/>
|
157
|
+
</td>
|
158
|
+
<td>
|
159
|
+
<p align="center">ancestors</p>
|
160
|
+
<img src="img/ancestors.png" alt="ancestors"/>
|
161
|
+
</td>
|
162
|
+
</tr>
|
163
|
+
<tr>
|
164
|
+
<td>
|
165
|
+
<p align="center">path</p>
|
166
|
+
<img src="img/path.png" alt="path"/>
|
167
|
+
</td>
|
168
|
+
<td>
|
169
|
+
<p align="center">children</p>
|
170
|
+
<img src="img/children.png" alt="children"/>
|
171
|
+
</td>
|
172
|
+
<td>
|
173
|
+
<p align="center">siblings</p>
|
174
|
+
<img src="img/siblings.png" alt="siblings"/>
|
175
|
+
</td>
|
176
|
+
</tr>
|
177
|
+
<tr>
|
178
|
+
<td>
|
179
|
+
<p align="center">descendants</p>
|
180
|
+
<img src="img/descendants.png" alt="descendants"/>
|
181
|
+
</td>
|
182
|
+
<td>
|
183
|
+
<p align="center">indirects</p>
|
184
|
+
<img src="img/indirects.png" alt="indirects"/>
|
185
|
+
</td>
|
186
|
+
<td>
|
187
|
+
<p align="center">subtree</p>
|
188
|
+
<img src="img/subtree.png" alt="subtree"/>
|
189
|
+
</td>
|
190
|
+
</tr>
|
191
|
+
</table>
|
192
|
+
|
142
193
|
# Options for `has_ancestry`
|
143
194
|
|
144
195
|
The has_ancestry method supports the following options:
|
@@ -148,23 +199,23 @@ The has_ancestry method supports the following options:
|
|
148
199
|
:destroy All children are destroyed as well (default)
|
149
200
|
:rootify The children of the destroyed node become root nodes
|
150
201
|
:restrict An AncestryException is raised if any children exist
|
151
|
-
:adopt The orphan subtree is added to the parent of the deleted node
|
152
|
-
If the deleted node is Root, then rootify the orphan subtree
|
202
|
+
:adopt The orphan subtree is added to the parent of the deleted node
|
203
|
+
If the deleted node is Root, then rootify the orphan subtree
|
153
204
|
:cache_depth Cache the depth of each node in the 'ancestry_depth' column (default: false)
|
154
205
|
If you turn depth_caching on for an existing model:
|
155
206
|
- Migrate: add_column [table], :ancestry_depth, :integer, :default => 0
|
156
207
|
- Build cache: TreeNode.rebuild_depth_cache!
|
157
208
|
:depth_cache_column Pass in a symbol to store depth cache in a different column
|
158
|
-
:primary_key_format Supply a regular expression that matches the format of your primary key
|
159
|
-
By default, primary keys only match integers ([0-9]+)
|
209
|
+
:primary_key_format Supply a regular expression that matches the format of your primary key
|
210
|
+
By default, primary keys only match integers ([0-9]+)
|
160
211
|
:touch Instruct Ancestry to touch the ancestors of a node when it changes, to
|
161
212
|
invalidate nested key-based caches. (default: false)
|
162
213
|
|
163
214
|
# (Named) Scopes
|
164
215
|
|
165
|
-
Where possible, the navigation methods return scopes instead of records
|
216
|
+
Where possible, the navigation methods return scopes instead of records. This
|
166
217
|
means additional ordering, conditions, limits, etc. can be applied and that
|
167
|
-
the result can be either retrieved, counted or checked for existence. For
|
218
|
+
the result can be either retrieved, counted, or checked for existence. For
|
168
219
|
example:
|
169
220
|
|
170
221
|
```ruby
|
@@ -237,7 +288,7 @@ on type for that.
|
|
237
288
|
# Arrangement
|
238
289
|
|
239
290
|
Ancestry can arrange an entire subtree into nested hashes for easy navigation
|
240
|
-
after retrieval from the database. TreeNode.arrange could for example return:
|
291
|
+
after retrieval from the database. `TreeNode.arrange` could for example return:
|
241
292
|
|
242
293
|
```ruby
|
243
294
|
{
|
@@ -250,14 +301,14 @@ after retrieval from the database. TreeNode.arrange could for example return:
|
|
250
301
|
}
|
251
302
|
```
|
252
303
|
|
253
|
-
The arrange method also works on a scoped class, for example:
|
304
|
+
The `arrange` method also works on a scoped class, for example:
|
254
305
|
|
255
306
|
```ruby
|
256
307
|
TreeNode.find_by_name('Crunchy').subtree.arrange
|
257
308
|
```
|
258
309
|
|
259
|
-
The arrange method takes `ActiveRecord` find options. If you want your hashes to
|
260
|
-
be ordered, you should pass the order to the arrange method instead of to the
|
310
|
+
The `arrange` method takes `ActiveRecord` find options. If you want your hashes to
|
311
|
+
be ordered, you should pass the order to the `arrange` method instead of to the
|
261
312
|
scope. example:
|
262
313
|
|
263
314
|
```ruby
|
@@ -266,7 +317,7 @@ TreeNode.find_by_name('Crunchy').subtree.arrange(:order => :name)
|
|
266
317
|
|
267
318
|
To get the arranged nodes as a nested array of hashes for serialization:
|
268
319
|
|
269
|
-
TreeNode.arrange_serializable
|
320
|
+
`TreeNode.arrange_serializable`
|
270
321
|
|
271
322
|
```ruby
|
272
323
|
[
|
@@ -299,15 +350,15 @@ TreeNode.arrange_serializable do |parent, children|
|
|
299
350
|
end
|
300
351
|
```
|
301
352
|
|
302
|
-
The result of arrange_serializable can easily be serialized to json with
|
353
|
+
The result of `arrange_serializable` can easily be serialized to json with
|
303
354
|
`to_json`, or some other format:
|
304
355
|
|
305
356
|
```
|
306
357
|
TreeNode.arrange_serializable.to_json
|
307
358
|
```
|
308
359
|
|
309
|
-
You can also pass the order to the arrange_serializable method just as you can
|
310
|
-
pass it to the arrange method:
|
360
|
+
You can also pass the order to the `arrange_serializable` method just as you can
|
361
|
+
pass it to the `arrange` method:
|
311
362
|
|
312
363
|
```
|
313
364
|
TreeNode.arrange_serializable(:order => :name)
|
@@ -329,8 +380,8 @@ the order of siblings depends on their order in the original array.
|
|
329
380
|
|
330
381
|
Most current tree plugins use a parent_id column (has_ancestry,
|
331
382
|
awesome_nested_set, better_nested_set, acts_as_nested_set). With ancestry it is
|
332
|
-
easy to migrate from any of these plugins
|
333
|
-
build_ancestry_from_parent_ids
|
383
|
+
easy to migrate from any of these plugins. To do so, use the
|
384
|
+
`build_ancestry_from_parent_ids!` method on your ancestry model. These steps
|
334
385
|
provide a more detailed explanation:
|
335
386
|
|
336
387
|
1. Add ancestry column to your table
|
@@ -352,7 +403,7 @@ provide a more detailed explanation:
|
|
352
403
|
|
353
404
|
|
354
405
|
4. Generate ancestry columns
|
355
|
-
* In '
|
406
|
+
* In 'rails console': **[model].build_ancestry_from_parent_ids!**
|
356
407
|
* Make sure it worked ok: **[model].check_ancestry_integrity!**
|
357
408
|
|
358
409
|
|
@@ -371,14 +422,14 @@ provide a more detailed explanation:
|
|
371
422
|
|
372
423
|
I don't see any way Ancestry tree integrity could get compromised without
|
373
424
|
explicitly setting cyclic parents or invalid ancestry and circumventing
|
374
|
-
validation with update_attribute
|
425
|
+
validation with update_attribute. If you do, please let me know.
|
375
426
|
|
376
427
|
Ancestry includes some methods for detecting integrity problems and restoring
|
377
|
-
integrity just to be sure. To check integrity use:
|
378
|
-
[Model].check_ancestry_integrity
|
428
|
+
integrity just to be sure. To check integrity, use:
|
429
|
+
`[Model].check_ancestry_integrity!`. An AncestryIntegrityException will be
|
379
430
|
raised if there are any problems. You can also specify :report => :list to
|
380
431
|
return an array of exceptions or :report => :echo to echo any error messages.
|
381
|
-
To restore integrity use: [Model].restore_ancestry_integrity
|
432
|
+
To restore integrity use: `[Model].restore_ancestry_integrity!`.
|
382
433
|
|
383
434
|
For example, from IRB:
|
384
435
|
|
data/ancestry.gemspec
CHANGED
@@ -42,12 +42,10 @@ EOF
|
|
42
42
|
'README.md'
|
43
43
|
]
|
44
44
|
|
45
|
-
s.required_ruby_version = '>=
|
46
|
-
s.add_runtime_dependency 'activerecord', '>=
|
47
|
-
s.add_development_dependency '
|
48
|
-
s.add_development_dependency 'yard'
|
49
|
-
s.add_development_dependency 'rake', '~> 10.0'
|
50
|
-
s.add_development_dependency 'test-unit'
|
45
|
+
s.required_ruby_version = '>= 2.0.0'
|
46
|
+
s.add_runtime_dependency 'activerecord', '>= 4.2.0'
|
47
|
+
s.add_development_dependency 'appraisal'
|
51
48
|
s.add_development_dependency 'minitest'
|
52
|
-
s.add_development_dependency '
|
49
|
+
s.add_development_dependency 'rake', '~> 13.0'
|
50
|
+
s.add_development_dependency 'yard'
|
53
51
|
end
|
data/lib/ancestry.rb
CHANGED
@@ -2,7 +2,11 @@ module Ancestry
|
|
2
2
|
module ClassMethods
|
3
3
|
# Fetch tree node if necessary
|
4
4
|
def to_node object
|
5
|
-
if object.is_a?(self.ancestry_base_class)
|
5
|
+
if object.is_a?(self.ancestry_base_class)
|
6
|
+
object
|
7
|
+
else
|
8
|
+
unscoped_where { |scope| scope.find(object.try(primary_key) || object) }
|
9
|
+
end
|
6
10
|
end
|
7
11
|
|
8
12
|
# Scope on relative depth options
|
@@ -129,7 +133,7 @@ module Ancestry
|
|
129
133
|
|
130
134
|
# Integrity restoration
|
131
135
|
def restore_ancestry_integrity!
|
132
|
-
|
136
|
+
parent_ids = {}
|
133
137
|
# Wrap the whole thing in a transaction ...
|
134
138
|
self.ancestry_base_class.transaction do
|
135
139
|
unscoped_where do |scope|
|
@@ -138,29 +142,29 @@ module Ancestry
|
|
138
142
|
# ... set its ancestry to nil if invalid
|
139
143
|
if !node.valid? and !node.errors[node.class.ancestry_column].blank?
|
140
144
|
node.without_ancestry_callbacks do
|
141
|
-
node.update_attribute
|
145
|
+
node.update_attribute :ancestor_ids, []
|
142
146
|
end
|
143
147
|
end
|
144
|
-
# ... save parent of this node in
|
145
|
-
|
148
|
+
# ... save parent id of this node in parent_ids array if it exists
|
149
|
+
parent_ids[node.id] = node.parent_id if exists? node.parent_id
|
146
150
|
|
147
151
|
# Reset parent id in array to nil if it introduces a cycle
|
148
|
-
|
149
|
-
until
|
150
|
-
|
152
|
+
parent_id = parent_ids[node.id]
|
153
|
+
until parent_id.nil? || parent_id == node.id
|
154
|
+
parent_id = parent_ids[parent_id]
|
151
155
|
end
|
152
|
-
|
156
|
+
parent_ids[node.id] = nil if parent_id == node.id
|
153
157
|
end
|
154
158
|
|
155
159
|
# For each node ...
|
156
160
|
scope.find_each do |node|
|
157
|
-
# ... rebuild ancestry from
|
158
|
-
|
159
|
-
until
|
160
|
-
|
161
|
+
# ... rebuild ancestry from parent_ids array
|
162
|
+
ancestor_ids, parent_id = [], parent_ids[node.id]
|
163
|
+
until parent_id.nil?
|
164
|
+
ancestor_ids, parent_id = [parent_id] + ancestor_ids, parent_ids[parent_id]
|
161
165
|
end
|
162
166
|
node.without_ancestry_callbacks do
|
163
|
-
node.update_attribute
|
167
|
+
node.update_attribute :ancestor_ids, ancestor_ids
|
164
168
|
end
|
165
169
|
end
|
166
170
|
end
|
@@ -168,13 +172,13 @@ module Ancestry
|
|
168
172
|
end
|
169
173
|
|
170
174
|
# Build ancestry from parent id's for migration purposes
|
171
|
-
def build_ancestry_from_parent_ids! parent_id = nil,
|
175
|
+
def build_ancestry_from_parent_ids! column=:parent_id, parent_id = nil, ancestor_ids = []
|
172
176
|
unscoped_where do |scope|
|
173
|
-
scope.where(
|
177
|
+
scope.where(column => parent_id).find_each do |node|
|
174
178
|
node.without_ancestry_callbacks do
|
175
|
-
node.update_attribute
|
179
|
+
node.update_attribute :ancestor_ids, ancestor_ids
|
176
180
|
end
|
177
|
-
build_ancestry_from_parent_ids! node.id,
|
181
|
+
build_ancestry_from_parent_ids! column, node.id, ancestor_ids + [node.id]
|
178
182
|
end
|
179
183
|
end
|
180
184
|
end
|
@@ -4,7 +4,7 @@ module Ancestry
|
|
4
4
|
# Check options
|
5
5
|
raise Ancestry::AncestryException.new("Options for has_ancestry must be in a hash.") unless options.is_a? Hash
|
6
6
|
options.each do |key, value|
|
7
|
-
unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column, :touch, :counter_cache].include? key
|
7
|
+
unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column, :touch, :counter_cache, :primary_key_format].include? key
|
8
8
|
raise Ancestry::AncestryException.new("Unknown option for has_ancestry: #{key.inspect} => #{value.inspect}.")
|
9
9
|
end
|
10
10
|
end
|
@@ -27,6 +27,7 @@ module Ancestry
|
|
27
27
|
# Include dynamic class methods
|
28
28
|
extend Ancestry::ClassMethods
|
29
29
|
|
30
|
+
validates_format_of self.ancestry_column, :with => derive_ancestry_pattern(options[:primary_key_format]), :allow_nil => true
|
30
31
|
extend Ancestry::MaterializedPath
|
31
32
|
|
32
33
|
# Create orphan strategy accessor and set to option or default (writer comes from DynamicClassMethods)
|
@@ -36,27 +37,6 @@ module Ancestry
|
|
36
37
|
# Validate that the ancestor ids don't include own id
|
37
38
|
validate :ancestry_exclude_self
|
38
39
|
|
39
|
-
# Named scopes
|
40
|
-
scope :roots, lambda { where(root_conditions) }
|
41
|
-
scope :ancestors_of, lambda { |object| where(ancestor_conditions(object)) }
|
42
|
-
scope :children_of, lambda { |object| where(child_conditions(object)) }
|
43
|
-
scope :indirects_of, lambda { |object| where(indirect_conditions(object)) }
|
44
|
-
scope :descendants_of, lambda { |object| where(descendant_conditions(object)) }
|
45
|
-
scope :subtree_of, lambda { |object| where(subtree_conditions(object)) }
|
46
|
-
scope :siblings_of, lambda { |object| where(sibling_conditions(object)) }
|
47
|
-
scope :ordered_by_ancestry, Proc.new { |order|
|
48
|
-
if %w(mysql mysql2 sqlite sqlite3 postgresql).include?(connection.adapter_name.downcase) && ActiveRecord::VERSION::MAJOR >= 5
|
49
|
-
reorder(
|
50
|
-
Arel::Nodes::Ascending.new(Arel::Nodes::NamedFunction.new('COALESCE', [arel_table[ancestry_column], Arel.sql("''")])),
|
51
|
-
order
|
52
|
-
)
|
53
|
-
else
|
54
|
-
reorder(Arel.sql("(CASE WHEN #{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)} IS NULL THEN 0 ELSE 1 END), #{connection.quote_table_name(table_name)}.#{connection.quote_column_name(ancestry_column)}"), order)
|
55
|
-
end
|
56
|
-
}
|
57
|
-
scope :ordered_by_ancestry_and, Proc.new { |order| ordered_by_ancestry(order) }
|
58
|
-
scope :path_of, lambda { |object| to_node(object).path }
|
59
|
-
|
60
40
|
# Update descendants with new ancestry before save
|
61
41
|
before_save :update_descendants_with_new_ancestry
|
62
42
|
|
@@ -114,6 +94,18 @@ module Ancestry
|
|
114
94
|
return super if defined?(super)
|
115
95
|
has_ancestry(*args)
|
116
96
|
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def derive_ancestry_pattern(primary_key_format, delimiter = '/')
|
101
|
+
primary_key_format ||= '[0-9]+'
|
102
|
+
|
103
|
+
if primary_key_format.to_s.include?('\A')
|
104
|
+
primary_key_format
|
105
|
+
else
|
106
|
+
/\A#{primary_key_format}(#{delimiter}#{primary_key_format})*\Z/
|
107
|
+
end
|
108
|
+
end
|
117
109
|
end
|
118
110
|
end
|
119
111
|
|
@@ -1,11 +1,8 @@
|
|
1
1
|
module Ancestry
|
2
2
|
module InstanceMethods
|
3
|
-
BEFORE_LAST_SAVE_SUFFIX = ActiveRecord::VERSION::STRING >= '5.1.0' ? '_before_last_save' : '_was'
|
4
|
-
IN_DATABASE_SUFFIX = ActiveRecord::VERSION::STRING >= '5.1.0' ? '_in_database' : '_was'
|
5
|
-
|
6
3
|
# Validate that the ancestors don't include itself
|
7
4
|
def ancestry_exclude_self
|
8
|
-
errors.add(:base, "#{self.class.
|
5
|
+
errors.add(:base, "#{self.class.model_name.human} cannot be a descendant of itself.") if ancestor_ids.include? self.id
|
9
6
|
end
|
10
7
|
|
11
8
|
# Update descendants with new ancestry (before save)
|
@@ -16,15 +13,8 @@ module Ancestry
|
|
16
13
|
unscoped_descendants.each do |descendant|
|
17
14
|
# ... replace old ancestry with new ancestry
|
18
15
|
descendant.without_ancestry_callbacks do
|
19
|
-
descendant.
|
20
|
-
|
21
|
-
descendant.read_attribute(descendant.class.ancestry_column).gsub(
|
22
|
-
# child_ancestry_was
|
23
|
-
/^#{self.child_ancestry}/,
|
24
|
-
# future child_ancestry
|
25
|
-
if ancestors? then "#{read_attribute self.class.ancestry_column }/#{id}" else id.to_s end
|
26
|
-
)
|
27
|
-
)
|
16
|
+
new_ancestor_ids = path_ids + (descendant.ancestor_ids - path_ids_in_database)
|
17
|
+
descendant.update_attribute(:ancestor_ids, new_ancestor_ids)
|
28
18
|
end
|
29
19
|
end
|
30
20
|
end
|
@@ -37,13 +27,7 @@ module Ancestry
|
|
37
27
|
when :rootify # make all children root if orphan strategy is rootify
|
38
28
|
unscoped_descendants.each do |descendant|
|
39
29
|
descendant.without_ancestry_callbacks do
|
40
|
-
|
41
|
-
nil
|
42
|
-
else
|
43
|
-
# child_ancestry did not change so child_ancestry_was will work here
|
44
|
-
descendant.ancestry.gsub(/^#{child_ancestry}\//, '')
|
45
|
-
end
|
46
|
-
descendant.update_attribute descendant.class.ancestry_column, new_ancestry
|
30
|
+
descendant.update_attribute :ancestor_ids, descendant.ancestor_ids - path_ids
|
47
31
|
end
|
48
32
|
end
|
49
33
|
when :destroy # destroy all descendants if orphan strategy is destroy
|
@@ -55,10 +39,7 @@ module Ancestry
|
|
55
39
|
when :adopt # make child elements of this node, child of its parent
|
56
40
|
descendants.each do |descendant|
|
57
41
|
descendant.without_ancestry_callbacks do
|
58
|
-
|
59
|
-
# check for empty string if it's then set to nil
|
60
|
-
new_ancestry = nil if new_ancestry.empty?
|
61
|
-
descendant.update_attribute descendant.class.ancestry_column, new_ancestry || nil
|
42
|
+
descendant.update_attribute :ancestor_ids, descendant.ancestor_ids.delete_if { |x| x == self.id }
|
62
43
|
end
|
63
44
|
end
|
64
45
|
when :restrict # throw an exception if it has children
|
@@ -79,19 +60,6 @@ module Ancestry
|
|
79
60
|
end
|
80
61
|
end
|
81
62
|
|
82
|
-
# The ancestry value for this record's children (before save)
|
83
|
-
# This is technically child_ancestry_was
|
84
|
-
def child_ancestry
|
85
|
-
# New records cannot have children
|
86
|
-
raise Ancestry::AncestryException.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?
|
87
|
-
|
88
|
-
if self.send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}").blank?
|
89
|
-
id.to_s
|
90
|
-
else
|
91
|
-
"#{self.send "#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}"}/#{id}"
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
63
|
# Counter Cache
|
96
64
|
def increase_parent_counter_cache
|
97
65
|
self.class.increment_counter _counter_cache_column, parent_id
|
@@ -133,8 +101,7 @@ module Ancestry
|
|
133
101
|
# Ancestors
|
134
102
|
|
135
103
|
def ancestors?
|
136
|
-
|
137
|
-
read_attribute(self.ancestry_base_class.ancestry_column).present?
|
104
|
+
ancestor_ids.present?
|
138
105
|
end
|
139
106
|
alias :has_parent? :ancestors?
|
140
107
|
|
@@ -149,50 +116,21 @@ module Ancestry
|
|
149
116
|
end
|
150
117
|
end
|
151
118
|
|
152
|
-
def ancestor_ids
|
153
|
-
parse_ancestry_column(read_attribute(self.ancestry_base_class.ancestry_column))
|
154
|
-
end
|
155
|
-
|
156
|
-
def ancestor_conditions
|
157
|
-
self.ancestry_base_class.ancestor_conditions(self)
|
158
|
-
end
|
159
|
-
|
160
119
|
def ancestors depth_options = {}
|
161
120
|
return self.ancestry_base_class.none unless ancestors?
|
162
|
-
self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.
|
163
|
-
end
|
164
|
-
|
165
|
-
# deprecate
|
166
|
-
def ancestor_was_conditions
|
167
|
-
{primary_key_with_table => ancestor_ids_before_last_save}
|
168
|
-
end
|
169
|
-
|
170
|
-
# deprecated - probably don't want to use anymore
|
171
|
-
def ancestor_ids_was
|
172
|
-
parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}_was"))
|
173
|
-
end
|
174
|
-
|
175
|
-
def ancestor_ids_before_last_save
|
176
|
-
parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}"))
|
177
|
-
end
|
178
|
-
|
179
|
-
def parent_id_before_last_save
|
180
|
-
ancestry_was = send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}")
|
181
|
-
return unless ancestry_was.present?
|
182
|
-
|
183
|
-
ancestry_was.split(ANCESTRY_DELIMITER).last.to_i
|
121
|
+
self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.ancestors_of(self)
|
184
122
|
end
|
185
123
|
|
186
124
|
def path_ids
|
187
125
|
ancestor_ids + [id]
|
188
126
|
end
|
189
127
|
|
190
|
-
def
|
191
|
-
|
128
|
+
def path_ids_in_database
|
129
|
+
ancestor_ids_in_database + [id]
|
192
130
|
end
|
193
131
|
|
194
132
|
def path depth_options = {}
|
195
|
-
self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.
|
133
|
+
self.ancestry_base_class.scope_depth(depth_options, depth).ordered_by_ancestry.inpath_of(self)
|
196
134
|
end
|
197
135
|
|
198
136
|
def depth
|
@@ -212,7 +150,7 @@ module Ancestry
|
|
212
150
|
# currently parent= does not work in after save callbacks
|
213
151
|
# assuming that parent hasn't changed
|
214
152
|
def parent= parent
|
215
|
-
|
153
|
+
self.ancestor_ids = parent ? parent.path_ids : []
|
216
154
|
end
|
217
155
|
|
218
156
|
def parent_id= new_parent_id
|
@@ -222,15 +160,12 @@ module Ancestry
|
|
222
160
|
def parent_id
|
223
161
|
ancestor_ids.last if ancestors?
|
224
162
|
end
|
163
|
+
alias :parent_id? :ancestors?
|
225
164
|
|
226
165
|
def parent
|
227
166
|
unscoped_find(parent_id) if ancestors?
|
228
167
|
end
|
229
168
|
|
230
|
-
def parent_id?
|
231
|
-
ancestors?
|
232
|
-
end
|
233
|
-
|
234
169
|
def parent_of?(node)
|
235
170
|
self.id == node.parent_id
|
236
171
|
end
|
@@ -246,7 +181,7 @@ module Ancestry
|
|
246
181
|
end
|
247
182
|
|
248
183
|
def is_root?
|
249
|
-
|
184
|
+
!ancestors?
|
250
185
|
end
|
251
186
|
alias :root? :is_root?
|
252
187
|
|
@@ -256,12 +191,8 @@ module Ancestry
|
|
256
191
|
|
257
192
|
# Children
|
258
193
|
|
259
|
-
def child_conditions
|
260
|
-
self.ancestry_base_class.child_conditions(self)
|
261
|
-
end
|
262
|
-
|
263
194
|
def children
|
264
|
-
self.ancestry_base_class.
|
195
|
+
self.ancestry_base_class.children_of(self)
|
265
196
|
end
|
266
197
|
|
267
198
|
def child_ids
|
@@ -284,14 +215,11 @@ module Ancestry
|
|
284
215
|
|
285
216
|
# Siblings
|
286
217
|
|
287
|
-
def sibling_conditions
|
288
|
-
self.ancestry_base_class.sibling_conditions(self)
|
289
|
-
end
|
290
|
-
|
291
218
|
def siblings
|
292
|
-
self.ancestry_base_class.
|
219
|
+
self.ancestry_base_class.siblings_of(self)
|
293
220
|
end
|
294
221
|
|
222
|
+
# NOTE: includes self
|
295
223
|
def sibling_ids
|
296
224
|
siblings.pluck(self.ancestry_base_class.primary_key)
|
297
225
|
end
|
@@ -307,17 +235,13 @@ module Ancestry
|
|
307
235
|
alias_method :only_child?, :is_only_child?
|
308
236
|
|
309
237
|
def sibling_of?(node)
|
310
|
-
self.
|
238
|
+
self.ancestor_ids == node.ancestor_ids
|
311
239
|
end
|
312
240
|
|
313
241
|
# Descendants
|
314
242
|
|
315
|
-
def descendant_conditions
|
316
|
-
self.ancestry_base_class.descendant_conditions(self)
|
317
|
-
end
|
318
|
-
|
319
243
|
def descendants depth_options = {}
|
320
|
-
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).
|
244
|
+
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).descendants_of(self)
|
321
245
|
end
|
322
246
|
|
323
247
|
def descendant_ids depth_options = {}
|
@@ -330,12 +254,8 @@ module Ancestry
|
|
330
254
|
|
331
255
|
# Indirects
|
332
256
|
|
333
|
-
def indirect_conditions
|
334
|
-
self.ancestry_base_class.indirect_conditions(self)
|
335
|
-
end
|
336
|
-
|
337
257
|
def indirects depth_options = {}
|
338
|
-
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).
|
258
|
+
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).indirects_of(self)
|
339
259
|
end
|
340
260
|
|
341
261
|
def indirect_ids depth_options = {}
|
@@ -348,12 +268,8 @@ module Ancestry
|
|
348
268
|
|
349
269
|
# Subtree
|
350
270
|
|
351
|
-
def subtree_conditions
|
352
|
-
self.ancestry_base_class.subtree_conditions(self)
|
353
|
-
end
|
354
|
-
|
355
271
|
def subtree depth_options = {}
|
356
|
-
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).
|
272
|
+
self.ancestry_base_class.ordered_by_ancestry.scope_depth(depth_options, depth).subtree_of(self)
|
357
273
|
end
|
358
274
|
|
359
275
|
def subtree_ids depth_options = {}
|
@@ -373,17 +289,9 @@ module Ancestry
|
|
373
289
|
end
|
374
290
|
|
375
291
|
private
|
376
|
-
ANCESTRY_DELIMITER = '/'.freeze
|
377
|
-
|
378
|
-
def parse_ancestry_column obj
|
379
|
-
return [] unless obj
|
380
|
-
obj_ids = obj.split(ANCESTRY_DELIMITER)
|
381
|
-
self.class.primary_key_is_an_integer? ? obj_ids.map!(&:to_i) : obj_ids
|
382
|
-
end
|
383
|
-
|
384
292
|
def unscoped_descendants
|
385
293
|
unscoped_where do |scope|
|
386
|
-
scope.where descendant_conditions
|
294
|
+
scope.where self.ancestry_base_class.descendant_conditions(self)
|
387
295
|
end
|
388
296
|
end
|
389
297
|
|
@@ -1,44 +1,56 @@
|
|
1
1
|
module Ancestry
|
2
2
|
module MaterializedPath
|
3
|
+
BEFORE_LAST_SAVE_SUFFIX = ActiveRecord::VERSION::STRING >= '5.1.0' ? '_before_last_save'.freeze : '_was'.freeze
|
4
|
+
IN_DATABASE_SUFFIX = ActiveRecord::VERSION::STRING >= '5.1.0' ? '_in_database'.freeze : '_was'.freeze
|
5
|
+
ANCESTRY_DELIMITER='/'.freeze
|
6
|
+
|
3
7
|
def self.extended(base)
|
4
|
-
base.validates_format_of base.ancestry_column, :with => Ancestry::ANCESTRY_PATTERN, :allow_nil => true
|
5
8
|
base.send(:include, InstanceMethods)
|
6
9
|
end
|
7
10
|
|
8
|
-
def
|
9
|
-
|
11
|
+
def path_of(object)
|
12
|
+
to_node(object).path
|
13
|
+
end
|
14
|
+
|
15
|
+
def roots
|
16
|
+
where(arel_table[ancestry_column].eq(nil))
|
10
17
|
end
|
11
18
|
|
12
|
-
def
|
19
|
+
def ancestors_of(object)
|
13
20
|
t = arel_table
|
14
21
|
node = to_node(object)
|
15
|
-
t[primary_key].in(node.ancestor_ids)
|
22
|
+
where(t[primary_key].in(node.ancestor_ids))
|
16
23
|
end
|
17
24
|
|
18
|
-
def
|
25
|
+
def inpath_of(object)
|
19
26
|
t = arel_table
|
20
27
|
node = to_node(object)
|
21
|
-
t[primary_key].in(node.path_ids)
|
28
|
+
where(t[primary_key].in(node.path_ids))
|
22
29
|
end
|
23
30
|
|
24
|
-
def
|
31
|
+
def children_of(object)
|
25
32
|
t = arel_table
|
26
33
|
node = to_node(object)
|
27
|
-
t[ancestry_column].eq(node.child_ancestry)
|
34
|
+
where(t[ancestry_column].eq(node.child_ancestry))
|
28
35
|
end
|
29
36
|
|
30
37
|
# indirect = anyone who is a descendant, but not a child
|
31
|
-
def
|
38
|
+
def indirects_of(object)
|
32
39
|
t = arel_table
|
33
40
|
node = to_node(object)
|
34
41
|
# rails has case sensitive matching.
|
35
42
|
if ActiveRecord::VERSION::MAJOR >= 5
|
36
|
-
t[ancestry_column].matches("#{node.child_ancestry}/%", nil, true)
|
43
|
+
where(t[ancestry_column].matches("#{node.child_ancestry}/%", nil, true))
|
37
44
|
else
|
38
|
-
t[ancestry_column].matches("#{node.child_ancestry}/%")
|
45
|
+
where(t[ancestry_column].matches("#{node.child_ancestry}/%"))
|
39
46
|
end
|
40
47
|
end
|
41
48
|
|
49
|
+
def descendants_of(object)
|
50
|
+
where(descendant_conditions(object))
|
51
|
+
end
|
52
|
+
|
53
|
+
# deprecated
|
42
54
|
def descendant_conditions(object)
|
43
55
|
t = arel_table
|
44
56
|
node = to_node(object)
|
@@ -50,23 +62,94 @@ module Ancestry
|
|
50
62
|
end
|
51
63
|
end
|
52
64
|
|
53
|
-
def
|
65
|
+
def subtree_of(object)
|
54
66
|
t = arel_table
|
55
67
|
node = to_node(object)
|
56
|
-
descendant_conditions(node).or(t[primary_key].eq(node.id))
|
68
|
+
where(descendant_conditions(node).or(t[primary_key].eq(node.id)))
|
57
69
|
end
|
58
70
|
|
59
|
-
def
|
71
|
+
def siblings_of(object)
|
60
72
|
t = arel_table
|
61
73
|
node = to_node(object)
|
62
|
-
t[ancestry_column].eq(node[ancestry_column])
|
74
|
+
where(t[ancestry_column].eq(node[ancestry_column]))
|
75
|
+
end
|
76
|
+
|
77
|
+
def ordered_by_ancestry(order = nil)
|
78
|
+
if %w(mysql mysql2 sqlite sqlite3).include?(connection.adapter_name.downcase)
|
79
|
+
reorder(arel_table[ancestry_column], order)
|
80
|
+
elsif %w(postgresql).include?(connection.adapter_name.downcase) && ActiveRecord::VERSION::STRING >= "6.1"
|
81
|
+
reorder(Arel::Nodes.new(arel_table[ancestry_column]).nulls_first)
|
82
|
+
else
|
83
|
+
reorder(
|
84
|
+
Arel::Nodes::Ascending.new(Arel::Nodes::NamedFunction.new('COALESCE', [arel_table[ancestry_column], Arel.sql("''")])),
|
85
|
+
order
|
86
|
+
)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def ordered_by_ancestry_and(order)
|
91
|
+
ordered_by_ancestry(order)
|
63
92
|
end
|
64
93
|
|
65
94
|
module InstanceMethods
|
95
|
+
|
66
96
|
# Validates the ancestry, but can also be applied if validation is bypassed to determine if children should be affected
|
67
97
|
def sane_ancestry?
|
68
98
|
ancestry_value = read_attribute(self.ancestry_base_class.ancestry_column)
|
69
|
-
ancestry_value.nil? ||
|
99
|
+
(ancestry_value.nil? || !ancestor_ids.include?(self.id)) && valid?
|
100
|
+
end
|
101
|
+
|
102
|
+
# optimization - better to go directly to column and avoid parsing
|
103
|
+
def ancestors?
|
104
|
+
read_attribute(self.ancestry_base_class.ancestry_column).present?
|
105
|
+
end
|
106
|
+
alias :has_parent? :ancestors?
|
107
|
+
|
108
|
+
def ancestor_ids=(value)
|
109
|
+
col = self.ancestry_base_class.ancestry_column
|
110
|
+
value.present? ? write_attribute(col, value.join(ANCESTRY_DELIMITER)) : write_attribute(col, nil)
|
111
|
+
end
|
112
|
+
|
113
|
+
def ancestor_ids
|
114
|
+
parse_ancestry_column(read_attribute(self.ancestry_base_class.ancestry_column))
|
115
|
+
end
|
116
|
+
|
117
|
+
def ancestor_ids_in_database
|
118
|
+
parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}"))
|
119
|
+
end
|
120
|
+
|
121
|
+
def ancestor_ids_before_last_save
|
122
|
+
parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}"))
|
123
|
+
end
|
124
|
+
|
125
|
+
def parent_id_before_last_save
|
126
|
+
ancestry_was = send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}")
|
127
|
+
return unless ancestry_was.present?
|
128
|
+
|
129
|
+
parse_ancestry_column(ancestry_was).last
|
130
|
+
end
|
131
|
+
|
132
|
+
# optimization - better to go directly to column and avoid parsing
|
133
|
+
def sibling_of?(node)
|
134
|
+
self.read_attribute(self.ancestry_base_class.ancestry_column) == node.read_attribute(self.ancestry_base_class.ancestry_column)
|
135
|
+
end
|
136
|
+
|
137
|
+
# private (public so class methods can find it)
|
138
|
+
# The ancestry value for this record's children (before save)
|
139
|
+
# This is technically child_ancestry_was
|
140
|
+
def child_ancestry
|
141
|
+
# New records cannot have children
|
142
|
+
raise Ancestry::AncestryException.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?
|
143
|
+
path_was = self.send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}")
|
144
|
+
path_was.blank? ? id.to_s : "#{path_was}/#{id}"
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def parse_ancestry_column obj
|
150
|
+
return [] unless obj
|
151
|
+
obj_ids = obj.split(ANCESTRY_DELIMITER)
|
152
|
+
self.class.primary_key_is_an_integer? ? obj_ids.map!(&:to_i) : obj_ids
|
70
153
|
end
|
71
154
|
end
|
72
155
|
end
|
data/lib/ancestry/version.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ancestry
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.0
|
4
|
+
version: 3.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stefan Kroes
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2020-08-03 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -17,16 +17,16 @@ dependencies:
|
|
17
17
|
requirements:
|
18
18
|
- - ">="
|
19
19
|
- !ruby/object:Gem::Version
|
20
|
-
version:
|
20
|
+
version: 4.2.0
|
21
21
|
type: :runtime
|
22
22
|
prerelease: false
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
25
|
- - ">="
|
26
26
|
- !ruby/object:Gem::Version
|
27
|
-
version:
|
27
|
+
version: 4.2.0
|
28
28
|
- !ruby/object:Gem::Dependency
|
29
|
-
name:
|
29
|
+
name: appraisal
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
31
31
|
requirements:
|
32
32
|
- - ">="
|
@@ -40,7 +40,7 @@ dependencies:
|
|
40
40
|
- !ruby/object:Gem::Version
|
41
41
|
version: '0'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
|
-
name:
|
43
|
+
name: minitest
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
45
45
|
requirements:
|
46
46
|
- - ">="
|
@@ -59,44 +59,16 @@ dependencies:
|
|
59
59
|
requirements:
|
60
60
|
- - "~>"
|
61
61
|
- !ruby/object:Gem::Version
|
62
|
-
version: '
|
62
|
+
version: '13.0'
|
63
63
|
type: :development
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
67
|
- - "~>"
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version: '
|
70
|
-
- !ruby/object:Gem::Dependency
|
71
|
-
name: test-unit
|
72
|
-
requirement: !ruby/object:Gem::Requirement
|
73
|
-
requirements:
|
74
|
-
- - ">="
|
75
|
-
- !ruby/object:Gem::Version
|
76
|
-
version: '0'
|
77
|
-
type: :development
|
78
|
-
prerelease: false
|
79
|
-
version_requirements: !ruby/object:Gem::Requirement
|
80
|
-
requirements:
|
81
|
-
- - ">="
|
82
|
-
- !ruby/object:Gem::Version
|
83
|
-
version: '0'
|
84
|
-
- !ruby/object:Gem::Dependency
|
85
|
-
name: minitest
|
86
|
-
requirement: !ruby/object:Gem::Requirement
|
87
|
-
requirements:
|
88
|
-
- - ">="
|
89
|
-
- !ruby/object:Gem::Version
|
90
|
-
version: '0'
|
91
|
-
type: :development
|
92
|
-
prerelease: false
|
93
|
-
version_requirements: !ruby/object:Gem::Requirement
|
94
|
-
requirements:
|
95
|
-
- - ">="
|
96
|
-
- !ruby/object:Gem::Version
|
97
|
-
version: '0'
|
69
|
+
version: '13.0'
|
98
70
|
- !ruby/object:Gem::Dependency
|
99
|
-
name:
|
71
|
+
name: yard
|
100
72
|
requirement: !ruby/object:Gem::Requirement
|
101
73
|
requirements:
|
102
74
|
- - ">="
|
@@ -150,7 +122,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
150
122
|
requirements:
|
151
123
|
- - ">="
|
152
124
|
- !ruby/object:Gem::Version
|
153
|
-
version:
|
125
|
+
version: 2.0.0
|
154
126
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
155
127
|
requirements:
|
156
128
|
- - ">="
|