ancestry 4.3.3 → 5.1.0

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.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Ancestry
2
4
  # store ancestry as grandparent_id/parent_id
3
5
  # root a=nil,id=1 children=id,id/% == 1, 1/%
@@ -28,16 +30,13 @@ module Ancestry
28
30
  end
29
31
 
30
32
  def children_of(object)
31
- t = arel_table
32
33
  node = to_node(object)
33
- where(t[ancestry_column].eq(node.child_ancestry))
34
+ where(arel_table[ancestry_column].eq(node.child_ancestry))
34
35
  end
35
36
 
36
- # indirect = anyone who is a descendant, but not a child
37
37
  def indirects_of(object)
38
- t = arel_table
39
38
  node = to_node(object)
40
- where(t[ancestry_column].matches("#{node.child_ancestry}#{ancestry_delimiter}%", nil, true))
39
+ where(MaterializedPath.indirects_condition(arel_table[ancestry_column], node.child_ancestry, ancestry_delimiter))
41
40
  end
42
41
 
43
42
  def descendants_of(object)
@@ -45,30 +44,27 @@ module Ancestry
45
44
  end
46
45
 
47
46
  def descendants_by_ancestry(ancestry)
48
- t = arel_table
49
- t[ancestry_column].matches("#{ancestry}#{ancestry_delimiter}%", nil, true).or(t[ancestry_column].eq(ancestry))
47
+ MaterializedPath.descendants_condition(arel_table[ancestry_column], ancestry, ancestry_delimiter)
50
48
  end
51
49
 
52
50
  def descendant_conditions(object)
53
51
  node = to_node(object)
54
- descendants_by_ancestry( node.child_ancestry )
52
+ descendants_by_ancestry(node.child_ancestry)
55
53
  end
56
54
 
57
- def descendant_before_save_conditions(object)
55
+ def descendant_before_last_save_conditions(object)
58
56
  node = to_node(object)
59
- descendants_by_ancestry( node.child_ancestry_before_save )
57
+ descendants_by_ancestry(node.child_ancestry_before_last_save)
60
58
  end
61
59
 
62
60
  def subtree_of(object)
63
- t = arel_table
64
61
  node = to_node(object)
65
- descendants_of(node).or(where(t[primary_key].eq(node.id)))
62
+ descendants_of(node).or(where(arel_table[primary_key].eq(node.id)))
66
63
  end
67
64
 
68
65
  def siblings_of(object)
69
- t = arel_table
70
66
  node = to_node(object)
71
- where(t[ancestry_column].eq(node[ancestry_column].presence))
67
+ where(arel_table[ancestry_column].eq(node[ancestry_column].presence))
72
68
  end
73
69
 
74
70
  def ordered_by_ancestry(order = nil)
@@ -92,86 +88,153 @@ module Ancestry
92
88
  nil
93
89
  end
94
90
 
95
- private
91
+ def child_ancestry_sql
92
+ MaterializedPath.child_ancestry_sql(table_name, ancestry_column, primary_key, ancestry_delimiter, connection.adapter_name.downcase)
93
+ end
96
94
 
