ancestry 4.2.0 → 4.3.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.
@@ -46,8 +46,10 @@ module Ancestry
46
46
  end
47
47
  end
48
48
 
49
- # Arrange array of nodes into a nested hash of the form
50
- # {node => children}, where children = {} if the node has no children
49
+ # arranges array of nodes to a hierarchical hash
50
+ #
51
+ # @param nodes [Array[Node]] nodes to be arranged
52
+ # @returns Hash{Node => {Node => {}, Node => {}}}
51
53
  # If a node's parent is not included, the node will be included as if it is a top level node
52
54
  def arrange_nodes(nodes)
53
55
  node_ids = Set.new(nodes.map(&:id))
@@ -60,6 +62,18 @@ module Ancestry
60
62
  end
61
63
  end
62
64
 
65
+ # convert a hash of the form {node => children} to an array of nodes, child first
66
+ #
67
+ # @param arranged [Hash{Node => {Node => {}, Node => {}}}] arranged nodes
68
+ # @returns [Array[Node]] array of nodes with the parent before the children
69
+ def flatten_arranged_nodes(arranged, nodes = [])
70
+ arranged.each do |node, children|
71
+ nodes << node
72
+ flatten_arranged_nodes(children, nodes) unless children.empty?
73
+ end
74
+ nodes
75
+ end
76
+
63
77
  # Arrangement to nested array for serialization
64
78
  # You can also supply your own serialization logic using blocks
65
79
  # also allows you to pass the order just as you can pass it to the arrange method
@@ -89,29 +103,21 @@ module Ancestry
89
103
  end
90
104
 
91
105
  # Pseudo-preordered array of nodes. Children will always follow parents,
106
+ # This is deterministic unless the parents are missing *and* a sort block is specified
92
107
  def sort_by_ancestry(nodes, &block)
93
108
  arranged = nodes if nodes.is_a?(Hash)
94
109
 
95
110
  unless arranged
96
111
  presorted_nodes = nodes.sort do |a, b|
97
- a_cestry, b_cestry = a.ancestry || '0', b.ancestry || '0'
98
-
99
- if block_given? && a_cestry == b_cestry
100
- yield a, b
101
- else
102
- a_cestry <=> b_cestry
103
- end
112
+ rank = (a.ancestry || ' ') <=> (b.ancestry || ' ')
113
+ rank = yield(a, b) if rank == 0 && block_given?
114
+ rank
104
115
  end
105
116
 
106
117
  arranged = arrange_nodes(presorted_nodes)
107
118
  end
108
119
 
109
- arranged.inject([]) do |sorted_nodes, pair|
110
- node, children = pair
111
- sorted_nodes << node
112
- sorted_nodes += sort_by_ancestry(children, &block) unless children.blank?
113
- sorted_nodes
114
- end
120
+ flatten_arranged_nodes(arranged)
115
121
  end
116
122
 
117
123
  # Integrity checking
@@ -4,15 +4,25 @@ module Ancestry
4
4
  # Check options
