search_cop 1.0.9 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: ec8667b85e0e2df63ebb83e3d4af5319e52483b9
4
- data.tar.gz: 0a1fd1d2c76c9395ac50adc9e1139dbc58ea2cd5
2
+ SHA256:
3
+ metadata.gz: bf37a33f6c9998a09a09817fe5f14024947a1f83ed63682c066dcf35c385ac57
4
+ data.tar.gz: 36df6f6110f2126240702636d93c0ed54322794ff5f558fa6dcb27484c3b2b28
5
5
  SHA512:
6
- metadata.gz: ca9b2397fb9bd9b7e3b339f001c58b36c42be50f5a9314094735f6633bb75cb26a91b9e08120a10750653ae5067a82eda96d2a562cff31b92ea3aaa390487c51
7
- data.tar.gz: 6479e2365da9aa3ae8c9a9f6587c6f00a0f0c7d2546d05438dc4738eb810b5eed9a0d5ff432b91a1a9757486d99f795e86f556d55e379a4ce240ed21d89714de
6
+ metadata.gz: 4c5233ee11b096ec18e1a53f1e76a6dbb0880a99de9ac41ec836993f317612cf728d81a1332c3f7bfeb42766cb7e28e388061d85cabec8355e7c32e969468761
7
+ data.tar.gz: f766262804751519e3566ea80af0358dba148540883d74b5947804e70876ae90555dcb539032f5e59ff1d64fb03b6b10e293e487842b47eeea31246c14a69024
@@ -1,45 +1,34 @@
1
1
 
2
- addons:
3
- postgresql: "9.3"
2
+ services:
3
+ - postgresql
4
+ - mysql
4
5
 
5
6
  before_script:
6
7
  - mysql -e 'create database search_cop;'
7
- - psql -c 'create database search_cop;' -U postgres
8
+ - psql -c "create user search_cop password 'secret';" -U postgres
9
+ - psql -c 'create database search_cop owner search_cop;' -U postgres
8
10
 
9
- rvm:
10
- - 2.1.10
11
- - 2.2.5
12
- - 2.3.1
13
- - rbx-3
14
- - jruby
15
- env:
16
- - DATABASE=sqlite
17
- - DATABASE=mysql
18
- - DATABASE=postgres
19
11
  matrix:
20
12
  include:
21
- - rvm: 2.4.1
22
- gemfile: gemfiles/5.0-Gemfile
13
+ - rvm: ruby-head
14
+ gemfile: gemfiles/4.2.gemfile
23
15
  env: DATABASE=sqlite
24
- - rvm: 2.4.1
25
- gemfile: gemfiles/5.0-Gemfile
16
+ - rvm: ruby-head
17
+ gemfile: gemfiles/4.2.gemfile
26
18
  env: DATABASE=mysql
27
- - rvm: 2.4.1
28
- gemfile: gemfiles/5.0-Gemfile
19
+ - rvm: ruby-head
20
+ gemfile: gemfiles/5.1.gemfile
21
+ env: DATABASE=mysql
22
+ - rvm: ruby-head
23
+ gemfile: gemfiles/4.2.gemfile
24
+ env: DATABASE=postgres
25
+ - rvm: ruby-head
26
+ gemfile: gemfiles/5.1.gemfile
29
27
  env: DATABASE=postgres
30
- allow_failures:
31
- - rvm: rbx-3
32
- - rvm: jruby
33
-
34
- gemfile:
35
- - gemfiles/3.2.gemfile
36
- - gemfiles/4.0.gemfile
37
- - gemfiles/4.1.gemfile
38
- - gemfiles/4.2.gemfile
39
28
 
40
29
  install:
41
30
  - "travis_retry bundle install"
42
31
 
43
- script: "bundle exec rake test"
32
+ script: "rake test --trace"
44
33
  sudo: false
45
34
 
@@ -1,6 +1,18 @@
1
1
 
2
2
  # Changelog
3
3
 
