searchable-by 0.5.0 → 0.5.6

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: '008edf75bb0e4b27410f7e889cbfd26eda27d0391dd7e70be3e984a962ddd756'
4
- data.tar.gz: 9b2fbbc4cc13b2cfe6e23998616ec48e49617bfb67db3143f800c5d35134b2df
3
+ metadata.gz: 2e2f1b8d0d05abf263dc93c1bd81ab818a88eedf135ff10cd5414d54d3ae17a1
4
+ data.tar.gz: 04d1e040390dcf8c77adbc1bf5bd491505373147798b1c95e4e4cc6c259daab5
5
5
  SHA512:
6
- metadata.gz: 0fa58611207ecef62a12c698be927aa34659e0b19f5c5a4ce6c8c2c87b77df49349fb72a4356683f406a80110727442e854448f4b1dc20bc04527e1981af03a2
7
- data.tar.gz: b9dbbde8350310301567ed1ba6cf8a45d80813e7f99415a6e0a6dc6d1a69b4dbd0f340a39851b8db4e291b503f21c2f4f9e8d74311cdaa5fe705a3a06a9201e1
6
+ metadata.gz: e6bcafc69897951760eba1a9fb049664aa3360536525d31e6f5b1d9094ce274cc82c6936aa69e19b8ece975e1f74d42f424646d5a6e4de4bc4c12b29b24a4b33
7
+ data.tar.gz: a5762fcbf915002a781e6d1996cae4df2ce80fc62d0948c0cb5965c97b07d0d5a9030cb7acf360bba4482a4f05026f1e082254f5f545a8fa5ba1bdb946ef520e
@@ -3,8 +3,8 @@ inherit_from:
3
3
  - https://gitlab.com/bsm/misc/raw/master/rubocop/default.yml
4
4
 
5
5
  AllCops:
6
- TargetRubyVersion: "2.4"
6
+ TargetRubyVersion: "2.5"
7
7
 
8
8
  Naming/FileName:
9
9
  Exclude:
10
- - lib/searchable-by.rb
10
+ - 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,70 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- searchable-by (0.5.0)
4
+ searchable-by (0.5.6)
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.0.3.3)
12
+ activesupport (= 6.0.3.3)
13
+ activerecord (6.0.3.3)
14
+ activemodel (= 6.0.3.3)
15
+ activesupport (= 6.0.3.3)
16
+ activesupport (6.0.3.3)
18
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
19
18
  i18n (>= 0.7, < 2)
20
19
  minitest (~> 5.1)
21
20
  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)
21
+ zeitwerk (~> 2.2, >= 2.2.2)
22
+ ast (2.4.1)
23
+ concurrent-ruby (1.1.7)
24
+ diff-lcs (1.4.4)
25
+ i18n (1.8.5)
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.2)
28
+ parallel (1.19.2)
29
+ parser (2.7.1.4)
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.1)
33
+ regexp_parser (1.7.1)
34
+ rexml (3.2.4)
35
+ rspec (3.9.0)
36
+ rspec-core (~> 3.9.0)
37
+ rspec-expectations (~> 3.9.0)
38
+ rspec-mocks (~> 3.9.0)
39
+ rspec-core (3.9.2)
40
+ rspec-support (~> 3.9.3)
41
+ rspec-expectations (3.9.2)
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.9.0)
44
+ rspec-mocks (3.9.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.9.0)
47
+ rspec-support (3.9.3)
48
+ rubocop (0.91.0)
50
49
  parallel (~> 1.10)
51
- parser (>= 2.5, != 2.5.1.1)
50
+ parser (>= 2.7.1.1)
52
51
  rainbow (>= 2.2.2, < 4.0)
52
+ regexp_parser (>= 1.7)
53
+ rexml
54
+ rubocop-ast (>= 0.4.0, < 1.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)
56
+ unicode-display_width (>= 1.4.0, < 2.0)
57
+ rubocop-ast (0.4.1)
58
+ parser (>= 2.7.1.4)
59
+ rubocop-performance (1.8.0)
60
+ rubocop (>= 0.87.0)
61
+ ruby-progressbar (1.10.1)
62
+ sqlite3 (1.4.2)
59
63
  thread_safe (0.3.6)
60
- tzinfo (1.2.5)
64
+ tzinfo (1.2.7)
61
65
  thread_safe (~> 0.1)
62
- unicode-display_width (1.5.0)
66
+ unicode-display_width (1.7.0)
67
+ zeitwerk (2.4.0)
63
68
 
64
69
  PLATFORMS
65
70
  ruby
@@ -74,4 +79,4 @@ DEPENDENCIES
74
79
  sqlite3
75
80
 
76
81
  BUNDLED WITH
77
- 2.0.1
82
+ 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
@@ -15,7 +15,8 @@ class Post < ActiveRecord::Base
15
15
  # Limit the number of terms per query to 3.
16
16
  searchable_by max_terms: 3 do
17
17
  # Allow to search strings.
18
- column :title
18
+ # Use btree index-friendly prefix match, e.g. `ILIKE 'term%'` instead of default `ILIKE '%term%'`.
19
+ column :title, match: :prefix
19
20
 
