searchable-by 0.5.5 → 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: 4b2c3092a9a334ae2ea2256953e9a1fa3198d6b6aa8d8f5fe7eb841920782dce
4
- data.tar.gz: 6e6ba3ee470d96063a91e24c3fdcbe2be411a13a567828b57c39d74dc1a8133a
3
+ metadata.gz: 2e2f1b8d0d05abf263dc93c1bd81ab818a88eedf135ff10cd5414d54d3ae17a1
4
+ data.tar.gz: 04d1e040390dcf8c77adbc1bf5bd491505373147798b1c95e4e4cc6c259daab5
5
5
  SHA512:
6
- metadata.gz: 52a694bb37c67c2f4a8905e41d0affa729f1ddf0a595428278af25f8d97e986fe6bb5aa8cb462932dee1258032f4f3adb8b2f4f1c804f3c049482b7ef64493b0
7
- data.tar.gz: ea1c883f1f9636c0baf7575918bf89330b22503efc132a65084a49a6e9ff63c96313efcc69259d4e26175b8f75744a65b8f6c39bfb130a9e4532a189aa19ed3f
6
+ metadata.gz: e6bcafc69897951760eba1a9fb049664aa3360536525d31e6f5b1d9094ce274cc82c6936aa69e19b8ece975e1f74d42f424646d5a6e4de4bc4c12b29b24a4b33
7
+ data.tar.gz: a5762fcbf915002a781e6d1996cae4df2ce80fc62d0948c0cb5965c97b07d0d5a9030cb7acf360bba4482a4f05026f1e082254f5f545a8fa5ba1bdb946ef520e
@@ -1,35 +1,36 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- searchable-by (0.5.5)
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 (6.0.3.1)
12
- activesupport (= 6.0.3.1)
13
- activerecord (6.0.3.1)
14
- activemodel (= 6.0.3.1)
15
- activesupport (= 6.0.3.1)
16
- activesupport (6.0.3.1)
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)
17
17
  concurrent-ruby (~> 1.0, >= 1.0.2)
18
18
  i18n (>= 0.7, < 2)
19
19
  minitest (~> 5.1)
20
20
  tzinfo (~> 1.1)
21
21
  zeitwerk (~> 2.2, >= 2.2.2)
22
- ast (2.4.0)
23
- concurrent-ruby (1.1.6)
24
- diff-lcs (1.3)
25
- i18n (1.8.2)
22
+ ast (2.4.1)
23
+ concurrent-ruby (1.1.7)
24
+ diff-lcs (1.4.4)
25
+ i18n (1.8.5)
26
26
  concurrent-ruby (~> 1.0)
27
- minitest (5.14.1)
28
- parallel (1.19.1)
29
- parser (2.7.1.3)
30
- 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)
31
31
  rainbow (3.0.0)
32
32
  rake (13.0.1)
33
+ regexp_parser (1.7.1)
33
34
  rexml (3.2.4)
34
35
  rspec (3.9.0)
35
36
  rspec-core (~> 3.9.0)
@@ -44,25 +45,26 @@ GEM
44
45
  diff-lcs (>= 1.2.0, < 2.0)
45
46
  rspec-support (~> 3.9.0)
46
47
  rspec-support (3.9.3)
47
- rubocop (0.84.0)
48
+ rubocop (0.91.0)
48
49
  parallel (~> 1.10)
49
- parser (>= 2.7.0.1)
50
+ parser (>= 2.7.1.1)
50
51
  rainbow (>= 2.2.2, < 4.0)
52
+ regexp_parser (>= 1.7)
51
53
  rexml
52
- rubocop-ast (>= 0.0.3)
54
+ rubocop-ast (>= 0.4.0, < 1.0)
53
55
  ruby-progressbar (~> 1.7)
54
56
  unicode-display_width (>= 1.4.0, < 2.0)
55
- rubocop-ast (0.0.3)
56
- parser (>= 2.7.0.1)
57
- rubocop-performance (1.6.0)
58
- rubocop (>= 0.71.0)
57
+ rubocop-ast (0.4.1)
58
+ parser (>= 2.7.1.4)
59
+ rubocop-performance (1.8.0)
60
+ rubocop (>= 0.87.0)
59
61
  ruby-progressbar (1.10.1)
60
62
  sqlite3 (1.4.2)
61
63
  thread_safe (0.3.6)
62
64
  tzinfo (1.2.7)
63
65
  thread_safe (~> 0.1)
64
66
  unicode-display_width (1.7.0)
65
- zeitwerk (2.3.0)
67
+ zeitwerk (2.4.0)
66
68
 
67
69
  PLATFORMS
68
70
  ruby
@@ -77,4 +79,4 @@ DEPENDENCIES
77
79
  sqlite3
78
80
 
79
81
  BUNDLED WITH
80
- 2.1.2
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,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)
149
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.5'
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'
@@ -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,38 +1,6 @@
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
-
3
+ describe SearchableBy do
36
4
  it 'should ignore bad inputs' do
37
5
  expect(Post.search_by(nil).count).to eq(5)
38
6
  expect(Post.search_by('').count).to eq(5)
@@ -69,6 +37,17 @@ describe ActiveRecord::SearchableBy do
69
37
  expect(Post.search_by('bob +"her recipe"').pluck(:title)).to match_array(%w[b1])
70
38
  end
71
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])
49
+ end
50
+
72
51
  it 'should search within scopes' do
73
52
  expect(Post.where(title: 'a1').search_by('ALICE').pluck(:title)).to match_array(%w[a1])
74
53
  expect(Post.where(title: 'a1').search_by('bOb').pluck(:title)).to match_array(%w[])
@@ -35,7 +35,7 @@ class Post < AbstractModel
35
35
 
36
36
  searchable_by do
37
37
  column :title, :body
38
- column { User.arel_table[:name] }
38
+ column proc { User.arel_table[:name] }, match: :prefix
39
39
  column { User.arel_table.alias('reviewers_posts')[:name] }
40
40
 
41
41
  scope do
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.5
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: 2020-05-27 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
@@ -166,5 +171,6 @@ 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