searchable-by 0.5.6 → 0.5.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e2f1b8d0d05abf263dc93c1bd81ab818a88eedf135ff10cd5414d54d3ae17a1
4
- data.tar.gz: 04d1e040390dcf8c77adbc1bf5bd491505373147798b1c95e4e4cc6c259daab5
3
+ metadata.gz: 2137729d80e65107cadf27ede26a60887b6888c41f6940caea9828e3416f137f
4
+ data.tar.gz: 6edd61ef3f32e0fa6e882b07f5d24004a5c7a4cd5ec2f9c99ba2e6e12332f5e2
5
5
  SHA512:
6
- metadata.gz: e6bcafc69897951760eba1a9fb049664aa3360536525d31e6f5b1d9094ce274cc82c6936aa69e19b8ece975e1f74d42f424646d5a6e4de4bc4c12b29b24a4b33
7
- data.tar.gz: a5762fcbf915002a781e6d1996cae4df2ce80fc62d0948c0cb5965c97b07d0d5a9030cb7acf360bba4482a4f05026f1e082254f5f545a8fa5ba1bdb946ef520e
6
+ metadata.gz: 704a8beb8c791dc20e6d156f73a3c3b21f6aed0df0d94d0cc7dda78440d9051f4a9f9a3d076fa3afa5af54a4312e9251335d2ab2911c228506c23d877ef67789
7
+ data.tar.gz: '0906f2424988065b3d22a177ab260d42029ad3e531a8b5e051fc7170d87e87784fa18225eba4f6631416af7a0c9a3df5c1b3e464babdb8d56ace5f226b18ee39'
@@ -1,4 +1,8 @@
1
- require: rubocop-performance
1
+ require:
2
+ - rubocop-performance
3
+ - rubocop-rake
4
+ - rubocop-rspec
5
+
2
6
  inherit_from:
3
7
  - https://gitlab.com/bsm/misc/raw/master/rubocop/default.yml
4
8
 
@@ -1,70 +1,75 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- searchable-by (0.5.6)
4
+ searchable-by (0.5.7)
5
5
  activerecord
6
6
  activesupport
7
7
 
8
8
  GEM
9
9
  remote: http://rubygems.org/
10
10
  specs:
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)
11
+ activemodel (6.1.1)
12
+ activesupport (= 6.1.1)
13
+ activerecord (6.1.1)
14
+ activemodel (= 6.1.1)
15
+ activesupport (= 6.1.1)
16
+ activesupport (6.1.1)
17
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
18
- i18n (>= 0.7, < 2)
19
- minitest (~> 5.1)
20
- tzinfo (~> 1.1)
21
- zeitwerk (~> 2.2, >= 2.2.2)
18
+ i18n (>= 1.6, < 2)
19
+ minitest (>= 5.1)
20
+ tzinfo (~> 2.0)
21
+ zeitwerk (~> 2.3)
22
22
  ast (2.4.1)
23
23
  concurrent-ruby (1.1.7)
24
24
  diff-lcs (1.4.4)
25
- i18n (1.8.5)
25
+ i18n (1.8.7)
26
26
  concurrent-ruby (~> 1.0)
27
- minitest (5.14.2)
28
- parallel (1.19.2)
29
- parser (2.7.1.4)
27
+ minitest (5.14.3)
28
+ parallel (1.20.1)
29
+ parser (3.0.0.0)
30
30
  ast (~> 2.4.1)
31
31
  rainbow (3.0.0)
32
- rake (13.0.1)
33
- regexp_parser (1.7.1)
32
+ rake (13.0.3)
33
+ regexp_parser (2.0.3)
34
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)
35
+ rspec (3.10.0)
36
+ rspec-core (~> 3.10.0)
37
+ rspec-expectations (~> 3.10.0)
38
+ rspec-mocks (~> 3.10.0)
39
+ rspec-core (3.10.1)
40
+ rspec-support (~> 3.10.0)
41
+ rspec-expectations (3.10.1)
42
42
  diff-lcs (>= 1.2.0, < 2.0)
