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 +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
|
[](http://travis-ci.org/mrkamel/search_cop)
|
4
4
|
[](https://codeclimate.com/github/mrkamel/search_cop)
|
5
|
-
[](https://gemnasium.com/mrkamel/search_cop)
|
6
5
|
[](http://badge.fury.io/rb/search_cop)
|
7
6
|
|
8
7
|

|
@@ -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 => "../"
|