97
- def ancestry_validation_options
98
- {
99
- format: { with: ancestry_format_regexp },
100
- allow_nil: ancestry_nil_allowed?
95
+ def ancestry_depth_sql
96
+ @ancestry_depth_sql ||= MaterializedPath.construct_depth_sql(table_name, ancestry_column, ancestry_delimiter)
97
+ end
98
+
99
+ def generate_ancestry(ancestor_ids)
100
+ MaterializedPath.generate(ancestor_ids, ancestry_delimiter, ancestry_root)
101
+ end
102
+
103
+ def parse_ancestry_column(obj)
104
+ MaterializedPath.parse(obj, ancestry_root, ancestry_delimiter, primary_key_is_an_integer?)
105
+ end
106
+
107
+ def ancestry_depth_change(old_value, new_value)
108
+ parse_ancestry_column(new_value).size - parse_ancestry_column(old_value).size
109
+ end
110
+
111
+ def self.generate(ancestor_ids, delimiter, root)
112
+ if ancestor_ids.present? && ancestor_ids.any?
113
+ ancestor_ids.join(delimiter)
114
+ else
115
+ root
116
+ end
117
+ end
118
+
119
+ def self.parse(obj, root, delimiter, integer_pk)
120
+ return [] if obj.nil? || obj == root
121
+
122
+ obj_ids = obj.split(delimiter).delete_if(&:blank?)
123
+ integer_pk ? obj_ids.map!(&:to_i) : obj_ids
124
+ end
125
+
126
+ def self.child_ancestry_value(ancestry_value, id, delimiter)
127
+ [ancestry_value, id].compact.join(delimiter)
128
+ end
129
+
130
+ # Arel condition: descendants have ancestry matching child_ancestry or starting with child_ancestry/
131
+ def self.descendants_condition(attr, child_ancestry, delimiter)
132
+ attr.matches("#{child_ancestry}#{delimiter}%", nil, true).or(attr.eq(child_ancestry))
133
+ end
134
+
135
+ # Arel condition: indirects have ancestry matching child_ancestry/*/
136
+ def self.indirects_condition(attr, child_ancestry, delimiter)
137
+ attr.matches("#{child_ancestry}#{delimiter}%", nil, true)
138
+ end
139
+
140
+ def concat(*args)
141
+ MaterializedPath.concat(connection.adapter_name.downcase, *args)
142
+ end
143
+
144
+ def self.concat(adapter, *args)
145
+ if %w(sqlite sqlite3).include?(adapter)
146
+ args.join('||')
147
+ else
148
+ %{CONCAT(#{args.join(', ')})}
149
+ end
150
+ end
151
+
152
+ def self.child_ancestry_sql(table_name, ancestry_column, primary_key, delimiter, adapter)
153
+ pk_sql = concat(adapter, "#{table_name}.#{primary_key}")
154
+ full_sql = concat(adapter, "#{table_name}.#{ancestry_column}", "'#{delimiter}'", "#{table_name}.#{primary_key}")
155
+ %{
156
+ CASE WHEN #{table_name}.#{ancestry_column} IS NULL THEN #{pk_sql}
157
+ ELSE #{full_sql}
158
+ END
101
159
  }
102
160
  end
103
161
 
104
- def ancestry_nil_allowed?
105
- true
162
+ def self.construct_depth_sql(table_name, ancestry_column, ancestry_delimiter)
163
+ tmp = %{(LENGTH(#{table_name}.#{ancestry_column}) - LENGTH(REPLACE(#{table_name}.#{ancestry_column},'#{ancestry_delimiter}','')))}
164
+ tmp += "/#{ancestry_delimiter.size}" if ancestry_delimiter.size > 1
165
+ "(CASE WHEN #{table_name}.#{ancestry_column} IS NULL THEN 0 ELSE 1 + #{tmp} END)"
106
166
  end
107
167
 
108
- def ancestry_format_regexp
109
- /\A#{ancestry_primary_key_format}(#{Regexp.escape(ancestry_delimiter)}#{ancestry_primary_key_format})*\z/.freeze
168
+ def self.validation_options(primary_key_format, delimiter)
169
+ {
170
+ format: {with: /\A#{primary_key_format}(#{Regexp.escape(delimiter)}#{primary_key_format})*\z/.freeze},
171
+ allow_nil: true
172
+ }
173
+ end
174
+
175
+ private
176
+
177
+ def ancestry_validation_options(ancestry_primary_key_format)
178
+ MaterializedPath.validation_options(ancestry_primary_key_format, ancestry_delimiter)
110
179
  end
111
180
 
112
181
  module InstanceMethods
113
182
  # optimization - better to go directly to column and avoid parsing
114
183
  def ancestors?
115
- read_attribute(self.ancestry_base_class.ancestry_column) != self.ancestry_base_class.ancestry_root
184
+ read_attribute(self.class.ancestry_column) != self.class.ancestry_root
116
185
  end
117
- alias :has_parent? :ancestors?
186
+ alias has_parent? ancestors?
118
187
 
119
188
  def ancestor_ids=(value)
120
- write_attribute(self.ancestry_base_class.ancestry_column, generate_ancestry(value))
189
+ write_attribute(self.class.ancestry_column, self.class.generate_ancestry(value))
121
190
  end
122
191
 
123
192
  def ancestor_ids
124
- parse_ancestry_column(read_attribute(self.ancestry_base_class.ancestry_column))
193
+ MaterializedPath.parse(read_attribute(self.class.ancestry_column), self.class.ancestry_root, self.class.ancestry_delimiter, self.class.primary_key_is_an_integer?)
125
194
  end
126
195
 
127
196
  def ancestor_ids_in_database
128
- parse_ancestry_column(attribute_in_database(self.class.ancestry_column))
197
+ MaterializedPath.parse(attribute_in_database(self.class.ancestry_column), self.class.ancestry_root, self.class.ancestry_delimiter, self.class.primary_key_is_an_integer?)
129
198
  end
130
199
 
131
200
  def ancestor_ids_before_last_save
132
- parse_ancestry_column(attribute_before_last_save(self.ancestry_base_class.ancestry_column))
201
+ MaterializedPath.parse(attribute_before_last_save(self.class.ancestry_column), self.class.ancestry_root, self.class.ancestry_delimiter, self.class.primary_key_is_an_integer?)
133
202
  end
134
203
 
135
204
  def parent_id_in_database
136
- parse_ancestry_column(attribute_in_database(self.class.ancestry_column)).last
205
+ MaterializedPath.parse(attribute_in_database(self.class.ancestry_column), self.class.ancestry_root, self.class.ancestry_delimiter, self.class.primary_key_is_an_integer?).last
137
206
  end
138
207
 
139
208
  def parent_id_before_last_save
140
- parse_ancestry_column(attribute_before_last_save(self.ancestry_base_class.ancestry_column)).last
209
+ MaterializedPath.parse(attribute_before_last_save(self.class.ancestry_column), self.class.ancestry_root, self.class.ancestry_delimiter, self.class.primary_key_is_an_integer?).last
141
210
  end
142
211
 
143
212
  # optimization - better to go directly to column and avoid parsing
144
213
  def sibling_of?(node)
145
- self.read_attribute(self.ancestry_base_class.ancestry_column) == node.read_attribute(self.ancestry_base_class.ancestry_column)
214
+ read_attribute(self.class.ancestry_column) == node.read_attribute(node.class.ancestry_column)
146
215
  end
147
216
 
148
- # private (public so class methods can find it)
149
- # The ancestry value for this record's children (before save)
150
- # This is technically child_ancestry_was
217
+ # The ancestry value for this record's children
218
+ # This can also be thought of as the ancestry value for the path
219
+ # If this is a new record, it has no id, and it is not valid.
220
+ # NOTE: This could have been called child_ancestry_in_database
221
+ # the child records were created from the version in the database
151
222
  def child_ancestry
152
- # New records cannot have children
153
- raise Ancestry::AncestryException.new(I18n.t("ancestry.no_child_for_new_record")) if new_record?
154
- [attribute_in_database(self.ancestry_base_class.ancestry_column), id].compact.join(self.ancestry_base_class.ancestry_delimiter)
155
- end
223
+ raise(Ancestry::AncestryException, I18n.t("ancestry.no_child_for_new_record")) if new_record?
156
224
 
157
- def child_ancestry_before_save
158
- # New records cannot have children
159
- raise Ancestry::AncestryException.new(I18n.t("ancestry.no_child_for_new_record")) if new_record?
160
- [attribute_before_last_save(self.ancestry_base_class.ancestry_column), id].compact.join(self.ancestry_base_class.ancestry_delimiter)
225
+ MaterializedPath.child_ancestry_value(attribute_in_database(self.class.ancestry_column), id, self.class.ancestry_delimiter)
161
226
  end
162
227
 
163
- def parse_ancestry_column(obj)
164
- return [] if obj.nil? || obj == self.ancestry_base_class.ancestry_root
165
- obj_ids = obj.split(self.ancestry_base_class.ancestry_delimiter).delete_if(&:blank?)
166
- self.class.primary_key_is_an_integer? ? obj_ids.map!(&:to_i) : obj_ids
167
- end
168
-
169
- def generate_ancestry(ancestor_ids)
170
- if ancestor_ids.present? && ancestor_ids.any?
171
- ancestor_ids.join(self.ancestry_base_class.ancestry_delimiter)
172
- else
173
- self.ancestry_base_class.ancestry_root
228
+ # The ancestry value for this record's old children
229
+ # Currently used in an after_update via unscoped_descendants_before_last_save
230
+ # to find the old children and bring them along (or to )
231
+ # This is not valid in a new record's after_save.
232
+ def child_ancestry_before_last_save
233
+ if new_record? || (respond_to?(:previously_new_record?) && previously_new_record?)
234
+ raise Ancestry::AncestryException, I18n.t("ancestry.no_child_for_new_record")
174
235
  end
236
+
237
+ MaterializedPath.child_ancestry_value(attribute_before_last_save(self.class.ancestry_column), id, self.class.ancestry_delimiter)
175
238
  end
176
239
  end
177
240
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Ancestry
2
4
  # store ancestry as /grandparent_id/parent_id/
3
5
  # root: a=/,id=1 children=#{a}#{id}/% == /1/%
@@ -11,9 +13,8 @@ module Ancestry
11
13
  end
12
14
 
13
15
  def indirects_of(object)
14
- t = arel_table
15
16
  node = to_node(object)
16
- where(t[ancestry_column].matches("#{node.child_ancestry}%#{ancestry_delimiter}%", nil, true))
17
+ where(MaterializedPath2.indirects_condition(arel_table[ancestry_column], node.child_ancestry, ancestry_delimiter))
17
18
  end
18
19
 
19
20
  def ordered_by_ancestry(order = nil)
@@ -21,42 +22,86 @@ module Ancestry
21
22
  end
22
23
 
23
24
  def descendants_by_ancestry(ancestry)
24
- arel_table[ancestry_column].matches("#{ancestry}%", nil, true)
25
+ MaterializedPath2.descendants_condition(arel_table[ancestry_column], ancestry, ancestry_delimiter)
25
26
  end
26
27
 
27
28
  def ancestry_root
28
29
  ancestry_delimiter
29
30
  end
30
31
 
31
- private
32
+ def child_ancestry_sql
33
+ MaterializedPath2.child_ancestry_sql(table_name, ancestry_column, primary_key, ancestry_delimiter, connection.adapter_name.downcase)
34
+ end
35
+
36
+ def ancestry_depth_sql
37
+ @ancestry_depth_sql ||= MaterializedPath2.construct_depth_sql(table_name, ancestry_column, ancestry_delimiter)
38
+ end
39
+
40
+ def generate_ancestry(ancestor_ids)
41
+ MaterializedPath2.generate(ancestor_ids, ancestry_delimiter, ancestry_root)
42
+ end
43
+
44
+ def self.generate(ancestor_ids, delimiter, root)
45
+ if ancestor_ids.present? && ancestor_ids.any?
46
+ "#{delimiter}#{ancestor_ids.join(delimiter)}#{delimiter}"
47
+ else
48
+ root
49
+ end
50
+ end
51
+
52
+ def self.child_ancestry_value(ancestry_value, id, delimiter)
53
+ "#{ancestry_value}#{id}#{delimiter}"
54
+ end
32
55
 
33
- def ancestry_nil_allowed?
34
- false
56
+ def self.child_ancestry_sql(table_name, ancestry_column, primary_key, delimiter, adapter)
57
+ MaterializedPath.concat(adapter, "#{table_name}.#{ancestry_column}", "#{table_name}.#{primary_key}", "'#{delimiter}'")
35
58
  end
36
59
 
37
- def ancestry_format_regexp
38
- /\A#{Regexp.escape(ancestry_delimiter)}(#{ancestry_primary_key_format}#{Regexp.escape(ancestry_delimiter)})*\z/.freeze
60
+ # mp2: descendants just use LIKE (trailing delimiter prevents false prefix matches)
61
+ def self.descendants_condition(attr, child_ancestry, _delimiter)
62
+ attr.matches("#{child_ancestry}%", nil, true)
63
+ end
64
+
65
+ # mp2: indirects match child_ancestry + at least one more segment
66
+ def self.indirects_condition(attr, child_ancestry, delimiter)
67
+ attr.matches("#{child_ancestry}%#{delimiter}%", nil, true)
68
+ end
69
+
70
+ # module method
71
+ def self.construct_depth_sql(table_name, ancestry_column, ancestry_delimiter)
72
+ tmp = %{(LENGTH(#{table_name}.#{ancestry_column}) - LENGTH(REPLACE(#{table_name}.#{ancestry_column},'#{ancestry_delimiter}','')))}
73
+ tmp += "/#{ancestry_delimiter.size}" if ancestry_delimiter.size > 1
74
+ "(#{tmp} -1)"
75
+ end
76
+
77
+ def self.validation_options(primary_key_format, delimiter)
78
+ {
79
+ format: {with: /\A#{Regexp.escape(delimiter)}(#{primary_key_format}#{Regexp.escape(delimiter)})*\z/.freeze},
80
+ allow_nil: false
81
+ }
82
+ end
83
+
84
+ private
85
+
86
+ def ancestry_validation_options(ancestry_primary_key_format)
87
+ MaterializedPath2.validation_options(ancestry_primary_key_format, ancestry_delimiter)
39
88
  end
40
89
 
41
90
  module InstanceMethods
91
+ # Please see notes for MaterializedPath#child_ancestry
42
92
  def child_ancestry
43
- # New records cannot have children
44
- raise Ancestry::AncestryException.new(I18n.t("ancestry.no_child_for_new_record")) if new_record?
45
- "#{attribute_in_database(self.ancestry_base_class.ancestry_column)}#{id}#{self.ancestry_base_class.ancestry_delimiter}"
46
- end
93
+ raise(Ancestry::AncestryException, I18n.t("ancestry.no_child_for_new_record")) if new_record?
47
94
 
48
- def child_ancestry_before_save
49
- # New records cannot have children
50
- raise Ancestry::AncestryException.new(I18n.t("ancestry.no_child_for_new_record")) if new_record?
51
- "#{attribute_before_last_save(self.ancestry_base_class.ancestry_column)}#{id}#{self.ancestry_base_class.ancestry_delimiter}"
95
+ MaterializedPath2.child_ancestry_value(attribute_in_database(self.class.ancestry_column), id, self.class.ancestry_delimiter)
52
96
  end
53
97
 
54
- def generate_ancestry(ancestor_ids)
55
- if ancestor_ids.present? && ancestor_ids.any?
56
- "#{self.ancestry_base_class.ancestry_delimiter}#{ancestor_ids.join(self.ancestry_base_class.ancestry_delimiter)}#{self.ancestry_base_class.ancestry_delimiter}"
57
- else
58
- self.ancestry_base_class.ancestry_root
98
+ # Please see notes for MaterializedPath#child_ancestry_before_last_save
99
+ def child_ancestry_before_last_save
100
+ if new_record? || (respond_to?(:previously_new_record?) && previously_new_record?)
101
+ raise(Ancestry::AncestryException, I18n.t("ancestry.no_child_for_new_record"))
59
102
  end
103
+
104
+ MaterializedPath2.child_ancestry_value(attribute_before_last_save(self.class.ancestry_column), id, self.class.ancestry_delimiter)
60
105
  end
61
106
  end
62
107
  end
@@ -1,23 +1,42 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Ancestry
2
4
  module MaterializedPathPg
3
- # Update descendants with new ancestry (after update)
4
- def update_descendants_with_new_ancestry
5
+ # Update descendants with new ancestry using a single SQL statement (after update)
6
+ def update_descendants_with_new_ancestry_sql
5
7
  # If enabled and node is existing and ancestry was updated and the new ancestry is sane ...
6
- if !ancestry_callbacks_disabled? && !new_record? && ancestry_changed? && sane_ancestor_ids?
7
- ancestry_column = ancestry_base_class.ancestry_column
8
- old_ancestry = generate_ancestry( path_ids_before_last_save )
9
- new_ancestry = generate_ancestry( path_ids )
10
- update_clause = [
11
- "#{ancestry_column} = regexp_replace(#{ancestry_column}, '^#{Regexp.escape(old_ancestry)}', '#{new_ancestry}')"
12
- ]
8
+ # The only way the ancestry could be bad is via `update_attribute` with a bad value
9
+ if !ancestry_callbacks_disabled? && sane_ancestor_ids?
10
+ old_ancestry = self.class.generate_ancestry(path_ids_before_last_save)
11
+ new_ancestry = self.class.generate_ancestry(path_ids)
12
+ # Replace old ancestry prefix with new ancestry:
13
+ # CONCAT(new_ancestry, SUBSTRING(column, LENGTH(old_ancestry) + 1))
14
+ column = self.class.ancestry_column
15
+ replace_sql = self.class.concat("'#{new_ancestry}'", "SUBSTRING(#{column}, #{old_ancestry.length + 1})")
16
+ update_clause = {
17
+ column => Arel.sql(replace_sql)
18
+ }
13
19
 
14
- if ancestry_base_class.respond_to?(:depth_cache_column) && respond_to?(ancestry_base_class.depth_cache_column)
15
- depth_cache_column = ancestry_base_class.depth_cache_column.to_s
16
- update_clause << "#{depth_cache_column} = length(regexp_replace(regexp_replace(ancestry, '^#{Regexp.escape(old_ancestry)}', '#{new_ancestry}'), '[^#{ancestry_base_class.ancestry_delimiter}]', '', 'g')) #{ancestry_base_class.ancestry_format == :materialized_path2 ? '-' : '+'} 1"
20
+ current_time = current_time_from_proper_timezone
21
+ timestamp_attributes_for_update_in_model.each do |column|
22
+ update_clause[column] = current_time
17
23
  end
18
24
 
19
- unscoped_descendants_before_save.update_all update_clause.join(', ')
25
+ update_descendants_hook(update_clause, old_ancestry, new_ancestry)
26
+ unscoped_descendants_before_last_save.update_all update_clause
27
+ end
28
+ end
29
+
30
+ def update_descendants_hook(update_clause, old_ancestry, new_ancestry)
31
+ if self.class.respond_to?(:depth_cache_column)
32
+ depth_cache_column = self.class.depth_cache_column
33
+ depth_change = self.class.ancestry_depth_change(old_ancestry, new_ancestry)
34
+
35
+ if depth_change != 0
36
+ update_clause[depth_cache_column] = Arel.sql("#{depth_cache_column} + #{depth_change}")
37
+ end
20
38
  end
39
+ update_clause
21
40
  end
22
41
  end
23
42
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Ancestry
2
- VERSION = '4.3.3'
4
+ VERSION = '5.1.0'
3
5
  end
data/lib/ancestry.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'ancestry/version'
2
4
  require_relative 'ancestry/class_methods'
3
5
  require_relative 'ancestry/instance_methods'
@@ -8,7 +10,7 @@ require_relative 'ancestry/materialized_path2'
8
10
  require_relative 'ancestry/materialized_path_pg'
9
11
 
10
12
  I18n.load_path += Dir[File.join(File.expand_path(File.dirname(__FILE__)),
11
- 'ancestry', 'locales', '*.{rb,yml}').to_s]
13
+ 'ancestry', 'locales', '*.{rb,yml}').to_s]
12
14
 
13
15
  module Ancestry
14
16
  @@default_update_strategy = :ruby
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ancestry
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.3
4
+ version: 5.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Kroes
8
8
  - Keenan Brock
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2023-04-10 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: activerecord
@@ -25,6 +24,20 @@ dependencies:
25
24
  - - ">="
26
25
  - !ruby/object:Gem::Version
27
26
  version: 5.2.6
27
+ - !ruby/object:Gem::Dependency
28
+ name: logger
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
28
41
  - !ruby/object:Gem::Dependency
29
42
  name: appraisal
30
43
  requirement: !ruby/object:Gem::Requirement
@@ -129,6 +142,7 @@ metadata:
129
142
  changelog_uri: https://github.com/stefankroes/ancestry/blob/master/CHANGELOG.md
130
143
  source_code_uri: https://github.com/stefankroes/ancestry/
131
144
  bug_tracker_uri: https://github.com/stefankroes/ancestry/issues
145
+ rubygems_mfa_required: 'true'
132
146
  post_install_message: Thank you for installing Ancestry. You can visit http://github.com/stefankroes/ancestry
133
147
  to read the documentation.
134
148
  rdoc_options: []
@@ -145,8 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
145
159
  - !ruby/object:Gem::Version
146
160
  version: '0'
147
161
  requirements: []
148
- rubygems_version: 3.2.32
149
- signing_key:
162
+ rubygems_version: 4.0.6
150
163
  specification_version: 4
151
164
  summary: Organize ActiveRecord model into a tree structure
152
165
  test_files: []