search_cop 1.0.9 → 1.2.1

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.
Files changed (54) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/test.yml +50 -0
  3. data/.rubocop.yml +134 -0
  4. data/CHANGELOG.md +29 -7
  5. data/Gemfile +4 -17
  6. data/README.md +69 -3
  7. data/Rakefile +0 -1
  8. data/docker-compose.yml +18 -0
  9. data/gemfiles/rails5.gemfile +13 -0
  10. data/gemfiles/rails6.gemfile +13 -0
  11. data/gemfiles/rails7.gemfile +13 -0
  12. data/lib/search_cop/grammar_parser.rb +3 -4
  13. data/lib/search_cop/hash_parser.rb +20 -18
  14. data/lib/search_cop/helpers.rb +15 -0
  15. data/lib/search_cop/query_builder.rb +3 -5
  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/mysql.rb +4 -2
  20. data/lib/search_cop/visitors/postgres.rb +5 -3
  21. data/lib/search_cop/visitors/visitor.rb +7 -5
  22. data/lib/search_cop/visitors.rb +0 -2
  23. data/lib/search_cop.rb +15 -13
  24. data/lib/search_cop_grammar/attributes.rb +53 -34
  25. data/lib/search_cop_grammar/nodes.rb +0 -2
  26. data/lib/search_cop_grammar.rb +13 -4
  27. data/lib/search_cop_grammar.treetop +6 -4
  28. data/search_cop.gemspec +9 -11
  29. data/test/and_test.rb +6 -8
  30. data/test/boolean_test.rb +7 -9
  31. data/test/database.yml +4 -1
  32. data/test/date_test.rb +38 -12
  33. data/test/datetime_test.rb +45 -12
  34. data/test/default_operator_test.rb +51 -0
  35. data/test/error_test.rb +2 -4
  36. data/test/float_test.rb +16 -11
  37. data/test/fulltext_test.rb +6 -8
  38. data/test/hash_test.rb +32 -34
  39. data/test/integer_test.rb +9 -11
  40. data/test/not_test.rb +6 -8
  41. data/test/or_test.rb +8 -10
  42. data/test/scope_test.rb +11 -13
  43. data/test/search_cop_test.rb +42 -36
  44. data/test/string_test.rb +67 -19
  45. data/test/test_helper.rb +28 -18
  46. data/test/visitor_test.rb +4 -6
  47. metadata +25 -42
  48. data/.travis.yml +0 -45
  49. data/Appraisals +0 -21
  50. data/gemfiles/3.2.gemfile +0 -25
  51. data/gemfiles/4.0.gemfile +0 -25
  52. data/gemfiles/4.1.gemfile +0 -25
  53. data/gemfiles/4.2.gemfile +0 -25
  54. data/gemfiles/5.0.gemfile +0 -25
@@ -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,14 +1,15 @@
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)
8
9
  @connection = connection
9
10
 
10
- extend(SearchCop::Visitors::Mysql) if @connection.class.name =~ /mysql/i
11
- extend(SearchCop::Visitors::Postgres) if @connection.class.name =~ /postgres/i
11
+ extend(SearchCop::Visitors::Mysql) if @connection.adapter_name =~ /mysql/i
12
+ extend(SearchCop::Visitors::Postgres) if @connection.adapter_name =~ /postgres|postgis/i
12
13
  end
13
14
 
14
15
  def visit(visit_node = node)
@@ -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,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
-
data/lib/search_cop.rb CHANGED
@@ -1,5 +1,5 @@
1
-
2
1
  require "search_cop/version"
2
+ require "search_cop/helpers"
3
3
  require "search_cop/search_scope"
4
4
  require "search_cop/query_info"
5
5
  require "search_cop/query_builder"
@@ -8,21 +8,24 @@ require "search_cop/hash_parser"
8
8
  require "search_cop/visitors"
9
9
 
10
10
  module SearchCop
11
- class SpecificationError < StandardError; end
11
+ class Error < StandardError; end
12
+
13
+ class SpecificationError < Error; end
12
14
  class UnknownAttribute < SpecificationError; end