5
5
  raise Ancestry::AncestryException.new(I18n.t("ancestry.option_must_be_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, :primary_key_format, :update_strategy, :strategy].include? key
7
+ unless [:ancestry_column, :orphan_strategy, :cache_depth, :depth_cache_column, :touch, :counter_cache, :primary_key_format, :update_strategy, :ancestry_format].include? key
8
8
  raise Ancestry::AncestryException.new(I18n.t("ancestry.unknown_option", key: key.inspect, value: value.inspect))
9
9
  end
10
10
  end
11
11
 
12
+ if options[:ancestry_format].present? && ![:materialized_path, :materialized_path2].include?( options[:ancestry_format] )
13
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.unknown_format", value: options[:ancestry_format]))
14
+ end
15
+
12
16
  # Create ancestry column accessor and set to option or default
13
17
  cattr_accessor :ancestry_column
14
18
  self.ancestry_column = options[:ancestry_column] || :ancestry
15
19
 
20
+ cattr_accessor :ancestry_primary_key_format
21
+ self.ancestry_primary_key_format = options[:primary_key_format].presence || Ancestry.default_primary_key_format
22
+
23
+ cattr_accessor :ancestry_delimiter
24
+ self.ancestry_delimiter = '/'
25
+
16
26
  # Save self as base class (for STI)
17
27
  cattr_accessor :ancestry_base_class
18
28
  self.ancestry_base_class = self
@@ -27,14 +37,19 @@ module Ancestry
27
37
  # Include dynamic class methods
28
38
  extend Ancestry::ClassMethods
29
39
 
30
- if options[:strategy] == :materialized_path2
31
- validates_format_of self.ancestry_column, :with => derive_materialized2_pattern(options[:primary_key_format]), :allow_nil => false
40
+ cattr_accessor :ancestry_format
41
+ self.ancestry_format = options[:ancestry_format] || Ancestry.default_ancestry_format
42
+
43
+ if ancestry_format == :materialized_path2
32
44
  extend Ancestry::MaterializedPath2
33
45
  else
34
- validates_format_of self.ancestry_column, :with => derive_materialized_pattern(options[:primary_key_format]), :allow_nil => true
35
46
  extend Ancestry::MaterializedPath
36
47
  end
37
48
 
49
+ attribute self.ancestry_column, default: self.ancestry_root
50
+
51
+ validates self.ancestry_column, ancestry_validation_options
52
+
38
53
  update_strategy = options[:update_strategy] || Ancestry.default_update_strategy
39
54
  include Ancestry::MaterializedPathPg if update_strategy == :sql
40
55
 
@@ -45,8 +60,8 @@ module Ancestry
45
60
  # Validate that the ancestor ids don't include own id
46
61
  validate :ancestry_exclude_self
47
62
 
48
- # Update descendants with new ancestry before save
49
- before_save :update_descendants_with_new_ancestry
63
+ # Update descendants with new ancestry after update
64
+ after_update :update_descendants_with_new_ancestry
50
65
 
51
66
  # Apply orphan strategy before destroy
52
67
  before_destroy :apply_orphan_strategy
@@ -99,31 +114,10 @@ module Ancestry
99
114
  return super if defined?(super)
100
115
  has_ancestry(*args)
101
116
  end
102
-
103
- private
104
-
105
- def derive_materialized_pattern(primary_key_format, delimiter = '/')
106
- primary_key_format ||= '[0-9]+'
107
-
108
- if primary_key_format.to_s.include?('\A')
109
- primary_key_format
110
- else
111
- /\A#{primary_key_format}(#{delimiter}#{primary_key_format})*\Z/
112
- end
113
- end
114
-
115
- def derive_materialized2_pattern(primary_key_format, delimiter = '/')
116
- primary_key_format ||= '[0-9]+'
117
-
118
- if primary_key_format.to_s.include?('\A')
119
- primary_key_format
120
- else
121
- /\A#{delimiter}(#{primary_key_format}#{delimiter})*\Z/
122
- end
123
- end
124
117
  end
125
118
  end
126
119
 
120
+ require 'active_support'
127
121
  ActiveSupport.on_load :active_record do
128
122
  extend Ancestry::HasAncestry
129
123
  end
@@ -5,15 +5,15 @@ module Ancestry
5
5
  errors.add(:base, I18n.t("ancestry.exclude_self", class_name: self.class.name.humanize)) if ancestor_ids.include? self.id
6
6
  end
7
7
 
8
- # Update descendants with new ancestry (before save)
8
+ # Update descendants with new ancestry (after update)
9
9
  def update_descendants_with_new_ancestry
10
10
  # If enabled and node is existing and ancestry was updated and the new ancestry is sane ...
11
11
  if !ancestry_callbacks_disabled? && !new_record? && ancestry_changed? && sane_ancestor_ids?
12
12
  # ... for each descendant ...
13
- unscoped_descendants.each do |descendant|
13
+ unscoped_descendants_before_save.each do |descendant|
14
14
  # ... replace old ancestry with new ancestry
15
15
  descendant.without_ancestry_callbacks do
16
- new_ancestor_ids = path_ids + (descendant.ancestor_ids - path_ids_in_database)
16
+ new_ancestor_ids = path_ids + (descendant.ancestor_ids - path_ids_before_last_save)
17
17
  descendant.update_attribute(:ancestor_ids, new_ancestor_ids)
18
18
  end
19
19
  end
@@ -133,8 +133,8 @@ module Ancestry
133
133
  ancestor_ids + [id]
134
134
  end
135
135
 
136
- def path_ids_in_database
137
- ancestor_ids_in_database + [id]
136
+ def path_ids_before_last_save
137
+ ancestor_ids_before_last_save + [id]
138
138
  end
139
139
 
140
140
  def path depth_options = {}
@@ -190,7 +190,7 @@ module Ancestry
190
190
 
191
191
  def root
192
192
  if has_parent?
193
- unscoped_where { |scope| scope.find_by(id: root_id) } || self
193
+ unscoped_where { |scope| scope.find_by(scope.primary_key => root_id) } || self
194
194
  else
195
195
  self
196
196
  end
@@ -312,10 +312,16 @@ module Ancestry
312
312
  end
313
313
  end
314
314
 
315
+ def unscoped_descendants_before_save
316
+ unscoped_where do |scope|
317
+ scope.where self.ancestry_base_class.descendant_before_save_conditions(self)
318
+ end
319
+ end
320
+
315
321
  # works with after save context (hence before_last_save)
316
322
  def unscoped_current_and_previous_ancestors
317
323
  unscoped_where do |scope|
318
- scope.where id: (ancestor_ids + ancestor_ids_before_last_save).uniq
324
+ scope.where scope.primary_key => (ancestor_ids + ancestor_ids_before_last_save).uniq
319
325
  end
320
326
  end
321
327
 
@@ -9,6 +9,7 @@ en:
9
9
 
10
10
  option_must_be_hash: "Options for has_ancestry must be in a hash."
11
11
  unknown_option: "Unknown option for has_ancestry: %{key} => %{value}."
12
+ unknown_format: "Unknown ancestry format: %{value}."
12
13
  named_scope_depth_cache: "Named scope '%{scope_name}' is only available when depth caching is enabled."
13
14
 
14
15
  exclude_self: "%{class_name} cannot be a descendant of itself."
@@ -3,11 +3,6 @@ module Ancestry
3
3
  # root a=nil,id=1 children=id,id/% == 1, 1/%
4
4
  # 3: a=1/2,id=3 children=a/id,a/id/% == 1/2/3, 1/2/3/%
5
5
  module MaterializedPath
6
- BEFORE_LAST_SAVE_SUFFIX = '_before_last_save'.freeze
7
- IN_DATABASE_SUFFIX = '_in_database'.freeze
8
- ANCESTRY_DELIMITER='/'.freeze
9
- ROOT=nil
10
-
11
6
  def self.extended(base)
12
7
  base.send(:include, InstanceMethods)
13
8
  end
@@ -17,7 +12,7 @@ module Ancestry
17
12
  end
18
13
 
19
14
  def roots
20
- where(arel_table[ancestry_column].eq(ROOT))
15
+ where(arel_table[ancestry_column].eq(ancestry_root))
21
16
  end
22
17
 
23
18
  def ancestors_of(object)
@@ -42,19 +37,26 @@ module Ancestry
42
37
  def indirects_of(object)
43
38
  t = arel_table
44
39
  node = to_node(object)
45
- where(t[ancestry_column].matches("#{node.child_ancestry}#{ANCESTRY_DELIMITER}%", nil, true))
40
+ where(t[ancestry_column].matches("#{node.child_ancestry}#{ancestry_delimiter}%", nil, true))
46
41
  end
47
42
 
48
43
  def descendants_of(object)
49
- node = to_node(object)
50
- indirects_of(node).or(children_of(node))
44
+ where(descendant_conditions(object))
51
45
  end
52
46
 
53
- # deprecated
54
- def descendant_conditions(object)
47
+ def descendants_by_ancestry(ancestry)
55
48
  t = arel_table
49
+ t[ancestry_column].matches("#{ancestry}#{ancestry_delimiter}%", nil, true).or(t[ancestry_column].eq(ancestry))
50
+ end
51
+
52
+ def descendant_conditions(object)
56
53
  node = to_node(object)
57
- t[ancestry_column].matches("#{node.child_ancestry}/%", nil, true).or(t[ancestry_column].eq(node.child_ancestry))
54
+ descendants_by_ancestry( node.child_ancestry )
55
+ end
56
+
57
+ def descendant_before_save_conditions(object)
58
+ node = to_node(object)
59
+ descendants_by_ancestry( node.child_ancestry_before_save )
58
60
  end
59
61
 
60
62
  def subtree_of(object)
@@ -86,35 +88,48 @@ module Ancestry
86
88
  ordered_by_ancestry(order)
87
89
  end
88
90
 
91
+ def ancestry_root
92
+ nil
93
+ end
94
+
95
+ private
96
+
97
+ def ancestry_validation_options
98
+ {
99
+ format: { with: ancestry_format_regexp },
100
+ allow_nil: ancestry_nil_allowed?
101
+ }
102
+ end
103
+
104
+ def ancestry_nil_allowed?
105
+ true
106
+ end
107
+
108
+ def ancestry_format_regexp
109
+ /\A#{ancestry_primary_key_format}(#{Regexp.escape(ancestry_delimiter)}#{ancestry_primary_key_format})*\z/.freeze
110
+ end
111
+
89
112
  module InstanceMethods
90
113
  # optimization - better to go directly to column and avoid parsing
91
114
  def ancestors?
92
- read_attribute(self.ancestry_base_class.ancestry_column) != ROOT
115
+ read_attribute(self.ancestry_base_class.ancestry_column) != self.ancestry_base_class.ancestry_root
93
116
  end
94
117
  alias :has_parent? :ancestors?
95
118
 
96
119
  def ancestor_ids=(value)
97
- col = self.ancestry_base_class.ancestry_column
98
- value.present? ? write_attribute(col, generate_ancestry(value)) : write_attribute(col, ROOT)
120
+ write_attribute(self.ancestry_base_class.ancestry_column, generate_ancestry(value))
99
121
  end
100
122
 
101
123
  def ancestor_ids
102
124
  parse_ancestry_column(read_attribute(self.ancestry_base_class.ancestry_column))
103
125
  end
104
126
 
105
- def ancestor_ids_in_database
106
- parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}"))
107
- end
108
-
109
127
  def ancestor_ids_before_last_save
110
- parse_ancestry_column(send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}"))
128
+ parse_ancestry_column(attribute_before_last_save(self.ancestry_base_class.ancestry_column))
111
129
  end
112
130
 
113
131
  def parent_id_before_last_save
114
- ancestry_was = send("#{self.ancestry_base_class.ancestry_column}#{BEFORE_LAST_SAVE_SUFFIX}")
115
- return if ancestry_was == ROOT
116
-
117
- parse_ancestry_column(ancestry_was).last
132
+ parse_ancestry_column(attribute_before_last_save(self.ancestry_base_class.ancestry_column)).last
118
133
  end
119
134
 
120
135
  # optimization - better to go directly to column and avoid parsing
@@ -128,18 +143,27 @@ module Ancestry
128
143
  def child_ancestry
129
144
  # New records cannot have children
130
145
  raise Ancestry::AncestryException.new(I18n.t("ancestry.no_child_for_new_record")) if new_record?
131
- path_was = self.send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}")
132
- path_was.blank? ? id.to_s : "#{path_was}#{ANCESTRY_DELIMITER}#{id}"
146
+ [attribute_in_database(self.ancestry_base_class.ancestry_column), id].compact.join(self.ancestry_base_class.ancestry_delimiter)
147
+ end
148
+
149
+ def child_ancestry_before_save
150
+ # New records cannot have children
151
+ raise Ancestry::AncestryException.new(I18n.t("ancestry.no_child_for_new_record")) if new_record?
152
+ [attribute_before_last_save(self.ancestry_base_class.ancestry_column), id].compact.join(self.ancestry_base_class.ancestry_delimiter)
133
153
  end
