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 +5 -5
- data/.travis.yml +18 -29
- data/CHANGELOG.md +12 -0
- data/Gemfile +0 -12
- data/README.md +58 -2
- data/docker-compose.yml +11 -0
- data/gemfiles/4.2.gemfile +4 -16
- data/gemfiles/5.1.gemfile +13 -0
- data/lib/search_cop.rb +30 -10
- data/lib/search_cop/grammar_parser.rb +2 -1
- data/lib/search_cop/hash_parser.rb +3 -1
- data/lib/search_cop/query_builder.rb +2 -2
- data/lib/search_cop/version.rb +1 -1
- data/lib/search_cop/visitors/visitor.rb +2 -2
- data/lib/search_cop_grammar.rb +12 -1
- data/lib/search_cop_grammar.treetop +6 -4
- data/lib/search_cop_grammar/attributes.rb +11 -3
- data/search_cop.gemspec +1 -2
- data/test/and_test.rb +1 -1
- data/test/database.yml +3 -1
- data/test/date_test.rb +28 -0
- data/test/datetime_test.rb +35 -0
- data/test/default_operator_test.rb +47 -0
- data/test/float_test.rb +7 -0
- data/test/search_cop_test.rb +1 -2
- data/test/test_helper.rb +7 -3
- metadata +8 -23
- data/Appraisals +0 -21
- data/gemfiles/3.2.gemfile +0 -25
- data/gemfiles/4.0.gemfile +0 -25
- data/gemfiles/4.1.gemfile +0 -25
- data/gemfiles/5.0.gemfile +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bf37a33f6c9998a09a09817fe5f14024947a1f83ed63682c066dcf35c385ac57
|
4
|
+
data.tar.gz: 36df6f6110f2126240702636d93c0ed54322794ff5f558fa6dcb27484c3b2b28
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4c5233ee11b096ec18e1a53f1e76a6dbb0880a99de9ac41ec836993f317612cf728d81a1332c3f7bfeb42766cb7e28e388061d85cabec8355e7c32e969468761
|
7
|
+
data.tar.gz: f766262804751519e3566ea80af0358dba148540883d74b5947804e70876ae90555dcb539032f5e59ff1d64fb03b6b10e293e487842b47eeea31246c14a69024
|
data/.travis.yml
CHANGED
@@ -1,45 +1,34 @@
|
|
1
1
|
|
2
|
-
|
3
|
-
postgresql
|
2
|
+
services:
|
3
|
+
- postgresql
|
4
|
+
- mysql
|
4
5
|
|
5
6
|
before_script:
|
6
7
|
- mysql -e 'create database search_cop;'
|
7
|
-
- psql -c
|
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:
|
22
|
-
gemfile: gemfiles/
|
13
|
+
- rvm: ruby-head
|
14
|
+
gemfile: gemfiles/4.2.gemfile
|
23
15
|
env: DATABASE=sqlite
|
24
|
-
- rvm:
|
25
|
-
gemfile: gemfiles/
|
16
|
+
- rvm: ruby-head
|
17
|
+
gemfile: gemfiles/4.2.gemfile
|
26
18
|
env: DATABASE=mysql
|
27
|
-
- rvm:
|
28
|
-
gemfile: gemfiles/5.
|
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: "
|
32
|
+
script: "rake test --trace"
|
44
33
|
sudo: false
|
45
34
|
|
data/CHANGELOG.md
CHANGED
@@ -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
|
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
|
|
data/docker-compose.yml
ADDED
data/gemfiles/4.2.gemfile
CHANGED
@@ -2,24 +2,12 @@
|
|
2
2
|
|
3
3
|
source "https://rubygems.org"
|
4
4
|
|
5
|
-
gem "activerecord", "~> 4.2
|
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", "
|
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 => "../"
|
data/lib/search_cop.rb
CHANGED
@@ -8,21 +8,24 @@ require "search_cop/hash_parser"
|
|
8
8
|
require "search_cop/visitors"
|
9
9
|
|
10
10
|
module SearchCop
|
11
|
-
class
|
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 <
|
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
|
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
|
data/lib/search_cop/version.rb
CHANGED
@@ -7,8 +7,8 @@ module SearchCop
|
|
7
7
|
def initialize(connection)
|
8
8
|
@connection = connection
|
9
9
|
|
10
|
-
extend(SearchCop::Visitors::Mysql) if @connection.
|
11
|
-
extend(SearchCop::Visitors::Postgres) if @connection.
|
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)
|
data/lib/search_cop_grammar.rb
CHANGED
@@ -4,12 +4,16 @@ require "search_cop_grammar/nodes"
|
|
4
4
|
|
5
5
|
module SearchCopGrammar
|
6
6
|
class BaseNode < Treetop::Runtime::SyntaxNode
|
7
|
-
|
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
|
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> / [
|
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> / [
|
50
|
+
"'" [^\']* "'" <SingleQuotedValue> / '"' [^\"]* '"' <DoubleQuotedValue> / [^[:blank:]()]+ <Value>
|
49
51
|
end
|
50
52
|
|
51
53
|
rule space
|
52
|
-
[
|
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 =~
|
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]{
|
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]{
|
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
|
data/search_cop.gemspec
CHANGED
@@ -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 "
|
27
|
-
spec.add_development_dependency "appraisal"
|
26
|
+
spec.add_development_dependency "factory_bot"
|
28
27
|
spec.add_development_dependency "minitest"
|
29
28
|
end
|
data/test/and_test.rb
CHANGED
@@ -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 => "
|
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")
|
data/test/database.yml
CHANGED
data/test/date_test.rb
CHANGED
@@ -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
|
data/test/datetime_test.rb
CHANGED
@@ -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
|
data/test/float_test.rb
CHANGED
@@ -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"
|
data/test/search_cop_test.rb
CHANGED
data/test/test_helper.rb
CHANGED
@@ -13,7 +13,7 @@ end
|
|
13
13
|
|
14
14
|
require "minitest/autorun"
|
15
15
|
require "active_record"
|
16
|
-
require "
|
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
|
-
|
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
|
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
|
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:
|
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:
|
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
|
-
-
|
129
|
-
- gemfiles/4.0.gemfile
|
130
|
-
- gemfiles/4.1.gemfile
|
113
|
+
- docker-compose.yml
|
131
114
|
- gemfiles/4.2.gemfile
|
132
|
-
- gemfiles/5.
|
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.
|
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
|
-
|
data/gemfiles/3.2.gemfile
DELETED
@@ -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 => "../"
|
data/gemfiles/4.0.gemfile
DELETED
@@ -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 => "../"
|
data/gemfiles/4.1.gemfile
DELETED
@@ -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 => "../"
|
data/gemfiles/5.0.gemfile
DELETED
@@ -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 => "../"
|