search_cop 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test.yml +42 -0
  3. data/.rubocop.yml +128 -0
  4. data/CHANGELOG.md +14 -9
  5. data/Gemfile +4 -5
  6. data/README.md +11 -1
  7. data/Rakefile +0 -1
  8. data/docker-compose.yml +8 -1
  9. data/gemfiles/{5.1.gemfile → rails5.gemfile} +2 -2
  10. data/gemfiles/rails6.gemfile +13 -0
  11. data/lib/search_cop.rb +5 -23
  12. data/lib/search_cop/grammar_parser.rb +1 -3
  13. data/lib/search_cop/hash_parser.rb +19 -19
  14. data/lib/search_cop/helpers.rb +15 -0
  15. data/lib/search_cop/query_builder.rb +0 -2
  16. data/lib/search_cop/query_info.rb +0 -2
  17. data/lib/search_cop/search_scope.rb +2 -4
  18. data/lib/search_cop/version.rb +1 -1
  19. data/lib/search_cop/visitors.rb +0 -2
  20. data/lib/search_cop/visitors/mysql.rb +4 -2
  21. data/lib/search_cop/visitors/postgres.rb +5 -3
  22. data/lib/search_cop/visitors/visitor.rb +5 -3
  23. data/lib/search_cop_grammar.rb +1 -3
  24. data/lib/search_cop_grammar/attributes.rb +45 -34
  25. data/lib/search_cop_grammar/nodes.rb +0 -2
  26. data/search_cop.gemspec +8 -8
  27. data/test/and_test.rb +6 -8
  28. data/test/boolean_test.rb +7 -9
  29. data/test/database.yml +2 -1
  30. data/test/date_test.rb +14 -16
  31. data/test/datetime_test.rb +15 -17
  32. data/test/default_operator_test.rb +14 -10
  33. data/test/error_test.rb +2 -4
  34. data/test/float_test.rb +9 -11
  35. data/test/fulltext_test.rb +6 -8
  36. data/test/hash_test.rb +32 -34
  37. data/test/integer_test.rb +9 -11
  38. data/test/not_test.rb +6 -8
  39. data/test/or_test.rb +8 -10
  40. data/test/scope_test.rb +11 -13
  41. data/test/search_cop_test.rb +32 -34
  42. data/test/string_test.rb +67 -19
  43. data/test/test_helper.rb +13 -15
  44. data/test/visitor_test.rb +4 -6
  45. metadata +28 -13
  46. data/.travis.yml +0 -34
  47. data/gemfiles/4.2.gemfile +0 -13
@@ -0,0 +1,15 @@
1
+ module SearchCop
2
+ module Helpers
3
+ def self.sanitize_default_operator(query_options)
4
+ return "and" unless query_options.key?(:default_operator)
5
+
6
+ default_operator = query_options[:default_operator].to_s.downcase
7
+
8
+ unless ["and", "or"].include?(default_operator)
9
+ raise(SearchCop::UnknownDefaultOperator, "Unknown default operator value #{default_operator}")
10
+ end
11
+
12
+ default_operator
13
+ end
14
+ end
15
+ end
@@ -1,4 +1,3 @@
1
-
2
1
  module SearchCop
3
2
  class QueryBuilder
4
3
  attr_accessor :query_info, :scope, :sql
@@ -32,4 +31,3 @@ module SearchCop
32
31
  end
33
32
  end
34
33
  end
35
-
@@ -1,4 +1,3 @@
1
-
2
1
  module SearchCop
3
2
  class QueryInfo
4
3
  attr_accessor :model, :scope, :references
@@ -10,4 +9,3 @@ module SearchCop
10
9
  end
11
10
  end
12
11
  end
13
-
@@ -1,4 +1,3 @@
1
-
2
1
  module SearchCop
3
2
  class Reflection
4
3
  attr_accessor :attributes, :options, :aliases, :scope, :generators
@@ -11,11 +10,11 @@ module SearchCop
11
10
  end
12
11
 
13
12
  def default_attributes
