searchable-by 0.5.2 → 0.5.8

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: 21e09cdcdb1a9bebd3df6bb7a28d4f4cfc566a478b757390ea7f71445f195edd
4
- data.tar.gz: 3bad5ea302c2c46bceee3a966b298799c133d1c2db0ffecf6a4bd38bf5393ccc
3
+ metadata.gz: 9577dc443c9400afd9bb866e8a53a351cf1e56420ce28b140c184e6647689411
4
+ data.tar.gz: 6a84939c7bb80af91d31b6d6bb3fd65acd672a4b41fbc25dbe08664e74d1f58d
5
5
  SHA512:
6
- metadata.gz: be511c786fae03cf6f37f392bc122446101ddb7dee1ed7af52bd1d21b92e4512d8493687baa9fa842aef5460f1b6d750f83f7ad9e54b30a2c74a42a8b14971b7
7
- data.tar.gz: 8029a6c09870555dcf1d9235127ae1d8312e1cb226b93cf7428176f74efee991235158b2faf12b2b0dd7b7a83f7cfc705ffdfa9e2bb032edbcc8a65a6a0e2bd3
6
+ metadata.gz: 86367f8084de15d5d8e11568c1dadd5beddb090bf4dc4c334133197e15906e0bced5396a5c95f9434e6902d2ae0752698590f1b2fc1eacba26cb49a66a3e5697
7
+ data.tar.gz: ac9f4157b9a1705b50c95c9e1e3ee4f9c19b5ea7be9fec12a5a42df53eb0a2761a0c90c902dce641f15628ac48f2b3be4e1b504cfb36bebd6db00620dda4d787
data/.rubocop.yml CHANGED
@@ -1,10 +1,13 @@
1
- require: rubocop-performance
2
- inherit_from:
3
- - https://gitlab.com/bsm/misc/raw/master/rubocop/default.yml
1
+ inherit_gem:
2
+ rubocop-bsm:
3
+ - default.yml
4
+ inherit_mode:
5
+ merge:
6
+ - Exclude
4
7
 
5
8
  AllCops:
6
- TargetRubyVersion: "2.4"
9
+ TargetRubyVersion: "2.6"
7
10
 
8
11
  Naming/FileName:
9
12
  Exclude:
10
- - lib/searchable-by.rb
13
+ - lib/searchable-by.rb
data/.travis.yml CHANGED
@@ -1,8 +1,8 @@
1
1
  language: ruby
2
2
  rvm:
3
+ - 3.0
4
+ - 2.7
3
5
  - 2.6
4
- - 2.5
5
- - 2.4
6
6
  cache: bundler
7
7
  before_install:
8
8
  - gem install bundler
data/Gemfile.lock CHANGED
@@ -1,65 +1,86 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- searchable-by (0.5.2)
4
+ searchable-by (0.5.8)
5
5
  activerecord
6
6
  activesupport
7
7
 
8
8
  GEM
9
9
  remote: http://rubygems.org/
10
10
  specs:
11
- activemodel (5.2.3)
12
- activesupport (= 5.2.3)
13
- activerecord (5.2.3)
14
- activemodel (= 5.2.3)
15
- activesupport (= 5.2.3)
16
- arel (>= 9.0)
17
- activesupport (5.2.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)
18
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
19
- i18n (>= 0.7, < 2)
20
- minitest (~> 5.1)
21
- tzinfo (~> 1.1)
22
- arel (9.0.0)
23
- ast (2.4.0)
24
- concurrent-ruby (1.1.5)
25
- diff-lcs (1.3)
26
- i18n (1.6.0)
18
+ i18n (>= 1.6, < 2)
19
+ minitest (>= 5.1)
20
+ tzinfo (~> 2.0)
21
+ zeitwerk (~> 2.3)
22
+ ast (2.4.2)
23
+ concurrent-ruby (1.1.8)
24
+ diff-lcs (1.4.4)
25
+ i18n (1.8.8)
27
26
  concurrent-ruby (~> 1.0)
28
- jaro_winkler (1.5.3)
29
- minitest (5.11.3)
30
- parallel (1.17.0)
31
- parser (2.6.3.0)
32
- ast (~> 2.4.0)
27
+ minitest (5.14.3)
28
+ parallel (1.20.1)
29
+ parser (3.0.0.0)
30
+ ast (~> 2.4.1)
31
+ rack (2.2.3)
33
32
  rainbow (3.0.0)