134
154
 
135
155
  def parse_ancestry_column(obj)
136
- return [] if obj == ROOT
137
- obj_ids = obj.split(ANCESTRY_DELIMITER)
156
+ return [] if obj.nil? || obj == self.ancestry_base_class.ancestry_root
157
+ obj_ids = obj.split(self.ancestry_base_class.ancestry_delimiter).delete_if(&:blank?)
138
158
  self.class.primary_key_is_an_integer? ? obj_ids.map!(&:to_i) : obj_ids
139
159
  end
140
160
 
141
161
  def generate_ancestry(ancestor_ids)
142
- ancestor_ids.join(ANCESTRY_DELIMITER)
162
+ if ancestor_ids.present? && ancestor_ids.any?
163
+ ancestor_ids.join(self.ancestry_base_class.ancestry_delimiter)
164
+ else
165
+ self.ancestry_base_class.ancestry_root
166
+ end
143
167
  end
144
168
  end
145
169
  end
@@ -1,53 +1,62 @@
1
1
  module Ancestry
2
2
  # store ancestry as /grandparent_id/parent_id/
3
- # root: a=/,id=1 children=a.id/% == /1/%
4
- # 3: a=/1/2/,id=3 children=a.id/% == /1/2/3/%
5
- module MaterializedPath2 < MaterializedPath
6
- def indirects_of(object)
7
- t = arel_table
8
- node = to_node(object)
9
- where(t[ancestry_column].matches("#{node.child_ancestry}%#{ANCESTRY_DELIMITER}%", nil, true))
10
- end
3
+ # root: a=/,id=1 children=#{a}#{id}/% == /1/%
4
+ # 3: a=/1/2/,id=3 children=#{a}#{id}/% == /1/2/3/%
5
+ module MaterializedPath2
6
+ include MaterializedPath
11
7
 
