activeset 0.6.5 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.rubocop.yml +0 -6
- data/CHANGELOG +5 -0
- data/README.md +58 -6
- data/activeset.gemspec +8 -8
- data/bin/console +14 -0
- data/lib/.DS_Store +0 -0
- data/lib/active_set.rb +28 -21
- data/lib/active_set/{instruction.rb → attribute_instruction.rb} +12 -8
- data/lib/active_set/column_instruction.rb +44 -0
- data/lib/active_set/exporting/operation.rb +66 -0
- data/lib/active_set/filtering/operation.rb +190 -0
- data/lib/active_set/paginating/operation.rb +102 -0
- data/lib/active_set/sorting/operation.rb +144 -0
- data/lib/helpers/throws.rb +2 -2
- data/lib/helpers/transform_to_sortable_numeric.rb +36 -0
- data/lib/patches/core_ext/hash/flatten_keys.rb +43 -14
- metadata +55 -39
- data/lib/active_set/.DS_Store +0 -0
- data/lib/active_set/adapter_activerecord.rb +0 -58
- data/lib/active_set/adapter_base.rb +0 -14
- data/lib/active_set/instructions.rb +0 -42
- data/lib/active_set/processor_base.rb +0 -28
- data/lib/active_set/processor_filter.rb +0 -21
- data/lib/active_set/processor_filter/active_record_adapter.rb +0 -50
- data/lib/active_set/processor_filter/enumerable_adapter.rb +0 -21
- data/lib/active_set/processor_paginate.rb +0 -35
- data/lib/active_set/processor_paginate/active_record_adapter.rb +0 -37
- data/lib/active_set/processor_paginate/enumerable_adapter.rb +0 -39
- data/lib/active_set/processor_sort.rb +0 -19
- data/lib/active_set/processor_sort/active_record_adapter.rb +0 -27
- data/lib/active_set/processor_sort/active_record_operation.rb +0 -55
- data/lib/active_set/processor_sort/enumerable_adapter.rb +0 -58
- data/lib/active_set/processor_transform.rb +0 -30
- data/lib/active_set/processor_transform/csv_adapter.rb +0 -77
- data/lib/active_set/version.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4c88778d3e9cdb17bb755baad18eccf5a1ec41f1
|
4
|
+
data.tar.gz: 25b78baf469767293cf301fd3589a00a5a0c9b94
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c8e848d5d4fde3130fec90980e8e0530db6612bf4e11663f0fdaf2d52c5f60e319bd162674a5fd298fa71249f15dfa15fda1320917c160487ba0485461a527b1
|
7
|
+
data.tar.gz: 6f695aa0590f3a8ead38041bc94818ac6faaf0e3cd3c2b85b831593c6ec3a64a910fd37fc87ebfef9f3755434f4613cef88317e2f64e2f09fee9e1aa28995c13
|
data/.gitignore
CHANGED
data/.rubocop.yml
CHANGED
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
|
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
|
-
|
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
|
-
|
25
|
+
## Filtering
|
26
26
|
|
27
|
-
|
27
|
+
`ActiveSet` allows for you to filter your collection by:
|
28
28
|
|
29
|
-
|
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
|
-
|
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('
|
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 =
|
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 '
|
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 '
|
7
|
-
require '
|
8
|
-
require 'active_set/
|
9
|
-
require 'active_set/
|
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(
|
42
|
-
filterer =
|
43
|
-
reinitialize(filterer.
|
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(
|
47
|
-
sorter =
|
48
|
-
reinitialize(sorter.
|
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(
|
52
|
-
paginater =
|
53
|
-
|
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
|
59
|
-
|
60
|
-
|
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
|
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
|
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
|