searchable-by 0.5.4 → 0.5.9

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: 412ef1f5428799d4fa13cd47f5fb61ff0f1f0ac2b24aa57080a78fe822cb3163
4
- data.tar.gz: eb464947796e011504c2b56d4f23d870b74569991d63e8bb56e1f3c6cde5803c
3
+ metadata.gz: 8b6f1b26859c89dfec031b51501c3f1dd6e8050d27bf7ae00bdb0ef9b8495422
4
+ data.tar.gz: a24dc8497c85648498fccffd56aaa4f995d7c31b48ab21e970356115a6a1ec50
5
5
  SHA512:
6
- metadata.gz: 3bd7cf0127afe48ffde887f891237e8875db6c84e78ed77d95c800cd77c7c6a34626b81965f4661be34d73f13200c9a1bfb73caf7e3f0aeeb3441f13e641a96f
7
- data.tar.gz: ae8f9671375e16ec060ac6952abad79c57f232011f799086322d3e409ce202076c94e546a2712b1b22401b0045832662438595e3b8e91129aa4504c8ff0500ff
6
+ metadata.gz: 33f4d29187d4bf9545af72632b1c17742be5ceeb3b699d36dc6e0f251151dabb06877269378d17f72dcb152f4413265866b9e076c537f5f0e1cf3f12689d3492
7
+ data.tar.gz: 6cba46e2c5ee6bf164490d5f7e2b7c715ae19dcc51d6eafbc3e93283e4d02e887e926eadc821753cf4b25522b5adb4322ac37a89d678beb76c33b7fd79fb193a
@@ -0,0 +1,21 @@
1
+ name: Ruby
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ ruby-version: ["2.6", "2.7", "3.0"]
15
+ steps:
16
+ - uses: actions/checkout@v2
17
+ - uses: ruby/setup-ruby@v1
18
+ with:
19
+ ruby-version: ${{ matrix.ruby-version }}
20
+ bundler-cache: true
21
+ - run: bundle exec rake
data/.rubocop.yml CHANGED
@@ -1,9 +1,12 @@
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.5"
9
+ TargetRubyVersion: "2.6"
7
10
 
8
11
  Naming/FileName:
9
12
  Exclude:
data/Gemfile.lock CHANGED
@@ -1,65 +1,86 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- searchable-by (0.5.4)
4
+ searchable-by (0.5.9)
5
5
  activerecord
6
6
  activesupport
7
7
 
8
8
  GEM
9
9
  remote: http://rubygems.org/
10
10
  specs:
11
- activemodel (6.0.1)
12
- activesupport (= 6.0.1)
13
- activerecord (6.0.1)
14
- activemodel (= 6.0.1)
15
- activesupport (= 6.0.1)
16
- activesupport (6.0.1)
11
+ activemodel (6.1.3)
12
+ activesupport (= 6.1.3)
13
+ activerecord (6.1.3)
14
+ activemodel (= 6.1.3)
15
+ activesupport (= 6.1.3)
16
+ activesupport (6.1.3)
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)
22
- ast (2.4.0)
23
- concurrent-ruby (1.1.5)
24
- diff-lcs (1.3)
25
- i18n (1.7.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.9)
26
26
  concurrent-ruby (~> 1.0)
27
- jaro_winkler (1.5.4)
28
- minitest (5.13.0)
29
- parallel (1.18.0)
30
- parser (2.6.5.0)
31
- ast (~> 2.4.0)
27
+ minitest (5.14.4)
28
+ parallel (1.20.1)
29
+ parser (3.0.0.0)
30
+ ast (~> 2.4.1)
31
+ rack (2.2.3)
32
32
  rainbow (3.0.0)
33
- rake (13.0.1)
34
- rspec (3.9.0)
35
- rspec-core (~> 3.9.0)
36
- rspec-expectations (~> 3.9.0)
37
- rspec-mocks (~> 3.9.0)
38
- rspec-core (3.9.0)
39
- rspec-support (~> 3.9.0)
40
- rspec-expectations (3.9.0)
33
+ rake (13.0.3)
34
+ regexp_parser (2.1.1)
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)
41
43
  diff-lcs (>= 1.2.0, < 2.0)
42
- rspec-support (~> 3.9.0)
43
- rspec-mocks (3.9.0)
44
+ rspec-support (~> 3.10.0)
45
+ rspec-mocks (3.10.2)
44
46
  diff-lcs (>= 1.2.0, < 2.0)