4
+ Version 1.2.0:
5
+
6
+ * Adds customizable default operator to concatenate conditions (#49)
7
+ * Make the postgis adapter use the postgres extensions
8
+
9
+ Version 1.0.9:
10
+
11
+ * Use [:blank:] instead of \s for space (#46)
12
+ * Updated SearchCop::Visitors::Visitor to check the connection's adapter_name when extending. (#47)
13
+ * Fix for negative numeric values
14
+ * allow searching for relative dates, like hours, days, weeks, months or years ago
15
+
4
16
  Version 1.0.8:
5
17
 
6
18
  * No longer add search scope methods globally #34
data/Gemfile CHANGED
@@ -3,21 +3,9 @@ source 'https://rubygems.org'
3
3
  # Specify your gem's dependencies in search_cop.gemspec
4
4
  gemspec
5
5
 
6
- platforms :jruby do
7
- gem 'activerecord-jdbcmysql-adapter'
8
- gem 'activerecord-jdbcsqlite3-adapter'
9
- gem 'activerecord-jdbcpostgresql-adapter'
10
- end
11
-
12
6
  platforms :ruby do
13
7
  gem 'sqlite3'
14
8
  gem 'mysql2'
15
9
  gem 'pg'
16
10
  end
17
11
 
18
- platforms :rbx do
19
- gem 'racc'
20
- gem 'rubysl', '~> 2.0'
21
- gem 'psych'
22
- end
23
-
data/README.md CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  [![Build Status](https://secure.travis-ci.org/mrkamel/search_cop.png?branch=master)](http://travis-ci.org/mrkamel/search_cop)
4
4
  [![Code Climate](https://codeclimate.com/github/mrkamel/search_cop.png)](https://codeclimate.com/github/mrkamel/search_cop)
5
- [![Dependency Status](https://gemnasium.com/mrkamel/search_cop.png?travis)](https://gemnasium.com/mrkamel/search_cop)
6
5
  [![Gem Version](https://badge.fury.io/rb/search_cop.svg)](http://badge.fury.io/rb/search_cop)
7
6
 
8
7
  ![search_cop](https://raw.githubusercontent.com/mrkamel/search_cop_logo/master/search_cop.png)
@@ -118,6 +117,24 @@ or post-process the search results in every possible way:
118
117
  Book.where(available: true).search("Harry Potter").order("books.id desc").paginate(page: params[:page])
119
118
  ```
120
119
 
120
+ ## Security
121
+
122
+ When you pass a query string to SearchCop, it gets parsed, analyzed and mapped
123
+ to finally build up an SQL query. To be more precise, when SearchCop parses the
124
+ query, it creates objects (nodes), which represent the query expressions (And-,
125
+ Or-, Not-, String-, Date-, etc Nodes). To build the SQL query, SearchCop uses
126
+ the concept of visitors like e.g. used in
127
+ [Arel](https://github.com/rails/arel), such that, for every node there must be
128
+ a [visitor](https://github.com/mrkamel/search_cop/blob/master/lib/search_cop/visitors/visitor.rb),
129
+ which transforms the node to SQL. When there is no visitor, an exception is
130
+ raised when the query builder tries to "visit" the node. The visitors are
131
+ responsible for sanitizing the user supplied input. This is primilarly done via
132
+ quoting (string-, table-name-, column-quoting, etc). SearchCop is using the
133
+ methods provided by the ActiveRecord connection adapter for sanitizing/quoting
134
+ to prevent SQL injection. While we can never be 100% safe from security issues,
135
+ SearchCop takes security issues seriously. Please report responsibly via
136
+ security at flakks dot com in case you find any security related issues.
137
+
121
138
  ## Fulltext index capabilities
122
139
 
123
140
  By default, i.e. if you don't tell SearchCop about your fulltext indices,
@@ -283,7 +300,7 @@ For more details about PostgreSQL fulltext indices visit
283
300
 
284
301
  In case you expose non-fulltext attributes to search queries (price, stock,
285
302
  etc.), the respective queries, like `Book.search("stock > 0")`, will profit
286
- from from the usual non-fulltext indices. Thus, you should add a usual index on
303
+ from the usual non-fulltext indices. Thus, you should add a usual index on
287
304
  every column you expose to search queries plus a fulltext index for every
288
305
  fulltext attribute.
289
306
 
@@ -312,6 +329,41 @@ User.search("admin")
312
329
  # ... WHERE users.username LIKE 'admin%'
313
330
  ```
314
331
 
332
+ ## Default operator
333
+
334
+ When you define multiple fields on a search scope, SearcCop will use
335
+ by default the AND operator to concatenate the conditions, e.g:
336
+
337
+ ```ruby
338
+ class User < ActiveRecord::Base
339
+ include SearchCop
340
+
341
+ search_scope :search do
342
+ attributes :username, :fullname
343
+ end
344
+
345
+ # ...
346
+ end
347
+ ```
348
+
349
+ So a search like `User.search("something")` will generate a query
350
+ with the following conditions:
351
+
352
+ ```sql
353
+ ... WHERE username LIKE '%something%' AND fullname LIKE '%something%'
354
+ ```
355
+
356
+ However, there are cases where using AND as the default operator is not desired,
357
+ so SearchCop allows you to override it and use OR as the default operator instead.
358
+ A query like `User.search("something", default_operator: :or)` will
359
+ generate the query using OR to concatenate the conditions
360
+
361
+ ```sql
362
+ ... WHERE username LIKE '%something%' OR fullname LIKE '%something%'
363
+ ```
364
+
365
+ Finally, please note that you can apply it to fulltext indices/queries as well.
366
+
315
367
  ## Associations
316
368
 
317
369
  If you specify searchable attributes from another model, like
@@ -454,6 +506,10 @@ SearchCop also provides the ability to define custom operators by defining a
454
506
  search. This is useful when you want to use database operators that are not
455
507
  supported by SearchCop.
456
508
 
509
+ Please note, when using generators, you are responsible for sanitizing/quoting
510
+ the values (see example below). Otherwise your generator will allow SQL
511
+ injection. Thus, please only use generators if you know what you're doing.
512
+
457
513
  For example, if you wanted to perform a `LIKE` query where a book title starts
458
514
  with a string, you can define the search scope like so:
459
515
 
@@ -0,0 +1,11 @@
1
+
2
+ version: '2'
3
+ services:
4
+ mysql:
5
+ image: percona:5.7
6
+ environment:
7
+ - MYSQL_ALLOW_EMPTY_PASSWORD=yes
8
+ - MYSQL_ROOT_PASSWORD=
9
+ ports:
10
+ - 3306:3306
11
+
@@ -2,24 +2,12 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gem "activerecord", "~> 4.2.4"
6
-
7
- platforms :jruby do
8
- gem "activerecord-jdbcmysql-adapter"
9
- gem "activerecord-jdbcsqlite3-adapter"
10
- gem "activerecord-jdbcpostgresql-adapter"
11
- end
5
+ gem "activerecord", "~> 4.2"
12
6
 
13
7
  platforms :ruby do
14
- gem "sqlite3"
15
- gem "mysql2", "~> 0.3.20"
16
- gem "pg"
17
- end
18
-
19
- platforms :rbx do
20
- gem "racc"
21
- gem "rubysl", "~> 2.0"
22
- gem "psych"
8
+ gem "sqlite3", "1.3.13"
9
+ gem "mysql2", "0.4.10"
10
+ gem "pg", "0.21.0"
23
11
  end
24
12
 
25
13
  gemspec :path => "../"
@@ -0,0 +1,13 @@
1
+ # This file was generated by Appraisal
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gem "activerecord", "~> 5.1"
6
+
7
+ platforms :ruby do
8
+ gem "sqlite3"
9
+ gem "mysql2"
10
+ gem "pg"
11
+ end
12
+
13
+ gemspec :path => "../"
@@ -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
26
  SearchCop::HashParser.new(query_info).parse(query)
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
+ self.send(:define_singleton_method, name) { |query, query_options={}| search_cop query, name, query_options }
48
+ self.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?
@@ -66,5 +69,22 @@ module SearchCop
66
69
  (scope || self).where(query_builder.sql)
67
70
  end
68
71
  end
72
+
73
+ module Helpers
74
+ def self.sanitize_default_operator(hash, delete_hash_option=false)
75
+ default_operator = :and
76
+ if hash.member?(:default_operator)
77
+ unless [:and, :or].include?(hash[:default_operator])
78
+ raise(SearchCop::UnknownDefaultOperator, "Unknown default operator value #{hash[:default_operator]}")
79
+ end
80
+
81
+ default_operator = hash[:default_operator]
82
+ end
83
+
84
+ hash.delete(:default_operator) if delete_hash_option
85
+
86
+ default_operator
87
+ end
88
+ end
69
89
  end
70
90
 
@@ -12,9 +12,10 @@ module SearchCop
12
12
  @query_info = query_info
13
13
  end
14
14
 
15
- def parse(string)
15
+ def parse(string, query_options)
16
16
  node = SearchCopGrammarParser.new.parse(string) || raise(ParseError)
17
17
  node.query_info = query_info
18
+ node.query_options = query_options
18
19
  node.evaluate
19
20
  end
20
21
  end
@@ -7,6 +7,8 @@ class SearchCop::HashParser
7
7
  end
8
8
 
9
9
  def parse(hash)
10
+ default_operator = SearchCop::Helpers.sanitize_default_operator(hash, true)
11
+
10
12
  res = hash.collect do |key, value|
11
13
  case key
12
14
  when :and
@@ -22,7 +24,7 @@ class SearchCop::HashParser
22
24
  end
23
25
  end
24
26
 
25
- res.inject :and
27
+ res.inject default_operator
26
28
  end
27
29
 
28
30
  private
@@ -3,11 +3,11 @@ module SearchCop
3
3
  class QueryBuilder
4
4
  attr_accessor :query_info, :scope, :sql
5
5
 
6
- def initialize(model, query, scope)
6
+ def initialize(model, query, scope, query_options)
7
7
  self.scope = scope
8
8
  self.query_info = QueryInfo.new(model, scope)
9
9
 
10
- arel = SearchCop::Parser.parse(query, query_info).optimize!
10
+ arel = SearchCop::Parser.parse(query, query_info, query_options).optimize!
11
11
 
12
12
  self.sql = SearchCop::Visitors::Visitor.new(model.connection).visit(arel)
13
13
  end
@@ -1,3 +1,3 @@
1
1
  module SearchCop
2
- VERSION = "1.0.9"
2
+ VERSION = "1.1.0"
3
3
  end
@@ -7,8 +7,8 @@ module SearchCop
7
7
  def initialize(connection)
8
8
  @connection = connection
9
9
 
10
- extend(SearchCop::Visitors::Mysql) if @connection.class.name =~ /mysql/i
11
- extend(SearchCop::Visitors::Postgres) if @connection.class.name =~ /postgres/i
10
+ extend(SearchCop::Visitors::Mysql) if @connection.adapter_name =~ /mysql/i
11
+ extend(SearchCop::Visitors::Postgres) if @connection.adapter_name =~ /postgres|postgis/i
12
12
  end
13
13
 
14
14
  def visit(visit_node = node)
@@ -4,12 +4,16 @@ require "search_cop_grammar/nodes"
4
4
 
5
5
  module SearchCopGrammar
6
6
  class BaseNode < Treetop::Runtime::SyntaxNode
7
- attr_accessor :query_info
7
+ attr_writer :query_info, :query_options
8
8
 
9
9
  def query_info
10
10
  (@query_info ||= nil) || parent.query_info
11
11
  end
12
12
 
13
+ def query_options
14
+ (@query_options ||= nil) || parent.query_options
15
+ end
16
+
13
17
  def evaluate
14
18
  elements.collect(&:evaluate).inject(:and)
15
19
  end
@@ -110,6 +114,13 @@ module SearchCopGrammar
110
114
  end
111
115
  end
112
116
 
117
+ class AndOrExpression < BaseNode
118
+ def evaluate
119
+ default_operator = SearchCop::Helpers.sanitize_default_operator(query_options)
120
+ [elements.first.evaluate, elements.last.evaluate].inject(default_operator)
121
+ end
122
+ end
123
+
113
124
  class OrExpression < BaseNode
114
125
  def evaluate
115
126
  [elements.first.evaluate, elements.last.evaluate].inject(:or)
@@ -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
 
@@ -176,7 +176,7 @@ module SearchCopGrammar
176
176
 
177
177
  class Float < WithoutMatches
178
178
  def compatible?(value)
179
- return true if value.to_s =~ /^[0-9]+(\.[0-9]+)?$/
179
+ return true if value.to_s =~ /^\-?[0-9]+(\.[0-9]+)?$/
180
180
 
181
181
  false
182
182
  end
@@ -198,7 +198,11 @@ module SearchCopGrammar
198
198
  def parse(value)
199
199
  return value .. value unless value.is_a?(::String)
200
200
 
201
- if value =~ /^[0-9]{4}$/
201
+ if value =~ /^[0-9]+ (hour|day|week|month|year)s{0,1} (ago)$/
202
+ number,period,ago = value.split(' ')
203
+ time = number.to_i.send(period.to_sym).send(ago.to_sym)
204
+ time .. ::Time.now
205
+ elsif value =~ /^[0-9]{4}$/
202
206
  ::Time.new(value).beginning_of_year .. ::Time.new(value).end_of_year
203
207
  elsif value =~ /^([0-9]{4})(\.|-|\/)([0-9]{1,2})$/
204
208
  ::Time.new($1, $3, 15).beginning_of_month .. ::Time.new($1, $3, 15).end_of_month
@@ -244,7 +248,11 @@ module SearchCopGrammar
244
248
  def parse(value)
245
249
  return value .. value unless value.is_a?(::String)
246
250
 
247
- if value =~ /^[0-9]{4}$/
251
+ if value =~ /^[0-9]+ (day|week|month|year)s{0,1} (ago)$/
252
+ number,period,ago = value.split(' ')
253
+ time = number.to_i.send(period.to_sym).send(ago.to_sym)
254
+ time.to_date .. ::Date.today
255
+ elsif value =~ /^[0-9]{4}$/
248
256
  ::Date.new(value.to_i).beginning_of_year .. ::Date.new(value.to_i).end_of_year
249
257
  elsif value =~ /^([0-9]{4})(\.|-|\/)([0-9]{1,2})$/
250
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
@@ -23,7 +23,6 @@ Gem::Specification.new do |spec|
23
23
  spec.add_development_dependency "bundler"
24
24
  spec.add_development_dependency "rake"
25
25
  spec.add_development_dependency "activerecord", ">= 3.0.0"
26
- spec.add_development_dependency "factory_girl"
27
- spec.add_development_dependency "appraisal"
26
+ spec.add_development_dependency "factory_bot"
28
27
  spec.add_development_dependency "minitest"
29
28
  end
@@ -3,7 +3,7 @@ require File.expand_path("../test_helper", __FILE__)
3
3
 
4
4
  class AndTest < SearchCop::TestCase
5
5
  def test_and_string
6
- expected = create(:product, :title => "Expected title", :description => "Description")
6
+ expected = create(:product, :title => "expected title", :description => "Description")
7
7
  rejected = create(:product, :title => "Rejected title", :description => "Description")
8
8
 
9
9
  results = Product.search("title: 'Expected title' description: Description")
@@ -10,8 +10,10 @@ mysql:
10
10
  encoding: utf8
11
11
 
12
12
  postgres:
13
+ host: localhost
13
14
  adapter: postgresql
14
15
  database: search_cop
15
- username: postgres
16
+ username: search_cop
17
+ password: secret
16
18
  encoding: utf8
17
19
 
@@ -66,6 +66,34 @@ class DateTest < SearchCop::TestCase
66
66
  refute_includes Product.search("created_on <= 2014-05-01"), product
67
67
  end
68
68
 
69
+ def test_days_ago
70
+ product = create(:product, :created_at => 2.days.ago.to_date)
71
+
72
+ assert_includes Product.search("created_at <= '1 day ago'"), product
73
+ refute_includes Product.search("created_at <= '3 days ago'"), product
74
+ end
75
+
76
+ def test_weeks_ago
77
+ product = create(:product, :created_at => 2.weeks.ago.to_date)
78
+
79
+ assert_includes Product.search("created_at <= '1 weeks ago'"), product
80
+ refute_includes Product.search("created_at <= '3 weeks ago'"), product
81
+ end
82
+
83
+ def test_months_ago
84
+ product = create(:product, :created_at => 2.months.ago.to_date)
85
+
86
+ assert_includes Product.search("created_at <= '1 months ago'"), product
87
+ refute_includes Product.search("created_at <= '3 months ago'"), product
88
+ end
89
+
90
+ def test_years_ago
91
+ product = create(:product, :created_at => 2.years.ago.to_date)
92
+
93
+ assert_includes Product.search("created_at <= '1 years ago'"), product
94
+ refute_includes Product.search("created_at <= '3 years ago'"), product
95
+ end
96
+
69
97
  def test_no_overflow
70
98
  assert_nothing_raised do
71
99
  Product.search("created_on: 1000000").to_a
@@ -67,6 +67,41 @@ class DatetimeTest < SearchCop::TestCase
67
67
  refute_includes Product.search("created_at <= 2014-05-01"), product
68
68
  end
69
69
 
70
+ def test_hours_ago
71
+ product = create(:product, :created_at => 5.hours.ago)
72
+
73
+ assert_includes Product.search("created_at <= '4 hours ago'"), product
74
+ refute_includes Product.search("created_at <= '6 hours ago'"), product
75
+ end
76
+
77
+ def test_days_ago
78
+ product = create(:product, :created_at => 2.days.ago)
79
+
80
+ assert_includes Product.search("created_at <= '1 day ago'"), product
81
+ refute_includes Product.search("created_at <= '3 days ago'"), product
82
+ end
83
+
84
+ def test_weeks_ago
85
+ product = create(:product, :created_at => 2.weeks.ago)
86
+
87
+ assert_includes Product.search("created_at <= '1 weeks ago'"), product
88
+ refute_includes Product.search("created_at <= '3 weeks ago'"), product
89
+ end
90
+
91
+ def test_months_ago
92
+ product = create(:product, :created_at => 2.months.ago)
93
+
94
+ assert_includes Product.search("created_at <= '1 months ago'"), product
95
+ refute_includes Product.search("created_at <= '3 months ago'"), product
96
+ end
97
+
98
+ def test_years_ago
99
+ product = create(:product, :created_at => 2.years.ago)
100
+
101
+ assert_includes Product.search("created_at <= '1 years ago'"), product
102
+ refute_includes Product.search("created_at <= '3 years ago'"), product
103
+ end
104
+
70
105
  def test_no_overflow
71
106
  assert_nothing_raised do
72
107
  Product.search("created_at: 1000000").to_a
@@ -0,0 +1,47 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class DefaultOperatorTest < SearchCop::TestCase
4
+
5
+ def test_without_default_operator
6
+ avengers = create(:product, title: "Title Avengers", description: "2012")
7
+ inception = create(:product, title: "Title Inception", description: "2010")
8
+
9
+ results = Product.search_multi_columns("Title Avengers")
10
+ assert_includes results, avengers
11
+ refute_includes results, inception
12
+
13
+ results = Product.search_multi_columns("Title AND Avengers")
14
+ assert_includes results, avengers
15
+ refute_includes results, inception
16
+
17
+ results = Product.search_multi_columns("Title OR Avengers")
18
+ assert_includes results, avengers
19
+ assert_includes results, inception
20
+
21
+ results = Product.search(title: "Avengers", description: "2012")
22
+ assert_includes results, avengers
23
+ refute_includes results, inception
24
+ end
25
+
26
+ def test_with_specific_default_operator
27
+ matrix = create(:product, title: "Matrix", description: "1999 Fantasy Sci-fi 2h 30m")
28
+ start_wars = create(:product, title: "Start Wars", description: "2010 Sci-fi Thriller 2h 28m")
29
+
30
+ results = Product.search_multi_columns("Matrix movie", default_operator: :or)
31
+ assert_includes results, matrix
32
+ refute_includes results, start_wars
33
+
34
+ results = Product.search(title: "Matrix", description: "2000", default_operator: :or)
35
+ assert_includes results, matrix
36
+ refute_includes results, start_wars
37
+ end
38
+
39
+ def test_with_invalid_default_operator
40
+ assert_raises SearchCop::UnknownDefaultOperator do
41
+ Product.search_multi_columns('Matrix movie', default_operator: :xpto)
42
+ end
43
+ assert_raises SearchCop::UnknownDefaultOperator do
44
+ Product.search_multi_columns(title: 'Matrix movie', default_operator: :xpto)
45
+ end
46
+ end
47
+ end
@@ -58,6 +58,13 @@ class FloatTest < SearchCop::TestCase
58
58
  refute_includes Product.search("price <= 10.4"), product
59
59
  end
60
60
 
61
+ def test_negative
62
+ product = create(:product, price: -10)
63
+
64
+ assert_includes Product.search("price = -10"), product
65
+ refute_includes Product.search("price = -11"), product
66
+ end
67
+
61
68
  def test_incompatible_datatype
62
69
  assert_raises SearchCop::IncompatibleDatatype do
63
70
  Product.unsafe_search "price: Value"
@@ -133,8 +133,7 @@ class SearchCopTest < SearchCop::TestCase
133
133
  end
134
134
 
135
135
  def test_not_adding_search_to_object
136
- Product
137
- assert_equal false, Object.respond_to?(:search)
136
+ refute Object.respond_to?(:search)
138
137
  end
139
138
  end
140
139
 
@@ -13,7 +13,7 @@ end
13
13
 
14
14
  require "minitest/autorun"
15
15
  require "active_record"
16
- require "factory_girl"
16
+ require "factory_bot"
17
17
  require "yaml"
18
18
 
19
19
  DATABASE = ENV["DATABASE"] || "sqlite"
@@ -68,13 +68,17 @@ class Product < ActiveRecord::Base
68
68
  aliases :users_products => User
69
69
  end
70
70
 
71
+ search_scope :search_multi_columns do
72
+ attributes all: [:title, :description]
73
+ end
74
+
71
75
  has_many :comments
72
76
  has_many :users, :through => :comments
73
77
 
74
78
  belongs_to :user
75
79
  end
76
80
 
77
- FactoryGirl.define do
81
+ FactoryBot.define do
78
82
  factory :product do
79
83
  end
80
84
 
@@ -122,7 +126,7 @@ if DATABASE == "mysql"
122
126
  end
123
127
 
124
128
  class SearchCop::TestCase
125
- include FactoryGirl::Syntax::Methods
129
+ include FactoryBot::Syntax::Methods
126
130
 
127
131
  def teardown
128
132
  Product.delete_all
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: search_cop
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.9
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin Vetter
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-07-12 00:00:00.000000000 Z
11
+ date: 2019-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: treetop
@@ -67,21 +67,7 @@ dependencies:
67
67
  - !ruby/object:Gem::Version
68
68
  version: 3.0.0
69
69
  - !ruby/object:Gem::Dependency
70
- name: factory_girl
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: appraisal
70
+ name: factory_bot
85
71
  requirement: !ruby/object:Gem::Requirement
86
72
  requirements:
87
73
  - - ">="
@@ -117,7 +103,6 @@ extra_rdoc_files: []
117
103
  files:
118
104
  - ".gitignore"
119
105
  - ".travis.yml"
120
- - Appraisals
121
106
  - CHANGELOG.md
122
107
  - CONTRIBUTING.md
123
108
  - Gemfile
@@ -125,11 +110,9 @@ files:
125
110
  - MIGRATION.md
126
111
  - README.md
127
112
  - Rakefile
128
- - gemfiles/3.2.gemfile
129
- - gemfiles/4.0.gemfile
130
- - gemfiles/4.1.gemfile
113
+ - docker-compose.yml
131
114
  - gemfiles/4.2.gemfile
132
- - gemfiles/5.0.gemfile
115
+ - gemfiles/5.1.gemfile
133
116
  - lib/search_cop.rb
134
117
  - lib/search_cop/grammar_parser.rb
135
118
  - lib/search_cop/hash_parser.rb
@@ -151,6 +134,7 @@ files:
151
134
  - test/database.yml
152
135
  - test/date_test.rb
153
136
  - test/datetime_test.rb
137
+ - test/default_operator_test.rb
154
138
  - test/error_test.rb
155
139
  - test/float_test.rb
156
140
  - test/fulltext_test.rb
@@ -183,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
183
167
  version: '0'
184
168
  requirements: []
185
169
  rubyforge_project:
186
- rubygems_version: 2.2.2
170
+ rubygems_version: 2.7.3
187
171
  signing_key:
188
172
  specification_version: 4
189
173
  summary: Easily perform complex search engine like fulltext queries on your ActiveRecord
@@ -194,6 +178,7 @@ test_files:
194
178
  - test/database.yml
195
179
  - test/date_test.rb
196
180
  - test/datetime_test.rb
181
+ - test/default_operator_test.rb
197
182
  - test/error_test.rb
198
183
  - test/float_test.rb
199
184
  - test/fulltext_test.rb
data/Appraisals DELETED
@@ -1,21 +0,0 @@
1
-
2
- appraise "3.2" do
3
- gem "activerecord", "~> 3.2"
4
- gem "search_cop", :path => "../"
5
- end
6
-
7
- appraise "4.1" do
8
- gem "activerecord", "~> 4.1"
9
- gem "search_cop", :path => "../"
10
- end
11
-
12
- appraise "4.2" do
13
- gem "activerecord", "~> 4.2"
14
- gem "search_cop", :path => "../"
15
- end
16
-
17
- appraise "5.0" do
18
- gem "activerecord", "~> 5.0"
19
- gem "search_cop", :path => "../"
20
- end
21
-
@@ -1,25 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "activerecord", "~> 3.2.22"
6
-
7
- platforms :jruby do
8
- gem "activerecord-jdbcmysql-adapter"
9
- gem "activerecord-jdbcsqlite3-adapter"
10
- gem "activerecord-jdbcpostgresql-adapter"
11
- end
12
-
13
- platforms :ruby do
14
- gem "sqlite3"
15
- gem "mysql2", "~> 0.3.20"
16
- gem "pg"
17
- end
18
-
19
- platforms :rbx do
20
- gem "racc"
21
- gem "rubysl", "~> 2.0"
22
- gem "psych"
23
- end
24
-
25
- gemspec :path => "../"
@@ -1,25 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "activerecord", "~> 4.0.13"
6
-
7
- platforms :jruby do
8
- gem "activerecord-jdbcmysql-adapter"
9
- gem "activerecord-jdbcsqlite3-adapter"
10
- gem "activerecord-jdbcpostgresql-adapter"
11
- end
12
-
13
- platforms :ruby do
14
- gem "sqlite3"
15
- gem "mysql2", "~> 0.3.20"
16
- gem "pg"
17
- end
18
-
19
- platforms :rbx do
20
- gem "racc"
21
- gem "rubysl", "~> 2.0"
22
- gem "psych"
23
- end
24
-
25
- gemspec :path => "../"
@@ -1,25 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "activerecord", "~> 4.1.13"
6
-
7
- platforms :jruby do
8
- gem "activerecord-jdbcmysql-adapter"
9
- gem "activerecord-jdbcsqlite3-adapter"
10
- gem "activerecord-jdbcpostgresql-adapter"
11
- end
12
-
13
- platforms :ruby do
14
- gem "sqlite3"
15
- gem "mysql2", "~> 0.3.20"
16
- gem "pg"
17
- end
18
-
19
- platforms :rbx do
20
- gem "racc"
21
- gem "rubysl", "~> 2.0"
22
- gem "psych"
23
- end
24
-
25
- gemspec :path => "../"
@@ -1,25 +0,0 @@
1
- # This file was generated by Appraisal
2
-
3
- source "https://rubygems.org"
4
-
5
- gem "activerecord", "~> 5.0"
6
-
7
- platforms :jruby do
8
- gem "activerecord-jdbcmysql-adapter"
9
- gem "activerecord-jdbcsqlite3-adapter"
10
- gem "activerecord-jdbcpostgresql-adapter"
11
- end
12
-
13
- platforms :ruby do
14
- gem "sqlite3"
15
- gem "mysql2", "~> 0.3.20"
16
- gem "pg"
17
- end
18
-
19
- platforms :rbx do
20
- gem "racc"
21
- gem "rubysl", "~> 2.0"
22
- gem "psych"
23
- end
24
-
25
- gemspec :path => "../"