43
- rspec-support (~> 3.9.0)
44
- rspec-mocks (3.9.1)
43
+ rspec-support (~> 3.10.0)
44
+ rspec-mocks (3.10.1)
45
45
  diff-lcs (>= 1.2.0, < 2.0)
46
- rspec-support (~> 3.9.0)
47
- rspec-support (3.9.3)
48
- rubocop (0.91.0)
46
+ rspec-support (~> 3.10.0)
47
+ rspec-support (3.10.1)
48
+ rubocop (1.8.1)
49
49
  parallel (~> 1.10)
50
- parser (>= 2.7.1.1)
50
+ parser (>= 3.0.0.0)
51
51
  rainbow (>= 2.2.2, < 4.0)
52
- regexp_parser (>= 1.7)
52
+ regexp_parser (>= 1.8, < 3.0)
53
53
  rexml
54
- rubocop-ast (>= 0.4.0, < 1.0)
54
+ rubocop-ast (>= 1.2.0, < 2.0)
55
55
  ruby-progressbar (~> 1.7)
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)
56
+ unicode-display_width (>= 1.4.0, < 3.0)
57
+ rubocop-ast (1.4.0)
58
+ parser (>= 2.7.1.5)
59
+ rubocop-performance (1.9.2)
60
+ rubocop (>= 0.90.0, < 2.0)
61
+ rubocop-ast (>= 0.4.0)
62
+ rubocop-rake (0.5.1)
63
+ rubocop
64
+ rubocop-rspec (2.1.0)
65
+ rubocop (~> 1.0)
66
+ rubocop-ast (>= 1.1.0)
67
+ ruby-progressbar (1.11.0)
62
68
  sqlite3 (1.4.2)
63
- thread_safe (0.3.6)
64
- tzinfo (1.2.7)
65
- thread_safe (~> 0.1)
66
- unicode-display_width (1.7.0)
67
- zeitwerk (2.4.0)
69
+ tzinfo (2.0.4)
70
+ concurrent-ruby (~> 1.0)
71
+ unicode-display_width (2.0.0)
72
+ zeitwerk (2.4.2)
68
73
 
69
74
  PLATFORMS
70
75
  ruby
@@ -75,6 +80,8 @@ DEPENDENCIES
75
80
  rspec
76
81
  rubocop
77
82
  rubocop-performance
83
+ rubocop-rake
84
+ rubocop-rspec
78
85
  searchable-by!
79
86
  sqlite3
80
87
 
data/README.md CHANGED
@@ -14,9 +14,10 @@ class Post < ActiveRecord::Base
14
14
 
15
15
  # Limit the number of terms per query to 3.
16
16
  searchable_by max_terms: 3 do
17
- # Allow to search strings.
17
+ # Allow to search strings with custom match type.
18
18
  # Use btree index-friendly prefix match, e.g. `ILIKE 'term%'` instead of default `ILIKE '%term%'`.
19
- column :title, match: :prefix
19
+ # For phrases use exact match type, e.g. searching for `"My Post"` will query `WHERE LOWER(title) = 'my post'`.
20
+ column :title, match: :prefix, match_phrase: :exact
20
21
 
21
22
  # ... and integers.
22
23
  column :id, type: :integer
@@ -6,7 +6,7 @@ module SearchableBy
6
6
  autoload :Config, 'searchable_by/config'
7
7
  autoload :Util, 'searchable_by/util'
8
8
 
9
- Value = Struct.new(:term, :negate)
9
+ Value = Struct.new(:term, :negate, :phrase)
10
10
  end
11
11
 
12
12
  ActiveRecord::Base.extend SearchableBy::Concern if defined?(::ActiveRecord::Base)
@@ -1,33 +1,50 @@
1
1
  module SearchableBy
2
2
  class Column
3
- attr_reader :attr, :type, :match
3
+ attr_reader :attr, :type, :match, :match_phrase
4
4
  attr_accessor :node