45
- rspec-support (~> 3.9.0)
46
- rspec-support (3.9.0)
47
- rubocop (0.76.0)
48
- jaro_winkler (~> 1.5.1)
47
+ rspec-support (~> 3.10.0)
48
+ rspec-support (3.10.2)
49
+ rubocop (1.11.0)
49
50
  parallel (~> 1.10)
50
- parser (>= 2.6)
51
+ parser (>= 3.0.0.0)
51
52
  rainbow (>= 2.2.2, < 4.0)
53
+ regexp_parser (>= 1.8, < 3.0)
54
+ rexml
55
+ rubocop-ast (>= 1.2.0, < 2.0)
52
56
  ruby-progressbar (~> 1.7)
53
- unicode-display_width (>= 1.4.0, < 1.7)
54
- rubocop-performance (1.5.0)
55
- rubocop (>= 0.71.0)
56
- ruby-progressbar (1.10.1)
57
- sqlite3 (1.4.1)
58
- thread_safe (0.3.6)
59
- tzinfo (1.2.5)
60
- thread_safe (~> 0.1)
61
- unicode-display_width (1.6.0)
62
- zeitwerk (2.2.1)
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.10.1)
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.2
98
+ 2.2.5
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,149 +1,12 @@
1
1
  require 'active_record'
2
2
 
3
- module ActiveRecord
4
- module SearchableBy
5
- class Column
6
- attr_reader :attr, :type
7
- 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'
8
8
 
9
- def initialize(attr, type: :string)
10
- @attr = attr
11
- @type = type.to_sym
12
- end
13
- end
14
-
15
- class Config
16
- attr_reader :columns, :scoping
17
- attr_accessor :max_terms
18
-
19
- def initialize
20
- @columns = []
21
- @max_terms = 5
22
- scope { all }
23
- end
24
-
25
- def initialize_copy(other)
26
- @columns = other.columns.dup
27
- super
28
- end
29
-
30
- def column(*attrs, &block)
31
- opts = attrs.extract_options!
32
- attrs.each do |attr|
33
- columns.push Column.new(attr, opts)
34
- end
35
- columns.push Column.new(block, opts) if block
36
- columns
37
- end
38
-
39
- def scope(&block)
40
- @scoping = block
41
- end
42
- end
43
-
44
- Value = Struct.new(:term, :negate)
45
-
46
- def self.norm_values(query)
47
- values = []
48
- query = query.to_s.dup
49
-
50
- # capture any terms inside double quotes
51
- # exclude from seach if preceded by '-'
52
- query.gsub!(/([\-\+]?)"+([^"]*)"+/) do |_|
53
- term = Regexp.last_match(2)
54
- negate = Regexp.last_match(1) == '-'
55
-
56
- values.push Value.new(term, negate) unless term.blank?
57
- ''
58
- end
59
-
60
- # for the remaining terms remove sign if precedes
61
- # exclude term from search if sign preceding is '-'
62
- query.split(' ').each do |term|
63
- negate = term[0] == '-'
64
- term.slice!(0) if negate || term[0] == '+'
65
-
66
- values.push Value.new(term, negate) unless term.blank?
67
- end
68
-
69
- values.uniq!
70
- values
71
- end
72
-
73
- def self.build_clauses(columns, values)
74
- clauses = values.map do |value|
75
- grouping = columns.map do |column|
76
- build_condition(column, value)
77
- end
78
- grouping.compact!
79
- next if grouping.empty?
80
-
81
- clause = grouping.inject(&:or)
82
- clause = clause.not if value.negate
83
- clause
84
- end
85
- clauses.compact!
86
- clauses
87
- end
88
-
89
- def self.build_condition(column, value)
90
- case column.type
91
- when :int, :integer
92
- begin
93
- column.node.not_eq(nil).and(column.node.eq(Integer(value.term)))
94
- rescue ArgumentError
95
- nil
96
- end
97
- else
98
- term = value.term.dup
99
- term.gsub!('%', '\%')
100
- term.gsub!('_', '\_')
101
- column.node.not_eq(nil).and(column.node.matches("%#{term}%"))
102
- end
103
- end
104
-
105
- module ClassMethods
106
- def self.extended(base) # :nodoc:
107
- base.class_attribute :_searchable_by_config, instance_accessor: false, instance_predicate: false
108
- base._searchable_by_config = Config.new
109
- super
110
- end
111
-
112
- def inherited(base) # :nodoc:
113
- base._searchable_by_config = _searchable_by_config.dup
114
- super
115
- end
116
-
117
- def searchable_by(max_terms: 5, &block)
118
- _searchable_by_config.instance_eval(&block)
119
- _searchable_by_config.max_terms = max_terms if max_terms
120
- end
121
-
122
- # @param [String] query the search query
123
- # @return [ActiveRecord::Relation] the scoped relation
124
- def search_by(query)
125
- columns = _searchable_by_config.columns
126
- return all if columns.empty?
127
-
128
- values = SearchableBy.norm_values(query).first(_searchable_by_config.max_terms)
129
- return all if values.empty?
130
-
131
- columns.each do |col|
132
- col.node ||= col.attr.is_a?(Proc) ? col.attr.call : arel_table[col.attr]
133
- end
134
- clauses = SearchableBy.build_clauses(columns, values)
135
- return all if clauses.empty?
136
-
137
- scope = instance_exec(&_searchable_by_config.scoping)
138
- clauses.each do |clause|
139
- scope = scope.where(clause)
140
- end
141
- scope
142
- end
143
- end
144
- end
145
-
146
- class Base
147
- extend SearchableBy::ClassMethods
148
- end
9
+ Value = Struct.new(:term, :negate, :phrase)
149
10
  end
