activeset 0.6.5 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.rubocop.yml +0 -6
  4. data/CHANGELOG +5 -0
  5. data/README.md +58 -6
  6. data/activeset.gemspec +8 -8
  7. data/bin/console +14 -0
  8. data/lib/.DS_Store +0 -0
  9. data/lib/active_set.rb +28 -21
  10. data/lib/active_set/{instruction.rb → attribute_instruction.rb} +12 -8
  11. data/lib/active_set/column_instruction.rb +44 -0
  12. data/lib/active_set/exporting/operation.rb +66 -0
  13. data/lib/active_set/filtering/operation.rb +190 -0
  14. data/lib/active_set/paginating/operation.rb +102 -0
  15. data/lib/active_set/sorting/operation.rb +144 -0
  16. data/lib/helpers/throws.rb +2 -2
  17. data/lib/helpers/transform_to_sortable_numeric.rb +36 -0
  18. data/lib/patches/core_ext/hash/flatten_keys.rb +43 -14
  19. metadata +55 -39
  20. data/lib/active_set/.DS_Store +0 -0
  21. data/lib/active_set/adapter_activerecord.rb +0 -58
  22. data/lib/active_set/adapter_base.rb +0 -14
  23. data/lib/active_set/instructions.rb +0 -42
  24. data/lib/active_set/processor_base.rb +0 -28
  25. data/lib/active_set/processor_filter.rb +0 -21
  26. data/lib/active_set/processor_filter/active_record_adapter.rb +0 -50
  27. data/lib/active_set/processor_filter/enumerable_adapter.rb +0 -21
  28. data/lib/active_set/processor_paginate.rb +0 -35
  29. data/lib/active_set/processor_paginate/active_record_adapter.rb +0 -37
  30. data/lib/active_set/processor_paginate/enumerable_adapter.rb +0 -39
  31. data/lib/active_set/processor_sort.rb +0 -19
  32. data/lib/active_set/processor_sort/active_record_adapter.rb +0 -27
  33. data/lib/active_set/processor_sort/active_record_operation.rb +0 -55
  34. data/lib/active_set/processor_sort/enumerable_adapter.rb +0 -58
  35. data/lib/active_set/processor_transform.rb +0 -30
  36. data/lib/active_set/processor_transform/csv_adapter.rb +0 -77
  37. data/lib/active_set/version.rb +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 58ac4ca493d503b7c0801bcad0c24bf2ab050732
4
- data.tar.gz: c5e1c3d571a14ed3abed1b2e26db9b4009457302
3
+ metadata.gz: 4c88778d3e9cdb17bb755baad18eccf5a1ec41f1
4
+ data.tar.gz: 25b78baf469767293cf301fd3589a00a5a0c9b94
5
5
  SHA512:
6
- metadata.gz: 42284a6d184c695acf3ff502e0d32de0f3a4c00707e375ba56d3e2c91fc099884ce1313783b16b1d5db9b488ef8199f60a2b789a6566f19b8784d3708658da47
7
- data.tar.gz: 8c693840d6b28e1ebd6e3a245e41c68cc6dbb8c60817355e2e913492502177377739345d119e9786c1275f247f108cff08125ad23577d83c7f1b048700b45678
6
+ metadata.gz: c8e848d5d4fde3130fec90980e8e0530db6612bf4e11663f0fdaf2d52c5f60e319bd162674a5fd298fa71249f15dfa15fda1320917c160487ba0485461a527b1
7
+ data.tar.gz: 6f695aa0590f3a8ead38041bc94818ac6faaf0e3cd3c2b85b831593c6ec3a64a910fd37fc87ebfef9f3755434f4613cef88317e2f64e2f09fee9e1aa28995c13
data/.gitignore CHANGED
@@ -12,3 +12,4 @@
12
12
  .rspec_status
13
13
  *.gem
14
14
  *.sqlite