5
5
 
6
- def initialize(attr, type: :string, match: :all)
6
+ def initialize(attr, type: :string, match: :all, match_phrase: nil)
7
7
  @attr = attr
8
8
  @type = type.to_sym
9
9
  @match = match
10
+ @match_phrase = match_phrase || match
10
11
  end
11
12
 
12
13
  def build_condition(value)
14
+ scope = node.not_eq(nil)
15
+
13
16
  case type
14
17
  when :int, :integer
15
- begin
16
- node.not_eq(nil).and(node.eq(Integer(value.term)))
17
- rescue ArgumentError
18
- nil
19
- end
18
+ int_condition(scope, value)
19
+ else
20
+ str_condition(scope, value)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def int_condition(scope, value)
27
+ scope.and(node.eq(Integer(value.term)))
28
+ rescue ArgumentError
29
+ nil
30
+ end
31
+
32
+ def str_condition(scope, value)
33
+ term = value.term.dup
34
+ type = value.phrase ? match_phrase : match
35
+
36
+ case type
37
+ when :exact
38
+ term.downcase!
39
+ scope.and(node.lower.eq(term))
40
+ when :prefix
41
+ term.gsub!('%', '\%')
42
+ term.gsub!('_', '\_')
43
+ scope.and(node.matches("#{term}%"))
20
44
  else
21
- term = value.term.dup
22
45
  term.gsub!('%', '\%')
23
46
  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))
47
+ scope.and(node.matches("%#{term}%"))
31
48
  end
32
49
  end
33
50
  end
@@ -4,23 +4,23 @@ module SearchableBy
4
4
  values = []
5
5
  query = query.to_s.dup
6
6
 
7
- # capture any terms inside double quotes
7
+ # capture any phrases inside double quotes
8
8
  # exclude from search if preceded by '-'
9
9
  query.gsub!(/([\-+]?)"+([^"]*)"+/) do |_|
10
10
  term = Regexp.last_match(2)
11
11
  negate = Regexp.last_match(1) == '-'
12
12
 
13
- values.push Value.new(term, negate) unless term.blank?
13
+ values.push Value.new(term, negate, true) unless term.blank?
14
14
  ''
15
15
  end
16
16
 
17
17
  # for the remaining terms remove sign if precedes
18
18
  # exclude term from search if sign preceding is '-'
19
- query.split(' ').each do |term|
19
+ query.split.each do |term|
20
20
  negate = term[0] == '-'
21
21
  term.slice!(0) if negate || term[0] == '+'
22
22
 
23
- values.push Value.new(term, negate) unless term.blank?
23
+ values.push Value.new(term, negate, false) unless term.blank?
24
24
  end
25
25
 
26
26
  values.uniq!
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'searchable-by'
3
- s.version = '0.5.6'
3
+ s.version = '0.5.7'
4
4
  s.authors = ['Dimitrij Denissenko']
5
5
  s.email = ['dimitrij@blacksquaremedia.com']
6
6
  s.summary = 'Generate search scopes'
@@ -21,5 +21,7 @@ Gem::Specification.new do |s|
21
21
  s.add_development_dependency 'rspec'
22
22
  s.add_development_dependency 'rubocop'
23
23
  s.add_development_dependency 'rubocop-performance'
24
+ s.add_development_dependency 'rubocop-rake'
25
+ s.add_development_dependency 'rubocop-rspec'
24
26
  s.add_development_dependency 'sqlite3'
25
27
  end
@@ -1,14 +1,14 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe SearchableBy::Util do
4
- context 'norm_values' do
4
+ context 'with norm_values' do
5
5
  def norm(str)
6
6
  described_class.norm_values(str).each_with_object({}) do |val, acc|
7
7
  acc[val.term] = val.negate
8
8
  end
9
9
  end
10
10
 
11
- it 'should tokenise strings' do
11
+ it 'tokenises strings' do
12
12
  expect(norm(nil)).to eq({})