14
- keys = options.select { |key, value| value[:default] == true }.keys
13
+ keys = options.select { |_key, value| value[:default] == true }.keys
15
14
  keys = attributes.keys.reject { |key| options[key] && options[key][:default] == false } if keys.empty?
16
15
  keys = keys.to_set
17
16
 
18
- attributes.select { |key, value| keys.include? key }
17
+ attributes.select { |key, _value| keys.include? key }
19
18
  end
20
19
  end
21
20
 
@@ -64,4 +63,3 @@ module SearchCop
64
63
  end
65
64
  end
66
65
  end
67
-
@@ -1,3 +1,3 @@
1
1
  module SearchCop
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -1,5 +1,3 @@
1
-
2
1
  require "search_cop/visitors/visitor"
3
2
  require "search_cop/visitors/mysql"
4
3
  require "search_cop/visitors/postgres"
5
-
@@ -1,7 +1,8 @@
1
-
2
1
  module SearchCop
3
2
  module Visitors
4
3
  module Mysql
4
+ # rubocop:disable Naming/MethodName
5
+
5
6
  class FulltextQuery < Visitor
6
7
  def visit_SearchCopGrammar_Nodes_MatchesFulltextNot(node)
7
8
  node.right.split(/[\s+'"<>()~-]+/).collect { |word| "-#{word}" }.join(" ")
@@ -38,6 +39,7 @@ module SearchCop
38
39
  "MATCH(#{visit node.collection}) AGAINST(#{visit FulltextQuery.new(connection).visit(node.node)} IN BOOLEAN MODE)"
39
40
  end
40
41
  end
42
+
43
+ # rubocop:enable Naming/MethodName
41
44
  end
42
45
  end
43
-
@@ -1,7 +1,8 @@
1
-
2
1
  module SearchCop
3
2
  module Visitors
4
3
  module Postgres
4
+ # rubocop:disable Naming/MethodName
5
+
5
6
  class FulltextQuery < Visitor
6
7
  def visit_SearchCopGrammar_Nodes_MatchesFulltextNot(node)
7
8
  "!'#{node.right.gsub(/[\s&|!:'"]+/, " ")}'"
@@ -21,7 +22,7 @@ module SearchCop
21
22
  end
22
23
 
23
24
  def visit_SearchCopGrammar_Nodes_Matches(node)
24
- "(#{visit node.left} IS NOT NULL AND #{visit node.left} ILIKE #{visit node.right})"
25
+ "(#{visit node.left} IS NOT NULL AND #{visit node.left} ILIKE #{visit node.right} ESCAPE #{visit "\\"})"
25
26
  end
26
27
 
27
28
  def visit_SearchCopGrammar_Attributes_Collection(node)
@@ -41,7 +42,8 @@ module SearchCop
41
42
 
42
43
  "to_tsvector(#{visit dictionary}, #{visit node.collection}) @@ to_tsquery(#{visit dictionary}, #{visit FulltextQuery.new(connection).visit(node.node)})"
43
44
  end
45
+
46
+ # rubocop:enable Naming/MethodName
44
47
  end
45
48
  end
46
49
  end
47
-
@@ -1,7 +1,8 @@
1
-
2
1
  module SearchCop
3
2
  module Visitors
4
3
  class Visitor
4
+ # rubocop:disable Naming/MethodName
5
+
5
6
  attr_accessor :connection
6
7
 
7
8
  def initialize(connection)
@@ -48,7 +49,7 @@ module SearchCop
48
49
  end
49
50
 
50
51
  def visit_SearchCopGrammar_Nodes_Matches(node)
51
- "(#{visit node.left} IS NOT NULL AND #{visit node.left} LIKE #{visit node.right})"
52
+ "(#{visit node.left} IS NOT NULL AND #{visit node.left} LIKE #{visit node.right} ESCAPE #{visit "\\"})"
52
53
  end
53
54
 
54
55
  def visit_SearchCopGrammar_Nodes_Not(node)
@@ -95,7 +96,8 @@ module SearchCop
95
96
  alias :visit_Fixnum :quote
96
97
  alias :visit_Symbol :quote
97
98
  alias :visit_Integer :quote
