searchable-by 0.5.1 → 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: 8c1c5cfdea465a6a9ff73159a062558c5475889d27e0119e81212bb8bb37bbc1
4
- data.tar.gz: c77c158c890b37b67d1243c93b4d5c982731b55fb48d284ecc404de58142da26
3
+ metadata.gz: 2137729d80e65107cadf27ede26a60887b6888c41f6940caea9828e3416f137f
4
+ data.tar.gz: 6edd61ef3f32e0fa6e882b07f5d24004a5c7a4cd5ec2f9c99ba2e6e12332f5e2
5
5
  SHA512:
6
- metadata.gz: 1ba79022ae7056fc1cd6f6ef7af109ed6e0a7228601241ac2bc8f501818b6f487a6fd929f77bc3ce0dba9956b879def3dc705159c3696246d4653e3197f3d71f
7
- data.tar.gz: c023dff5eb1bb4685dcfe7d10b7164242a090a930189ccc1b336c6de3437ab05d1de0dc90d13f52412e8e15b16a0a1386d5dacb289b208e00eefca5eab0cc2f7
6
+ metadata.gz: 704a8beb8c791dc20e6d156f73a3c3b21f6aed0df0d94d0cc7dda78440d9051f4a9f9a3d076fa3afa5af54a4312e9251335d2ab2911c228506c23d877ef67789
7
+ data.tar.gz: '0906f2424988065b3d22a177ab260d42029ad3e531a8b5e051fc7170d87e87784fa18225eba4f6631416af7a0c9a3df5c1b3e464babdb8d56ace5f226b18ee39'
@@ -1,10 +1,14 @@
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
 
5
9
  AllCops:
6
- TargetRubyVersion: "2.4"
10
+ TargetRubyVersion: "2.5"
7
11
 
8
12
  Naming/FileName:
9
13
  Exclude:
10
- - lib/searchable-by.rb
14
+ - lib/searchable-by.rb
@@ -2,7 +2,6 @@ language: ruby
2
2
  rvm:
3
3
  - 2.6
4
4
  - 2.5
5
- - 2.4
6
5
  cache: bundler
7
6
  before_install:
8
7
  - gem install bundler
@@ -1,65 +1,75 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- searchable-by (0.5.1)
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 (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.1)
23
+ concurrent-ruby (1.1.7)
24
+ diff-lcs (1.4.4)
25
+ i18n (1.8.7)
27
26
  concurrent-ruby (~> 1.0)
28
- jaro_winkler (1.5.2)
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)
33
31
  rainbow (3.0.0)
34
- rake (12.3.2)
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.0)
40
- rspec-support (~> 3.8.0)
41
- rspec-expectations (3.8.3)
32
+ rake (13.0.3)
33
+ regexp_parser (2.0.3)
34
+ rexml (3.2.4)
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.8.0)
44
- rspec-mocks (3.8.0)
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.8.0)
47
- rspec-support (3.8.0)
48
- rubocop (0.68.1)
49
- jaro_winkler (~> 1.5.1)
46
+ rspec-support (~> 3.10.0)
47
+ rspec-support (3.10.1)
48
+ rubocop (1.8.1)
50
49
  parallel (~> 1.10)
51
- parser (>= 2.5, != 2.5.1.1)
50
+ parser (>= 3.0.0.0)
52
51
  rainbow (>= 2.2.2, < 4.0)
52
+ regexp_parser (>= 1.8, < 3.0)
53
+ rexml
54
+ rubocop-ast (>= 1.2.0, < 2.0)
53
55
  ruby-progressbar (~> 1.7)
54
- unicode-display_width (>= 1.4.0, < 1.6)
55
- rubocop-performance (1.2.0)
56
- rubocop (>= 0.68.0)
57
- ruby-progressbar (1.10.0)
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.5.0)
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)
68
+ sqlite3 (1.4.2)
69
+ tzinfo (2.0.4)
70
+ concurrent-ruby (~> 1.0)
71
+ unicode-display_width (2.0.0)
72
+ zeitwerk (2.4.2)
63
73
 
64
74
  PLATFORMS
65
75
  ruby
@@ -70,8 +80,10 @@ DEPENDENCIES
70
80
  rspec
71
81
  rubocop
72
82
  rubocop-performance
83
+ rubocop-rake
84
+ rubocop-rspec
73
85
  searchable-by!
74
86
  sqlite3
75
87
 
76
88
  BUNDLED WITH
77
- 2.0.1
89
+ 2.1.4
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,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.
18
- column :title
17
+ # Allow to search strings with custom match type.
18
+ # Use btree index-friendly prefix match, e.g. `ILIKE 'term%'` instead of default `ILIKE '%term%'`.
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
19
21
 
20
22
  # ... and integers.
21
23
  column :id, type: :integer
@@ -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.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.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,51 @@
1
+ module SearchableBy
2
+ class Column
3
+ attr_reader :attr, :type, :match, :match_phrase
4
+ attr_accessor :node
5
+
6
+ def initialize(attr, type: :string, match: :all, match_phrase: nil)
7
+ @attr = attr
8
+ @type = type.to_sym
9
+ @match = match
10
+ @match_phrase = match_phrase || match
11
+ end
12
+
13
+ def build_condition(value)
14
+ scope = node.not_eq(nil)
15
+
16
+ case type
17
+ when :int, :integer
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}%"))
44
+ else
45
+ term.gsub!('%', '\%')
46
+ term.gsub!('_', '\_')
47
+ scope.and(node.matches("%#{term}%"))
48
+ end
49
+ end
50
+ end
51
+ 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.1'
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'
@@ -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.5'
15
15
 