13
13
  expect(norm('""')).to eq({})
14
14
  expect(norm('-+""')).to eq({})
@@ -17,7 +17,7 @@ describe SearchableBy::Util do
17
17
  expect(norm('with with duplicates with')).to eq('with' => false, 'duplicates' => false)
18
18
  expect(norm('with "full term"')).to eq('full term' => false, 'with' => false)
19
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)
20
+ expect(norm('""even double quotes around""')).to eq('even double quotes around' => false)
21
21
  expect(norm('with\'apostrophe')).to eq("with'apostrophe" => false)
22
22
  expect(norm('with -minus')).to eq('minus' => true, 'with' => false)
23
23
  expect(norm('with +plus')).to eq('plus' => false, 'with' => false)
@@ -1,59 +1,72 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe SearchableBy do
4
- it 'should ignore bad inputs' do
4
+ it 'ignores bad inputs' do
5
5
  expect(Post.search_by(nil).count).to eq(5)
6
6
  expect(Post.search_by('').count).to eq(5)
7
7
  end
8
8
 
9
- it 'should configure correctly' do
9
+ it 'configures correctly' do
10
10
  expect(AbstractModel._searchable_by_config.columns.size).to eq(1)
11
11
  expect(Post._searchable_by_config.columns.size).to eq(5)
12
12
  end
13
13
 
14
- it 'should generate SQL' do
14
+ it 'generates SQL' do
15
15
  sql = Post.search_by('123').to_sql
16
- expect(sql).to include(%("posts"."id" = 123))
17
- expect(sql).to include(%("posts"."title" LIKE '%123%'))
16
+ expect(sql).to include(%("posts"."id" IS NOT NULL AND "posts"."id" = 123))
17
+ expect(sql).to include(%("posts"."title" IS NOT NULL AND "posts"."title" LIKE '123%'))
18
+ expect(sql).to include(%("posts"."body" IS NOT NULL AND "posts"."body" LIKE '%123%'))
19
+ expect(sql).to include(%("users"."name" IS NOT NULL AND LOWER("users"."name") = '123'))
18
20
 
19
21
  sql = Post.search_by('foo%bar').to_sql
20
22
  expect(sql).not_to include(%("posts"."id"))
21
- expect(sql).to include(%("posts"."title" LIKE '%foo\\%bar%'))
23
+ expect(sql).to include(%("posts"."title" LIKE 'foo\\%bar%'))
24
+ expect(sql).to include(%("posts"."body" LIKE '%foo\\%bar%'))
22
25
  end
23
26
 
24
- it 'should search' do
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])
27
+ it 'searches' do
28
+ expect(Post.search_by('ALICE').pluck(:title)).to match_array(%w[ax1 ax2 ab1])
29
+ expect(Post.search_by('bOb').pluck(:title)).to match_array(%w[bx1 bx2 ab1])
27
30
  end
28
31
 
29
- it 'should search across multiple words' do
30
- expect(Post.search_by('ALICE your').pluck(:title)).to match_array(%w[a2])
32
+ it 'searches across multiple words' do
33
+ expect(Post.search_by('ALICE your').pluck(:title)).to match_array(%w[ax2])
31
34
  end
32
35
 
33
- it 'should support search markers' do
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])
36
+ it 'supports search markers' do
37
+ expect(Post.search_by('aLiCe -your').pluck(:title)).to match_array(%w[ax1 ab1])
38
+ expect(Post.search_by('+alice "your recipe"').pluck(:title)).to match_array(%w[ax2])
39
+ expect(Post.search_by('bob -"her recipe"').pluck(:title)).to match_array(%w[bx2 ab1])
40
+ expect(Post.search_by('bob +"her recipe"').pluck(:title)).to match_array(%w[bx1])
38
41
  end