99
+
100
+ # rubocop:enable Naming/MethodName
98
101
  end
99
102
  end
100
103
  end
101
-
@@ -1,4 +1,3 @@
1
-
2
1
  require "search_cop_grammar/attributes"
3
2
  require "search_cop_grammar/nodes"
4
3
 
@@ -19,7 +18,7 @@ module SearchCopGrammar
19
18
  end
20
19
 
21
20
  def elements
22
- super.select { |element| element.class != Treetop::Runtime::SyntaxNode }
21
+ super.reject { |element| element.instance_of?(Treetop::Runtime::SyntaxNode) }
23
22
  end
24
23
 
25
24
  def collection_for(key)
@@ -153,4 +152,3 @@ module SearchCopGrammar
153
152
 
154
153
  class Value < BaseNode; end
155
154
  end
156
-
@@ -1,4 +1,3 @@
1
-
2
1
  require "treetop"
3
2
 
4
3
  module SearchCopGrammar
@@ -28,9 +27,9 @@ module SearchCopGrammar
28
27
  end
29
28
 
30
29
  [:eq, :not_eq, :lt, :lteq, :gt, :gteq].each do |method|
31
- define_method method do |value|
32
- attributes.collect! { |attribute| attribute.send method, value }.inject(:or)
33
- end
30
+ define_method method do |value|
31
+ attributes.collect! { |attribute| attribute.send method, value }.inject(:or)
32
+ end
34
33
  end
35
34
 
36
35
  def generator(generator, value)
@@ -137,7 +136,7 @@ module SearchCopGrammar
137
136
  false
138
137
  end
139
138
 
140
- { :eq => "Equality", :not_eq => "NotEqual", :lt => "LessThan", :lteq => "LessThanOrEqual", :gt => "GreaterThan", :gteq => "GreaterThanOrEqual", :matches => "Matches" }.each do |method, class_name|
139
+ { eq: "Equality", not_eq: "NotEqual", lt: "LessThan", lteq: "LessThanOrEqual", gt: "GreaterThan", gteq: "GreaterThanOrEqual", matches: "Matches" }.each do |method, class_name|
141
140
  define_method method do |value|
142
141
  raise(SearchCop::IncompatibleDatatype, "Incompatible datatype for #{value}") unless compatible?(value)
143
142
 
@@ -146,19 +145,32 @@ module SearchCopGrammar
146
145
  end
147
146
 
148
147
  def method_missing(name, *args, &block)
149
- @attribute.send(name, *args, &block)
148
+ if @attribute.respond_to?(name)
149
+ @attribute.send(name, *args, &block)
150
+ else
151
+ super
152
+ end
150
153
  end
151
154
 
152
- def respond_to?(*args)
153
- super(*args) || @attribute.respond_to?(*args)
155
+ def respond_to_missing?(*args)
156
+ @attribute.respond_to?(*args) || super
154
157
  end
155
158
  end
156
159
 
157
160
  class String < Base
158
161
  def matches_value(value)
159
- return value.gsub(/\*/, "%") if (options[:left_wildcard] != false && value.strip =~ /^[^*]+\*$|^\*[^*]+$/) || value.strip =~ /^[^*]+\*$/
162
+ res = value.gsub(/[%_\\]/) { |char| "\\#{char}" }
160
163
 
161
- options[:left_wildcard] != false ? "%#{value}%" : "#{value}%"
164
+ if value.strip =~ /^\*|\*$/
165
+ res = res.gsub(/^\*/, "%") if options[:left_wildcard] != false
166
+ res = res.gsub(/\*$/, "%") if options[:right_wildcard] != false
167
+
168
+ return res
169
+ end
170
+
171
+ res = "%#{res}" if options[:left_wildcard] != false
172
+ res = "#{res}%" if options[:right_wildcard] != false
173
+ res
162
174
  end
163
175
 
164
176
  def matches(value)
@@ -176,7 +188,7 @@ module SearchCopGrammar
176
188
 
177
189
  class Float < WithoutMatches
178
190
  def compatible?(value)