16
16
  s.add_dependency 'activerecord'
17
17
  s.add_dependency 'activesupport'
@@ -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
@@ -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,48 +1,72 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe ActiveRecord::SearchableBy do
4
-
5
- it 'should ignore bad inputs' do
6
- expect(Post.search_by(nil).count).to eq(4)
7
- expect(Post.search_by('').count).to eq(4)
3
+ describe SearchableBy do
4
+ it 'ignores bad inputs' do
5
+ expect(Post.search_by(nil).count).to eq(5)
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
- expect(Post._searchable_by_config.columns.size).to eq(4)
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])
23
30
  end
24
31
 
25
- it 'should search' do
26
- expect(Post.search_by('ALICE').pluck(:title)).to match_array(%w[titla title])
27
- expect(Post.search_by('bOb').pluck(:title)).to match_array(%w[titlo titlu])
32
+ it 'searches across multiple words' do
33
+ expect(Post.search_by('ALICE your').pluck(:title)).to match_array(%w[ax2])
28
34
  end
29
35
 
30
- it 'should search across multiple words' do
31
- expect(Post.search_by('ALICE title').pluck(:title)).to match_array(%w[title])
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])
32
41
  end
33
42
 
34
- it 'should support search markers' do
35
- expect(Post.search_by('aLiCe -title').pluck(:title)).to match_array(%w[titla])
36
- expect(Post.search_by('+alice "pie recipe"').pluck(:title)).to match_array(%w[title])
37
- expect(Post.search_by('bob -"piu recipe"').pluck(:title)).to match_array(%w[titlo])
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])
38
62
  end
39
63
 
40
- it 'should search within scopes' do
41
- expect(Post.where(title: 'title').search_by('ALICE').pluck(:title)).to match_array(%w[title])
42
- expect(Post.where(title: 'title').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
43
67
  end
44
68
 
45
- it 'should search integers' do
46
- expect(Post.search_by(POSTS[:alice1].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)
47
71
  end
48
72
  end
@@ -2,15 +2,16 @@ 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
- create_table :authors do |t|
9
+ create_table :users do |t|
10
10
  t.string :name
11
11
  end
12
12
  create_table :posts do |t|
13
13
  t.integer :author_id, null: false
14
+ t.integer :reviewer_id
14
15
  t.string :title
15
16
  t.text :body
16
17
  end
@@ -24,31 +25,35 @@ class AbstractModel < ActiveRecord::Base
24
25
  end
25
26
  end
26
27
 
27
- class Author < AbstractModel
28
- has_many :posts
28
+ class User < AbstractModel
29
+ has_many :posts, foreign_key: :author_id
29
30
  end
30
31
 
31
32
  class Post < AbstractModel
32
- belongs_to :author
33
+ belongs_to :author, class_name: 'User'
34
+ belongs_to :reviewer, class_name: 'User'
33
35
 
34
36
  searchable_by do
35
- column :title, :body
36
- column { Author.arel_table[:name] }
37
+ column :title, match: :prefix, match_phrase: :exact
38
+ column :body
39
+ column proc { User.arel_table[:name] }, match: :exact
40
+ column { User.arel_table.alias('reviewers_posts')[:name] }
37
41
 
38
42
  scope do
39
- joins(:author)
43
+ joins(:author).left_outer_joins(:reviewer)
40
44
  end
41
45
  end
42
46
  end
43
47
 
44
- AUTHORS = {
45
- alice: Author.create!(name: 'Alice'),
46
- bob: Author.create!(name: 'Bob'),
48
+ USERS = {
49
+ a: User.create!(name: 'Alice'),
50
+ b: User.create!(name: 'Bob'),
47
51
  }.freeze
48
52
 
49
53
  POSTS = {
50
- alice1: AUTHORS[:alice].posts.create!(title: 'titla', body: 'my pia recipe '),
51
- alice2: AUTHORS[:alice].posts.create!(title: 'title', body: 'your pie recipe'),
52
- bob1: AUTHORS[:bob].posts.create!(title: 'titlo', body: 'her pio recipe'),
53
- bob2: AUTHORS[:bob].posts.create!(title: 'titlu', body: 'our piu 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'),
54
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.1
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: 2019-05-10 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
@@ -139,7 +167,12 @@ files:
139
167
  - Rakefile
140
168
  - lib/searchable-by.rb
141
169
  - lib/searchable_by.rb
170
+ - lib/searchable_by/column.rb
171
+ - lib/searchable_by/concern.rb
172
+ - lib/searchable_by/config.rb
173
+ - lib/searchable_by/util.rb
142
174
  - searchable-by.gemspec
175
+ - spec/searchable_by/util_spec.rb
143
176
  - spec/searchable_by_spec.rb
144
177
  - spec/spec_helper.rb
145
178
  homepage: https://github.com/bsm/sortable-by
@@ -154,17 +187,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
154
187
  requirements:
155
188
  - - ">="
156
189
  - !ruby/object:Gem::Version
157
- version: '2.4'
190
+ version: '2.5'
158
191
  required_rubygems_version: !ruby/object:Gem::Requirement
159
192
  requirements:
160
193
  - - ">="
161
194
  - !ruby/object:Gem::Version
162
195
  version: '0'
163
196
  requirements: []
164
- rubygems_version: 3.0.3
197
+ rubygems_version: 3.1.4
165
198
  signing_key:
166
199
  specification_version: 4
167
200
  summary: Generate search scopes
168
201
  test_files:
202
+ - spec/searchable_by/util_spec.rb
169
203
  - spec/searchable_by_spec.rb
170
204
  - spec/spec_helper.rb