20
21
  # ... and integers.
21
22
  column :id, type: :integer
@@ -1,111 +1,12 @@
1
1
  require 'active_record'
2
- require 'shellwords'
3
2
 
4
- module ActiveRecord
5
- module SearchableBy
6
- class Config < Hash
7
- def initialize
8
- update columns: [], max_terms: 5
9
- scope { all }
10
- end
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'
11
8
 
12
- def column(*attrs, &block)
13
- opts = attrs.extract_options!
14
- cols = self[:columns]
15
- attrs.each do |attr|
16
- cols.push(opts.merge(column: attr))
17
- end
18
- cols.push(opts.merge(column: block)) if block
19
- cols
20
- end
21
-
22
- def scope(&block)
23
- self[:scope] = block
24
- end
25
- end
26
-
27
- def self.norm_values(query)
28
- values = Shellwords.split(query.to_s)
29
- values.flatten!
30
- values.reject!(&:blank?)
31
- values.uniq!
32
- values
33
- end
34
-
35
- def self.build_clauses(relations, values)
36
- clauses = values.map do |value|
37
- negate = value[0] == '-'
38
- value.slice!(0) if negate || value[0] == '+'
39
-
40
- c0, *cn = relations.map do |opts|
41
- build_condition(opts, value)
42
- end.compact
43
- next unless c0
44
-
45
- [cn.inject(c0) {|x, part| x.or(part) }, negate]
46
- end
47
- clauses.compact!
48
- clauses
49
- end
50
-
51
- def self.build_condition(opts, value)
52
- case opts[:type]
53
- when :int, :integer
54
- begin
55
- opts[:rel].eq(Integer(value))
56
- rescue ArgumentError
57
- nil
58
- end
59
- else
60
- value = value.dup
61
- value.gsub!('%', '\%')
62
- value.gsub!('_', '\_')
63
- opts[:rel].matches("%#{value}%")
64
- end
65
- end
66
-
67
- module ClassMethods
68
- def self.extended(base) # :nodoc:
69
- base.class_attribute :_searchable_by_config, instance_accessor: false, instance_predicate: false
70
- base._searchable_by_config = Config.new
71
- super
72
- end
73
-
74
- def inherited(base) # :nodoc:
75
- base._searchable_by_config = _searchable_by_config.deep_dup
76
- super
77
- end
78
-
79
- def searchable_by(max_terms: 5, &block)
80
- _searchable_by_config.instance_eval(&block)
81
- _searchable_by_config[:max_terms] = max_terms if max_terms
82
- end
83
-
84
- # @param [String] query the search query
85
- # @return [ActiveRecord::Relation] the scoped relation
86
- def search_by(query)
87
- columns = _searchable_by_config[:columns]
88
- return all if columns.empty?
89
-
90
- values = SearchableBy.norm_values(query).first(_searchable_by_config[:max_terms])
91
- return all if values.empty?
92
-
93
- relations = columns.map do |opts|
94
- rel = opts[:column].is_a?(Proc) ? opts[:column].call : arel_table[opts[:column]]
95
- opts.merge(rel: rel)
96
- end
97
- clauses = SearchableBy.build_clauses(relations, values)
98
- return all if clauses.empty?
99
-
100
- scope = instance_exec(&_searchable_by_config[:scope])
101
- clauses.inject(scope) do |x, (clause, negate)|
102
- negate ? x.where.not(clause) : x.where(clause)
103
- end
104
- end
105
- end
106
- end
107
-
108
- class Base
109
- extend SearchableBy::ClassMethods
110
- end
9
+ Value = Struct.new(:term, :negate)
111
10
  end
11
+
12
+ ActiveRecord::Base.extend SearchableBy::Concern if defined?(::ActiveRecord::Base)
@@ -0,0 +1,34 @@
1
+ module SearchableBy
2
+ class Column
3
+ attr_reader :attr, :type, :match
4
+ attr_accessor :node
5
+
6
+ def initialize(attr, type: :string, match: :all)
7
+ @attr = attr
8
+ @type = type.to_sym
9
+ @match = match
10
+ end
11
+
12
+ def build_condition(value)
13
+ case type
14
+ when :int, :integer
15
+ begin
16
+ node.not_eq(nil).and(node.eq(Integer(value.term)))
17
+ rescue ArgumentError
18
+ nil
19
+ end
20
+ else
21
+ term = value.term.dup
22
+ term.gsub!('%', '\%')
23
+ term.gsub!('_', '\_')
24
+ case match
25
+ when :prefix
26
+ term << '%'
27
+ else
28
+ term = "%#{term}%"
29
+ end
30
+ node.not_eq(nil).and(node.matches(term))
31
+ end
32
+ end
33
+ end
34
+ 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 terms 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) 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) 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.0'
3
+ s.version = '0.5.6'
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'
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe SearchableBy::Util 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
+ end
@@ -1,9 +1,14 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe ActiveRecord::SearchableBy do
3
+ describe SearchableBy do
4
4
  it 'should ignore bad inputs' do
