searchable-by 0.5.5 → 0.5.6

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: 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