searchable-by 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '008edf75bb0e4b27410f7e889cbfd26eda27d0391dd7e70be3e984a962ddd756'
4
+ data.tar.gz: 9b2fbbc4cc13b2cfe6e23998616ec48e49617bfb67db3143f800c5d35134b2df
5
+ SHA512:
6
+ metadata.gz: 0fa58611207ecef62a12c698be927aa34659e0b19f5c5a4ce6c8c2c87b77df49349fb72a4356683f406a80110727442e854448f4b1dc20bc04527e1981af03a2
7
+ data.tar.gz: b9dbbde8350310301567ed1ba6cf8a45d80813e7f99415a6e0a6dc6d1a69b4dbd0f340a39851b8db4e291b503f21c2f4f9e8d74311cdaa5fe705a3a06a9201e1
@@ -0,0 +1,4 @@
1
+ spec/tmp
2
+ pkg
3
+ *.gem
4
+ .rubocop-*
@@ -0,0 +1,10 @@
1
+ require: rubocop-performance
2
+ inherit_from:
3
+ - https://gitlab.com/bsm/misc/raw/master/rubocop/default.yml
4
+
5
+ AllCops:
6
+ TargetRubyVersion: "2.4"
7
+
8
+ Naming/FileName:
9
+ Exclude:
10
+ - lib/searchable-by.rb
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.6
4
+ - 2.5
5
+ - 2.4
6
+ cache: bundler
7
+ before_install:
8
+ - gem install bundler
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'http://rubygems.org'
2
+ gemspec
@@ -0,0 +1,77 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ searchable-by (0.5.0)
5
+ activerecord
6
+ activesupport
7
+
8
+ GEM
9
+ remote: http://rubygems.org/
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)
18
+ concurrent-ruby (~> 1.0, >= 1.0.2)
19
+ i18n (>= 0.7, < 2)
20
+ minitest (~> 5.1)
21
+ tzinfo (~> 1.1)
22
+ arel (9.0.0)
23
+ ast (2.4.0)
24
+ concurrent-ruby (1.1.5)
25
+ diff-lcs (1.3)
26
+ i18n (1.6.0)
27
+ 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)
33
+ 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)
42
+ diff-lcs (>= 1.2.0, < 2.0)
43
+ rspec-support (~> 3.8.0)
44
+ rspec-mocks (3.8.0)
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)
50
+ parallel (~> 1.10)
51
+ parser (>= 2.5, != 2.5.1.1)
52
+ rainbow (>= 2.2.2, < 4.0)
53
+ ruby-progressbar (~> 1.7)
54
+ unicode-display_width (>= 1.4.0, < 1.6)
55
+ rubocop-performance (1.2.0)
56
+ rubocop (>= 0.68.0)
57
+ ruby-progressbar (1.10.0)
58
+ sqlite3 (1.4.1)
59
+ thread_safe (0.3.6)
60
+ tzinfo (1.2.5)
61
+ thread_safe (~> 0.1)
62
+ unicode-display_width (1.5.0)
63
+
64
+ PLATFORMS
65
+ ruby
66
+
67
+ DEPENDENCIES
68
+ bundler
69
+ rake
70
+ rspec
71
+ rubocop
72
+ rubocop-performance
73
+ searchable-by!
74
+ sqlite3
75
+
76
+ BUNDLED WITH
77
+ 2.0.1
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2019 Black Square Media Ltd
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,42 @@
1
+ # Searchable By
2
+
3
+ ActiveRecord plugin to quickly create search scopes.
4
+
5
+ ## Installation
6
+
7
+ Add `gem 'searchable-by'` to your Gemfile.
8
+
9
+ ## Usage
10
+
11
+ ```ruby
12
+ class Post < ActiveRecord::Base
13
+ belongs_to :author
14
+
15
+ # Limit the number of terms per query to 3.
16
+ searchable_by max_terms: 3 do
17
+ # Allow to search strings.
18
+ column :title
19
+
20
+ # ... and integers.
21
+ column :id, type: :integer
22
+
23
+ # Allow custom arel nodes.
24
+ column { Author.arel_table[:name] }
25
+ column { Arel::Nodes::NamedFunction.new('CONCAT', [arel_table[:prefix], arel_table[:suffix]]) }
26
+
27
+ # Support custom scopes.
28
+ scope do
29
+ joins(:author)
30
+ end
31
+ end
32
+ end
33
+
34
+ # Search for 'alice'
35
+ Post.search_by('alice') # => ActiveRecord::Relation
36
+
37
+ # Search for 'alice' AND 'pie recipe'
38
+ Post.search_by('alice "pie recipe"')
39
+
40
+ # Search for 'alice' but NOT for 'pie recipe'
41
+ Post.search_by('alice -"pie recipe"')
42
+ ```
@@ -0,0 +1,9 @@
1
+ require 'bundler/setup'
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+ require 'rubocop/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+ RuboCop::RakeTask.new(:rubocop)
8
+
9
+ task default: %i[rubocop spec]
@@ -0,0 +1 @@
1
+ require 'searchable_by'
@@ -0,0 +1,111 @@
1
+ require 'active_record'
2
+ require 'shellwords'
3
+
4
+ module ActiveRecord
5
+ module SearchableBy
6
+ class Config < Hash
7
+ def initialize
8
+ update columns: [], max_terms: 5
9
+ scope { all }
10
+ end
11
+
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
111
+ end
@@ -0,0 +1,25 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'searchable-by'
3
+ s.version = '0.5.0'
4
+ s.authors = ['Dimitrij Denissenko']
5
+ s.email = ['dimitrij@blacksquaremedia.com']
6
+ s.summary = 'Generate search scopes'
7
+ s.description = 'ActiveRecord plugin'
8
+ s.homepage = 'https://github.com/bsm/sortable-by'
9
+ s.license = 'MIT'
10
+
11
+ s.files = `git ls-files -z`.split("\x0").reject {|f| f.match(%r{^spec/}) }
12
+ s.test_files = `git ls-files -z -- spec/*`.split("\x0")
13
+ s.require_paths = ['lib']
14
+ s.required_ruby_version = '>= 2.4'
15
+
16
+ s.add_dependency 'activerecord'
17
+ s.add_dependency 'activesupport'
18
+
19
+ s.add_development_dependency 'bundler'
20
+ s.add_development_dependency 'rake'
21
+ s.add_development_dependency 'rspec'
22
+ s.add_development_dependency 'rubocop'
23
+ s.add_development_dependency 'rubocop-performance'
24
+ s.add_development_dependency 'sqlite3'
25
+ end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveRecord::SearchableBy do
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)
7
+ end
8
+
9
+ it 'should generate SQL' do
10
+ sql = Post.search_by('123').to_sql
11
+ expect(sql).to include(%("posts"."id" = 123))
12
+ expect(sql).to include(%("posts"."title" LIKE '%123%'))
13
+
14
+ sql = Post.search_by('foo%bar').to_sql
15
+ expect(sql).not_to include(%("posts"."id"))
16
+ expect(sql).to include(%("posts"."title" LIKE '%foo\\%bar%'))
17
+ end
18
+
19
+ 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])
22
+ end
23
+
24
+ it 'should search across multiple words' do
25
+ expect(Post.search_by('ALICE title').pluck(:title)).to match_array(%w[title])
26
+ end
27
+
28
+ 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])
32
+ end
33
+
34
+ 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[])
37
+ end
38
+
39
+ it 'should search integers' do
40
+ expect(Post.search_by(POSTS[:alice1].id.to_s).count).to eq(1)
41
+ end
42
+ end
@@ -0,0 +1,54 @@
1
+ ENV['RACK_ENV'] ||= 'test'
2
+ require 'searchable-by'
3
+ require 'rspec'
4
+
5
+ ActiveRecord::Base.configurations['test'] = { 'adapter' => 'sqlite3', 'database' => ':memory:' }
6
+ ActiveRecord::Base.establish_connection :test
7
+
8
+ ActiveRecord::Base.connection.instance_eval do
9
+ create_table :authors do |t|
10
+ t.string :name
11
+ end
12
+ create_table :posts do |t|
13
+ t.integer :author_id, null: false
14
+ t.string :title
15
+ t.text :body
16
+ end
17
+ end
18
+
19
+ class AbstractModel < ActiveRecord::Base
20
+ self.abstract_class = true
21
+
22
+ searchable_by do
23
+ column :id, type: :integer
24
+ end
25
+ end
26
+
27
+ class Author < AbstractModel
28
+ has_many :posts
29
+ end
30
+
31
+ class Post < AbstractModel
32
+ belongs_to :author
33
+
34
+ searchable_by do
35
+ column :title, :body
36
+ column { Author.arel_table[:name] }
37
+
38
+ scope do
39
+ joins(:author)
40
+ end
41
+ end
42
+ end
43
+
44
+ AUTHORS = {
45
+ alice: Author.create!(name: 'Alice'),
46
+ bob: Author.create!(name: 'Bob'),
47
+ }.freeze
48
+
49
+ POSTS = {
50
+ alice1: AUTHORS[:alice].posts.create!(title: 'titla', body: 'my pia recipe '),
51
+ alice2: AUTHORS[:alice].posts.create!(title: 'title', body: 'your pie recipe'),
52
+ bob1: AUTHORS[:bob].posts.create!(title: 'titlo', body: 'her pio recipe'),
53
+ bob2: AUTHORS[:bob].posts.create!(title: 'titlu', body: 'our piu recipe'),
54
+ }.freeze
metadata ADDED
@@ -0,0 +1,170 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: searchable-by
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Dimitrij Denissenko
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-05-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
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
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop-performance
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: sqlite3
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ description: ActiveRecord plugin
126
+ email:
127
+ - dimitrij@blacksquaremedia.com
128
+ executables: []
129
+ extensions: []
130
+ extra_rdoc_files: []
131
+ files:
132
+ - ".gitignore"
133
+ - ".rubocop.yml"
134
+ - ".travis.yml"
135
+ - Gemfile
136
+ - Gemfile.lock
137
+ - LICENSE
138
+ - README.md
139
+ - Rakefile
140
+ - lib/searchable-by.rb
141
+ - lib/searchable_by.rb
142
+ - searchable-by.gemspec
143
+ - spec/searchable_by_spec.rb
144
+ - spec/spec_helper.rb
145
+ homepage: https://github.com/bsm/sortable-by
146
+ licenses:
147
+ - MIT
148
+ metadata: {}
149
+ post_install_message:
150
+ rdoc_options: []
151
+ require_paths:
152
+ - lib
153
+ required_ruby_version: !ruby/object:Gem::Requirement
154
+ requirements:
155
+ - - ">="
156
+ - !ruby/object:Gem::Version
157
+ version: '2.4'
158
+ required_rubygems_version: !ruby/object:Gem::Requirement
159
+ requirements:
160
+ - - ">="
161
+ - !ruby/object:Gem::Version
162
+ version: '0'
163
+ requirements: []
164
+ rubygems_version: 3.0.3
165
+ signing_key:
166
+ specification_version: 4
167
+ summary: Generate search scopes
168
+ test_files:
169
+ - spec/searchable_by_spec.rb
170
+ - spec/spec_helper.rb