34
- rake (12.3.3)
35
- rspec (3.8.0)
36
- rspec-core (~> 3.8.0)
37
- rspec-expectations (~> 3.8.0)
38
- rspec-mocks (~> 3.8.0)
39
- rspec-core (3.8.2)
40
- rspec-support (~> 3.8.0)
41
- rspec-expectations (3.8.4)
33
+ rake (13.0.3)
34
+ regexp_parser (2.0.3)
35
+ rexml (3.2.4)
36
+ rspec (3.10.0)
37
+ rspec-core (~> 3.10.0)
38
+ rspec-expectations (~> 3.10.0)
39
+ rspec-mocks (~> 3.10.0)
40
+ rspec-core (3.10.1)
41
+ rspec-support (~> 3.10.0)
42
+ rspec-expectations (3.10.1)
42
43
  diff-lcs (>= 1.2.0, < 2.0)
43
- rspec-support (~> 3.8.0)
44
- rspec-mocks (3.8.1)
44
+ rspec-support (~> 3.10.0)
45
+ rspec-mocks (3.10.2)
45
46
  diff-lcs (>= 1.2.0, < 2.0)
46
- rspec-support (~> 3.8.0)
47
- rspec-support (3.8.2)
48
- rubocop (0.73.0)
49
- jaro_winkler (~> 1.5.1)
47
+ rspec-support (~> 3.10.0)
48
+ rspec-support (3.10.2)
49
+ rubocop (1.9.1)
50
50
  parallel (~> 1.10)
51
- parser (>= 2.6)
51
+ parser (>= 3.0.0.0)
52
52
  rainbow (>= 2.2.2, < 4.0)
53
+ regexp_parser (>= 1.8, < 3.0)
54
+ rexml
55
+ rubocop-ast (>= 1.2.0, < 2.0)
53
56
  ruby-progressbar (~> 1.7)
54
- unicode-display_width (>= 1.4.0, < 1.7)
55
- rubocop-performance (1.4.0)
56
- rubocop (>= 0.71.0)
57
- ruby-progressbar (1.10.1)
58
- sqlite3 (1.4.1)
59
- thread_safe (0.3.6)
60
- tzinfo (1.2.5)
61
- thread_safe (~> 0.1)
62
- unicode-display_width (1.6.0)
57
+ unicode-display_width (>= 1.4.0, < 3.0)
58
+ rubocop-ast (1.4.1)
59
+ parser (>= 2.7.1.5)
60
+ rubocop-bsm (0.5.4)
61
+ rubocop (~> 1.0)
62
+ rubocop-performance
63
+ rubocop-rails
64
+ rubocop-rake
65
+ rubocop-rspec
66
+ rubocop-performance (1.9.2)
67
+ rubocop (>= 0.90.0, < 2.0)
68
+ rubocop-ast (>= 0.4.0)
69
+ rubocop-rails (2.9.1)
70
+ activesupport (>= 4.2.0)
71
+ rack (>= 1.1)
72
+ rubocop (>= 0.90.0, < 2.0)
73
+ rubocop-rake (0.5.1)
74
+ rubocop
75
+ rubocop-rspec (2.2.0)
76
+ rubocop (~> 1.0)
77
+ rubocop-ast (>= 1.1.0)
78
+ ruby-progressbar (1.11.0)
79
+ sqlite3 (1.4.2)
80
+ tzinfo (2.0.4)
81
+ concurrent-ruby (~> 1.0)
82
+ unicode-display_width (2.0.0)
83
+ zeitwerk (2.4.2)
63
84
 
64
85
  PLATFORMS
65
86
  ruby
@@ -69,9 +90,9 @@ DEPENDENCIES
69
90
  rake
70
91
  rspec
71
92
  rubocop
72
- rubocop-performance
93
+ rubocop-bsm
73
94
  searchable-by!
74
95
  sqlite3
75
96
 
76
97
  BUNDLED WITH
77
- 2.0.1
98
+ 2.2.3
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright 2019 Black Square Media Ltd
1
+ Copyright 2020 Black Square Media Ltd
2
2
 
3
3
  Licensed under the Apache License, Version 2.0 (the "License");
4
4
  you may not use this file except in compliance with the License.
