acts_as_recursive_tree 2.1.0 → 3.0.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +37 -0
  3. data/.github/workflows/lint.yml +31 -0
  4. data/.github/workflows/rubygem.yml +37 -0
  5. data/.gitignore +3 -1
  6. data/.rubocop.yml +30 -1
  7. data/.rubocop_todo.yml +28 -281
  8. data/Appraisals +16 -0
  9. data/CHANGELOG.md +17 -1
  10. data/Gemfile +2 -0
  11. data/README.md +3 -0
  12. data/Rakefile +8 -8
  13. data/acts_as_recursive_tree.gemspec +27 -18
  14. data/gemfiles/ar_52.gemfile +8 -0
  15. data/gemfiles/ar_60.gemfile +8 -0
  16. data/gemfiles/ar_61.gemfile +8 -0
  17. data/lib/acts_as_recursive_tree.rb +7 -11
  18. data/lib/acts_as_recursive_tree/acts_macro.rb +6 -6
  19. data/lib/acts_as_recursive_tree/associations.rb +10 -8
  20. data/lib/acts_as_recursive_tree/builders/ancestors.rb +3 -5
  21. data/lib/acts_as_recursive_tree/builders/descendants.rb +3 -3
  22. data/lib/acts_as_recursive_tree/builders/leaves.rb +7 -8
  23. data/lib/acts_as_recursive_tree/builders/relation_builder.rb +25 -28
  24. data/lib/acts_as_recursive_tree/builders/{strategy.rb → strategies.rb} +4 -7
  25. data/lib/acts_as_recursive_tree/builders/strategies/ancestor.rb +19 -0
  26. data/lib/acts_as_recursive_tree/builders/strategies/descendant.rb +19 -0
  27. data/lib/acts_as_recursive_tree/builders/{strategy → strategies}/join.rb +10 -4
  28. data/lib/acts_as_recursive_tree/builders/{strategy → strategies}/subselect.rb +3 -1
  29. data/lib/acts_as_recursive_tree/config.rb +2 -0
  30. data/lib/acts_as_recursive_tree/model.rb +9 -8
  31. data/lib/acts_as_recursive_tree/options/depth_condition.rb +3 -2
  32. data/lib/acts_as_recursive_tree/options/query_options.rb +10 -1
  33. data/lib/acts_as_recursive_tree/options/values.rb +28 -18
  34. data/lib/acts_as_recursive_tree/railtie.rb +2 -0
  35. data/lib/acts_as_recursive_tree/scopes.rb +8 -4
  36. data/lib/acts_as_recursive_tree/version.rb +3 -1
  37. data/spec/builders_spec.rb +21 -12
  38. data/spec/db/database.rb +11 -4
  39. data/spec/db/database.yml +2 -5
  40. data/spec/db/models.rb +12 -11
  41. data/spec/db/schema.rb +3 -4
  42. data/spec/model/location_spec.rb +7 -11
  43. data/spec/model/node_spec.rb +35 -49
  44. data/spec/model/relation_spec.rb +6 -11
  45. data/spec/spec_helper.rb +54 -55
  46. data/spec/values_spec.rb +23 -19
  47. metadata +111 -25
  48. data/lib/acts_as_recursive_tree/builders.rb +0 -14
  49. data/lib/acts_as_recursive_tree/options.rb +0 -9
@@ -1,14 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActsAsRecursiveTree
2
4
  module Builders
3
5
  #
4
6
  # Strategy module for different strategies of how to build the resulting query.
5
7
  #
6
- module Strategy
7
- extend ActiveSupport::Autoload
8
-
9
- autoload :Join
10
- autoload :Subselect
11
-
8
+ module Strategies
12
9
  #
13
10
  # Returns a Strategy appropriate for query_opts
14
11
  #
@@ -16,7 +13,7 @@ module ActsAsRecursiveTree
16
13
  #
17
14
  # @return a strategy class best suited for the opts
18
15
  def self.for_query_options(query_opts)
19
- if query_opts.depth_present? || query_opts.ensure_ordering
16
+ if query_opts.ensure_ordering || query_opts.query_strategy == :join
20
17
  Join