15
+ class UnknownDefaultOperator < SpecificationError; end
13
16
 
14
- class RuntimeError < StandardError; end
17
+ class RuntimeError < Error; end
15
18
  class UnknownColumn < RuntimeError; end
16
19
  class NoSearchableAttributes < RuntimeError; end
17
20
  class IncompatibleDatatype < RuntimeError; end
18
21
  class ParseError < RuntimeError; end
19
22
 
20
23
  module Parser
21
- def self.parse(query, query_info)
24
+ def self.parse(query, query_info, query_options = {})
22
25
  if query.is_a?(Hash)
23
- SearchCop::HashParser.new(query_info).parse(query)
26
+ SearchCop::HashParser.new(query_info).parse(query, query_options)
24
27
  else
25
- SearchCop::GrammarParser.new(query_info).parse(query)
28
+ SearchCop::GrammarParser.new(query_info).parse(query, query_options)
26
29
  end
27
30
  end
28
31
  end
@@ -41,24 +44,24 @@ module SearchCop
41
44
  search_scopes[name] = SearchScope.new(name, self)
42
45
  search_scopes[name].instance_exec(&block)
43
46
 
44
- self.send(:define_singleton_method, name) { |query| search_cop query, name }
45
- self.send(:define_singleton_method, "unsafe_#{name}") { |query| unsafe_search_cop query, name }
47
+ send(:define_singleton_method, name) { |query, query_options = {}| search_cop(query, name, query_options) }
48
+ send(:define_singleton_method, "unsafe_#{name}") { |query, query_options = {}| unsafe_search_cop(query, name, query_options) }
46
49
  end
47
50
 
48
51
  def search_reflection(scope_name)
49
52
  search_scopes[scope_name].reflection
50
53
  end
51
54
 
52
- def search_cop(query, scope_name)
53
- unsafe_search_cop query, scope_name
55
+ def search_cop(query, scope_name, query_options)
56
+ unsafe_search_cop(query, scope_name, query_options)
54
57
  rescue SearchCop::RuntimeError
55
58
  respond_to?(:none) ? none : where("1 = 0")
56
59
  end
57
60
 
58
- def unsafe_search_cop(query, scope_name)
61
+ def unsafe_search_cop(query, scope_name, query_options)
59
62
  return respond_to?(:scoped) ? scoped : all if query.blank?
60
63
 
61
- query_builder = QueryBuilder.new(self, query, search_scopes[scope_name])
64
+ query_builder = QueryBuilder.new(self, query, search_scopes[scope_name], query_options)
62
65
 
63
66
  scope = instance_exec(&search_scopes[scope_name].reflection.scope) if search_scopes[scope_name].reflection.scope
64
67
  scope ||= eager_load(query_builder.associations) if query_builder.associations.any?
@@ -67,4 +70,3 @@ module SearchCop
67
70
  end
68
71
  end
69
72
  end
70
-
@@ -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,20 +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)
200
-
201
- if value =~ /^[0-9]{4}$/
202
- ::Time.new(value).beginning_of_year .. ::Time.new(value).end_of_year
203
- elsif value =~ /^([0-9]{4})(\.|-|\/)([0-9]{1,2})$/
204
- ::Time.new($1, $3, 15).beginning_of_month .. ::Time.new($1, $3, 15).end_of_month
205
- elsif value =~ /^([0-9]{1,2})(\.|-|\/)([0-9]{4})$/
206
- ::Time.new($3, $1, 15).beginning_of_month .. ::Time.new($3, $1, 15).end_of_month
207
- 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}$/
211
+ return value..value unless value.is_a?(::String)
212
+
213
+ if value =~ /^[0-9]+ (hour|day|week|month|year)s{0,1} (ago)$/
214
+ number, period, ago = value.split(" ")
215
+ time = number.to_i.send(period.to_sym).send(ago.to_sym)
216
+ time..::Time.now
217
+ elsif value =~ /^[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}$}
208
224
  time = ::Time.parse(value)
