search_cop 1.0.9 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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 => "../"