11
+
12
+ ActiveRecord::Base.extend SearchableBy::Concern if defined?(::ActiveRecord::Base)
@@ -0,0 +1,56 @@
1
+ module SearchableBy
2
+ class Column
3
+ attr_reader :attr, :type, :match, :match_phrase, :min_length, :wildcard
4
+ attr_accessor :node
5
+
6
+ def initialize(attr, type: :string, match: :all, match_phrase: nil, min_length: 0, wildcard: nil) # rubocop:disable Metrics/ParameterLists
7
+ @attr = attr
8
+ @type = type.to_sym
9
+ @match = match
10
+ @match_phrase = match_phrase || match
11
+ @min_length = min_length
12
+ @wildcard = wildcard
13
+ end
14
+
15
+ def build_condition(value)
16
+ return Arel::Nodes::False.new if value.term.length < min_length # no-match
17
+
18
+ scope = node.not_eq(nil)
19
+
20
+ case type
21
+ when :int, :integer
22
+ int_condition(scope, value)
23
+ else
24
+ str_condition(scope, value)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def int_condition(scope, value)
31
+ scope.and(node.eq(Integer(value.term)))
32
+ rescue ArgumentError
33
+ nil
34
+ end
35
+
36
+ def str_condition(scope, value)
37
+ term = value.term.dup
38
+ type = value.phrase ? match_phrase : match
39
+
40
+ case type
41
+ when :exact
42
+ term.downcase!
43
+ scope.and(node.lower.eq(term))
44
+ when :prefix
45
+ term.gsub!('%', '\%')
46
+ term.gsub!('_', '\_')
47
+ scope.and(node.matches("#{term}%"))
48
+ else
49
+ term.gsub!('%', '\%')
50
+ term.gsub!('_', '\_')
51
+ term.gsub!(wildcard, '%') if wildcard
52
+ scope.and(node.matches("%#{term}%"))
53
+ end
54
+ end
55
+ end
56
+ 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,17 +1,17 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'searchable-by'
3
- s.version = '0.5.4'
3
+ s.version = '0.5.9'
4
4
  s.authors = ['Dimitrij Denissenko']
5
5
  s.email = ['dimitrij@blacksquaremedia.com']
6
6
  s.summary = 'Generate search scopes'
7
7
  s.description = 'ActiveRecord plugin'
8
8
  s.homepage = 'https://github.com/bsm/sortable-by'
9
- s.license = 'MIT'
9
+ s.license = 'Apache-2.0'
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.5'
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,80 +1,89 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe ActiveRecord::SearchableBy 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
-
36
- it 'should ignore bad inputs' do
3
+ describe SearchableBy do
4
+ it 'ignores bad inputs' do
37
5
  expect(Post.search_by(nil).count).to eq(5)
38
6
  expect(Post.search_by('').count).to eq(5)
39
7
  end
40
8
 
41
- it 'should configure correctly' do
9
+ it 'configures correctly' do
42
10
  expect(AbstractModel._searchable_by_config.columns.size).to eq(1)