179
- return true if value.to_s =~ /^\-?[0-9]+(\.[0-9]+)?$/
191
+ return true if value.to_s =~ /^-?[0-9]+(\.[0-9]+)?$/
180
192
 
181
193
  false
182
194
  end
@@ -196,24 +208,24 @@ module SearchCopGrammar
196
208
 
197
209
  class Datetime < WithoutMatches
198
210
  def parse(value)
199
- return value .. value unless value.is_a?(::String)
211
+ return value..value unless value.is_a?(::String)
200
212
 
201
213
  if value =~ /^[0-9]+ (hour|day|week|month|year)s{0,1} (ago)$/
202
- number,period,ago = value.split(' ')
214
+ number, period, ago = value.split(" ")
203
215
  time = number.to_i.send(period.to_sym).send(ago.to_sym)
204
- time .. ::Time.now
216
+ time..::Time.now
205
217
  elsif value =~ /^[0-9]{4}$/
206
- ::Time.new(value).beginning_of_year .. ::Time.new(value).end_of_year
207
- elsif value =~ /^([0-9]{4})(\.|-|\/)([0-9]{1,2})$/
208
- ::Time.new($1, $3, 15).beginning_of_month .. ::Time.new($1, $3, 15).end_of_month
209
- elsif value =~ /^([0-9]{1,2})(\.|-|\/)([0-9]{4})$/
210
- ::Time.new($3, $1, 15).beginning_of_month .. ::Time.new($3, $1, 15).end_of_month
211
- elsif value =~ /^[0-9]{4}(\.|-|\/)[0-9]{1,2}(\.|-|\/)[0-9]{1,2}$/ || value =~ /^[0-9]{1,2}(\.|-|\/)[0-9]{1,2}(\.|-|\/)[0-9]{4}$/
218
+ ::Time.new(value).beginning_of_year..::Time.new(value).end_of_year
219
+ elsif value =~ %r{^([0-9]{4})(\.|-|/)([0-9]{1,2})$}
220
+ ::Time.new(Regexp.last_match(1), Regexp.last_match(3), 15).beginning_of_month..::Time.new(Regexp.last_match(1), Regexp.last_match(3), 15).end_of_month
221
+ elsif value =~ %r{^([0-9]{1,2})(\.|-|/)([0-9]{4})$}
222
+ ::Time.new(Regexp.last_match(3), Regexp.last_match(1), 15).beginning_of_month..::Time.new(Regexp.last_match(3), Regexp.last_match(1), 15).end_of_month
223
+ elsif value =~ %r{^[0-9]{4}(\.|-|/)[0-9]{1,2}(\.|-|/)[0-9]{1,2}$} || value =~ %r{^[0-9]{1,2}(\.|-|/)[0-9]{1,2}(\.|-|/)[0-9]{4}$}
212
224
  time = ::Time.parse(value)
213
- time.beginning_of_day .. time.end_of_day
214
- elsif value =~ /[0-9]{4}(\.|-|\/)[0-9]{1,2}(\.|-|\/)[0-9]{1,2}/ || value =~ /[0-9]{1,2}(\.|-|\/)[0-9]{1,2}(\.|-|\/)[0-9]{4}/
225
+ time.beginning_of_day..time.end_of_day
226
+ elsif value =~ %r{[0-9]{4}(\.|-|/)[0-9]{1,2}(\.|-|/)[0-9]{1,2}} || value =~ %r{[0-9]{1,2}(\.|-|/)[0-9]{1,2}(\.|-|/)[0-9]{4}}
215
227
  time = ::Time.parse(value)
216
- time .. time
228
+ time..time
217
229
  else
218
230
  raise ArgumentError
219
231
  end
@@ -246,21 +258,21 @@ module SearchCopGrammar
246
258
 
247
259
  class Date < Datetime
248
260
  def parse(value)
249
- return value .. value unless value.is_a?(::String)
261
+ return value..value unless value.is_a?(::String)
250
262
 
251
263
  if value =~ /^[0-9]+ (day|week|month|year)s{0,1} (ago)$/