5
- expect(Post.search_by(nil).count).to eq(4)
6
- expect(Post.search_by('').count).to eq(4)
5
+ expect(Post.search_by(nil).count).to eq(5)
6
+ expect(Post.search_by('').count).to eq(5)
7
+ end
8
+
9
+ it 'should configure correctly' do
10
+ expect(AbstractModel._searchable_by_config.columns.size).to eq(1)
11
+ expect(Post._searchable_by_config.columns.size).to eq(5)
7
12
  end
8
13
 
9
14
  it 'should generate SQL' do
@@ -17,26 +22,38 @@ describe ActiveRecord::SearchableBy do
17
22
  end
18
23
 
19
24
  it 'should search' do
20
- expect(Post.search_by('ALICE').pluck(:title)).to match_array(%w[titla title])
21
- expect(Post.search_by('bOb').pluck(:title)).to match_array(%w[titlo titlu])
25
+ expect(Post.search_by('ALICE').pluck(:title)).to match_array(%w[a1 a2 ab])
26
+ expect(Post.search_by('bOb').pluck(:title)).to match_array(%w[b1 b2 ab])
22
27
  end
23
28
 
24
29
  it 'should search across multiple words' do
25
- expect(Post.search_by('ALICE title').pluck(:title)).to match_array(%w[title])
30
+ expect(Post.search_by('ALICE your').pluck(:title)).to match_array(%w[a2])
26
31
  end
27
32
 
28
33
  it 'should support search markers' do
29
- expect(Post.search_by('aLiCe -title').pluck(:title)).to match_array(%w[titla])
30
- expect(Post.search_by('+alice "pie recipe"').pluck(:title)).to match_array(%w[title])
31
- expect(Post.search_by('bob -"piu recipe"').pluck(:title)).to match_array(%w[titlo])
34
+ expect(Post.search_by('aLiCe -your').pluck(:title)).to match_array(%w[a1 ab])
35
+ expect(Post.search_by('+alice "your recipe"').pluck(:title)).to match_array(%w[a2])
36
+ expect(Post.search_by('bob -"her recipe"').pluck(:title)).to match_array(%w[b2 ab])
37
+ expect(Post.search_by('bob +"her recipe"').pluck(:title)).to match_array(%w[b1])
38
+ end
39
+
40
+ it 'should respect match options' do
41
+ # name uses match: :prefix
42
+ expect(Post.search_by('alice').pluck(:title)).to match_array(%w[a1 a2 ab])
43
+ expect(Post.search_by('ali').pluck(:title)).to match_array(%w[a1 a2 ab])
44
+ expect(Post.search_by('lice').pluck(:title)).to be_empty
45
+ expect(Post.search_by('li').pluck(:title)).to be_empty
46
+
47
+ # title uses match: :all (default)
48
+ expect(Post.search_by('recip').pluck(:title)).to match_array(%w[a1 a2 b1 b2 ab])
32
49
  end
33
50
 
34
51
  it 'should search within scopes' do
35
- expect(Post.where(title: 'title').search_by('ALICE').pluck(:title)).to match_array(%w[title])
36
- expect(Post.where(title: 'title').search_by('bOb').pluck(:title)).to match_array(%w[])
52
+ expect(Post.where(title: 'a1').search_by('ALICE').pluck(:title)).to match_array(%w[a1])
53
+ expect(Post.where(title: 'a1').search_by('bOb').pluck(:title)).to match_array(%w[])
37
54
  end
38
55
 
39
56
  it 'should search integers' do
40
- expect(Post.search_by(POSTS[:alice1].id.to_s).count).to eq(1)
57
+ expect(Post.search_by(POSTS[:ab].id.to_s).count).to eq(1)
41
58
  end
42
59
  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,34 @@ 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
37
  column :title, :body
36
- column { Author.arel_table[:name] }
38
+ column proc { User.arel_table[:name] }, match: :prefix
39
+ column { User.arel_table.alias('reviewers_posts')[:name] }
37
40
 
38
41
  scope do
39
- joins(:author)
42
+ joins(:author).left_outer_joins(:reviewer)
40
43
  end
41
44
  end
42
45
  end
43
46
 
44
- AUTHORS = {
45
- alice: Author.create!(name: 'Alice'),
46
- bob: Author.create!(name: 'Bob'),
47
+ USERS = {
48
+ a: User.create!(name: 'Alice'),
49
+ b: User.create!(name: 'Bob'),
47
50
  }.freeze
48
51
 
49
52
  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'),
53
+ a1: USERS[:a].posts.create!(title: 'a1', body: 'my recipe '),
54
+ a2: USERS[:a].posts.create!(title: 'a2', body: 'your recipe'),
55
+ b1: USERS[:b].posts.create!(title: 'b1', body: 'her recipe'),
56
+ b2: USERS[:b].posts.create!(title: 'b2', body: 'our recipe'),
57
+ ab: USERS[:a].posts.create!(title: 'ab', reviewer: USERS[:b], body: 'their recipe'),
54
58
  }.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.0
4
+ version: 0.5.6
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: 2020-09-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -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.5'
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.2
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