21
18
  else
22
19
  Subselect
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsRecursiveTree
4
+ module Builders
5
+ module Strategies
6
+ #
7
+ # Strategy for building ancestors relation
8
+ #
9
+ module Ancestor
10
+ #
11
+ # Builds the relation
12
+ #
13
+ def self.build(builder)
14
+ builder.travers_loc_table[builder.parent_key].eq(builder.base_table[builder.primary_key])
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActsAsRecursiveTree
4
+ module Builders
5
+ module Strategies
6
+ #
7
+ # Strategy for building descendants relation
8
+ #
9
+ module Descendant
10
+ #
11
+ # Builds the relation
12
+ #
13
+ def self.build(builder)
14
+ builder.base_table[builder.parent_key].eq(builder.travers_loc_table[builder.primary_key])
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActsAsRecursiveTree
2
4
  module Builders
3
- module Strategy
5
+ module Strategies
4
6
  #
5
7
  # Build a relation using an INNER JOIN.
6
8
  #
@@ -19,9 +21,13 @@ module ActsAsRecursiveTree
19
21
 
20
22
  relation = builder.klass.joins(final_select_mgr.join_sources)
21
23
 
22
- relation = builder.apply_depth(relation)
23
- relation = builder.apply_order(relation)
24
- relation
24
+ apply_order(builder, relation)
25
+ end
26
+
27
+ def self.apply_order(builder, relation)
28
+ return relation unless builder.ensure_ordering
29
+
30
+ relation.order(builder.recursive_temp_table[builder.depth_column].asc)
25
31
  end
26
32
  end
27
33
  end
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActsAsRecursiveTree
2
4
  module Builders
3
- module Strategy
5
+ module Strategies
4
6
  #
5
7
  # Strategy for building a relation using an WHERE ID IN(...).
6
8
  #
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActsAsRecursiveTree
2
4
  #
3
5
  # Stores the configuration of one Model class
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActsAsRecursiveTree
2
4
  module Model
3
5
  extend ActiveSupport::Concern
@@ -40,7 +42,7 @@ module ActsAsRecursiveTree
40
42
  ##
41
43
  # Returns the root node of the tree.
42
44
  def root
43
- self_and_ancestors.where(self._recursive_tree_config.parent_key => nil).first
45
+ self_and_ancestors.where(_recursive_tree_config.parent_key => nil).first
44
46
  end
45
47
 
46
48
  ##
@@ -48,7 +50,7 @@ module ActsAsRecursiveTree
48
50
  #
49
51
  # subchild1.siblings # => [subchild2]
50
52
  def siblings
51
- self_and_siblings.where.not(id: self.id)
53
+ self_and_siblings.where.not(id: id)
52
54
  end
53
55
 
54
56
  ##
@@ -57,11 +59,11 @@ module ActsAsRecursiveTree
57
59
  # root.self_and_children # => [root, child1]
58
60
  def self_and_children
59
61
  table = self.class.arel_table
60
- id = self.attributes[self._recursive_tree_config.primary_key.to_s]
62
+ id = attributes[_recursive_tree_config.primary_key.to_s]
61
63
 
