searchable-by 0.5.0 → 0.5.6
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/.rubocop.yml +2 -2
- data/.travis.yml +0 -1
- data/Gemfile.lock +46 -41
- data/LICENSE +1 -1
- data/README.md +2 -1
- data/lib/searchable_by.rb +8 -107
- data/lib/searchable_by/column.rb +34 -0
- data/lib/searchable_by/concern.rb +43 -0
- data/lib/searchable_by/config.rb +31 -0
- data/lib/searchable_by/util.rb +46 -0
- data/searchable-by.gemspec +3 -3
- data/spec/searchable_by/util_spec.rb +35 -0
- data/spec/searchable_by_spec.rb +29 -12
- data/spec/spec_helper.rb +18 -14
- metadata +10 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e2f1b8d0d05abf263dc93c1bd81ab818a88eedf135ff10cd5414d54d3ae17a1
|
|
4
|
+
data.tar.gz: 04d1e040390dcf8c77adbc1bf5bd491505373147798b1c95e4e4cc6c259daab5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e6bcafc69897951760eba1a9fb049664aa3360536525d31e6f5b1d9094ce274cc82c6936aa69e19b8ece975e1f74d42f424646d5a6e4de4bc4c12b29b24a4b33
|
|
7
|
+
data.tar.gz: a5762fcbf915002a781e6d1996cae4df2ce80fc62d0948c0cb5965c97b07d0d5a9030cb7acf360bba4482a4f05026f1e082254f5f545a8fa5ba1bdb946ef520e
|
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,65 +1,70 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
searchable-by (0.5.
|
|
4
|
+
searchable-by (0.5.6)
|
|
5
5
|
activerecord
|
|
6
6
|
activesupport
|
|
7
7
|
|
|
8
8
|
GEM
|
|
9
9
|
remote: http://rubygems.org/
|
|
10
10
|
specs:
|
|
11
|
-
activemodel (
|
|
12
|
-
activesupport (=
|
|
13
|
-
activerecord (
|
|
14
|
-
activemodel (=
|
|
15
|
-
activesupport (=
|
|
16
|
-
|
|
17
|
-
activesupport (5.2.3)
|
|
11
|
+
activemodel (6.0.3.3)
|
|
12
|
+
activesupport (= 6.0.3.3)
|
|
13
|
+
activerecord (6.0.3.3)
|
|
14
|
+
activemodel (= 6.0.3.3)
|
|
15
|
+
activesupport (= 6.0.3.3)
|
|
16
|
+
activesupport (6.0.3.3)
|
|
18
17
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
19
18
|
i18n (>= 0.7, < 2)
|
|
20
19
|
minitest (~> 5.1)
|
|
21
20
|
tzinfo (~> 1.1)
|
|
22
|
-
|
|
23
|
-
ast (2.4.
|
|
24
|
-
concurrent-ruby (1.1.
|
|
25
|
-
diff-lcs (1.
|
|
26
|
-
i18n (1.
|
|
21
|
+
zeitwerk (~> 2.2, >= 2.2.2)
|
|
22
|
+
ast (2.4.1)
|
|
23
|
+
concurrent-ruby (1.1.7)
|
|
24
|
+
diff-lcs (1.4.4)
|
|
25
|
+
i18n (1.8.5)
|
|
27
26
|
concurrent-ruby (~> 1.0)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
ast (~> 2.4.0)
|
|
27
|
+
minitest (5.14.2)
|
|
28
|
+
parallel (1.19.2)
|
|
29
|
+
parser (2.7.1.4)
|
|
30
|
+
ast (~> 2.4.1)
|
|
33
31
|
rainbow (3.0.0)
|
|
34
|
-
rake (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
rspec-
|
|
39
|
-
|
|
40
|
-
rspec-
|
|
41
|
-
rspec-
|
|
32
|
+
rake (13.0.1)
|
|
33
|
+
regexp_parser (1.7.1)
|
|
34
|
+
rexml (3.2.4)
|
|
35
|
+
rspec (3.9.0)
|
|
36
|
+
rspec-core (~> 3.9.0)
|
|
37
|
+
rspec-expectations (~> 3.9.0)
|
|
38
|
+
rspec-mocks (~> 3.9.0)
|
|
39
|
+
rspec-core (3.9.2)
|
|
40
|
+
rspec-support (~> 3.9.3)
|
|
41
|
+
rspec-expectations (3.9.2)
|
|
42
42
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
43
|
-
rspec-support (~> 3.
|
|
44
|
-
rspec-mocks (3.
|
|
43
|
+
rspec-support (~> 3.9.0)
|
|
44
|
+
rspec-mocks (3.9.1)
|
|
45
45
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
46
|
-
rspec-support (~> 3.
|
|
47
|
-
rspec-support (3.
|
|
48
|
-
rubocop (0.
|
|
49
|
-
jaro_winkler (~> 1.5.1)
|
|
46
|
+
rspec-support (~> 3.9.0)
|
|
47
|
+
rspec-support (3.9.3)
|
|
48
|
+
rubocop (0.91.0)
|
|
50
49
|
parallel (~> 1.10)
|
|
51
|
-
parser (>= 2.
|
|
50
|
+
parser (>= 2.7.1.1)
|
|
52
51
|
rainbow (>= 2.2.2, < 4.0)
|
|
52
|
+
regexp_parser (>= 1.7)
|
|
53
|
+
rexml
|
|
54
|
+
rubocop-ast (>= 0.4.0, < 1.0)
|
|
53
55
|
ruby-progressbar (~> 1.7)
|
|
54
|
-
unicode-display_width (>= 1.4.0, <
|
|
55
|
-
rubocop-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
unicode-display_width (>= 1.4.0, < 2.0)
|
|
57
|
+
rubocop-ast (0.4.1)
|
|
58
|
+
parser (>= 2.7.1.4)
|
|
59
|
+
rubocop-performance (1.8.0)
|
|
60
|
+
rubocop (>= 0.87.0)
|
|
61
|
+
ruby-progressbar (1.10.1)
|
|
62
|
+
sqlite3 (1.4.2)
|
|
59
63
|
thread_safe (0.3.6)
|
|
60
|
-
tzinfo (1.2.
|
|
64
|
+
tzinfo (1.2.7)
|
|
61
65
|
thread_safe (~> 0.1)
|
|
62
|
-
unicode-display_width (1.
|
|
66
|
+
unicode-display_width (1.7.0)
|
|
67
|
+
zeitwerk (2.4.0)
|
|
63
68
|
|
|
64
69
|
PLATFORMS
|
|
65
70
|
ruby
|
|
@@ -74,4 +79,4 @@ DEPENDENCIES
|
|
|
74
79
|
sqlite3
|
|
75
80
|
|
|
76
81
|
BUNDLED WITH
|
|
77
|
-
2.
|
|
82
|
+
2.1.4
|
data/LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -15,7 +15,8 @@ class Post < ActiveRecord::Base
|
|
|
15
15
|
# Limit the number of terms per query to 3.
|
|
16
16
|
searchable_by max_terms: 3 do
|
|
17
17
|
# Allow to search strings.
|
|
18
|
-
|
|
18
|
+
# Use btree index-friendly prefix match, e.g. `ILIKE 'term%'` instead of default `ILIKE '%term%'`.
|
|
19
|
+
column :title, match: :prefix
|
|
19
20
|
|
|
20
21
|
# ... and integers.
|
|
21
22
|
column :id, type: :integer
|
data/lib/searchable_by.rb
CHANGED
|
@@ -1,111 +1,12 @@
|
|
|
1
1
|
require 'active_record'
|
|
2
|
-
require 'shellwords'
|
|
3
2
|
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
scope { all }
|
|
10
|
-
end
|
|
3
|
+
module SearchableBy
|
|
4
|
+
autoload :Column, 'searchable_by/column'
|
|
5
|
+
autoload :Concern, 'searchable_by/concern'
|
|
6
|
+
autoload :Config, 'searchable_by/config'
|
|
7
|
+
autoload :Util, 'searchable_by/util'
|
|
11
8
|
|
|
12
|
-
|
|
13
|
-
opts = attrs.extract_options!
|
|
14
|
-
cols = self[:columns]
|
|
15
|
-
attrs.each do |attr|
|
|
16
|
-
cols.push(opts.merge(column: attr))
|
|
17
|
-
end
|
|
18
|
-
cols.push(opts.merge(column: block)) if block
|
|
19
|
-
cols
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def scope(&block)
|
|
23
|
-
self[:scope] = block
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def self.norm_values(query)
|
|
28
|
-
values = Shellwords.split(query.to_s)
|
|
29
|
-
values.flatten!
|
|
30
|
-
values.reject!(&:blank?)
|
|
31
|
-
values.uniq!
|
|
32
|
-
values
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def self.build_clauses(relations, values)
|
|
36
|
-
clauses = values.map do |value|
|
|
37
|
-
negate = value[0] == '-'
|
|
38
|
-
value.slice!(0) if negate || value[0] == '+'
|
|
39
|
-
|
|
40
|
-
c0, *cn = relations.map do |opts|
|
|
41
|
-
build_condition(opts, value)
|
|
42
|
-
end.compact
|
|
43
|
-
next unless c0
|
|
44
|
-
|
|
45
|
-
[cn.inject(c0) {|x, part| x.or(part) }, negate]
|
|
46
|
-
end
|
|
47
|
-
clauses.compact!
|
|
48
|
-
clauses
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def self.build_condition(opts, value)
|
|
52
|
-
case opts[:type]
|
|
53
|
-
when :int, :integer
|
|
54
|
-
begin
|
|
55
|
-
opts[:rel].eq(Integer(value))
|
|
56
|
-
rescue ArgumentError
|
|
57
|
-
nil
|
|
58
|
-
end
|
|
59
|
-
else
|
|
60
|
-
value = value.dup
|
|
61
|
-
value.gsub!('%', '\%')
|
|
62
|
-
value.gsub!('_', '\_')
|
|
63
|
-
opts[:rel].matches("%#{value}%")
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
module ClassMethods
|
|
68
|
-
def self.extended(base) # :nodoc:
|
|
69
|
-
base.class_attribute :_searchable_by_config, instance_accessor: false, instance_predicate: false
|
|
70
|
-
base._searchable_by_config = Config.new
|
|
71
|
-
super
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def inherited(base) # :nodoc:
|
|
75
|
-
base._searchable_by_config = _searchable_by_config.deep_dup
|
|
76
|
-
super
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def searchable_by(max_terms: 5, &block)
|
|
80
|
-
_searchable_by_config.instance_eval(&block)
|
|
81
|
-
_searchable_by_config[:max_terms] = max_terms if max_terms
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# @param [String] query the search query
|
|
85
|
-
# @return [ActiveRecord::Relation] the scoped relation
|
|
86
|
-
def search_by(query)
|
|
87
|
-
columns = _searchable_by_config[:columns]
|
|
88
|
-
return all if columns.empty?
|
|
89
|
-
|
|
90
|
-
values = SearchableBy.norm_values(query).first(_searchable_by_config[:max_terms])
|
|
91
|
-
return all if values.empty?
|
|
92
|
-
|
|
93
|
-
relations = columns.map do |opts|
|
|
94
|
-
rel = opts[:column].is_a?(Proc) ? opts[:column].call : arel_table[opts[:column]]
|
|
95
|
-
opts.merge(rel: rel)
|
|
96
|
-
end
|
|
97
|
-
clauses = SearchableBy.build_clauses(relations, values)
|
|
98
|
-
return all if clauses.empty?
|
|
99
|
-
|
|
100
|
-
scope = instance_exec(&_searchable_by_config[:scope])
|
|
101
|
-
clauses.inject(scope) do |x, (clause, negate)|
|
|
102
|
-
negate ? x.where.not(clause) : x.where(clause)
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
class Base
|
|
109
|
-
extend SearchableBy::ClassMethods
|
|
110
|
-
end
|
|
9
|
+
Value = Struct.new(:term, :negate)
|
|
111
10
|
end
|
|
11
|
+
|
|
12
|
+
ActiveRecord::Base.extend SearchableBy::Concern if defined?(::ActiveRecord::Base)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module SearchableBy
|
|
2
|
+
class Column
|
|
3
|
+
attr_reader :attr, :type, :match
|
|
4
|
+
attr_accessor :node
|
|
5
|
+
|
|
6
|
+
def initialize(attr, type: :string, match: :all)
|
|
7
|
+
@attr = attr
|
|
8
|
+
@type = type.to_sym
|
|
9
|
+
@match = match
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def build_condition(value)
|
|
13
|
+
case type
|
|
14
|
+
when :int, :integer
|
|
15
|
+
begin
|
|
16
|
+
node.not_eq(nil).and(node.eq(Integer(value.term)))
|
|
17
|
+
rescue ArgumentError
|
|
18
|
+
nil
|
|
19
|
+
end
|
|
20
|
+
else
|
|
21
|
+
term = value.term.dup
|
|
22
|
+
term.gsub!('%', '\%')
|
|
23
|
+
term.gsub!('_', '\_')
|
|
24
|
+
case match
|
|
25
|
+
when :prefix
|
|
26
|
+
term << '%'
|
|
27
|
+
else
|
|
28
|
+
term = "%#{term}%"
|
|
29
|
+
end
|
|
30
|
+
node.not_eq(nil).and(node.matches(term))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module SearchableBy
|
|
2
|
+
module Concern
|
|
3
|
+
def self.extended(base) # :nodoc:
|
|
4
|
+
base.class_attribute :_searchable_by_config, instance_accessor: false, instance_predicate: false
|
|
5
|
+
base._searchable_by_config = Config.new
|
|
6
|
+
super
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def inherited(base) # :nodoc:
|
|
10
|
+
base._searchable_by_config = _searchable_by_config.dup
|
|
11
|
+
super
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def searchable_by(max_terms: nil, **options, &block)
|
|
15
|
+
_searchable_by_config.instance_eval(&block)
|
|
16
|
+
_searchable_by_config.max_terms = max_terms if max_terms
|
|
17
|
+
_searchable_by_config.options.update(options) unless options.empty?
|
|
18
|
+
_searchable_by_config
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @param [String] query the search query
|
|
22
|
+
# @return [ActiveRecord::Relation] the scoped relation
|
|
23
|
+
def search_by(query)
|
|
24
|
+
columns = _searchable_by_config.columns
|
|
25
|
+
return all if columns.empty?
|
|
26
|
+
|
|
27
|
+
values = Util.norm_values(query).first(_searchable_by_config.max_terms)
|
|
28
|
+
return all if values.empty?
|
|
29
|
+
|
|
30
|
+
columns.each do |col|
|
|
31
|
+
col.node ||= col.attr.is_a?(Proc) ? col.attr.call : arel_table[col.attr]
|
|
32
|
+
end
|
|
33
|
+
clauses = Util.build_clauses(columns, values)
|
|
34
|
+
return all if clauses.empty?
|
|
35
|
+
|
|
36
|
+
scope = instance_exec(&_searchable_by_config.scoping)
|
|
37
|
+
clauses.each do |clause|
|
|
38
|
+
scope = scope.where(clause)
|
|
39
|
+
end
|
|
40
|
+
scope
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module SearchableBy
|
|
2
|
+
class Config
|
|
3
|
+
attr_reader :columns, :scoping, :options
|
|
4
|
+
attr_accessor :max_terms
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@columns = []
|
|
8
|
+
@max_terms = 5
|
|
9
|
+
@options = {}
|
|
10
|
+
scope { all }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize_copy(other)
|
|
14
|
+
@columns = other.columns.dup
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def column(*attrs, &block)
|
|
19
|
+
opts = attrs.extract_options!
|
|
20
|
+
attrs.each do |attr|
|
|
21
|
+
columns.push Column.new(attr, **@options, **opts)
|
|
22
|
+
end
|
|
23
|
+
columns.push Column.new(block, **@options, **opts) if block
|
|
24
|
+
columns
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def scope(&block)
|
|
28
|
+
@scoping = block
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module SearchableBy
|
|
2
|
+
module Util
|
|
3
|
+
def self.norm_values(query)
|
|
4
|
+
values = []
|
|
5
|
+
query = query.to_s.dup
|
|
6
|
+
|
|
7
|
+
# capture any terms inside double quotes
|
|
8
|
+
# exclude from search if preceded by '-'
|
|
9
|
+
query.gsub!(/([\-+]?)"+([^"]*)"+/) do |_|
|
|
10
|
+
term = Regexp.last_match(2)
|
|
11
|
+
negate = Regexp.last_match(1) == '-'
|
|
12
|
+
|
|
13
|
+
values.push Value.new(term, negate) unless term.blank?
|
|
14
|
+
''
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# for the remaining terms remove sign if precedes
|
|
18
|
+
# exclude term from search if sign preceding is '-'
|
|
19
|
+
query.split(' ').each do |term|
|
|
20
|
+
negate = term[0] == '-'
|
|
21
|
+
term.slice!(0) if negate || term[0] == '+'
|
|
22
|
+
|
|
23
|
+
values.push Value.new(term, negate) unless term.blank?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
values.uniq!
|
|
27
|
+
values
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.build_clauses(columns, values)
|
|
31
|
+
clauses = values.map do |value|
|
|
32
|
+
grouping = columns.map do |column|
|
|
33
|
+
column.build_condition(value)
|
|
34
|
+
end
|
|
35
|
+
grouping.compact!
|
|
36
|
+
next if grouping.empty?
|
|
37
|
+
|
|
38
|
+
clause = grouping.inject(&:or)
|
|
39
|
+
clause = clause.not if value.negate
|
|
40
|
+
clause
|
|
41
|
+
end
|
|
42
|
+
clauses.compact!
|
|
43
|
+
clauses
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/searchable-by.gemspec
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Gem::Specification.new do |s|
|
|
2
2
|
s.name = 'searchable-by'
|
|
3
|
-
s.version = '0.5.
|
|
3
|
+
s.version = '0.5.6'
|
|
4
4
|
s.authors = ['Dimitrij Denissenko']
|
|
5
5
|
s.email = ['dimitrij@blacksquaremedia.com']
|
|
6
6
|
s.summary = 'Generate search scopes'
|
|
@@ -8,10 +8,10 @@ Gem::Specification.new do |s|
|
|
|
8
8
|
s.homepage = 'https://github.com/bsm/sortable-by'
|
|
9
9
|
s.license = 'MIT'
|
|
10
10
|
|
|
11
|
-
s.files = `git ls-files -z`.split("\x0").reject {|f| f.
|
|
11
|
+
s.files = `git ls-files -z`.split("\x0").reject {|f| f.start_with?('spec/') }
|
|
12
12
|
s.test_files = `git ls-files -z -- spec/*`.split("\x0")
|
|
13
13
|
s.require_paths = ['lib']
|
|
14
|
-
s.required_ruby_version = '>= 2.
|
|
14
|
+
s.required_ruby_version = '>= 2.5'
|
|
15
15
|
|
|
16
16
|
s.add_dependency 'activerecord'
|
|
17
17
|
s.add_dependency 'activesupport'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe SearchableBy::Util do
|
|
4
|
+
context 'norm_values' do
|
|
5
|
+
def norm(str)
|
|
6
|
+
described_class.norm_values(str).each_with_object({}) do |val, acc|
|
|
7
|
+
acc[val.term] = val.negate
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'should tokenise strings' do
|
|
12
|
+
expect(norm(nil)).to eq({})
|
|
13
|
+
expect(norm('""')).to eq({})
|
|
14
|
+
expect(norm('-+""')).to eq({})
|
|
15
|
+
expect(norm('simple words')).to eq('simple' => false, 'words' => false)
|
|
16
|
+
expect(norm(" with \t spaces\n")).to eq('with' => false, 'spaces' => false)
|
|
17
|
+
expect(norm('with with duplicates with')).to eq('with' => false, 'duplicates' => false)
|
|
18
|
+
expect(norm('with "full term"')).to eq('full term' => false, 'with' => false)
|
|
19
|
+
expect(norm('"""odd double quotes around"""')).to eq('odd double quotes around' => false)
|
|
20
|
+
expect(norm('""even double quotes around""')).to eq('even double quotes around'=> false)
|
|
21
|
+
expect(norm('with\'apostrophe')).to eq("with'apostrophe" => false)
|
|
22
|
+
expect(norm('with -minus')).to eq('minus' => true, 'with' => false)
|
|
23
|
+
expect(norm('with +plus')).to eq('plus' => false, 'with' => false)
|
|
24
|
+
expect(norm('with-minus')).to eq('with-minus' => false)
|
|
25
|
+
expect(norm('with+plus')).to eq('with+plus' => false)
|
|
26
|
+
expect(norm('with -"minus before"')).to eq('minus before' => true, 'with' => false)
|
|
27
|
+
expect(norm('with "-minus within"')).to eq('-minus within' => false, 'with' => false)
|
|
28
|
+
expect(norm('with +"plus before"')).to eq('plus before' => false, 'with' => false)
|
|
29
|
+
expect(norm('with "+plus within"')).to eq('+plus within' => false, 'with' => false)
|
|
30
|
+
expect(norm('+plus "in other term"')).to eq('in other term' => false, 'plus' => false)
|
|
31
|
+
expect(norm('with_blank \'\'')).to eq('with_blank' => false, '\'\'' => false)
|
|
32
|
+
expect(norm('with_blank_doubles ""')).to eq('with_blank_doubles' => false)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/spec/searchable_by_spec.rb
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
2
|
|
|
3
|
-
describe
|
|
3
|
+
describe SearchableBy do
|
|
4
4
|
it 'should ignore bad inputs' do
|
|
5
|
-
expect(Post.search_by(nil).count).to eq(
|
|
6
|
-
expect(Post.search_by('').count).to eq(
|
|
5
|
+
expect(Post.search_by(nil).count).to eq(5)
|
|
6
|
+
expect(Post.search_by('').count).to eq(5)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
it 'should configure correctly' do
|
|
10
|
+
expect(AbstractModel._searchable_by_config.columns.size).to eq(1)
|
|
11
|
+
expect(Post._searchable_by_config.columns.size).to eq(5)
|
|
7
12
|
end
|
|
8
13
|
|
|
9
14
|
it 'should generate SQL' do
|
|
@@ -17,26 +22,38 @@ describe ActiveRecord::SearchableBy do
|
|
|
17
22
|
end
|
|
18
23
|
|
|
19
24
|
it 'should search' do
|
|
20
|
-
expect(Post.search_by('ALICE').pluck(:title)).to match_array(%w[
|
|
21
|
-
expect(Post.search_by('bOb').pluck(:title)).to match_array(%w[
|
|
25
|
+
expect(Post.search_by('ALICE').pluck(:title)).to match_array(%w[a1 a2 ab])
|
|
26
|
+
expect(Post.search_by('bOb').pluck(:title)).to match_array(%w[b1 b2 ab])
|
|
22
27
|
end
|
|
23
28
|
|
|
24
29
|
it 'should search across multiple words' do
|
|
25
|
-
expect(Post.search_by('ALICE
|
|
30
|
+
expect(Post.search_by('ALICE your').pluck(:title)).to match_array(%w[a2])
|
|
26
31
|
end
|
|
27
32
|
|
|
28
33
|
it 'should support search markers' do
|
|
29
|
-
expect(Post.search_by('aLiCe -
|
|
30
|
-
expect(Post.search_by('+alice "
|
|
31
|
-
expect(Post.search_by('bob -"
|
|
34
|
+
expect(Post.search_by('aLiCe -your').pluck(:title)).to match_array(%w[a1 ab])
|
|
35
|
+
expect(Post.search_by('+alice "your recipe"').pluck(:title)).to match_array(%w[a2])
|
|
36
|
+
expect(Post.search_by('bob -"her recipe"').pluck(:title)).to match_array(%w[b2 ab])
|
|
37
|
+
expect(Post.search_by('bob +"her recipe"').pluck(:title)).to match_array(%w[b1])
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'should respect match options' do
|
|
41
|
+
# name uses match: :prefix
|
|
42
|
+
expect(Post.search_by('alice').pluck(:title)).to match_array(%w[a1 a2 ab])
|
|
43
|
+
expect(Post.search_by('ali').pluck(:title)).to match_array(%w[a1 a2 ab])
|
|
44
|
+
expect(Post.search_by('lice').pluck(:title)).to be_empty
|
|
45
|
+
expect(Post.search_by('li').pluck(:title)).to be_empty
|
|
46
|
+
|
|
47
|
+
# title uses match: :all (default)
|
|
48
|
+
expect(Post.search_by('recip').pluck(:title)).to match_array(%w[a1 a2 b1 b2 ab])
|
|
32
49
|
end
|
|
33
50
|
|
|
34
51
|
it 'should search within scopes' do
|
|
35
|
-
expect(Post.where(title: '
|
|
36
|
-
expect(Post.where(title: '
|
|
52
|
+
expect(Post.where(title: 'a1').search_by('ALICE').pluck(:title)).to match_array(%w[a1])
|
|
53
|
+
expect(Post.where(title: 'a1').search_by('bOb').pluck(:title)).to match_array(%w[])
|
|
37
54
|
end
|
|
38
55
|
|
|
39
56
|
it 'should search integers' do
|
|
40
|
-
expect(Post.search_by(POSTS[:
|
|
57
|
+
expect(Post.search_by(POSTS[:ab].id.to_s).count).to eq(1)
|
|
41
58
|
end
|
|
42
59
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -2,15 +2,16 @@ ENV['RACK_ENV'] ||= 'test'
|
|
|
2
2
|
require 'searchable-by'
|
|
3
3
|
require 'rspec'
|
|
4
4
|
|
|
5
|
-
ActiveRecord::Base.configurations
|
|
5
|
+
ActiveRecord::Base.configurations = { 'test' => { 'adapter' => 'sqlite3', 'database' => ':memory:' } }
|
|
6
6
|
ActiveRecord::Base.establish_connection :test
|
|
7
7
|
|
|
8
8
|
ActiveRecord::Base.connection.instance_eval do
|
|
9
|
-
create_table :
|
|
9
|
+
create_table :users do |t|
|
|
10
10
|
t.string :name
|
|
11
11
|
end
|
|
12
12
|
create_table :posts do |t|
|
|
13
13
|
t.integer :author_id, null: false
|
|
14
|
+
t.integer :reviewer_id
|
|
14
15
|
t.string :title
|
|
15
16
|
t.text :body
|
|
16
17
|
end
|
|
@@ -24,31 +25,34 @@ class AbstractModel < ActiveRecord::Base
|
|
|
24
25
|
end
|
|
25
26
|
end
|
|
26
27
|
|
|
27
|
-
class
|
|
28
|
-
has_many :posts
|
|
28
|
+
class User < AbstractModel
|
|
29
|
+
has_many :posts, foreign_key: :author_id
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
class Post < AbstractModel
|
|
32
|
-
belongs_to :author
|
|
33
|
+
belongs_to :author, class_name: 'User'
|
|
34
|
+
belongs_to :reviewer, class_name: 'User'
|
|
33
35
|
|
|
34
36
|
searchable_by do
|
|
35
37
|
column :title, :body
|
|
36
|
-
column {
|
|
38
|
+
column proc { User.arel_table[:name] }, match: :prefix
|
|
39
|
+
column { User.arel_table.alias('reviewers_posts')[:name] }
|
|
37
40
|
|
|
38
41
|
scope do
|
|
39
|
-
joins(:author)
|
|
42
|
+
joins(:author).left_outer_joins(:reviewer)
|
|
40
43
|
end
|
|
41
44
|
end
|
|
42
45
|
end
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
+
USERS = {
|
|
48
|
+
a: User.create!(name: 'Alice'),
|
|
49
|
+
b: User.create!(name: 'Bob'),
|
|
47
50
|
}.freeze
|
|
48
51
|
|
|
49
52
|
POSTS = {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
53
|
+
a1: USERS[:a].posts.create!(title: 'a1', body: 'my recipe '),
|
|
54
|
+
a2: USERS[:a].posts.create!(title: 'a2', body: 'your recipe'),
|
|
55
|
+
b1: USERS[:b].posts.create!(title: 'b1', body: 'her recipe'),
|
|
56
|
+
b2: USERS[:b].posts.create!(title: 'b2', body: 'our recipe'),
|
|
57
|
+
ab: USERS[:a].posts.create!(title: 'ab', reviewer: USERS[:b], body: 'their recipe'),
|
|
54
58
|
}.freeze
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: searchable-by
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.5.
|
|
4
|
+
version: 0.5.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dimitrij Denissenko
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2020-09-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -139,7 +139,12 @@ files:
|
|
|
139
139
|
- Rakefile
|
|
140
140
|
- lib/searchable-by.rb
|
|
141
141
|
- lib/searchable_by.rb
|
|
142
|
+
- lib/searchable_by/column.rb
|
|
143
|
+
- lib/searchable_by/concern.rb
|
|
144
|
+
- lib/searchable_by/config.rb
|
|
145
|
+
- lib/searchable_by/util.rb
|
|
142
146
|
- searchable-by.gemspec
|
|
147
|
+
- spec/searchable_by/util_spec.rb
|
|
143
148
|
- spec/searchable_by_spec.rb
|
|
144
149
|
- spec/spec_helper.rb
|
|
145
150
|
homepage: https://github.com/bsm/sortable-by
|
|
@@ -154,17 +159,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
154
159
|
requirements:
|
|
155
160
|
- - ">="
|
|
156
161
|
- !ruby/object:Gem::Version
|
|
157
|
-
version: '2.
|
|
162
|
+
version: '2.5'
|
|
158
163
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
159
164
|
requirements:
|
|
160
165
|
- - ">="
|
|
161
166
|
- !ruby/object:Gem::Version
|
|
162
167
|
version: '0'
|
|
163
168
|
requirements: []
|
|
164
|
-
rubygems_version: 3.
|
|
169
|
+
rubygems_version: 3.1.2
|
|
165
170
|
signing_key:
|
|
166
171
|
specification_version: 4
|
|
167
172
|
summary: Generate search scopes
|
|
168
173
|
test_files:
|
|
174
|
+
- spec/searchable_by/util_spec.rb
|
|
169
175
|
- spec/searchable_by_spec.rb
|
|
170
176
|
- spec/spec_helper.rb
|