252
- number,period,ago = value.split(' ')
264
+ number, period, ago = value.split(" ")
253
265
  time = number.to_i.send(period.to_sym).send(ago.to_sym)
254
- time.to_date .. ::Date.today
266
+ time.to_date..::Date.today
255
267
  elsif value =~ /^[0-9]{4}$/
256
- ::Date.new(value.to_i).beginning_of_year .. ::Date.new(value.to_i).end_of_year
257
- elsif value =~ /^([0-9]{4})(\.|-|\/)([0-9]{1,2})$/
258
- ::Date.new($1.to_i, $3.to_i, 15).beginning_of_month .. ::Date.new($1.to_i, $3.to_i, 15).end_of_month
259
- elsif value =~ /^([0-9]{1,2})(\.|-|\/)([0-9]{4})$/
260
- ::Date.new($3.to_i, $1.to_i, 15).beginning_of_month .. ::Date.new($3.to_i, $1.to_i, 15).end_of_month
261
- elsif value =~ /[0-9]{4}(\.|-|\/)[0-9]{1,2}(\.|-|\/)[0-9]{1,2}/ || value =~ /[0-9]{1,2}(\.|-|\/)[0-9]{1,2}(\.|-|\/)[0-9]{4}/
268
+ ::Date.new(value.to_i).beginning_of_year..::Date.new(value.to_i).end_of_year
269
+ elsif value =~ %r{^([0-9]{4})(\.|-|/)([0-9]{1,2})$}
270
+ ::Date.new(Regexp.last_match(1).to_i, Regexp.last_match(3).to_i, 15).beginning_of_month..::Date.new(Regexp.last_match(1).to_i, Regexp.last_match(3).to_i, 15).end_of_month
271
+ elsif value =~ %r{^([0-9]{1,2})(\.|-|/)([0-9]{4})$}
272
+ ::Date.new(Regexp.last_match(3).to_i, Regexp.last_match(1).to_i, 15).beginning_of_month..::Date.new(Regexp.last_match(3).to_i, Regexp.last_match(1).to_i, 15).end_of_month
273
+ elsif value =~ %r{[0-9]{4}(\.|-|/)[0-9]{1,2}(\.|-|/)[0-9]{1,2}} || value =~ %r{[0-9]{1,2}(\.|-|/)[0-9]{1,2}(\.|-|/)[0-9]{4}}
262
274
  date = ::Date.parse(value)
263
- date .. date
275
+ date..date
264
276
  else
265
277
  raise ArgumentError
266
278
  end
@@ -281,4 +293,3 @@ module SearchCopGrammar
281
293
  end
282
294
  end
283
295
  end
284
-
@@ -1,4 +1,3 @@
1
-
2
1
  require "treetop"
3
2
 
4
3
  module SearchCopGrammar
@@ -187,4 +186,3 @@ module SearchCopGrammar
187
186
  end
188
187
  end
189
188
  end
190
-
data/search_cop.gemspec CHANGED
@@ -1,28 +1,28 @@
1
- # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
1
+ lib = File.expand_path("lib", __dir__)
3
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
- require 'search_cop/version'
3
+ require "search_cop/version"
5
4
 
6
5
  Gem::Specification.new do |spec|
7
6
  spec.name = "search_cop"
8
7
  spec.version = SearchCop::VERSION
9
8
  spec.authors = ["Benjamin Vetter"]
10
9
  spec.email = ["vetter@flakks.com"]
11
- spec.description = %q{Search engine like fulltext query support for ActiveRecord}
12
- spec.summary = %q{Easily perform complex search engine like fulltext queries on your ActiveRecord models}
10
+ spec.description = "Search engine like fulltext query support for ActiveRecord"
11
+ spec.summary = "Easily perform complex search engine like fulltext queries on your ActiveRecord models"
13
12
  spec.homepage = "https://github.com/mrkamel/search_cop"
14
13
  spec.license = "MIT"
15
14
 
16
- spec.files = `git ls-files`.split($/)
15
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
17
16
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
18
  spec.require_paths = ["lib"]
20
19
 
21
20
  spec.add_dependency "treetop"