209
- time.beginning_of_day .. time.end_of_day
210
- 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}}
211
227
  time = ::Time.parse(value)
212
- time .. time
228
+ time..time
213
229
  else
214
230
  raise ArgumentError
215
231
  end
@@ -242,17 +258,21 @@ module SearchCopGrammar
242
258
 
243
259
  class Date < Datetime
244
260
  def parse(value)
245
- return value .. value unless value.is_a?(::String)
246
-
247
- if value =~ /^[0-9]{4}$/
248
- ::Date.new(value.to_i).beginning_of_year .. ::Date.new(value.to_i).end_of_year
249
- elsif value =~ /^([0-9]{4})(\.|-|\/)([0-9]{1,2})$/
250
- ::Date.new($1.to_i, $3.to_i, 15).beginning_of_month .. ::Date.new($1.to_i, $3.to_i, 15).end_of_month
251
- elsif value =~ /^([0-9]{1,2})(\.|-|\/)([0-9]{4})$/
252
- ::Date.new($3.to_i, $1.to_i, 15).beginning_of_month .. ::Date.new($3.to_i, $1.to_i, 15).end_of_month
253
- 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}/
261
+ return value..value unless value.is_a?(::String)
262
+
263
+ if value =~ /^[0-9]+ (day|week|month|year)s{0,1} (ago)$/
264
+ number, period, ago = value.split(" ")
265
+ time = number.to_i.send(period.to_sym).send(ago.to_sym)
266
+ time.to_date..::Date.today
267
+ elsif value =~ /^[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}}
254
274
  date = ::Date.parse(value)
255
- date .. date
275
+ date..date
256
276
  else
257
277
  raise ArgumentError
258
278
  end
@@ -273,4 +293,3 @@ module SearchCopGrammar
273
293
  end
274
294
  end
275
295
  end
276
-
@@ -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
-
@@ -1,21 +1,24 @@
1
-
2
1
  require "search_cop_grammar/attributes"
3
2
  require "search_cop_grammar/nodes"
4
3
 
5
4
  module SearchCopGrammar
6
5
  class BaseNode < Treetop::Runtime::SyntaxNode
7
- attr_accessor :query_info
6
+ attr_writer :query_info, :query_options
8
7
 
9
8
  def query_info
10
9
  (@query_info ||= nil) || parent.query_info
11
10
  end
12
11
 
12
+ def query_options
13
+ (@query_options ||= nil) || parent.query_options
14
+ end
15
+
13
16
  def evaluate
14
17
  elements.collect(&:evaluate).inject(:and)
15
18
  end
16
19
 
17
20
  def elements
18
- super.select { |element| element.class != Treetop::Runtime::SyntaxNode }
21
+ super.reject { |element| element.instance_of?(Treetop::Runtime::SyntaxNode) }
19
22
  end
20
23
 
21
24
  def collection_for(key)
@@ -110,6 +113,13 @@ module SearchCopGrammar
110
113
  end
111
114
  end
112
115
 
116
+ class AndOrExpression < BaseNode
117
+ def evaluate
118
+ default_operator = SearchCop::Helpers.sanitize_default_operator(query_options)
119
+ [elements.first.evaluate, elements.last.evaluate].inject(default_operator)
120
+ end
121
+ end
122
+
113
123
  class OrExpression < BaseNode
114
124
  def evaluate
115
125
  [elements.first.evaluate, elements.last.evaluate].inject(:or)
@@ -142,4 +152,3 @@ module SearchCopGrammar
142
152
 
143
153
  class Value < BaseNode; end
144
154
  end
145
-
@@ -9,7 +9,9 @@ grammar SearchCopGrammar
9
9
  end
10
10
 
11
11
  rule and_expression
12
- or_expression (space? ('AND' / 'and') space? / space !('OR' / 'or')) complex_expression <AndExpression> / or_expression
12
+ or_expression space? ('AND' / 'and') space? complex_expression <AndExpression> /
13
+ or_expression space !('OR' / 'or') complex_expression <AndOrExpression> /
14
+ or_expression
13
15
  end
