searchable-by 0.5.5 → 0.6.0
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 +4 -4
- data/.github/workflows/test.yml +21 -0
- data/.rubocop.yml +7 -4
- data/Gemfile.lock +60 -49
- data/LICENSE +1 -1
- data/README.md +8 -3
- data/lib/searchable_by.rb +8 -145
- data/lib/searchable_by/column.rb +66 -0
- data/lib/searchable_by/concern.rb +45 -0
- data/lib/searchable_by/config.rb +32 -0
- data/lib/searchable_by/util.rb +49 -0
- data/searchable-by.gemspec +4 -6
- data/spec/searchable_by/util_spec.rb +36 -0
- data/spec/searchable_by_spec.rb +70 -54
- data/spec/spec_helper.rb +17 -9
- metadata +13 -21
- data/.travis.yml +0 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 615602791390c9de0ab159cbbd0c81c52c0441f61ad559082dad189787c7606f
|
|
4
|
+
data.tar.gz: ad765f8a22cd76196f7a04b7a7e16f38b3bbccacda0f1c7bda659e3ab2783f4f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ae524aa23d507f4339592c4c6a2955d3d0664e7f9829be7071ed1d28a065e1ad77ff9a810a800769cf417db4d315344a487b058530e168325403cb26a1832e12
|
|
7
|
+
data.tar.gz: 71a1349d3738997571306d5326cf81bef66b101bbd03b9451d3b3d9b5851c35f1d732486d96b29d73e1efce792a0c5c840c42442a2f83bd5be171a47be28a17c
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
ruby:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
ruby-version: ["2.6", "2.7", "3.0"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v2
|
|
17
|
+
- uses: ruby/setup-ruby@v1
|
|
18
|
+
with:
|
|
19
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
20
|
+
bundler-cache: true
|
|
21
|
+
- run: bundle exec rake
|
data/.rubocop.yml
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
inherit_gem:
|
|
2
|
+
rubocop-bsm:
|
|
3
|
+
- default.yml
|
|
4
|
+
inherit_mode:
|
|
5
|
+
merge:
|
|
6
|
+
- Exclude
|
|
4
7
|
|
|
5
8
|
AllCops:
|
|
6
|
-
TargetRubyVersion: "2.
|
|
9
|
+
TargetRubyVersion: "2.6"
|
|
7
10
|
|
|
8
11
|
Naming/FileName:
|
|
9
12
|
Exclude:
|
data/Gemfile.lock
CHANGED
|
@@ -1,68 +1,79 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
searchable-by (0.
|
|
4
|
+
searchable-by (0.6.0)
|
|
5
5
|
activerecord
|
|
6
|
-
activesupport
|
|
7
6
|
|
|
8
7
|
GEM
|
|
9
8
|
remote: http://rubygems.org/
|
|
10
9
|
specs:
|
|
11
|
-
activemodel (6.
|
|
12
|
-
activesupport (= 6.
|
|
13
|
-
activerecord (6.
|
|
14
|
-
activemodel (= 6.
|
|
15
|
-
activesupport (= 6.
|
|
16
|
-
activesupport (6.
|
|
10
|
+
activemodel (6.1.4)
|
|
11
|
+
activesupport (= 6.1.4)
|
|
12
|
+
activerecord (6.1.4)
|
|
13
|
+
activemodel (= 6.1.4)
|
|
14
|
+
activesupport (= 6.1.4)
|
|
15
|
+
activesupport (6.1.4)
|
|
17
16
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
18
|
-
i18n (>=
|
|
19
|
-
minitest (
|
|
20
|
-
tzinfo (~>
|
|
21
|
-
zeitwerk (~> 2.
|
|
22
|
-
ast (2.4.
|
|
23
|
-
concurrent-ruby (1.1.
|
|
24
|
-
diff-lcs (1.
|
|
25
|
-
i18n (1.8.
|
|
17
|
+
i18n (>= 1.6, < 2)
|
|
18
|
+
minitest (>= 5.1)
|
|
19
|
+
tzinfo (~> 2.0)
|
|
20
|
+
zeitwerk (~> 2.3)
|
|
21
|
+
ast (2.4.2)
|
|
22
|
+
concurrent-ruby (1.1.9)
|
|
23
|
+
diff-lcs (1.4.4)
|
|
24
|
+
i18n (1.8.10)
|
|
26
25
|
concurrent-ruby (~> 1.0)
|
|
27
|
-
minitest (5.14.
|
|
28
|
-
parallel (1.
|
|
29
|
-
parser (
|
|
30
|
-
ast (~> 2.4.
|
|
26
|
+
minitest (5.14.4)
|
|
27
|
+
parallel (1.20.1)
|
|
28
|
+
parser (3.0.2.0)
|
|
29
|
+
ast (~> 2.4.1)
|
|
31
30
|
rainbow (3.0.0)
|
|
32
|
-
rake (13.0.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
rspec-
|
|
37
|
-
rspec-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
rake (13.0.6)
|
|
32
|
+
regexp_parser (2.1.1)
|
|
33
|
+
rexml (3.2.5)
|
|
34
|
+
rspec (3.10.0)
|
|
35
|
+
rspec-core (~> 3.10.0)
|
|
36
|
+
rspec-expectations (~> 3.10.0)
|
|
37
|
+
rspec-mocks (~> 3.10.0)
|
|
38
|
+
rspec-core (3.10.1)
|
|
39
|
+
rspec-support (~> 3.10.0)
|
|
40
|
+
rspec-expectations (3.10.1)
|
|
41
41
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
42
|
-
rspec-support (~> 3.
|
|
43
|
-
rspec-mocks (3.
|
|
42
|
+
rspec-support (~> 3.10.0)
|
|
43
|
+
rspec-mocks (3.10.2)
|
|
44
44
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
45
|
-
rspec-support (~> 3.
|
|
46
|
-
rspec-support (3.
|
|
47
|
-
rubocop (
|
|
45
|
+
rspec-support (~> 3.10.0)
|
|
46
|
+
rspec-support (3.10.2)
|
|
47
|
+
rubocop (1.18.3)
|
|
48
48
|
parallel (~> 1.10)
|
|
49
|
-
parser (>=
|
|
49
|
+
parser (>= 3.0.0.0)
|
|
50
50
|
rainbow (>= 2.2.2, < 4.0)
|
|
51
|
+
regexp_parser (>= 1.8, < 3.0)
|
|
51
52
|
rexml
|
|
52
|
-
rubocop-ast (>= 0.0
|
|
53
|
+
rubocop-ast (>= 1.7.0, < 2.0)
|
|
53
54
|
ruby-progressbar (~> 1.7)
|
|
54
|
-
unicode-display_width (>= 1.4.0, <
|
|
55
|
-
rubocop-ast (
|
|
56
|
-
parser (>=
|
|
57
|
-
rubocop-
|
|
58
|
-
rubocop (
|
|
59
|
-
|
|
55
|
+
unicode-display_width (>= 1.4.0, < 3.0)
|
|
56
|
+
rubocop-ast (1.7.0)
|
|
57
|
+
parser (>= 3.0.1.1)
|
|
58
|
+
rubocop-bsm (0.6.0)
|
|
59
|
+
rubocop (~> 1.0)
|
|
60
|
+
rubocop-performance
|
|
61
|
+
rubocop-rake
|
|
62
|
+
rubocop-rspec
|
|
63
|
+
rubocop-performance (1.11.4)
|
|
64
|
+
rubocop (>= 1.7.0, < 2.0)
|
|
65
|
+
rubocop-ast (>= 0.4.0)
|
|
66
|
+
rubocop-rake (0.6.0)
|
|
67
|
+
rubocop (~> 1.0)
|
|
68
|
+
rubocop-rspec (2.4.0)
|
|
69
|
+
rubocop (~> 1.0)
|
|
70
|
+
rubocop-ast (>= 1.1.0)
|
|
71
|
+
ruby-progressbar (1.11.0)
|
|
60
72
|
sqlite3 (1.4.2)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
zeitwerk (2.3.0)
|
|
73
|
+
tzinfo (2.0.4)
|
|
74
|
+
concurrent-ruby (~> 1.0)
|
|
75
|
+
unicode-display_width (2.0.0)
|
|
76
|
+
zeitwerk (2.4.2)
|
|
66
77
|
|
|
67
78
|
PLATFORMS
|
|
68
79
|
ruby
|
|
@@ -72,9 +83,9 @@ DEPENDENCIES
|
|
|
72
83
|
rake
|
|
73
84
|
rspec
|
|
74
85
|
rubocop
|
|
75
|
-
rubocop-
|
|
86
|
+
rubocop-bsm
|
|
76
87
|
searchable-by!
|
|
77
88
|
sqlite3
|
|
78
89
|
|
|
79
90
|
BUNDLED WITH
|
|
80
|
-
2.
|
|
91
|
+
2.2.21
|
data/LICENSE
CHANGED
data/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Searchable By
|
|
2
2
|
|
|
3
|
+
[](https://github.com/bsm/searchable-by/actions/workflows/test.yml)
|
|
4
|
+
|
|
3
5
|
ActiveRecord plugin to quickly create search scopes.
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
@@ -13,9 +15,12 @@ class Post < ActiveRecord::Base
|
|
|
13
15
|
belongs_to :author
|
|
14
16
|
|
|
15
17
|
# Limit the number of terms per query to 3.
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
# Ignore search terms shorter than 3 characters (useful for trigram indexes).
|
|
19
|
+
searchable_by max_terms: 3, min_length: 3 do
|
|
20
|
+
# Allow to search strings with custom match type.
|
|
21
|
+
column :title,
|
|
22
|
+
match: :prefix, # Use btree index-friendly prefix match, e.g. `ILIKE 'term%'` instead of default `ILIKE '%term%'`.
|
|
23
|
+
match_phrase: :exact, # For phrases use exact match type, e.g. searching for `"My Post"` will query `WHERE LOWER(title) = 'my post'`.
|
|
19
24
|
|
|
20
25
|
# ... and integers.
|
|
21
26
|
column :id, type: :integer
|
data/lib/searchable_by.rb
CHANGED
|
@@ -1,149 +1,12 @@
|
|
|
1
1
|
require 'active_record'
|
|
2
2
|
|
|
3
|
-
module
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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, :phrase)
|
|
149
10
|
end
|
|
11
|
+
|
|
12
|
+
ActiveRecord::Base.extend SearchableBy::Concern if defined?(::ActiveRecord::Base)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module SearchableBy
|
|
2
|
+
class Column
|
|
3
|
+
attr_reader :attr, :type, :match, :match_phrase, :wildcard
|
|
4
|
+
attr_accessor :node
|
|
5
|
+
|
|
6
|
+
def initialize(attr, type: :string, match: :all, match_phrase: nil, wildcard: nil, **opts) # rubocop:disable Metrics/ParameterLists
|
|
7
|
+
if opts.key?(:min_length)
|
|
8
|
+
ActiveSupport::Deprecation.warn(
|
|
9
|
+
'Setting min_length for individual columns is deprecated and will be removed in the next release.' \
|
|
10
|
+
'Please pass it as an option to searchable_by instead',
|
|
11
|
+
)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
@attr = attr
|
|
15
|
+
@type = type.to_sym
|
|
16
|
+
@match = match
|
|
17
|
+
@match_phrase = match_phrase || match
|
|
18
|
+
@min_length = opts[:min_length].to_i
|
|
19
|
+
@wildcard = wildcard
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build_condition(value)
|
|
23
|
+
scope = node.not_eq(nil)
|
|
24
|
+
|
|
25
|
+
case type
|
|
26
|
+
when :int, :integer
|
|
27
|
+
int_condition(scope, value)
|
|
28
|
+
else
|
|
29
|
+
str_condition(scope, value)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# TODO: remove when removing min_length option from columns
|
|
36
|
+
def usable?(value)
|
|
37
|
+
value.term.length >= @min_length
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def int_condition(scope, value)
|
|
41
|
+
scope.and(node.eq(Integer(value.term)))
|
|
42
|
+
rescue ArgumentError
|
|
43
|
+
nil
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def str_condition(scope, value)
|
|
47
|
+
term = value.term.dup
|
|
48
|
+
type = value.phrase ? match_phrase : match
|
|
49
|
+
|
|
50
|
+
case type
|
|
51
|
+
when :exact
|
|
52
|
+
term.downcase!
|
|
53
|
+
scope.and(node.lower.eq(term))
|
|
54
|
+
when :prefix
|
|
55
|
+
term.gsub!('%', '\%')
|
|
56
|
+
term.gsub!('_', '\_')
|
|
57
|
+
scope.and(node.matches("#{term}%"))
|
|
58
|
+
else
|
|
59
|
+
term.gsub!('%', '\%')
|
|
60
|
+
term.gsub!('_', '\_')
|
|
61
|
+
term.gsub!(wildcard, '%') if wildcard
|
|
62
|
+
scope.and(node.matches("%#{term}%"))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
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, min_length: 0, **options, &block)
|
|
15
|
+
_searchable_by_config.instance_eval(&block)
|
|
16
|
+
_searchable_by_config.max_terms = max_terms if max_terms
|
|
17
|
+
_searchable_by_config.min_length = min_length
|
|
18
|
+
_searchable_by_config.options.update(options) unless options.empty?
|
|
19
|
+
_searchable_by_config
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @param [String] query the search query
|
|
23
|
+
# @return [ActiveRecord::Relation] the scoped relation
|
|
24
|
+
def search_by(query)
|
|
25
|
+
config = _searchable_by_config
|
|
26
|
+
columns = config.columns
|
|
27
|
+
return all if columns.empty?
|
|
28
|
+
|
|
29
|
+
values = Util.norm_values(query, min_length: config.min_length).first(config.max_terms)
|
|
30
|
+
return all if values.empty?
|
|
31
|
+
|
|
32
|
+
columns.each do |col|
|
|
33
|
+
col.node ||= col.attr.is_a?(Proc) ? col.attr.call : arel_table[col.attr]
|
|
34
|
+
end
|
|
35
|
+
clauses = Util.build_clauses(columns, values)
|
|
36
|
+
return all if clauses.empty?
|
|
37
|
+
|
|
38
|
+
scope = instance_exec(&config.scoping)
|
|
39
|
+
clauses.each do |clause|
|
|
40
|
+
scope = scope.where(clause)
|
|
41
|
+
end
|
|
42
|
+
scope
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module SearchableBy
|
|
2
|
+
class Config
|
|
3
|
+
attr_reader :columns, :scoping, :options
|
|
4
|
+
attr_accessor :max_terms, :min_length
|
|
5
|
+
|
|
6
|
+
def initialize
|
|
7
|
+
@columns = []
|
|
8
|
+
@max_terms = 5
|
|
9
|
+
@min_length = 0
|
|
10
|
+
@options = {}
|
|
11
|
+
scope { all }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize_copy(other)
|
|
15
|
+
@columns = other.columns.dup
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def column(*attrs, &block)
|
|
20
|
+
opts = attrs.extract_options!
|
|
21
|
+
attrs.each do |attr|
|
|
22
|
+
columns.push Column.new(attr, **@options, **opts)
|
|
23
|
+
end
|
|
24
|
+
columns.push Column.new(block, **@options, **opts) if block
|
|
25
|
+
columns
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def scope(&block)
|
|
29
|
+
@scoping = block
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module SearchableBy
|
|
2
|
+
module Util
|
|
3
|
+
def self.norm_values(query, min_length: 0)
|
|
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? || term.length < min_length
|
|
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? || term.length < min_length
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
values.uniq!
|
|
27
|
+
values
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.build_clauses(columns, values)
|
|
31
|
+
values.map do |value|
|
|
32
|
+
# TODO: remove when removing min_length option from columns
|
|
33
|
+
usable = columns.all? do |column|
|
|
34
|
+
column.send(:usable?, value)
|
|
35
|
+
end
|
|
36
|
+
next unless usable
|
|
37
|
+
|
|
38
|
+
group = columns.map do |column|
|
|
39
|
+
column.build_condition(value)
|
|
40
|
+
end.tap(&:compact!)
|
|
41
|
+
next if group.empty?
|
|
42
|
+
|
|
43
|
+
clause = group.inject(&:or)
|
|
44
|
+
clause = clause.not if value.negate
|
|
45
|
+
clause
|
|
46
|
+
end.tap(&:compact!)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/searchable-by.gemspec
CHANGED
|
@@ -1,25 +1,23 @@
|
|
|
1
1
|
Gem::Specification.new do |s|
|
|
2
2
|
s.name = 'searchable-by'
|
|
3
|
-
s.version = '0.
|
|
3
|
+
s.version = '0.6.0'
|
|
4
4
|
s.authors = ['Dimitrij Denissenko']
|
|
5
5
|
s.email = ['dimitrij@blacksquaremedia.com']
|
|
6
6
|
s.summary = 'Generate search scopes'
|
|
7
7
|
s.description = 'ActiveRecord plugin'
|
|
8
8
|
s.homepage = 'https://github.com/bsm/sortable-by'
|
|
9
|
-
s.license = '
|
|
9
|
+
s.license = 'Apache-2.0'
|
|
10
10
|
|
|
11
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.
|
|
14
|
+
s.required_ruby_version = '>= 2.6'
|
|
15
15
|
|
|
16
16
|
s.add_dependency 'activerecord'
|
|
17
|
-
s.add_dependency 'activesupport'
|
|
18
|
-
|
|
19
17
|
s.add_development_dependency 'bundler'
|
|
20
18
|
s.add_development_dependency 'rake'
|
|
21
19
|
s.add_development_dependency 'rspec'
|
|
22
20
|
s.add_development_dependency 'rubocop'
|
|
23
|
-
s.add_development_dependency 'rubocop-
|
|
21
|
+
s.add_development_dependency 'rubocop-bsm'
|
|
24
22
|
s.add_development_dependency 'sqlite3'
|
|
25
23
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe SearchableBy::Util do
|
|
4
|
+
context 'with norm_values' do
|
|
5
|
+
def norm(str, **opts)
|
|
6
|
+
described_class.norm_values(str, **opts).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
|
+
expect(norm('with min length', min_length: 4)).to eq('length' => false, 'with' => false)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/spec/searchable_by_spec.rb
CHANGED
|
@@ -1,80 +1,96 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
2
|
|
|
3
|
-
describe
|
|
4
|
-
|
|
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
|
-
|
|
36
|
-
it 'should ignore bad inputs' do
|
|
3
|
+
describe SearchableBy do
|
|
4
|
+
it 'ignores bad inputs' do
|
|
37
5
|
expect(Post.search_by(nil).count).to eq(5)
|
|
38
6
|
expect(Post.search_by('').count).to eq(5)
|
|
39
7
|
end
|
|
40
8
|
|
|
41
|
-
it '
|
|
9
|
+
it 'configures correctly' do
|
|
42
10
|
expect(AbstractModel._searchable_by_config.columns.size).to eq(1)
|
|
43
11
|
expect(Post._searchable_by_config.columns.size).to eq(5)
|
|
44
12
|
end
|
|
45
13
|
|
|
46
|
-
it '
|
|
14
|
+
it 'generates SQL' do
|
|
47
15
|
sql = Post.search_by('123').to_sql
|
|
48
|
-
expect(sql).to include(%("posts"."id" = 123))
|
|
49
|
-
expect(sql).to include(%("posts"."title" LIKE '
|
|
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'))
|
|
50
20
|
|
|
51
21
|
sql = Post.search_by('foo%bar').to_sql
|
|
52
22
|
expect(sql).not_to include(%("posts"."id"))
|
|
53
|
-
expect(sql).to include(%("posts"."title" LIKE '
|
|
23
|
+
expect(sql).to include(%("posts"."title" LIKE 'foo\\%bar%'))
|
|
24
|
+
expect(sql).to include(%("posts"."body" LIKE '%foo\\%bar%'))
|
|
25
|
+
|
|
26
|
+
sql = User.search_by('uni*dom').to_sql
|
|
27
|
+
expect(sql).to include(%("users"."country" LIKE '%uni%dom%'))
|
|
28
|
+
|
|
29
|
+
sql = User.search_by('"uni * dom"').to_sql
|
|
30
|
+
expect(sql).to include(%("users"."country" LIKE '%uni % dom%'))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'searches' do
|
|
34
|
+
expect(Post.search_by('ALICE').pluck(:title)).to match_array(%w[ax1 ax2 ab1])
|
|
35
|
+
expect(Post.search_by('bOb').pluck(:title)).to match_array(%w[bx1 bx2 ab1])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'searches across multiple words' do
|
|
39
|
+
expect(Post.search_by('ALICE your').pluck(:title)).to match_array(%w[ax2])
|
|
54
40
|
end
|
|
55
41
|
|
|
56
|
-
it '
|
|
57
|
-
expect(Post.search_by('
|
|
58
|
-
expect(Post.search_by('
|
|
42
|
+
it 'supports search markers' do
|
|
43
|
+
expect(Post.search_by('aLiCe -your').pluck(:title)).to match_array(%w[ax1 ab1])
|
|
44
|
+
expect(Post.search_by('+alice "your recipe"').pluck(:title)).to match_array(%w[ax2])
|
|
45
|
+
expect(Post.search_by('bob -"her recipe"').pluck(:title)).to match_array(%w[bx2 ab1])
|
|
46
|
+
expect(Post.search_by('bob +"her recipe"').pluck(:title)).to match_array(%w[bx1])
|
|
59
47
|
end
|
|
60
48
|
|
|
61
|
-
it '
|
|
62
|
-
|
|
49
|
+
it 'respects match options' do
|
|
50
|
+
# name uses match: :exact
|
|
51
|
+
expect(Post.search_by('alice').pluck(:title)).to match_array(%w[ax1 ax2 ab1])
|
|
52
|
+
expect(Post.search_by('ali').pluck(:title)).to be_empty
|
|
53
|
+
expect(Post.search_by('lice').pluck(:title)).to be_empty
|
|
54
|
+
expect(Post.search_by('li').pluck(:title)).to be_empty
|
|
55
|
+
|
|
56
|
+
# title uses match: :prefix
|
|
57
|
+
expect(Post.search_by('ax').pluck(:title)).to match_array(%w[ax1 ax2])
|
|
58
|
+
expect(Post.search_by('bx').pluck(:title)).to match_array(%w[bx1 bx2])
|
|
59
|
+
expect(Post.search_by('ab').pluck(:title)).to match_array(%w[ab1])
|
|
60
|
+
expect(Post.search_by('ba').pluck(:title)).to be_empty
|
|
61
|
+
|
|
62
|
+
# title uses match_phrase: :exact
|
|
63
|
+
expect(Post.search_by('"ab"').pluck(:title)).to be_empty
|
|
64
|
+
expect(Post.search_by('"ab1"').pluck(:title)).to match_array(%w[ab1])
|
|
65
|
+
|
|
66
|
+
# body uses match: :all (default)
|
|
67
|
+
expect(Post.search_by('recip').pluck(:title)).to match_array(%w[ax1 ax2 bx1 bx2 ab1])
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'supports min term length in context' do
|
|
71
|
+
# values are discarded - too short for bio
|
|
72
|
+
expect(User.search_by('+be')).to match_array(USERS.values_at(:a, :b))
|
|
73
|
+
expect(User.search_by('is')).to match_array(USERS.values_at(:a, :b))
|
|
74
|
+
|
|
75
|
+
# value is used to scope
|
|
76
|
+
expect(User.search_by('ear')).to match_array(USERS.values_at(:a))
|
|
77
|
+
|
|
78
|
+
# one used, one discarded
|
|
79
|
+
expect(User.search_by('beard is')).to match_array(USERS.values_at(:a))
|
|
63
80
|
end
|
|
64
81
|
|
|
65
|
-
it '
|
|
66
|
-
expect(Post.search_by('
|
|
67
|
-
expect(Post.search_by('
|
|
68
|
-
expect(Post.search_by('bob -"her recipe"').pluck(:title)).to match_array(%w[b2 ab])
|
|
69
|
-
expect(Post.search_by('bob +"her recipe"').pluck(:title)).to match_array(%w[b1])
|
|
82
|
+
it 'searches within scopes' do
|
|
83
|
+
expect(Post.where(title: 'ax1').search_by('ALICE').pluck(:title)).to match_array(%w[ax1])
|
|
84
|
+
expect(Post.where(title: 'ax1').search_by('bOb').pluck(:title)).to be_empty
|
|
70
85
|
end
|
|
71
86
|
|
|
72
|
-
it '
|
|
73
|
-
expect(Post.
|
|
74
|
-
expect(Post.where(title: 'a1').search_by('bOb').pluck(:title)).to match_array(%w[])
|
|
87
|
+
it 'searches integers' do
|
|
88
|
+
expect(Post.search_by(POSTS[:ab1].id.to_s).count).to eq(1)
|
|
75
89
|
end
|
|
76
90
|
|
|
77
|
-
it '
|
|
78
|
-
expect(
|
|
91
|
+
it 'supports wildcard searching' do
|
|
92
|
+
expect(User.search_by('uni*dom')).to match_array(USERS.values_at(:a))
|
|
93
|
+
expect(User.search_by('uni*o')).to match_array(USERS.values_at(:a, :b))
|
|
94
|
+
expect(User.search_by('uni*of*dom')).to be_empty
|
|
79
95
|
end
|
|
80
96
|
end
|
data/spec/spec_helper.rb
CHANGED
|
@@ -8,6 +8,8 @@ ActiveRecord::Base.establish_connection :test
|
|
|
8
8
|
ActiveRecord::Base.connection.instance_eval do
|
|
9
9
|
create_table :users do |t|
|
|
10
10
|
t.string :name
|
|
11
|
+
t.string :bio
|
|
12
|
+
t.string :country
|
|
11
13
|
end
|
|
12
14
|
create_table :posts do |t|
|
|
13
15
|
t.integer :author_id, null: false
|
|
@@ -27,6 +29,11 @@ end
|
|
|
27
29
|
|
|
28
30
|
class User < AbstractModel
|
|
29
31
|
has_many :posts, foreign_key: :author_id
|
|
32
|
+
|
|
33
|
+
searchable_by min_length: 3 do
|
|
34
|
+
column :bio
|
|
35
|
+
column :country, wildcard: '*'
|
|
36
|
+
end
|
|
30
37
|
end
|
|
31
38
|
|
|
32
39
|
class Post < AbstractModel
|
|
@@ -34,8 +41,9 @@ class Post < AbstractModel
|
|
|
34
41
|
belongs_to :reviewer, class_name: 'User'
|
|
35
42
|
|
|
36
43
|
searchable_by do
|
|
37
|
-
column :title, :
|
|
38
|
-
column
|
|
44
|
+
column :title, match: :prefix, match_phrase: :exact
|
|
45
|
+
column :body
|
|
46
|
+
column proc { User.arel_table[:name] }, match: :exact
|
|
39
47
|
column { User.arel_table.alias('reviewers_posts')[:name] }
|
|
40
48
|
|
|
41
49
|
scope do
|
|
@@ -45,14 +53,14 @@ class Post < AbstractModel
|
|
|
45
53
|
end
|
|
46
54
|
|
|
47
55
|
USERS = {
|
|
48
|
-
a: User.create!(name: 'Alice'),
|
|
49
|
-
b: User.create!(name: 'Bob'),
|
|
56
|
+
a: User.create!(name: 'Alice', bio: "Chuck Norris' beard is immutable.", country: 'United Kingdom'),
|
|
57
|
+
b: User.create!(name: 'Bob', bio: 'Chuck Norris can divide by zero.', country: 'United States of America'),
|
|
50
58
|
}.freeze
|
|
51
59
|
|
|
52
60
|
POSTS = {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
ax1: USERS[:a].posts.create!(title: 'ax1', body: 'my recipe '),
|
|
62
|
+
ax2: USERS[:a].posts.create!(title: 'ax2', body: 'your recipe'),
|
|
63
|
+
bx1: USERS[:b].posts.create!(title: 'bx1', body: 'her recipe'),
|
|
64
|
+
bx2: USERS[:b].posts.create!(title: 'bx2', body: 'our recipe'),
|
|
65
|
+
ab1: USERS[:a].posts.create!(title: 'ab1', reviewer: USERS[:b], body: 'their recipe'),
|
|
58
66
|
}.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.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dimitrij Denissenko
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2021-07-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activerecord
|
|
@@ -24,20 +24,6 @@ dependencies:
|
|
|
24
24
|
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '0'
|
|
27
|
-
- !ruby/object:Gem::Dependency
|
|
28
|
-
name: activesupport
|
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
|
30
|
-
requirements:
|
|
31
|
-
- - ">="
|
|
32
|
-
- !ruby/object:Gem::Version
|
|
33
|
-
version: '0'
|
|
34
|
-
type: :runtime
|
|
35
|
-
prerelease: false
|
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
-
requirements:
|
|
38
|
-
- - ">="
|
|
39
|
-
- !ruby/object:Gem::Version
|
|
40
|
-
version: '0'
|
|
41
27
|
- !ruby/object:Gem::Dependency
|
|
42
28
|
name: bundler
|
|
43
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -95,7 +81,7 @@ dependencies:
|
|
|
95
81
|
- !ruby/object:Gem::Version
|
|
96
82
|
version: '0'
|
|
97
83
|
- !ruby/object:Gem::Dependency
|
|
98
|
-
name: rubocop-
|
|
84
|
+
name: rubocop-bsm
|
|
99
85
|
requirement: !ruby/object:Gem::Requirement
|
|
100
86
|
requirements:
|
|
101
87
|
- - ">="
|
|
@@ -129,9 +115,9 @@ executables: []
|
|
|
129
115
|
extensions: []
|
|
130
116
|
extra_rdoc_files: []
|
|
131
117
|
files:
|
|
118
|
+
- ".github/workflows/test.yml"
|
|
132
119
|
- ".gitignore"
|
|
133
120
|
- ".rubocop.yml"
|
|
134
|
-
- ".travis.yml"
|
|
135
121
|
- Gemfile
|
|
136
122
|
- Gemfile.lock
|
|
137
123
|
- LICENSE
|
|
@@ -139,12 +125,17 @@ files:
|
|
|
139
125
|
- Rakefile
|
|
140
126
|
- lib/searchable-by.rb
|
|
141
127
|
- lib/searchable_by.rb
|
|
128
|
+
- lib/searchable_by/column.rb
|
|
129
|
+
- lib/searchable_by/concern.rb
|
|
130
|
+
- lib/searchable_by/config.rb
|
|
131
|
+
- lib/searchable_by/util.rb
|
|
142
132
|
- searchable-by.gemspec
|
|
133
|
+
- spec/searchable_by/util_spec.rb
|
|
143
134
|
- spec/searchable_by_spec.rb
|
|
144
135
|
- spec/spec_helper.rb
|
|
145
136
|
homepage: https://github.com/bsm/sortable-by
|
|
146
137
|
licenses:
|
|
147
|
-
-
|
|
138
|
+
- Apache-2.0
|
|
148
139
|
metadata: {}
|
|
149
140
|
post_install_message:
|
|
150
141
|
rdoc_options: []
|
|
@@ -154,17 +145,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
154
145
|
requirements:
|
|
155
146
|
- - ">="
|
|
156
147
|
- !ruby/object:Gem::Version
|
|
157
|
-
version: '2.
|
|
148
|
+
version: '2.6'
|
|
158
149
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
159
150
|
requirements:
|
|
160
151
|
- - ">="
|
|
161
152
|
- !ruby/object:Gem::Version
|
|
162
153
|
version: '0'
|
|
163
154
|
requirements: []
|
|
164
|
-
rubygems_version: 3.
|
|
155
|
+
rubygems_version: 3.2.15
|
|
165
156
|
signing_key:
|
|
166
157
|
specification_version: 4
|
|
167
158
|
summary: Generate search scopes
|
|
168
159
|
test_files:
|
|
160
|
+
- spec/searchable_by/util_spec.rb
|
|
169
161
|
- spec/searchable_by_spec.rb
|
|
170
162
|
- spec/spec_helper.rb
|