data/README.md CHANGED
@@ -14,8 +14,11 @@ 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.
18
- column :title
17
+ # Allow to search strings with custom match type.
18
+ column :title,
19
+ match: :prefix, # Use btree index-friendly prefix match, e.g. `ILIKE 'term%'` instead of default `ILIKE '%term%'`.
20
+ match_phrase: :exact, # For phrases use exact match type, e.g. searching for `"My Post"` will query `WHERE LOWER(title) = 'my post'`.
21
+ min_length: 3 # Return no-match if search term is too short (useful for trigram indexes).
19
22
 
20
23
  # ... and integers.
21
24
  column :id, type: :integer
data/lib/searchable_by.rb CHANGED
@@ -1,132 +1,12 @@
1
1
  require 'active_record'
2
- require 'shellwords'
3
2
 
4
- module ActiveRecord
5
- module SearchableBy
6
- class Column
7
- attr_reader :attr, :type
8
- attr_accessor :node
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'
9
8
 
10
- def initialize(attr, type: :string)
11
- @attr = attr
12
- @type = type.to_sym
13
- end
14
- end
15
-
16
- class Config
17
- attr_reader :columns, :scoping
18
- attr_accessor :max_terms
19
-
20
- def initialize
21
- @columns = []
22
- @max_terms = 5
23
- scope { all }
24
- end
25
-
26
- def initialize_copy(other)
27
- @columns = other.columns.dup
28
- super
29
- end
30
-
31
- def column(*attrs, &block)
32
- opts = attrs.extract_options!
33
- attrs.each do |attr|
34
- columns.push Column.new(attr, opts)
35
- end
36
- columns.push Column.new(block, opts) if block
37
- columns
38
- end
39
-
40
- def scope(&block)
41
- @scoping = block
42
- end
43
- end
44
-
45
- def self.norm_values(query)
46
- values = Shellwords.split(query.to_s)
47
- values.flatten!
48
- values.reject!(&:blank?)
49
- values.uniq!
50
- values
51
- end
52
-
53
- def self.build_clauses(columns, values)
54
- clauses = values.map do |value|
55
- negate = value[0] == '-'
56
- value.slice!(0) if negate || value[0] == '+'
57
-
58
- grouping = columns.map do |column|
59
- build_condition(column, value)
60
- end
61
- grouping.compact!
62
- next if grouping.empty?
63
-
64
- clause = grouping.inject(&:or)
65
- clause = clause.not if negate
66
- clause
67
- end
68
- clauses.compact!
69
- clauses
70
- end
71
-
72
- def self.build_condition(column, value)
73
- case column.type
74
- when :int, :integer
75
- begin
76
- column.node.not_eq(nil).and(column.node.eq(Integer(value)))
77
- rescue ArgumentError
78
- nil
79
- end
80
- else
81
- value = value.dup
82
- value.gsub!('%', '\%')
83
- value.gsub!('_', '\_')
84
- column.node.not_eq(nil).and(column.node.matches("%#{value}%"))
85
- end
86
- end
87
-
88
- module ClassMethods
89
- def self.extended(base) # :nodoc:
90
- base.class_attribute :_searchable_by_config, instance_accessor: false, instance_predicate: false
91
- base._searchable_by_config = Config.new
92
- super
93
- end
94
-
95
- def inherited(base) # :nodoc:
96
- base._searchable_by_config = _searchable_by_config.dup
97
- super
98
- end
99
-
100
- def searchable_by(max_terms: 5, &block)
101
- _searchable_by_config.instance_eval(&block)
102
- _searchable_by_config.max_terms = max_terms if max_terms
103
- end
104
-
105
- # @param [String] query the search query
106
- # @return [ActiveRecord::Relation] the scoped relation
107
- def search_by(query)
108
- columns = _searchable_by_config.columns
109
- return all if columns.empty?
110
-
111
- values = SearchableBy.norm_values(query).first(_searchable_by_config.max_terms)
112
- return all if values.empty?
113
-
114
- columns.each do |col|
115
- col.node ||= col.attr.is_a?(Proc) ? col.attr.call : arel_table[col.attr]
116
- end
117
- clauses = SearchableBy.build_clauses(columns, values)
118
- return all if clauses.empty?
119
-
120
- scope = instance_exec(&_searchable_by_config.scoping)
121
- clauses.each do |clause|
122
- scope = scope.where(clause)
123
- end
124
- scope
125
- end
126
- end
127
- end
128
-
129
- class Base
130
- extend SearchableBy::ClassMethods
131
- end
9
+ Value = Struct.new(:term, :negate, :phrase)
132
10
  end
