ancestry 4.2.0 → 4.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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