searchable-by 0.5.1 → 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: 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