14
16
 
15
17
  rule or_expression
@@ -37,7 +39,7 @@ grammar SearchCopGrammar
37
39
  end
38
40
 
39
41
  rule anywhere_expression
40
- "'" [^\']* "'" <SingleQuotedAnywhereExpression> / '"' [^\"]* '"' <DoubleQuotedAnywhereExpression> / [^\s()]+ <AnywhereExpression>
42
+ "'" [^\']* "'" <SingleQuotedAnywhereExpression> / '"' [^\"]* '"' <DoubleQuotedAnywhereExpression> / [^[:blank:]()]+ <AnywhereExpression>
41
43
  end
42
44
 
43
45
  rule simple_column
@@ -45,11 +47,11 @@ grammar SearchCopGrammar
45
47
  end
46
48
 
47
49
  rule value
48
- "'" [^\']* "'" <SingleQuotedValue> / '"' [^\"]* '"' <DoubleQuotedValue> / [^\s()]+ <Value>
50
+ "'" [^\']* "'" <SingleQuotedValue> / '"' [^\"]* '"' <DoubleQuotedValue> / [^[:blank:]()]+ <Value>
49
51
  end
50
52
 
51
53
  rule space
52
- [\s]+
54
+ [[:blank:]]+
53
55
  end
54
56
  end
55
57
 
data/search_cop.gemspec CHANGED
@@ -1,29 +1,27 @@
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
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
17
  spec.require_paths = ["lib"]
20
18
 
21
19
  spec.add_dependency "treetop"
22
20
 
23
- spec.add_development_dependency "bundler"
24
- spec.add_development_dependency "rake"
25
21
  spec.add_development_dependency "activerecord", ">= 3.0.0"
26
- spec.add_development_dependency "factory_girl"
27
- spec.add_development_dependency "appraisal"
22
+ spec.add_development_dependency "bundler"
23
+ spec.add_development_dependency "factory_bot"
28
24
  spec.add_development_dependency "minitest"
25
+ spec.add_development_dependency "rake"
26
+ spec.add_development_dependency "rubocop"
29
27
  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
-
data/test/database.yml CHANGED
@@ -6,12 +6,15 @@ sqlite:
6
6
  mysql:
7
7
  adapter: mysql2
8
8
  database: search_cop
9
+ host: 127.0.0.1
9
10
  username: root
10
11
  encoding: utf8
11
12
 
12
13
  postgres:
14
+ host: 127.0.0.1
13
15
  adapter: postgresql
14
16
  database: search_cop
15
- username: postgres
17
+ username: search_cop
18
+ password: secret
16
19
  encoding: utf8
17
20
 
data/test/date_test.rb CHANGED
@@ -1,9 +1,8 @@
1
-
2
- require File.expand_path("../test_helper", __FILE__)
1
+ require File.expand_path("test_helper", __dir__)
3
2
 
4
3
  class DateTest < SearchCop::TestCase
5
4
  def test_mapping
6
- product = create(:product, :created_on => Date.parse("2014-05-01"))
5
+ product = create(:product, created_on: Date.parse("2014-05-01"))
7
6
 
8
7
  assert_includes Product.search("created_on: 2014"), product
9
8
  assert_includes Product.search("created_on: 2014-05"), product
@@ -11,61 +10,89 @@ class DateTest < SearchCop::TestCase
11
10
  end
12
11
 
13
12
  def test_anywhere
14
- product = create(:product, :created_on => Date.parse("2014-05-01"))
13
+ product = create(:product, created_on: Date.parse("2014-05-01"))
15
14
 
16
15
  assert_includes Product.search("2014-05-01"), product
17
16
  refute_includes Product.search("2014-05-02"), product
18
17
  end
19
18
 
20
19
  def test_includes
21
- product = create(:product, :created_on => Date.parse("2014-05-01"))
20
+ product = create(:product, created_on: Date.parse("2014-05-01"))
22
21
 
23
22
  assert_includes Product.search("created_on: 2014-05-01"), product
24
23
  refute_includes Product.search("created_on: 2014-05-02"), product
25
24
  end
26
25
 
27
26
  def test_equals
28
- product = create(:product, :created_on => Date.parse("2014-05-01"))
27
+ product = create(:product, created_on: Date.parse("2014-05-01"))
29
28
 
30
29
  assert_includes Product.search("created_on = 2014-05-01"), product
31
30
  refute_includes Product.search("created_on = 2014-05-02"), product
32
31
  end
33
32
 
34
33
  def test_equals_not
35
- product = create(:product, :created_on => Date.parse("2014-05-01"))
34
+ product = create(:product, created_on: Date.parse("2014-05-01"))
36
35
 
37
36
  assert_includes Product.search("created_on != 2014-05-02"), product
38
37
  refute_includes Product.search("created_on != 2014-05-01"), product
39
38
  end
40
39
 
41
40
  def test_greater
42
- product = create(:product, :created_on => Date.parse("2014-05-01"))
41
+ product = create(:product, created_on: Date.parse("2014-05-01"))
43
42
 
44
43
  assert_includes Product.search("created_on > 2014-04-01"), product
45
44
  refute_includes Product.search("created_on > 2014-05-01"), product
46
45
  end
47
46
 
48
47
  def test_greater_equals
49
- product = create(:product, :created_on => Date.parse("2014-05-01"))
48
+ product = create(:product, created_on: Date.parse("2014-05-01"))
50
49
 
51
50
  assert_includes Product.search("created_on >= 2014-05-01"), product
52
51
  refute_includes Product.search("created_on >= 2014-05-02"), product
53
52
  end
54
53
 
55
54
  def test_less
56
- product = create(:product, :created_on => Date.parse("2014-05-01"))
55
+ product = create(:product, created_on: Date.parse("2014-05-01"))
57
56
 
58
57
  assert_includes Product.search("created_on < 2014-05-02"), product
59
58
  refute_includes Product.search("created_on < 2014-05-01"), product
60
59
  end
61
60
 
62
61
  def test_less_equals
63
- product = create(:product, :created_on => Date.parse("2014-05-02"))
62
+ product = create(:product, created_on: Date.parse("2014-05-02"))
64
63
 
65
64
  assert_includes Product.search("created_on <= 2014-05-02"), product
66
65
  refute_includes Product.search("created_on <= 2014-05-01"), product
67
66
  end
68
67
 
68
+ def test_days_ago
69
+ product = create(:product, created_at: 2.days.ago.to_date)
70
+
71
+ assert_includes Product.search("created_at <= '1 day ago'"), product
72
+ refute_includes Product.search("created_at <= '3 days ago'"), product
73
+ end
74
+
75
+ def test_weeks_ago
76
+ product = create(:product, created_at: 2.weeks.ago.to_date)
77
+
78
+ assert_includes Product.search("created_at <= '1 weeks ago'"), product
79
+ refute_includes Product.search("created_at <= '3 weeks ago'"), product
80
+ end
81
+
82
+ def test_months_ago
83
+ product = create(:product, created_at: 2.months.ago.to_date)
84
+
85
+ assert_includes Product.search("created_at <= '1 months ago'"), product
86
+ refute_includes Product.search("created_at <= '3 months ago'"), product
87
+ end
88
+
89
+ def test_years_ago
90
+ product = create(:product, created_at: 2.years.ago.to_date)
91
+
92
+ assert_includes Product.search("created_at <= '1 years ago'"), product
93
+ refute_includes Product.search("created_at <= '3 years ago'"), product
94
+ end
95
+
69
96
  def test_no_overflow
70
97
  assert_nothing_raised do
71
98
  Product.search("created_on: 1000000").to_a
@@ -78,4 +105,3 @@ class DateTest < SearchCop::TestCase
78
105
  end
79
106
  end
80
107
  end
81
-