43
11
  expect(Post._searchable_by_config.columns.size).to eq(5)
44
12
  end
45
13
 
46
- it 'should generate SQL' do
14
+ it 'generates SQL' do
47
15
  sql = Post.search_by('123').to_sql
48
- expect(sql).to include(%("posts"."id" = 123))
49
- 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'))
50
20
 
51
21
  sql = Post.search_by('foo%bar').to_sql
52
22
  expect(sql).not_to include(%("posts"."id"))
53
- 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
+
26
+ sql = User.search_by('uni*dom').to_sql
27
+ expect(sql).to include(%("users"."country" LIKE '%uni%dom%'))
28
+
29
+ sql = User.search_by('"uni * dom"').to_sql
30
+ expect(sql).to include(%("users"."country" LIKE '%uni % dom%'))
54
31
  end
55
32
 
56
- it 'should search' do
57
- expect(Post.search_by('ALICE').pluck(:title)).to match_array(%w[a1 a2 ab])
58
- expect(Post.search_by('bOb').pluck(:title)).to match_array(%w[b1 b2 ab])
33
+ it 'searches' do
34
+ expect(Post.search_by('ALICE').pluck(:title)).to match_array(%w[ax1 ax2 ab1])
35
+ expect(Post.search_by('bOb').pluck(:title)).to match_array(%w[bx1 bx2 ab1])
36
+ end
37
+
38
+ it 'searches across multiple words' do
39
+ expect(Post.search_by('ALICE your').pluck(:title)).to match_array(%w[ax2])
40
+ end
41
+
42
+ it 'supports search markers' do
43
+ expect(Post.search_by('aLiCe -your').pluck(:title)).to match_array(%w[ax1 ab1])
44
+ expect(Post.search_by('+alice "your recipe"').pluck(:title)).to match_array(%w[ax2])
45
+ expect(Post.search_by('bob -"her recipe"').pluck(:title)).to match_array(%w[bx2 ab1])
46
+ expect(Post.search_by('bob +"her recipe"').pluck(:title)).to match_array(%w[bx1])
47
+ end
48
+
49
+ it 'respects match options' do
50
+ # name uses match: :exact
51
+ expect(Post.search_by('alice').pluck(:title)).to match_array(%w[ax1 ax2 ab1])
52
+ expect(Post.search_by('ali').pluck(:title)).to be_empty
53
+ expect(Post.search_by('lice').pluck(:title)).to be_empty
54
+ expect(Post.search_by('li').pluck(:title)).to be_empty
55
+
56
+ # title uses match: :prefix
57
+ expect(Post.search_by('ax').pluck(:title)).to match_array(%w[ax1 ax2])
58
+ expect(Post.search_by('bx').pluck(:title)).to match_array(%w[bx1 bx2])
59
+ expect(Post.search_by('ab').pluck(:title)).to match_array(%w[ab1])
60
+ expect(Post.search_by('ba').pluck(:title)).to be_empty
61
+
62
+ # title uses match_phrase: :exact
63
+ expect(Post.search_by('"ab"').pluck(:title)).to be_empty
64
+ expect(Post.search_by('"ab1"').pluck(:title)).to match_array(%w[ab1])
65
+
66
+ # body uses match: :all (default)
67
+ expect(Post.search_by('recip').pluck(:title)).to match_array(%w[ax1 ax2 bx1 bx2 ab1])
59
68
  end
60
69
 
61
- it 'should search across multiple words' do
62
- expect(Post.search_by('ALICE your').pluck(:title)).to match_array(%w[a2])
70
+ it 'supports min term length' do
71
+ expect(User.search_by('+ir')).to be_empty
72
+ expect(User.search_by('irs')).to match_array([USERS[:a]])
63
73
  end
64
74
 
65
- it 'should support search markers' do
66
- expect(Post.search_by('aLiCe -your').pluck(:title)).to match_array(%w[a1 ab])
67
- expect(Post.search_by('+alice "your recipe"').pluck(:title)).to match_array(%w[a2])
68
- expect(Post.search_by('bob -"her recipe"').pluck(:title)).to match_array(%w[b2 ab])
69
- expect(Post.search_by('bob +"her recipe"').pluck(:title)).to match_array(%w[b1])
75
+ it 'searches within scopes' do
76
+ expect(Post.where(title: 'ax1').search_by('ALICE').pluck(:title)).to match_array(%w[ax1])
77
+ expect(Post.where(title: 'ax1').search_by('bOb').pluck(:title)).to be_empty
70
78
  end