15
+ .DS_Store
data/.rubocop.yml CHANGED
@@ -1,11 +1,5 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 2.3
3
- Include:
4
- - Rakefile
5
- Exclude:
6
- - bin/*
7
- - db/**/*
8
- - spec/*_helper.rb
9
3
  Metrics/LineLength:
10
4
  Max: 120
11
5
  # To make it possible to copy or click on URIs in the code, we allow lines
data/CHANGELOG CHANGED
@@ -1,3 +1,8 @@
1
+ v 0.7.0
2
+ - another rewrite from the ground up
3
+ - allow each operation to exist and function in its own way and in its own context
4
+ - only write tests against the public interface of the ActiveSet class, not any internal classes
5
+ - attempt a proper and complete sorting implementation that offers multi-dimensional, multi-directional sorting for either ActiveRecord or Enumerable sets
1
6
  v 0.6.5
2
7
  - Fix the ActiveRecord filtering adapter to use the correct operator (=) for Oracle WHERE clauses
3
8
  v 0.6.4
data/README.md CHANGED
@@ -18,17 +18,69 @@ Or install it yourself as:
18
18
 
19
19
  ## Usage
20
20
 
21
- The `ActiveSet` class is an extension of `Enumerable` that adds methods for filtering, sorting, paginating, and transforming (as of right now).
21
+ The `ActiveSet` class provides convenience methods for filtering, sorting, paginating, and transforming collections of data-objects, whether `ActiveRecord::Relation`s or Ruby `Array`s or custom classes that extend the `Enumerable` module.
22
22
 
23
- Every convenience method added to the `ActiveSet` class is handled via a `Processor` class, and that `Processor` class will then use 1 or more `Adapter` classes to actually fulfill the functional contract.
23
+ When calling a convenience method on an instance of `ActiveSet`, you pass only 1 argument: a plain-old-Ruby-hash that encodes the instructions for that operation. Each convenience method works with hashes of differing signatures.
24
24
 
25
- an `Adapter` for a particular `Processor` simply handles doing the job of the processor for a particular kind of Set.
25
+ ## Filtering
26
26
 
27
- So, for example, the `Filter::Processor` will have an `EnumerableAdapter` (to work with generic enumerable sets) and an `ActiveRecordAdapter` (to work with active record relations).
27
+ `ActiveSet` allows for you to filter your collection by:
28
28
 
29
- When calling a convenience method on an instance of `ActiveSet`, you pass only 1 argument: what I am currently calling a `structure`, which is a plain-old-Ruby-hash. Each convenience method works with hashes of differing signatures.
29
+ - "direct" attributes (i.e. for an `ActiveRecord` model, a database attribute)
30
+ - "computed" attributes (i.e. Ruby getter-methods on your data-objects)
31
+ - "associated" attributes (i.e. either direct or computed attributes on objects associated with your data-objects)
32
+ - "called" attributes (i.e. Ruby methods with non-zero arity)
30
33
 
31
- e.g. `filter(attribute: 'value', association: { field: 'value' })` or `sort(attribute: :asc, association: { field: 'desc' })` or `paginate(page: 1, size: 10)`
34
+ The syntax for the instructions hash is relatively simple:
35
+
36
+ ```ruby
37
+ {
38
+ attribute: 'value',
39
+ association: {
40
+ field: 'value'
41
+ }
42
+ }
43
+ ```
44
+
45
+ Every entry in the instructions hash is treated and processed as an independent operation, and all operations are _conjoined_ ("AND"-ed). At the moment, you cannot use disjointed ("OR"-ed) operations.
46
+
47
+ The logic of this method is to attempt to process every instruction with the ActiveRecordStrategy, marking all successful attempts. If we successfully processed every instruction, we simply returned the processed result. If there are any instructions that went unprocessed, we take only those instructions and process them against the set processed by the ActiveRecordStrategy.
48
+
49
+ This filtering operation does not preserve the order of the filters, enforces conjunction, and will functionally discard any unprocessable instruction.
50
+
51
+ ## Sorting
52
+
53
+ `ActiveSet` allows for multi-dimensional, multi-directional sorting across the same kinds of attributes as filtering ("direct", "computed", "associated", and "called").
54
+
55
+ The syntax for the instructions hash is relatively simple:
56
+
57
+ ```ruby
58
+ {
59
+ attribute: :desc,
60
+ association: {
61
+ field: :asc
62
+ }
63
+ }
64
+ ```
65
+
66
+ The logic for this method is to check whether all of the instructions appear to be processable by the ActiveRecordStrategy, and if they are to attempt to sort using the ActiveRecordStrategy (with all of the instructions, as you can't split sorting instructions). We then double check that all of the instructions were indeed successfully processed by the ActiveRecordStrategy, and if they were, we return that result. Otherwise (if either some instructions don't appear to be processable by ActiveRecord or some instructions weren't processed by ActiveRecord), we sort with the EnumerableStrategy.
67
+
68
+ ## Paginating
69
+
70
+ `ActiveSet` also allows for paginating both ActiveRecord or plain Ruby enumerable sets.
71
+
72
+ The syntax for the instructions hash remains relatively simple:
73
+
74
+ ```ruby
75
+ {
76
+ size: 25,
77
+ page: 1
78
+ }
79
+ ```
80
+
81
+ Unlike the filtering or sorting operations, you do not have to pass an instructions hash, as the operation will default to paginating with a `size` of 25 and starting on `page` 1.
82
+
83
+ Paginating as an operation works with "direct" instructions (that is, the instructions don't represent attribute paths or column structures; the instructions hash is a simple, flat hash), and the operation requires all instruction entries together (as opposed to filtering for example, where we can process each instruction entry separately).
32
84
 
33
85
  ## Future Feature Ideas
34
86
 
data/activeset.gemspec CHANGED
@@ -1,14 +1,12 @@
1
- # coding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
- lib = File.expand_path('../lib', __FILE__)
3
+ lib = File.expand_path('lib', __dir__)
5
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
- require 'active_set/version'
7
5
 
8
6
  Gem::Specification.new do |spec|
9
7
  spec.platform = Gem::Platform::RUBY
10
8
  spec.name = 'activeset'
11
- spec.version = ActiveSet::VERSION
9
+ spec.version = '0.7.0'
12
10
  spec.authors = ['Stephen Margheim']
13
11
  spec.email = ['stephen.margheim@gmail.com']
14
12
 
@@ -27,12 +25,14 @@ Gem::Specification.new do |spec|
27
25
  spec.add_dependency 'activesupport', '>= 4.0.2'
28
26
 
29
27
  spec.add_development_dependency 'bundler', '~> 1.15'
30
- spec.add_development_dependency 'rake', '~> 10.0'
31
- spec.add_development_dependency 'rspec', '~> 3.0'
32
- spec.add_development_dependency 'database_cleaner', '~> 1.6.1'
33
28
  spec.add_development_dependency 'combustion', '~> 0.7.0'
34
- spec.add_development_dependency 'factory_girl', '~> 4.8.0'
29
+ spec.add_development_dependency 'database_cleaner', '~> 1.6.1'
30
+ spec.add_development_dependency 'factory_bot', '~> 4.8.0'
35
31
  spec.add_development_dependency 'faker', '~> 1.8.4'
32
+ spec.add_development_dependency 'rails', '~> 5.1.0'
33
+ spec.add_development_dependency 'rake', '~> 10.0'
34
+ spec.add_development_dependency 'rspec', '~> 3.0'
35
+ spec.add_development_dependency 'rubocop'
36
36
  spec.add_development_dependency 'simplecov', '~> 0.15.0'
37
37
  spec.add_development_dependency 'simplecov-console', '~> 0.4.2'
38
38
  end
data/bin/console CHANGED
@@ -1,11 +1,25 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ ENV['RAILS_ENV'] ||= 'test'
5
+
6
+ require 'bundler'
7
+ Bundler.require :default, :development
2
8
 
3
9
  require 'bundler/setup'
4
10
  require 'active_set'
11
+ require 'ostruct'
5
12
 
6
13
  # You can add fixtures and/or initialization code here to make experimenting
7
14
  # with your gem easier. You can also use a different console, if you like.
8
15
 
16
+ Combustion.initialize! :active_record
17
+
18
+ begin
19
+ FactoryBot.find_definitions
20
+ rescue FactoryBot::DuplicateDefinitionError
21
+ end
22
+
9
23
  # (If you use this, don't forget to add pry to your Gemfile!)
10
24
  # require "pry"
11
25
  # Pry.start
data/lib/.DS_Store CHANGED
Binary file
data/lib/active_set.rb CHANGED
@@ -1,12 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_set/version'
4
-
5
3
  require 'active_support/core_ext/hash/reverse_merge'
6
- require 'active_set/processor_filter'
7
- require 'active_set/processor_sort'
8
- require 'active_set/processor_paginate'
9
- require 'active_set/processor_transform'
4
+ require 'patches/core_ext/hash/flatten_keys'
5
+ require 'helpers/throws'
6
+ require 'active_set/attribute_instruction'
7
+ require 'active_set/filtering/operation'
8
+ require 'active_set/sorting/operation'
9
+ require 'active_set/paginating/operation'
10
+ require 'active_set/exporting/operation'
10
11
 
11
12
  class ActiveSet
12
13
  include Enumerable
@@ -23,8 +24,14 @@ class ActiveSet
23
24
  @view.each(&block)
24
25
  end
25
26
 
27
+ # :nocov:
28
+ def inspect
29
+ "#<ActiveSet:#{format('0x00%x', (object_id << 1))} @instructions=#{@instructions.inspect}>"
30
+ end
31
+
26
32
  def ==(other)
27
33
  return @view == other unless other.is_a?(ActiveSet)
34
+
28
35
  @view == other.view
29
36
  end
30
37
 
@@ -37,27 +44,26 @@ class ActiveSet
37
44
  def respond_to_missing?(method_name, include_private = false)
38
45
  @view.respond_to?(method_name) || super
39
46
  end
47
+ # :nocov:
40
48
 
41
- def filter(instructions)
42
- filterer = Processor::Filter.new(@view, instructions)
43
- reinitialize(filterer.process, :filter, instructions)
49
+ def filter(instructions_hash)
50
+ filterer = Filtering::Operation.new(@view, instructions_hash)
51
+ reinitialize(filterer.execute, :filter, filterer.operation_instructions)
44
52
  end
45
53
 
46
- def sort(instructions)
47
- sorter = Processor::Sort.new(@view, instructions)
48
- reinitialize(sorter.process, :sort, instructions)
54
+ def sort(instructions_hash)
55
+ sorter = Sorting::Operation.new(@view, instructions_hash)
56
+ reinitialize(sorter.execute, :sort, sorter.operation_instructions)
49
57
  end
50
58
 
51
- def paginate(instructions)
52
- paginater = Processor::Paginate.new(@view, instructions)
53
- full_instructions = instructions.reverse_merge(page: paginater.instructions.get(:page),
54
- size: paginater.instructions.get(:size))
55
- reinitialize(paginater.process, :paginate, full_instructions)
59
+ def paginate(instructions_hash)
60
+ paginater = Paginating::Operation.new(@view, instructions_hash)
61
+ reinitialize(paginater.execute, :paginate, paginater.operation_instructions)
56
62
  end
57
63
 
58
- def transform(instructions)
59
- transformer = Processor::Transform.new(@view, instructions)
60
- transformer.process
64
+ def export(instructions_hash)
65
+ exporter = Exporting::Operation.new(@view, instructions_hash)
66
+ exporter.execute
61
67
  end
62
68
 
63
69
  private
@@ -66,6 +72,7 @@ class ActiveSet
66
72
  self.class.new(@set,
67
73
  view: processed_set,
68
74
  instructions: @instructions.merge(
69
- method => instructions))
75
+ method => instructions
76
+ ))
70
77
  end
71
78
  end
@@ -1,16 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/core_ext/array/wrap'
4
-
5
3
  class ActiveSet
6
- class Instruction
4
+ class AttributeInstruction
5
+ attr_accessor :processed
7
6
  attr_reader :keypath, :value
8
7
 
9
8
  def initialize(keypath, value)
10
- # `keypath` can be an Array (e.g. [:parent, :child, :grandchild])
11
- # or a String (e.g. 'parent.child.grandchild')
12
- @keypath = Array.wrap(keypath).map(&:to_s).flat_map { |x| x.split('.') }
9
+ # `keypath` can be an Array (e.g. [:parent, :child, :grandchild, :attribute])
10
+ # or a String (e.g. 'parent.child.grandchild.attribute')
11
+ @keypath = Array(keypath).map(&:to_s).flat_map { |x| x.split('.') }
13
12
  @value = value
13
+ @processed = false
14
+ end
15
+
16
+ def processed?
17
+ @processed
14
18
  end
15
19
 
16
20
  def attribute
@@ -43,7 +47,7 @@ class ActiveSet
43
47
 
44
48
  def value_for(item:)
45
49
  resource_for(item: item).public_send(attribute)
46
- rescue
50
+ rescue StandardError
47
51
  # :nocov:
48
52
  nil
49
53
  # :nocov:
@@ -55,7 +59,7 @@ class ActiveSet
55
59
 
56
60
  resource.public_send(association)
57
61
  end
58
- rescue
62
+ rescue StandardError
59
63
  # :nocov:
60
64
  nil
61
65
  # :nocov:
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './attribute_instruction'
4
+
5
+ class ActiveSet
6
+ class ColumnInstruction
7
+ def initialize(instructions_hash, item)
8
+ @instructions_hash = instructions_hash.symbolize_keys
9
+ @item = item
10
+ end
11
+
12
+ def key
13
+ return @instructions_hash[:key] if @instructions_hash.key? :key
14
+
15
+ titleized = attribute_instruction.keypath.map(&:titleize).join(' ')
16
+ return titleized unless attribute_instruction.attribute
17
+
18
+ attribute_resource = attribute_instruction.resource_for(item: @item)
19
+ return titleized unless attribute_resource
20
+ return titleized unless attribute_resource.class.respond_to?(:human_attribute_name)
21
+
22
+ attribute_resource.class.human_attribute_name(attribute_instruction.attribute)
23
+ end
24
+
25
+ def value
26
+ return default unless @instructions_hash.key?(:value)
27
+ return @instructions_hash[:value].call(@item) if @instructions_hash[:value]&.respond_to? :call
28
+
29
+ attribute_instruction.value_for(item: @item)
30
+ end
31
+
32
+ private
33
+
34
+ def attribute_instruction
35
+ AttributeInstruction.new(@instructions_hash[:value], nil)
36
+ end
37
+
38
+ def default
39
+ return @instructions_hash[:default] if @instructions_hash.key? :default
40
+
41
+ '—'
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../column_instruction'
4
+
5
+ class ActiveSet
6
+ module Exporting
7
+ class Operation
8
+ def initialize(set, instructions_hash)
9
+ @set = set
10
+ @instructions_hash = instructions_hash
11
+ end
12
+
13
+ def execute
14
+ strategy_for(format: operation_instructions[:format].to_s.downcase)
15
+ .new(@set, operation_instructions[:columns])
16
+ .execute
17
+ end
18
+
19
+ def operation_instructions
20
+ @instructions_hash.symbolize_keys
21
+ end
22
+
23
+ private
24
+
25
+ def strategy_for(format:)
26
+ return CSVStrategy if format == 'csv'
27
+ end
28
+ end
29
+
30
+ class CSVStrategy
31
+ require 'csv'
32
+
33
+ def initialize(set, column_instructions)
34
+ @set = set
35
+ @column_instructions = column_instructions
36
+ end
37
+
38
+ def execute
39
+ ::CSV.generate do |output|
40
+ output << column_keys_for(item: @set.first)
41
+ @set.each do |item|
42
+ output << column_values_for(item: item)
43
+ end
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def column_keys_for(item:)
50
+ columns.map do |column|
51
+ ColumnInstruction.new(column, item).key
52
+ end
53
+ end
54
+
55
+ def column_values_for(item:)
56
+ columns.map do |column|
57
+ ColumnInstruction.new(column, item).value
58
+ end
59
+ end
60
+
61
+ def columns
62
+ @column_instructions.compact
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../attribute_instruction'
4
+
5
+ class ActiveSet
6
+ module Filtering
7
+ class Operation
8
+ def initialize(set, instructions_hash)
9
+ @set = set
10
+ @instructions_hash = instructions_hash
11
+ end
12
+
13
+ def execute
14
+ attribute_instructions = @instructions_hash
15
+ .flatten_keys
16
+ .map { |k, v| AttributeInstruction.new(k, v) }
17
+
18
+ activerecord_filtered_set = attribute_instructions.reduce(@set) do |set, attribute_instruction|
19
+ maybe_set_or_false = ActiveRecordStrategy.new(set, attribute_instruction).execute
20
+ next set unless maybe_set_or_false
21
+
22
+ attribute_instruction.processed = true
23
+ maybe_set_or_false
24
+ end
25
+
26
+ return activerecord_filtered_set if attribute_instructions.all?(&:processed?)
27
+
28
+ attribute_instructions.reject(&:processed?).reduce(activerecord_filtered_set) do |set, attribute_instruction|
29
+ maybe_set_or_false = EnumerableStrategy.new(set, attribute_instruction).execute
30
+ maybe_set_or_false.presence || set
31
+ end
32
+ end
33
+
34
+ def operation_instructions
35
+ @instructions_hash.symbolize_keys
36
+ end
37
+ end
38
+
39
+ class EnumerableStrategy
40
+ def initialize(set, attribute_instruction)
41
+ @set = set
42
+ @attribute_instruction = attribute_instruction
43
+ end
44
+
45
+ def execute
46
+ return false unless @set.respond_to? :select
47
+
48
+ @set.select do |item|
49
+ if can_match_attribute_for?(item)
50
+ next attribute_matches_for?(item)
51
+ elsif can_match_class_method_for?(item)
52
+ next class_method_matches_for?(item)
53
+ else
54
+ next false
55
+ end
56
+ end
57
+ end
58
+
59
+ private
60
+
61
+ def can_match_attribute_for?(item)
62
+ attribute_item = attribute_item_for(item)
63
+
64
+ return false unless attribute_item
65
+ return false unless attribute_item.respond_to?(@attribute_instruction.attribute)
66
+ return false unless attribute_item.method(@attribute_instruction.attribute).arity.zero?
67
+
68
+ true
69
+ end
70
+
71
+ def can_match_class_method_for?(item)
72
+ attribute_item = attribute_item_for(item)
73
+
74
+ return false unless attribute_item
75
+ return false unless attribute_item.class
76
+ return false unless attribute_item.class.respond_to?(@attribute_instruction.attribute)
77
+ return false if attribute_item.class.method(@attribute_instruction.attribute).arity.zero?
78
+
79
+ true
80
+ end
81
+
82
+ def attribute_matches_for?(item)
83
+ @attribute_instruction
84
+ .value_for(item: item)
85
+ .public_send(
86
+ @attribute_instruction.operator,
87
+ @attribute_instruction.value
88
+ )
89
+ end
90
+
91
+ def class_method_matches_for?(item)
92
+ maybe_item_or_collection_or_nil = attribute_item_for(item)
93
+ .class
94
+ .public_send(
95
+ @attribute_instruction.attribute,
96
+ @attribute_instruction.value
97
+ )
98
+ if maybe_item_or_collection_or_nil.nil?
99
+ false
100
+ elsif maybe_item_or_collection_or_nil.respond_to?(:each)
101
+ maybe_item_or_collection_or_nil.include? attribute_item_for(item)
102
+ else
103
+ maybe_item_or_collection_or_nil.present?
104
+ end
105
+ end
106
+
107
+ def attribute_item_for(item)
108
+ @attribute_instruction
109
+ .resource_for(item: item)
110
+ end
111
+ end
112
+
113
+ class ActiveRecordStrategy
114
+ def initialize(set, attribute_instruction)
115
+ @set = set
116
+ @attribute_instruction = attribute_instruction
117
+ end
118
+
119
+ def execute
120
+ return false unless @set.respond_to? :to_sql
121
+
122
+ if execute_where_operation?
123
+ statement = where_operation
124
+ elsif execute_merge_operation?
125
+ statement = merge_operation
126
+ else
127
+ return false
128
+ end
129
+
130
+ return false if throws?(ActiveRecord::StatementInvalid) { statement.load }
131
+
132
+ statement
133
+ end
134
+
135
+ private
136
+
137
+ def execute_where_operation?
138
+ return false unless attribute_model
139
+ return false unless attribute_model.respond_to?(:attribute_names)
140
+ return false unless attribute_model.attribute_names.include?(@attribute_instruction.attribute)
141
+
142
+ true
143
+ end
144
+
145
+ def execute_merge_operation?
146
+ return false unless attribute_model
147
+ return false unless attribute_model.respond_to?(@attribute_instruction.attribute)
148
+ return false if attribute_model.method(@attribute_instruction.attribute).arity.zero?
149
+
150
+ true
151
+ end
152
+
153
+ def where_operation
154
+ arel_operator = @attribute_instruction.operator(default: '=')
155
+ arel_column = Arel::Table.new(attribute_model.table_name)[@attribute_instruction.attribute]
156
+ arel_value = Arel.sql(ActiveRecord::Base.connection.quote(@attribute_instruction.value))
157
+
158
+ @set.eager_load(@attribute_instruction.associations_hash)
159
+ .where(
160
+ Arel::Nodes::InfixOperation.new(
161
+ arel_operator,
162
+ arel_column,
163
+ arel_value
164
+ )
165
+ )
166
+ end
167
+
168
+ def merge_operation
169
+ @set.eager_load(@attribute_instruction.associations_hash)
170
+ .merge(
171
+ attribute_model.public_send(
172
+ @attribute_instruction.attribute,
173
+ @attribute_instruction.value
174
+ )
175
+ )
176
+ end
177
+
178
+ def attribute_model
179
+ return @set.klass if @attribute_instruction.associations_array.empty?
180
+ return @attribute_model if defined? @attribute_model
181
+
182
+ @attribute_model = @attribute_instruction
183
+ .associations_array
184
+ .reduce(@set) do |obj, assoc|
185
+ obj.reflections[assoc.to_s]&.klass
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end