11
+
12
+ ActiveRecord::Base.extend SearchableBy::Concern if defined?(::ActiveRecord::Base)
@@ -0,0 +1,54 @@
1
+ module SearchableBy
2
+ class Column
3
+ attr_reader :attr, :type, :match, :match_phrase, :min_length
4
+ attr_accessor :node
5
+
6
+ def initialize(attr, type: :string, match: :all, match_phrase: nil, min_length: 0)
7
+ @attr = attr
8
+ @type = type.to_sym
9
+ @match = match
10
+ @match_phrase = match_phrase || match
11
+ @min_length = min_length
12
+ end
13
+
14
+ def build_condition(value)
15
+ return Arel::Nodes::False.new if value.term.length < min_length # no-match
16
+
17
+ scope = node.not_eq(nil)
18
+
19
+ case type
20
+ when :int, :integer
21
+ int_condition(scope, value)
22
+ else
23
+ str_condition(scope, value)
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def int_condition(scope, value)
30
+ scope.and(node.eq(Integer(value.term)))
31
+ rescue ArgumentError
32
+ nil
33
+ end
34
+
35
+ def str_condition(scope, value)
36
+ term = value.term.dup
37
+ type = value.phrase ? match_phrase : match
38
+
39
+ case type
40
+ when :exact
41
+ term.downcase!
42
+ scope.and(node.lower.eq(term))
43
+ when :prefix
44
+ term.gsub!('%', '\%')
45
+ term.gsub!('_', '\_')
46
+ scope.and(node.matches("#{term}%"))
47
+ else
48
+ term.gsub!('%', '\%')
49
+ term.gsub!('_', '\_')
50
+ scope.and(node.matches("%#{term}%"))
51
+ end
52
+ end
53
+ end
54
+ 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 phrases 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, true) 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, false) 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
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'searchable-by'
3
- s.version = '0.5.2'
3
+ s.version = '0.5.8'
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.match(%r{^spec/}) }
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.4'
14
+ s.required_ruby_version = '>= 2.6'
15
15
 
16
16
  s.add_dependency 'activerecord'
17
17
  s.add_dependency 'activesupport'
@@ -20,6 +20,6 @@ Gem::Specification.new do |s|
20
20
  s.add_development_dependency 'rake'
21
21
  s.add_development_dependency 'rspec'
22
22
  s.add_development_dependency 'rubocop'
23
- s.add_development_dependency 'rubocop-performance'
23
+ s.add_development_dependency 'rubocop-bsm'
24
24
  s.add_development_dependency 'sqlite3'
25
25
  end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe SearchableBy::Util do
4
+ context 'with 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 'tokenises 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
@@ -1,49 +1,77 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe ActiveRecord::SearchableBy do
4
-
5
- it 'should ignore bad inputs' do
3
+ describe SearchableBy do
4
+ it 'ignores bad inputs' do
6
5
  expect(Post.search_by(nil).count).to eq(5)
7
6
  expect(Post.search_by('').count).to eq(5)
8
7
  end
9
8
 
10
- it 'should configure correctly' do
9
+ it 'configures correctly' do
11
10
  expect(AbstractModel._searchable_by_config.columns.size).to eq(1)
12
11
  expect(Post._searchable_by_config.columns.size).to eq(5)
13
12
  end
14
13
 
15
- it 'should generate SQL' do
14
+ it 'generates SQL' do
16
15
  sql = Post.search_by('123').to_sql
17
- expect(sql).to include(%("posts"."id" = 123))
18
- 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'))
19
20
 
20
21
  sql = Post.search_by('foo%bar').to_sql
21
22
  expect(sql).not_to include(%("posts"."id"))
22
- 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%'))
25
+ end
26
+
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])
30
+ end
31
+
32
+ it 'searches across multiple words' do
33
+ expect(Post.search_by('ALICE your').pluck(:title)).to match_array(%w[ax2])
23
34
  end
24
35
 
25
- it 'should search' do
26
- expect(Post.search_by('ALICE').pluck(:title)).to match_array(%w[a1 a2 ab])
27
- expect(Post.search_by('bOb').pluck(:title)).to match_array(%w[b1 b2 ab])
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])
28
41
  end
29
42
 
30
- it 'should search across multiple words' do
31
- expect(Post.search_by('ALICE your').pluck(:title)).to match_array(%w[a2])
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
47
+ expect(Post.search_by('lice').pluck(:title)).to be_empty
48
+ expect(Post.search_by('li').pluck(:title)).to be_empty
49
+
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])
32
62
  end