12
- def subtree_of(object)
13
- t = arel_table
14
- node = to_node(object)
15
- where(descendant_conditions(node).or(t[primary_key].eq(node.id)))
8
+ def self.extended(base)
9
+ base.send(:include, MaterializedPath::InstanceMethods)
10
+ base.send(:include, InstanceMethods)
16
11
  end
17
12
 
18
- def siblings_of(object)
13
+ def indirects_of(object)
19
14
  t = arel_table
20
15
  node = to_node(object)
21
- where(t[ancestry_column].eq(node[ancestry_column]))
16
+ where(t[ancestry_column].matches("#{node.child_ancestry}%#{ancestry_delimiter}%", nil, true))
22
17
  end
23
18
 
24
19
  def ordered_by_ancestry(order = nil)
25
20
  reorder(Arel::Nodes::Ascending.new(arel_table[ancestry_column]), order)
26
21
  end
27
22
 
28
- # deprecated
29
- def descendant_conditions(object)
30
- t = arel_table
31
- node = to_node(object)
32
- t[ancestry_column].matches("#{node.child_ancestry}%", nil, true)
23
+ def descendants_by_ancestry(ancestry)
24
+ arel_table[ancestry_column].matches("#{ancestry}%", nil, true)
25
+ end
26
+
27
+ def ancestry_root
28
+ ancestry_delimiter
29
+ end
30
+
31
+ private
32
+
33
+ def ancestry_nil_allowed?
34
+ false
35
+ end
36
+
37
+ def ancestry_format_regexp
38
+ /\A#{Regexp.escape(ancestry_delimiter)}(#{ancestry_primary_key_format}#{Regexp.escape(ancestry_delimiter)})*\z/.freeze
33
39
  end