39
42
 
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])
43
+ it 'respects match options' do
44
+ # name uses match: :exact
45
+ expect(Post.search_by('alice').pluck(:title)).to match_array(%w[ax1 ax2 ab1])
46
+ expect(Post.search_by('ali').pluck(:title)).to be_empty
44
47
  expect(Post.search_by('lice').pluck(:title)).to be_empty
45
48
  expect(Post.search_by('li').pluck(:title)).to be_empty
46
49
 
47
- # title uses match: :all (default)
48
- expect(Post.search_by('recip').pluck(:title)).to match_array(%w[a1 a2 b1 b2 ab])
50
+ # title uses match: :prefix
51
+ expect(Post.search_by('ax').pluck(:title)).to match_array(%w[ax1 ax2])
52
+ expect(Post.search_by('bx').pluck(:title)).to match_array(%w[bx1 bx2])
53
+ expect(Post.search_by('ab').pluck(:title)).to match_array(%w[ab1])
54
+ expect(Post.search_by('ba').pluck(:title)).to be_empty
55
+
56
+ # title uses match_phrase: :exact
57
+ expect(Post.search_by('"ab"').pluck(:title)).to be_empty
58
+ expect(Post.search_by('"ab1"').pluck(:title)).to match_array(%w[ab1])
59
+
60
+ # body uses match: :all (default)
61
+ expect(Post.search_by('recip').pluck(:title)).to match_array(%w[ax1 ax2 bx1 bx2 ab1])
49
62
  end
50
63
 
51
- it 'should search within scopes' do
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[])
64
+ it 'searches within scopes' do
65
+ expect(Post.where(title: 'ax1').search_by('ALICE').pluck(:title)).to match_array(%w[ax1])
66
+ expect(Post.where(title: 'ax1').search_by('bOb').pluck(:title)).to be_empty
54
67
  end
55
68
 
56
- it 'should search integers' do
57
- expect(Post.search_by(POSTS[:ab].id.to_s).count).to eq(1)
69
+ it 'searches integers' do
70
+ expect(Post.search_by(POSTS[:ab1].id.to_s).count).to eq(1)
58
71
  end
59
72
  end
@@ -34,8 +34,9 @@ class Post < AbstractModel
34
34
  belongs_to :reviewer, class_name: 'User'
35
35
 
36
36
  searchable_by do
37
- column :title, :body
38
- column proc { User.arel_table[:name] }, match: :prefix
37
+ column :title, match: :prefix, match_phrase: :exact
38
+ column :body
39
+ column proc { User.arel_table[:name] }, match: :exact
39
40
  column { User.arel_table.alias('reviewers_posts')[:name] }
40
41
 
41
42
  scope do
@@ -50,9 +51,9 @@ USERS = {
50
51
  }.freeze
51
52
 
52
53
  POSTS = {
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
+ ax1: USERS[:a].posts.create!(title: 'ax1', body: 'my recipe '),
55
+ ax2: USERS[:a].posts.create!(title: 'ax2', body: 'your recipe'),
56
+ bx1: USERS[:b].posts.create!(title: 'bx1', body: 'her recipe'),
57
+ bx2: USERS[:b].posts.create!(title: 'bx2', body: 'our recipe'),
58
+ ab1: USERS[:a].posts.create!(title: 'ab1', reviewer: USERS[:b], body: 'their recipe'),
58
59
  }.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.6
4
+ version: 0.5.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dimitrij Denissenko
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-21 00:00:00.000000000 Z
11
+ date: 2021-01-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -108,6 +108,34 @@ dependencies:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rubocop-rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rubocop-rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
111
139
  - !ruby/object:Gem::Dependency
112
140
  name: sqlite3
113
141
  requirement: !ruby/object:Gem::Requirement
@@ -166,7 +194,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
166
194
  - !ruby/object:Gem::Version
167
195
  version: '0'
168
196
  requirements: []
169
- rubygems_version: 3.1.2
197
+ rubygems_version: 3.1.4
170
198
  signing_key:
171
199
  specification_version: 4
172
200
  summary: Generate search scopes