33
63
 
34
- it 'should support search markers' do
35
- expect(Post.search_by('aLiCe -your').pluck(:title)).to match_array(%w[a1 ab])
36
- expect(Post.search_by('+alice "your recipe"').pluck(:title)).to match_array(%w[a2])
37
- expect(Post.search_by('bob -"her recipe"').pluck(:title)).to match_array(%w[b2 ab])
38
- expect(Post.search_by('bob +"her recipe"').pluck(:title)).to match_array(%w[b1])
64
+ it 'supports min term length' do
65
+ expect(User.search_by('+ir')).to be_empty
66
+ expect(User.search_by('irs')).to match_array([USERS[:a]])
39
67
  end
40
68
 
41
- it 'should search within scopes' do
42
- expect(Post.where(title: 'a1').search_by('ALICE').pluck(:title)).to match_array(%w[a1])
43
- expect(Post.where(title: 'a1').search_by('bOb').pluck(:title)).to match_array(%w[])
69
+ it 'searches within scopes' do
70
+ expect(Post.where(title: 'ax1').search_by('ALICE').pluck(:title)).to match_array(%w[ax1])
71
+ expect(Post.where(title: 'ax1').search_by('bOb').pluck(:title)).to be_empty
44
72
  end
45
73
 
46
- it 'should search integers' do
47
- expect(Post.search_by(POSTS[:ab].id.to_s).count).to eq(1)
74
+ it 'searches integers' do
75
+ expect(Post.search_by(POSTS[:ab1].id.to_s).count).to eq(1)
48
76
  end
49
77
  end
data/spec/spec_helper.rb CHANGED
@@ -2,12 +2,13 @@ ENV['RACK_ENV'] ||= 'test'
2
2
  require 'searchable-by'
3
3
  require 'rspec'
4
4
 
5
- ActiveRecord::Base.configurations['test'] = { 'adapter' => 'sqlite3', 'database' => ':memory:' }
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
9
  create_table :users do |t|
10
10
  t.string :name
11
+ t.string :bio
11
12
  end
12
13
  create_table :posts do |t|
13
14
  t.integer :author_id, null: false
@@ -27,6 +28,10 @@ end
27
28
 
28
29
  class User < AbstractModel
29
30
  has_many :posts, foreign_key: :author_id
31
+
32
+ searchable_by do
33
+ column :bio, min_length: 3
34
+ end
30
35
  end
31
36
 
32
37
  class Post < AbstractModel
@@ -34,8 +39,9 @@ class Post < AbstractModel
34
39
  belongs_to :reviewer, class_name: 'User'
35
40
 
36
41
  searchable_by do
37
- column :title, :body
38
- column { User.arel_table[:name] }
42
+ column :title, match: :prefix, match_phrase: :exact
43
+ column :body
44
+ column proc { User.arel_table[:name] }, match: :exact
39
45
  column { User.arel_table.alias('reviewers_posts')[:name] }
40
46
 
41
47
  scope do
@@ -45,14 +51,14 @@ class Post < AbstractModel
45
51
  end
46
52
 
47
53
  USERS = {
48
- a: User.create!(name: 'Alice'),
49
- b: User.create!(name: 'Bob'),
54
+ a: User.create!(name: 'Alice', bio: 'First user'),
55
+ b: User.create!(name: 'Bob', bio: 'Second user'),
50
56
  }.freeze
51
57
 
52
58
  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'),
59
+ ax1: USERS[:a].posts.create!(title: 'ax1', body: 'my recipe '),
60
+ ax2: USERS[:a].posts.create!(title: 'ax2', body: 'your recipe'),
61
+ bx1: USERS[:b].posts.create!(title: 'bx1', body: 'her recipe'),
62
+ bx2: USERS[:b].posts.create!(title: 'bx2', body: 'our recipe'),
63
+ ab1: USERS[:a].posts.create!(title: 'ab1', reviewer: USERS[:b], body: 'their recipe'),
58
64
  }.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.2
4
+ version: 0.5.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dimitrij Denissenko
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-07-26 00:00:00.000000000 Z
11
+ date: 2021-02-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -95,7 +95,7 @@ dependencies:
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: rubocop-performance
98
+ name: rubocop-bsm
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - ">="
@@ -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.4'
162
+ version: '2.6'
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.0.3
169
+ rubygems_version: 3.1.4
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