bharat_filter_engine 0.1.2 → 0.2.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.
- checksums.yaml +4 -4
- data/Gemfile +12 -9
- data/README.md +1 -1
- data/Rakefile +5 -3
- data/lib/bharat_filter_engine/association_resolver.rb +18 -37
- data/lib/bharat_filter_engine/column_helper.rb +28 -0
- data/lib/bharat_filter_engine/dynamic_filter_values.rb +17 -18
- data/lib/bharat_filter_engine/engine.rb +249 -91
- data/lib/bharat_filter_engine/errors.rb +1 -1
- data/lib/bharat_filter_engine/join_tracker.rb +37 -0
- data/lib/bharat_filter_engine/rule_normalizer.rb +88 -30
- data/lib/bharat_filter_engine/search_builder.rb +48 -24
- data/lib/bharat_filter_engine/version.rb +1 -1
- data/lib/bharat_filter_engine.rb +14 -12
- data/spec/association_resolver_spec.rb +60 -41
- data/spec/dynamic_filter_values_spec.rb +161 -29
- data/spec/engine_spec.rb +1144 -538
- data/spec/join_tracker_spec.rb +70 -0
- data/spec/rule_normalizer_spec.rb +181 -32
- data/spec/same_table_join_spec.rb +199 -0
- data/spec/search_builder_spec.rb +222 -42
- data/spec/spec_helper.rb +11 -12
- metadata +14 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 45380eb2ba5afe790b2cbc8f0abe65cbab402d2489fb025bd2d2407c5700a57f
|
|
4
|
+
data.tar.gz: 8529dbb3f163c708c819ab166e1d946d23900ef685b9bb44b758f734c44c913b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4ae9d8027f95b37c0f7f43235a56cc0b234ab444aa0079bfa1425b44ca408677def18f0f11216dced37321b68765a7e64fcd2217add708dc097a8da6e44c536e
|
|
7
|
+
data.tar.gz: a2ea4acb0d03be39e10d59b0c01b7da206489e95fbad43a7d60c1f04e83c9de7031de5d5779599b6c22348e684c17b326b2d9f3fa3b39a022e69b3ab429cd1be
|
data/Gemfile
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
source
|
|
3
|
+
source 'https://rubygems.org'
|
|
4
4
|
|
|
5
5
|
gemspec
|
|
6
6
|
|
|
7
|
-
gem
|
|
8
|
-
gem
|
|
9
|
-
gem
|
|
7
|
+
gem 'activemodel', '>= 6.1'
|
|
8
|
+
gem 'activerecord', '>= 6.1'
|
|
9
|
+
gem 'activesupport', '>= 6.1'
|
|
10
10
|
|
|
11
11
|
group :development, :test do
|
|
12
|
-
gem
|
|
13
|
-
gem
|
|
14
|
-
gem
|
|
12
|
+
gem 'rake'
|
|
13
|
+
gem 'rspec'
|
|
14
|
+
gem 'rubocop-rails-omakase', require: false
|
|
15
|
+
gem 'rubocop-rake', require: false
|
|
16
|
+
gem 'rubocop-rspec', require: false
|
|
17
|
+
gem 'sqlite3'
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
group :test do
|
|
18
|
-
gem
|
|
19
|
-
end
|
|
21
|
+
gem 'database_cleaner-active_record'
|
|
22
|
+
end
|
data/README.md
CHANGED
|
@@ -8,7 +8,7 @@ query string) into a filtered `ActiveRecord::Relation` — including
|
|
|
8
8
|
filters on associated models, free-text search, and dynamically
|
|
9
9
|
populated dropdown values.
|
|
10
10
|
|
|
11
|
-
[](CHANGELOG.md)
|
|
12
12
|
[](LICENSE)
|
|
13
13
|
|
|
14
14
|
---
|
data/Rakefile
CHANGED
|
@@ -7,19 +7,14 @@ module BharatFilterEngine
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def resolve(path)
|
|
10
|
-
parts = path.to_s.split(
|
|
11
|
-
|
|
10
|
+
parts = path.to_s.split('__').map(&:to_sym)
|
|
12
11
|
return nil if parts.empty?
|
|
13
12
|
|
|
14
13
|
current_klass = @scope.klass
|
|
15
14
|
reflections = []
|
|
16
15
|
|
|
17
16
|
parts.each do |association_name|
|
|
18
|
-
reflection =
|
|
19
|
-
current_klass.reflect_on_association(
|
|
20
|
-
association_name
|
|
21
|
-
)
|
|
22
|
-
|
|
17
|
+
reflection = current_klass.reflect_on_association(association_name)
|
|
23
18
|
return nil unless reflection
|
|
24
19
|
|
|
25
20
|
reflections << reflection
|
|
@@ -36,11 +31,6 @@ module BharatFilterEngine
|
|
|
36
31
|
|
|
37
32
|
def resolve_column(field)
|
|
38
33
|
field = field.to_s
|
|
39
|
-
|
|
40
|
-
# ----------------------------------------
|
|
41
|
-
# Direct column
|
|
42
|
-
# ----------------------------------------
|
|
43
|
-
|
|
44
34
|
if @scope.klass.column_names.include?(field)
|
|
45
35
|
return {
|
|
46
36
|
column: field,
|
|
@@ -50,41 +40,32 @@ module BharatFilterEngine
|
|
|
50
40
|
}
|
|
51
41
|
end
|
|
52
42
|
|
|
53
|
-
# ----------------------------------------
|
|
54
43
|
# Nested column
|
|
55
|
-
#
|
|
56
44
|
# lead__client__name
|
|
57
|
-
#
|
|
58
45
|
# lead -> client -> name
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
column =
|
|
64
|
-
parts.pop
|
|
65
|
-
|
|
66
|
-
association_path =
|
|
67
|
-
parts.join("__")
|
|
68
|
-
|
|
69
|
-
result =
|
|
70
|
-
resolve(association_path)
|
|
46
|
+
parts = field.split('__')
|
|
47
|
+
column = parts.pop
|
|
48
|
+
association_path = parts.join('__')
|
|
49
|
+
result = resolve(association_path)
|
|
71
50
|
|
|
72
51
|
return nil unless result
|
|
52
|
+
return nil unless result[:klass].column_names.include?(column)
|
|
73
53
|
|
|
74
|
-
|
|
75
|
-
result[:klass].column_names.include?(column)
|
|
76
|
-
|
|
77
|
-
result.merge(
|
|
78
|
-
column: column
|
|
79
|
-
)
|
|
54
|
+
result.merge(column: column)
|
|
80
55
|
end
|
|
81
56
|
|
|
82
57
|
def join(scope, associations)
|
|
83
58
|
return scope if associations.blank?
|
|
84
59
|
|
|
85
|
-
scope.joins(
|
|
86
|
-
|
|
87
|
-
|
|
60
|
+
scope.joins(build_nested_join(associations))
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def subquery(associations)
|
|
64
|
+
klass = @scope.klass
|
|
65
|
+
relation = klass.unscoped.joins(build_nested_join(associations))
|
|
66
|
+
relation = yield(relation) if block_given?
|
|
67
|
+
|
|
68
|
+
relation.select(klass.arel_table[klass.primary_key])
|
|
88
69
|
end
|
|
89
70
|
|
|
90
71
|
private
|
|
@@ -99,4 +80,4 @@ module BharatFilterEngine
|
|
|
99
80
|
end
|
|
100
81
|
end
|
|
101
82
|
end
|
|
102
|
-
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BharatFilterEngine
|
|
4
|
+
module ColumnHelper
|
|
5
|
+
STRING_COLUMN_TYPES = %i[string text].freeze
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def string_like?(klass, column)
|
|
10
|
+
db_column = klass&.columns_hash&.[](column.to_s)
|
|
11
|
+
db_column.nil? || STRING_COLUMN_TYPES.include?(db_column.type)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def present_predicate(arel_column, klass, column)
|
|
15
|
+
predicate = arel_column.not_eq(nil)
|
|
16
|
+
return predicate unless string_like?(klass, column)
|
|
17
|
+
|
|
18
|
+
predicate.and(arel_column.not_eq(''))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def blank_predicate(arel_column, klass, column)
|
|
22
|
+
predicate = arel_column.eq(nil)
|
|
23
|
+
return predicate unless string_like?(klass, column)
|
|
24
|
+
|
|
25
|
+
predicate.or(arel_column.eq(''))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module BharatFilterEngine
|
|
4
4
|
class DynamicFilterValues
|
|
5
|
-
|
|
5
|
+
LIKE_ESCAPE_CHAR = '\\'
|
|
6
|
+
|
|
7
|
+
def initialize(scope:, field:, model: nil, term: nil, limit: nil)
|
|
6
8
|
@scope = scope
|
|
7
9
|
@model = model || scope.klass
|
|
8
10
|
@field = field.to_s
|
|
11
|
+
@term = term.presence
|
|
12
|
+
@limit = limit.presence&.to_i
|
|
9
13
|
end
|
|
10
14
|
|
|
11
15
|
def call
|
|
@@ -14,26 +18,21 @@ module BharatFilterEngine
|
|
|
14
18
|
result = resolver.resolve_column(@field)
|
|
15
19
|
return nil unless result
|
|
16
20
|
|
|
17
|
-
scoped =
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
column = result[:column]
|
|
24
|
-
nested = result[:associations].present?
|
|
21
|
+
scoped = resolver.join(@scope, result[:associations])
|
|
22
|
+
arel_column = result[:klass].arel_table[result[:column]]
|
|
23
|
+
scoped = scoped.where(ColumnHelper.present_predicate(arel_column, result[:klass], result[:column]))
|
|
24
|
+
scoped = apply_term(scoped, arel_column) if @term
|
|
25
|
+
scoped = scoped.distinct.order(arel_column)
|
|
26
|
+
scoped = scoped.limit(@limit) if @limit&.positive?
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
scoped.pluck(arel_column)
|
|
29
|
+
end
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
nested ? Arel.sql("#{result[:table_name]}.#{column}") : column
|
|
31
|
+
private
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
.order(qualified_column)
|
|
36
|
-
.pluck(qualified_column)
|
|
33
|
+
def apply_term(scoped, arel_column)
|
|
34
|
+
escaped = @term.to_s.gsub(/[\\%_]/) { |char| "#{LIKE_ESCAPE_CHAR}#{char}" }
|
|
35
|
+
scoped.where(arel_column.matches("%#{escaped}%", LIKE_ESCAPE_CHAR))
|
|
37
36
|
end
|
|
38
37
|
end
|
|
39
38
|
end
|