22
21
 
23
- spec.add_development_dependency "bundler"
24
- spec.add_development_dependency "rake"
25
22
  spec.add_development_dependency "activerecord", ">= 3.0.0"
23
+ spec.add_development_dependency "bundler"
26
24
  spec.add_development_dependency "factory_bot"
27
25
  spec.add_development_dependency "minitest"
26
+ spec.add_development_dependency "rake"
27
+ spec.add_development_dependency "rubocop"
28
28
  end
data/test/and_test.rb CHANGED
@@ -1,10 +1,9 @@
1
-
2
- require File.expand_path("../test_helper", __FILE__)
1
+ require File.expand_path("test_helper", __dir__)
3
2
 
4
3
  class AndTest < SearchCop::TestCase
5
4
  def test_and_string
6
- expected = create(:product, :title => "expected title", :description => "Description")
7
- rejected = create(:product, :title => "Rejected title", :description => "Description")
5
+ expected = create(:product, title: "expected title", description: "Description")
6
+ rejected = create(:product, title: "Rejected title", description: "Description")
8
7
 
9
8
  results = Product.search("title: 'Expected title' description: Description")
10
9
 
@@ -15,13 +14,12 @@ class AndTest < SearchCop::TestCase
15
14
  end
16
15
 
17
16
  def test_and_hash
18
- expected = create(:product, :title => "Expected title", :description => "Description")
19
- rejected = create(:product, :title => "Rejected title", :description => "Description")
17
+ expected = create(:product, title: "Expected title", description: "Description")
18
+ rejected = create(:product, title: "Rejected title", description: "Description")
20
19
 
21
- results = Product.search(:and => [{:title => "Expected title"}, {:description => "Description"}])
20
+ results = Product.search(and: [{ title: "Expected title" }, { description: "Description" }])
22
21
 
23
22
  assert_includes results, expected
24
23
  refute_includes results, rejected
25
24
  end
26
25
  end
27
-
data/test/boolean_test.rb CHANGED
@@ -1,15 +1,14 @@
1
-
2
- require File.expand_path("../test_helper", __FILE__)
1
+ require File.expand_path("test_helper", __dir__)
3
2
 
4
3
  class BooleanTest < SearchCop::TestCase
5
4
  def test_mapping
6
- product = create(:product, :available => true)
5
+ product = create(:product, available: true)
7
6
 
8
7
  assert_includes Product.search("available: 1"), product
9
8
  assert_includes Product.search("available: true"), product
10
9
  assert_includes Product.search("available: yes"), product
11
10
 
12
- product = create(:product, :available => false)
11
+ product = create(:product, available: false)
13
12
 
14
13
  assert_includes Product.search("available: 0"), product
15
14
  assert_includes Product.search("available: false"), product
@@ -17,28 +16,28 @@ class BooleanTest < SearchCop::TestCase
17
16
  end
18
17
 
19
18
  def test_anywhere
20
- product = create(:product, :available => true)
19
+ product = create(:product, available: true)
21
20
 
22
21
  assert_includes Product.search("true"), product
23
22
  refute_includes Product.search("false"), product
24
23
  end
25
24
 
26
25
  def test_includes
27
- product = create(:product, :available => true)
26
+ product = create(:product, available: true)
28
27
 
29
28
  assert_includes Product.search("available: true"), product
30
29
  refute_includes Product.search("available: false"), product
31
30
  end
32
31
 
33
32
  def test_equals
34
- product = create(:product, :available => true)
33
+ product = create(:product, available: true)
35
34
 
36
35
  assert_includes Product.search("available = true"), product
37
36
  refute_includes Product.search("available = false"), product
38
37
  end
39
38
 
40
39
  def test_equals_not
41
- product = create(:product, :available => false)
40
+ product = create(:product, available: false)
42
41
 
43
42
  assert_includes Product.search("available != true"), product
44
43
  refute_includes Product.search("available != false"), product
@@ -50,4 +49,3 @@ class BooleanTest < SearchCop::TestCase
50
49
  end
51
50
  end
52
51
  end
53
-