searchable-by 0.5.6 → 0.5.7

Sign up to get free protection for your applications and to get access to all the features.
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