34
40
 
35
41
  module InstanceMethods
36
42
  def child_ancestry
37
43
  # New records cannot have children
38
- raise Ancestry::AncestryException.new('No child ancestry for new record. Save record before performing tree operations.') if new_record?
39
- path_was = self.send("#{self.ancestry_base_class.ancestry_column}#{IN_DATABASE_SUFFIX}")
40
- "#{path_was}#{id}#{ANCESTRY_DELIMITER}"
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}"
41
46
  end
42
47
 
43
- def parse_ancestry_column(obj)
44
- return [] if obj == ROOT
45
- obj_ids = obj.split(ANCESTRY_DELIMITER).delete_if(&:blank?)
46
- self.class.primary_key_is_an_integer? ? obj_ids.map!(&:to_i) : obj_ids
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}"
47
52
  end
48
53
 
49
54
  def generate_ancestry(ancestor_ids)
50
- "#{ANCESTRY_DELIMITER}#{ancestor_ids.join(ANCESTRY_DELIMITER)}#{ANCESTRY_DELIMITER}"
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
59
+ end
51
60
  end
52
61
  end
53
62
  end
@@ -1,22 +1,22 @@
1
1
  module Ancestry
2
2
  module MaterializedPathPg
3
- # Update descendants with new ancestry (before save)
3
+ # Update descendants with new ancestry (after update)
4
4
  def update_descendants_with_new_ancestry