71
79
 
72
- it 'should search within scopes' do
73
- expect(Post.where(title: 'a1').search_by('ALICE').pluck(:title)).to match_array(%w[a1])
74
- expect(Post.where(title: 'a1').search_by('bOb').pluck(:title)).to match_array(%w[])
80
+ it 'searches integers' do
81
+ expect(Post.search_by(POSTS[:ab1].id.to_s).count).to eq(1)
75
82
  end
76
83
 
77
- it 'should search integers' do
78
- expect(Post.search_by(POSTS[:ab].id.to_s).count).to eq(1)
84
+ it 'supports wildcard searching' do
85
+ expect(User.search_by('uni*dom')).to match_array([USERS[:a]])
86
+ expect(User.search_by('uni*o')).to match_array([USERS[:a], USERS[:b]])
87
+ expect(User.search_by('uni*of*dom')).to be_empty
79
88
  end
80
89
  end
data/spec/spec_helper.rb CHANGED
@@ -8,6 +8,8 @@ ActiveRecord::Base.establish_connection :test
8
8
  ActiveRecord::Base.connection.instance_eval do
9
9
  create_table :users do |t|
10
10
  t.string :name
11
+ t.string :bio
12
+ t.string :country
11
13
  end
12
14
  create_table :posts do |t|
13
15
  t.integer :author_id, null: false
@@ -27,6 +29,11 @@ end
27
29
 
28
30
  class User < AbstractModel
29
31
  has_many :posts, foreign_key: :author_id
32
+
33
+ searchable_by do
34
+ column :bio, min_length: 3
35
+ column :country, wildcard: '*'
36
+ end
30
37
  end
31
38
 
32
39
  class Post < AbstractModel
@@ -34,8 +41,9 @@ class Post < AbstractModel
34
41
  belongs_to :reviewer, class_name: 'User'
35
42
 
36
43
  searchable_by do
37
- column :title, :body
38
- column { User.arel_table[:name] }
44
+ column :title, match: :prefix, match_phrase: :exact
45
+ column :body
46
+ column proc { User.arel_table[:name] }, match: :exact
39
47
  column { User.arel_table.alias('reviewers_posts')[:name] }
40
48
 
41
49
  scope do
@@ -45,14 +53,14 @@ class Post < AbstractModel
45
53
  end
46
54
 
47
55
  USERS = {
48
- a: User.create!(name: 'Alice'),
49
- b: User.create!(name: 'Bob'),
56
+ a: User.create!(name: 'Alice', bio: 'First user', country: 'United Kingdom'),
57
+ b: User.create!(name: 'Bob', bio: 'Second user', country: 'United States of America'),
50
58
  }.freeze
51
59
 
52
60
  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'),
61
+ ax1: USERS[:a].posts.create!(title: 'ax1', body: 'my recipe '),
62
+ ax2: USERS[:a].posts.create!(title: 'ax2', body: 'your recipe'),
63
+ bx1: USERS[:b].posts.create!(title: 'bx1', body: 'her recipe'),
64
+ bx2: USERS[:b].posts.create!(title: 'bx2', body: 'our recipe'),
65
+ ab1: USERS[:a].posts.create!(title: 'ab1', reviewer: USERS[:b], body: 'their recipe'),
58
66
  }.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
4
+ version: 0.5.9
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-11-13 00:00:00.000000000 Z
11
+ date: 2021-04-27 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
  - - ">="
@@ -129,9 +129,9 @@ executables: []
129
129
  extensions: []
130
130
  extra_rdoc_files: []
131
131
  files:
132
+ - ".github/workflows/ruby.yml"
132
133
  - ".gitignore"
133
134
  - ".rubocop.yml"
134
- - ".travis.yml"
135
135
  - Gemfile
136
136
  - Gemfile.lock
137
137
  - LICENSE
@@ -139,12 +139,17 @@ 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
146
151
  licenses:
147
- - MIT
152
+ - Apache-2.0
148
153
  metadata: {}
149
154
  post_install_message:
150
155
  rdoc_options: []
@@ -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.5'
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.6
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
data/.travis.yml DELETED
@@ -1,7 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.6
4
- - 2.5
5
- cache: bundler
6
- before_install:
7
- - gem install bundler