62
64
  base_class.where(
63
- table[self._recursive_tree_config.primary_key].eq(id).or(
64
- table[self._recursive_tree_config.parent_key].eq(id)
65
+ table[_recursive_tree_config.primary_key].eq(id).or(
66
+ table[_recursive_tree_config.parent_key].eq(id)
65
67
  )
66
68
  )
67
69
  end
@@ -73,13 +75,12 @@ module ActsAsRecursiveTree
73
75
  base_class.leaves_of(self)
74
76
  end
75
77
 
76
-
77
78
  # Returns true if node has no parent, false otherwise
78
79
  #
79
80
  # subchild1.root? # => false
80
81
  # root.root? # => true
81
82
  def root?
82
- self.attributes[self._recursive_tree_config.parent_key.to_s].blank?
83
+ attributes[_recursive_tree_config.parent_key.to_s].blank?
83
84
  end
84
85
 
85
86
  # Returns true if node has no children, false otherwise
@@ -87,7 +88,7 @@ module ActsAsRecursiveTree
87
88
  # subchild1.leaf? # => true
88
89
  # child1.leaf? # => false
89
90
  def leaf?
90
- !children.any?
91
+ children.none?
91
92
  end
92
93
 
93
94
  def base_class
@@ -1,7 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActsAsRecursiveTree
2
4
  module Options
3
5
  class DepthCondition
4
-
5
6
  def ==(other)
6
7
  @value = Values.create(other)
7
8
  @operation = true
@@ -45,4 +46,4 @@ module ActsAsRecursiveTree
45
46
  end
46
47
  end
47
48
  end
48
- end
49
+ end
@@ -1,9 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActsAsRecursiveTree
2
4
  module Options
3
5
  class QueryOptions
6
+ STRATEGIES = %i[subselect join].freeze
4
7
 
5
8
  attr_accessor :condition
6
- attr_reader :ensure_ordering
9
+ attr_reader :ensure_ordering, :query_strategy
7
10
 
8
11
  def depth
9
12
  @depth ||= DepthCondition.new
@@ -16,6 +19,12 @@ module ActsAsRecursiveTree
16
19
  def depth_present?
17
20
  @depth.present?
18
21
  end
22
+
23
+ def query_strategy=(strategy)
24
+ raise "invalid strategy #{strategy} - only #{STRATEGIES} are allowed" unless STRATEGIES.include?(strategy)
25
+
26
+ @query_strategy = strategy
27
+ end
19
28
  end
20
29
  end
21
30
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActsAsRecursiveTree
2
4
  module Options
3
5
  module Values
@@ -13,13 +15,9 @@ module ActsAsRecursiveTree
13
15
  value
14
16
  end
15
17
 
16
- def apply_to(attribute)
17
-
18
- end
19
-
20
- def apply_negated_to(attribute)
18
+ def apply_to(attribute); end
21
19
 
22
- end
20
+ def apply_negated_to(attribute); end
23
21
  end
24
22
 
25
23
  class SingleValue < Base
@@ -38,6 +36,16 @@ module ActsAsRecursiveTree
38
36
  end
39
37
  end
40
38
 
39
+ class RangeValue < Base
40
+ def apply_to(attribute)
41
+ attribute.between(prepared_value)
42
+ end
43
+
44
+ def apply_negated_to(attribute)
45
+ attribute.not_between(prepared_value)
46
+ end
47
+ end
48
+
41
49
  class MultiValue < Base
42
50
  def apply_to(attribute)
43
51
  attribute.in(prepared_value)
@@ -58,20 +66,22 @@ module ActsAsRecursiveTree
58
66
 
59
67
  def self.create(value, config = nil)
60
68
  klass = case value
61
- when ::Numeric, ::String
62
- SingleValue
63
- when ::ActiveRecord::Relation
64
- Relation
65
- when Enumerable
66
- MultiValue
67
- when ::ActiveRecord::Base
68
- ActiveRecord
69
- else
70
- raise "#{value.class} is not supported"
71
- end
69
+ when ::Numeric, ::String
70
+ SingleValue
71
+ when ::ActiveRecord::Relation
72
+ Relation
73
+ when Range
74
+ RangeValue
75
+ when Enumerable
76
+ MultiValue
77
+ when ::ActiveRecord::Base
78
+ ActiveRecord
79
+ else
80
+ raise "#{value.class} is not supported"
81
+ end
72
82
 
73
83
  klass.new(value, config)
74
84
  end
75
85
  end
76
86
  end
77
- end
87
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActsAsRecursiveTree
2
4
  class Railtie < Rails::Railtie
3
5
  initializer 'acts_as_recursive_tree.active_record_initializer' do
@@ -1,13 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActsAsRecursiveTree
2
4
  module Scopes
3
5
  extend ActiveSupport::Concern
4
6
 
5
7
  included do
6
- scope :roots, -> {
8
+ scope :roots, lambda {
7
9
  rel = where(_recursive_tree_config.parent_key => nil)
8
- rel = rel.or(
9
- where.not(_recursive_tree_config.parent_type_column => self.to_s)
10
- ) if _recursive_tree_config.parent_type_column
10
+ if _recursive_tree_config.parent_type_column
11
+ rel = rel.or(
12
+ where.not(_recursive_tree_config.parent_type_column => to_s)
13
+ )
14
+ end
11
15
 
12
16
  rel
13
17
  }
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ActsAsRecursiveTree
2
- VERSION = '2.1.0'.freeze
4
+ VERSION = '3.0.0'
3
5
  end
@@ -1,13 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'spec_helper'
2
4
 
3
5
  shared_context 'setup with enforced ordering' do
4
6
  let(:ordering) { false }
5
7
  include_context 'base_setup' do
6
- let(:proc) { -> (config) { config.ensure_ordering! } }
8
+ let(:proc) { ->(config) { config.ensure_ordering! } }
7
9
  end
8
10
  end
9
11
 
10
12
  shared_context 'base_setup' do
13
+ subject(:query) { builder.build.to_sql }
14
+
11
15
  let(:model_id) { 1 }
12
16
  let(:model_class) { Node }
13
17
  let(:exclude_ids) { false }
@@ -15,17 +19,20 @@ shared_context 'base_setup' do
15
19
  let(:builder) do
16
20
  described_class.new(model_class, model_id, exclude_ids: exclude_ids, &proc)
17
21
  end
18
- subject(:query) { builder.build.to_sql }
19
22
  end
20
23
 
21
24
  shared_examples 'basic recursive examples' do
22
25
  it { is_expected.to start_with "SELECT \"#{model_class.table_name}\".* FROM \"#{model_class.table_name}\"" }
23
- it { is_expected.to match /WHERE "#{model_class.table_name}"."#{model_class.primary_key}" = #{model_id}/ }
24
- it { is_expected.to match /WITH RECURSIVE "#{builder.travers_loc_table.name}" AS/ }
25
- it { is_expected.to match /SELECT "#{model_class.table_name}"."#{model_class.primary_key}", "#{model_class.table_name}"."#{model_class._recursive_tree_config.parent_key}", 0 AS recursive_depth FROM "#{model_class.table_name}"/ }
26
- it { is_expected.to match /SELECT "#{model_class.table_name}"."#{model_class.primary_key}", "#{model_class.table_name}"."#{model_class._recursive_tree_config.parent_key}", \("#{builder.travers_loc_table.name}"."recursive_depth" \+ 1\) AS recursive_depth FROM "#{model_class.table_name}"/ }
27
- it { is_expected.to match /#{Regexp.escape(builder.travers_loc_table.project(Arel.star).to_sql)}/ }
28
- it { is_expected.to match /"#{model_class.table_name}"."#{model_class.primary_key}" = "#{builder.recursive_temp_table.name}"."#{model_class.primary_key}"/ }
26
+
27
+ it { is_expected.to match(/WHERE "#{model_class.table_name}"."#{model_class.primary_key}" = #{model_id}/) }
28
+
29
+ it { is_expected.to match(/WITH RECURSIVE "#{builder.travers_loc_table.name}" AS/) }
30
+
31
+ it { is_expected.to match(/SELECT "#{model_class.table_name}"."#{model_class.primary_key}", "#{model_class.table_name}"."#{model_class._recursive_tree_config.parent_key}", 0 AS recursive_depth FROM "#{model_class.table_name}"/) }
32
+
33
+ it {
34
+ expect(subject).to match(/SELECT "#{model_class.table_name}"."#{model_class.primary_key}", "#{model_class.table_name}"."#{model_class._recursive_tree_config.parent_key}", \("#{builder.travers_loc_table.name}"."recursive_depth" \+ 1\) AS recursive_depth FROM "#{model_class.table_name}"/)
35
+ }
29
36
  end
30
37
 
31
38
  shared_examples 'build recursive query' do
@@ -63,13 +70,14 @@ end
63
70
  shared_examples 'ancestor query' do
64
71
  include_context 'base_setup'
65
72
 
66
- it { is_expected.to match /"#{builder.travers_loc_table.name}"."#{model_class._recursive_tree_config.parent_key}" = "#{model_class.table_name}"."#{model_class.primary_key}"/ }
73
+ it { is_expected.to match(/"#{builder.travers_loc_table.name}"."#{model_class._recursive_tree_config.parent_key}" = "#{model_class.table_name}"."#{model_class.primary_key}"/) }
67
74
  end
68
75
 
69
76
  shared_examples 'descendant query' do
70
77
  include_context 'base_setup'
71
78
 
72
- it { is_expected.to match /"#{model_class.table_name}"."#{model_class._recursive_tree_config.parent_key}" = "#{builder.travers_loc_table.name}"."#{model_class.primary_key}"/ }
79
+ it { is_expected.to match(/"#{model_class.table_name}"."#{model_class._recursive_tree_config.parent_key}" = "#{builder.travers_loc_table.name}"."#{model_class.primary_key}"/) }
80
+ it { is_expected.to match(/#{Regexp.escape(builder.travers_loc_table.project(builder.travers_loc_table[model_class.primary_key]).to_sql)}/) }
73
81
  end
74
82
 
75
83
  shared_context 'context with ordering' do
@@ -85,11 +93,11 @@ shared_context 'context without ordering' do
85
93
  end
86
94
 
87
95
  shared_examples 'with ordering' do
88
- it { is_expected.to match /ORDER BY #{Regexp.escape(builder.recursive_temp_table[model_class._recursive_tree_config.depth_column].asc.to_sql)}/ }
96
+ it { is_expected.to match(/ORDER BY #{Regexp.escape(builder.recursive_temp_table[model_class._recursive_tree_config.depth_column].asc.to_sql)}/) }
89
97
  end
90
98
 
91
99
  shared_examples 'without ordering' do
92
- it { is_expected.to_not match /ORDER BY/ }
100
+ it { is_expected.not_to match(/ORDER BY/) }
93
101
  end
94
102
 
95
103
  describe ActsAsRecursiveTree::Builders::Descendants do
@@ -113,6 +121,7 @@ describe ActsAsRecursiveTree::Builders::Ancestors do
113
121
  it_behaves_like 'ancestor query'
114
122
  include_context 'context with ordering'
115
123
  end
124
+
116
125
  context 'with options' do
117
126
  include_context 'setup with enforced ordering' do
118
127
  it_behaves_like 'with ordering'
data/spec/db/database.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  database_folder = "#{File.dirname(__FILE__)}/../db"
2
4
  database_adapter = 'sqlite'
3
5
 
@@ -6,12 +8,18 @@ ActiveRecord::Base.logger = nil
6
8
 
7
9
  ActiveRecord::Migration.verbose = false
8
10
 
9
- ActiveRecord::Base.configurations = YAML::load(File.read("#{database_folder}/database.yml"))
11
+ ActiveRecord::Base.configurations = YAML.safe_load(File.read("#{database_folder}/database.yml"))
10
12
 
11
- config = ActiveRecord::Base.configurations[database_adapter]
13
+ if ActiveRecord.version >= Gem::Version.new('6.1.0')
14
+ config = ActiveRecord::Base.configurations.configs_for env_name: database_adapter, name: 'primary'
15
+ database = config.database
16
+ else
17
+ config = ActiveRecord::Base.configurations[database_adapter]
18
+ database = config['database']
19
+ end
12
20
 
13
21
  # remove database if present
14
- FileUtils.rm config['database'], force: true
22
+ FileUtils.rm database, force: true
15
23
 
16
24
  ActiveRecord::Base.establish_connection(database_adapter.to_sym)
17
25
  ActiveRecord::Base.establish_connection(config)
@@ -19,4 +27,3 @@ ActiveRecord::Base.establish_connection(config)
19
27
  # require schemata and models
20
28
  require_relative 'schema'
21
29
  require_relative 'models'
22
-