5
5
  # If enabled and node is existing and ancestry was updated and the new ancestry is sane ...
6
6
  if !ancestry_callbacks_disabled? && !new_record? && ancestry_changed? && sane_ancestor_ids?
7
7
  ancestry_column = ancestry_base_class.ancestry_column
8
- old_ancestry = path_ids_in_database.join(Ancestry::MaterializedPath::ANCESTRY_DELIMITER)
9
- new_ancestry = path_ids.join(Ancestry::MaterializedPath::ANCESTRY_DELIMITER)
8
+ old_ancestry = generate_ancestry( path_ids_before_last_save )
9
+ new_ancestry = generate_ancestry( path_ids )
10
10
  update_clause = [
11
- "#{ancestry_column} = regexp_replace(#{ancestry_column}, '^#{old_ancestry}', '#{new_ancestry}')"
11
+ "#{ancestry_column} = regexp_replace(#{ancestry_column}, '^#{Regexp.escape(old_ancestry)}', '#{new_ancestry}')"
12
12
  ]
13
13
 
14
14
  if ancestry_base_class.respond_to?(:depth_cache_column) && respond_to?(ancestry_base_class.depth_cache_column)
15
15
  depth_cache_column = ancestry_base_class.depth_cache_column.to_s
16
- update_clause << "#{depth_cache_column} = length(regexp_replace(regexp_replace(ancestry, '^#{old_ancestry}', '#{new_ancestry}'), '\\d', '', 'g')) + 1"
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"
17
17
  end
18
18
 
19
- unscoped_descendants.update_all update_clause.join(', ')
19
+ unscoped_descendants_before_save.update_all update_clause.join(', ')
20
20
  end
21
21
  end
22
22
  end
@@ -0,0 +1,46 @@
1
+ module Ancestry
2
+ class MaterializedPathString < ActiveRecord::Type::Value
3
+ def initialize(casting: :to_i, delimiter: '/')
4
+ @casting = casting&.to_proc
5
+ @delimiter = delimiter
6
+ end
7
+
8
+ def type
9
+ :materialized_path_string
10
+ end
11
+
12
+ # convert to database type
13
+ def serialize(value)
14
+ if value.kind_of?(Array)
15
+ value.map(&:to_s).join(@delimiter).presence
16
+ elsif value.kind_of?(Integer)
17
+ value.to_s
18
+ elsif value.nil? || value.kind_of?(String)
19
+ value
20
+ else
21
+ byebug
22
+ puts "curious type: #{value.class}"
23
+ end
24
+ end
25
+
26
+ def cast(value)
27
+ cast_value(value) #unless value.nil? (want to get rid of this - fix default value)
28
+ end
29
+
30
+ # called by cast (form or setter) or deserialize (database)
31
+ def cast_value(value)
32
+ if value.kind_of?(Array)
33
+ super
34
+ elsif value.nil?
35
+ # would prefer to use default here
36
+ # but with default, it kept thinking the field had changed when it hadn't
37
+ # (that may be a rails bug though)
38
+ super([])
39
+ else
40
+ #TODO: test ancestry=1
41
+ super(value.to_s.split(@delimiter).map(&@casting))
42
+ end
43
+ end
44
+ end
45
+ end
46
+ ActiveRecord::Type.register(:materialized_path_string, Ancestry::MaterializedPathString)
@@ -0,0 +1,46 @@
1
+ # used for materialized path
2
+ class MaterializedPathString2 < ActiveRecord::Type::Value
3
+ def initialize(casting: :to_i, delimiter: '/')
4
+ @casting = casting&.to_proc
5
+ @delimiter = delimiter
6
+ end
7
+
8
+ def type
9
+ :materialized_path_string2
10
+ end
11
+
12
+ # convert to database type
13
+ def serialize(value)
14
+ if value.kind_of?(Array)
15
+ # TODO: check all values to ensure no extra slashes
16
+ value.empty? ? @delimiter : "#{@delimiter}#{value.map(&:to_s).join(@delimiter)}#{@delimiter}"
17
+ elsif value.kind_of?(Integer)
18
+ value.to_s
19
+ elsif value.nil? || value.kind_of?(String)
20
+ value
21
+ else
22
+ byebug
23
+ puts "curious type: #{value.class}"
24
+ end
25
+ end
26
+
27
+ def cast(value)
28
+ cast_value(value) #unless value.nil? (want to get rid of this - fix default value)
29
+ end
30
+
31
+ # called by cast (form or setter) or deserialize (database)
32
+ def cast_value(value)
33
+ if value.kind_of?(Array)
34
+ super
35
+ elsif value.nil?
36
+ # would prefer to use default here
37
+ # but with default, it kept thinking the field had changed when it hadn't
38
+ super([])
39
+ else
40
+ #TODO: test ancestry=1
41
+ super(value.to_s.split(@delimiter).map(&@casting))
42
+ end
43
+ end
44
+ end
45
+ end
46
+ ActiveRecord::Type.register(:materialized_path_string2, Ancestry::MaterializedPathString)
@@ -1,3 +1,3 @@
1
1
  module Ancestry
2
- VERSION = '4.2.0'
2
+ VERSION = '4.3.0'
3
3
  end
data/lib/ancestry.rb CHANGED
@@ -4,6 +4,7 @@ require_relative 'ancestry/instance_methods'
4
4
  require_relative 'ancestry/exceptions'
5
5
  require_relative 'ancestry/has_ancestry'
6
6
  require_relative 'ancestry/materialized_path'
7
+ require_relative 'ancestry/materialized_path2'
7
8
  require_relative 'ancestry/materialized_path_pg'
8
9
 
9
10
  I18n.load_path += Dir[File.join(File.expand_path(File.dirname(__FILE__)),
@@ -11,6 +12,8 @@ I18n.load_path += Dir[File.join(File.expand_path(File.dirname(__FILE__)),
11
12
 
12
13
  module Ancestry
13
14
  @@default_update_strategy = :ruby
15
+ @@default_ancestry_format = :materialized_path
16
+ @@default_primary_key_format = '[0-9]+'
14
17
 
15
18
  # @!default_update_strategy
16
19
  # @return [Symbol] the default strategy for updating ancestry
@@ -26,7 +29,6 @@ module Ancestry
26
29
  #
27
30
  # Child records are updated in sql and callbacks will not get called.
28
31
  # Associated records in memory will have the wrong ancestry value
29
-
30
32
  def self.default_update_strategy
31
33
  @@default_update_strategy
32
34
  end
@@ -34,4 +36,39 @@ module Ancestry
34
36
  def self.default_update_strategy=(value)
35
37
  @@default_update_strategy = value
36
38
  end
39
+
40
+ # @!default_ancestry_format
41
+ # @return [Symbol] the default strategy for updating ancestry
42
+ #
43
+ # The value changes the default way that ancestry is stored in the database
44
+ #
45
+ # :materialized_path (default and legacy)
46
+ #
47
+ # Ancestry is of the form null (for no ancestors) and 1/2/ for children
48
+ #
49
+ # :materialized_path2 (preferred)
50
+ #
51
+ # Ancestry is of the form '/' (for no ancestors) and '/1/2/' for children
52
+ def self.default_ancestry_format
53
+ @@default_ancestry_format
54
+ end
55
+
56
+ def self.default_ancestry_format=(value)
57
+ @@default_ancestry_format = value
58
+ end
59
+
60
+ # @!default_primary_key_format
61
+ # @return [Symbol] the regular expression representing the primary key
62
+ #
63
+ # The value represents the way the id looks for validation
64
+ #
65
+ # '[0-9]+' (default) for integer ids
66
+ # '[-A-Fa-f0-9]{36}' for uuids (though you can find other regular expressions)
67
+ def self.default_primary_key_format
68
+ @@default_primary_key_format
69
+ end
70
+
71
+ def self.default_primary_key_format=(value)
72
+ @@default_primary_key_format = value
73
+ end
37
74
  end