search_cop 1.0.8 → 1.0.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d4a775b683b6b8ac71ce65abf70db3c1765bc7af
4
- data.tar.gz: b6ddb81eb3bb8655cfe5db72ed4b15643591cc02
3
+ metadata.gz: ec8667b85e0e2df63ebb83e3d4af5319e52483b9
4
+ data.tar.gz: 0a1fd1d2c76c9395ac50adc9e1139dbc58ea2cd5
5
5
  SHA512:
6
- metadata.gz: 0eddb0f8dabfe5628931ecf79f2663c58b76e0e373beb77dc6e3e0dbf7b1020e415494bcd00422bc9ff7cd991b4b9acfd0ca469819dab48fa80ebb00f0bc7677
7
- data.tar.gz: 3c7abb35d981b0c8e5068eefdcf4096b66bc785fea90c161ff0c5de453d0dd766753ebf8863415f4ee9eac791490dbe357343f87d61b25f47245d0ef593020fb
6
+ metadata.gz: ca9b2397fb9bd9b7e3b339f001c58b36c42be50f5a9314094735f6633bb75cb26a91b9e08120a10750653ae5067a82eda96d2a562cff31b92ea3aaa390487c51
7
+ data.tar.gz: 6479e2365da9aa3ae8c9a9f6587c6f00a0f0c7d2546d05438dc4738eb810b5eed9a0d5ff432b91a1a9757486d99f795e86f556d55e379a4ce240ed21d89714de
data/.travis.yml CHANGED
@@ -18,13 +18,13 @@ env:
18
18
  - DATABASE=postgres
19
19
  matrix:
20
20
  include:
21
- - rvm: 2.3.1
21
+ - rvm: 2.4.1
22
22
  gemfile: gemfiles/5.0-Gemfile
23
23
  env: DATABASE=sqlite
24
- - rvm: 2.3.1
24
+ - rvm: 2.4.1
25
25
  gemfile: gemfiles/5.0-Gemfile
26
26
  env: DATABASE=mysql
27
- - rvm: 2.3.1
27
+ - rvm: 2.4.1
28
28
  gemfile: gemfiles/5.0-Gemfile
29
29
  env: DATABASE=postgres
30
30
  allow_failures:
data/README.md CHANGED
@@ -29,16 +29,17 @@ to make optimal use of them. Read more below.
29
29
  Complex hash-based queries are supported as well:
30
30
 
31
31
  ```ruby
32
- Book.search(:author => "Rowling", :title => "Harry Potter")
33
- Book.search(:or => [{:author => "Rowling"}, {:author => "Tolkien"}])
34
- Book.search(:and => [{:price => {:gt => 10}}, {:not => {:stock => 0}}, :or => [{:title => "Potter"}, {:author => "Rowling"}]])
35
- Book.search(:or => [{:query => "Rowling -Potter"}, {:query => "Tolkien -Rings"}])
32
+ Book.search(author: "Rowling", title: "Harry Potter")
33
+ Book.search(or: [{author: "Rowling"}, {author: "Tolkien"}])
34
+ Book.search(and: [{price: {gt: 10}}, {not: {stock: 0}}, or: [{title: "Potter"}, {author: "Rowling"}]])
35
+ Book.search(or: [{query: "Rowling -Potter"}, {query: "Tolkien -Rings"}])
36
+ Book.search(title: {my_custom_sql_query: "Rowl"}})
36
37
  # ...
37
38
  ```
38
39
 
39
40
  ## Installation
40
41
 
41
- For Rails/ActiveRecord 3 (or 4), add this line to your application's Gemfile:
42
+ Add this line to your application's Gemfile:
42
43
 
43
44
  gem 'search_cop'
44
45
 
@@ -61,8 +62,8 @@ class Book < ActiveRecord::Base
61
62
 
62
63
  search_scope :search do
63
64
  attributes :title, :description, :stock, :price, :created_at, :available
64
- attributes :comment => ["comments.title", "comments.message"]
65
- attributes :author => "author.name"
65
+ attributes comment: ["comments.title", "comments.message"]
66
+ attributes author: "author.name"
66
67
  # ...
67
68
  end
68
69
 
@@ -114,7 +115,7 @@ As `Book.search(...)` returns an `ActiveRecord::Relation`, you are free to pre-
114
115
  or post-process the search results in every possible way:
115
116
 
116
117
  ```ruby
117
- Book.where(:available => true).search("Harry Potter").order("books.id desc").paginate(:page => params[:page])
118
+ Book.where(available: true).search("Harry Potter").order("books.id desc").paginate(page: params[:page])
118
119
  ```
119
120
 
120
121
  ## Fulltext index capabilities
@@ -164,12 +165,12 @@ search in, such that SearchCop must no longer search within all fields:
164
165
 
165
166
  ```ruby
166
167
  search_scope :search do
167
- attributes :all => [:author, :title]
168
+ attributes all: [:author, :title]
168
169
 
169
- options :all, :type => :fulltext, :default => true
170
+ options :all, :type => :fulltext, default: true
170
171
 
171
- # Use :default => true to explicitly enable fields as default fields (whitelist approach)
172
- # Use :default => false to explicitly disable fields as default fields (blacklist approach)
172
+ # Use default: true to explicitly enable fields as default fields (whitelist approach)
173
+ # Use default: false to explicitly disable fields as default fields (blacklist approach)
173
174
  end
174
175
  ```
175
176
 
@@ -298,7 +299,7 @@ class User < ActiveRecord::Base
298
299
  search_scope :search do
299
300
  attributes :username
300
301
 
301
- options :username, :left_wildcard => false
302
+ options :username, left_wildcard: false
302
303
  end
303
304
 
304
305
  # ...
@@ -322,7 +323,7 @@ class Book < ActiveRecord::Base
322
323
  belongs_to :author
323
324
 
324
325
  search_scope :search do
325
- attributes :author => "author.name"
326
+ attributes author: "author.name"
326
327
  end
327
328
 
328
329
  # ...
@@ -356,13 +357,13 @@ class Book < ActiveRecord::Base
356
357
  # ...
357
358
 
358
359
  search_scope :search do
359
- attributes :similar => ["similar_books.title", "similar_books.description"]
360
+ attributes similar: ["similar_books.title", "similar_books.description"]
360
361
 
361
362
  scope do
362
363
  joins "left outer join books similar_books on ..."
363
364
  end
364
365
 
365
- aliases :similar_books => Book # Tell SearchCop how to map SQL aliases to models
366
+ aliases similar_books: Book # Tell SearchCop how to map SQL aliases to models
366
367
  end
367
368
 
368
369
  # ...
@@ -379,7 +380,7 @@ class Book < ActiveRecord::Base
379
380
  has_many :users, :through => :comments
380
381
 
381
382
  search_scope :search do
382
- attributes :user => "users.username"
383
+ attributes user: "users.username"
383
384
  end
384
385
 
385
386
  # ...
@@ -402,7 +403,7 @@ class Book < ActiveRecord::Base
402
403
  belongs_to :user
403
404
 
404
405
  search_scope :search do
405
- attributes :user => ["user.username", "users_books.username"]
406
+ attributes user: ["user.username", "users_books.username"]
406
407
  end
407
408
 
408
409
  # ...
@@ -440,12 +441,44 @@ Query string queries support `AND/and`, `OR/or`, `:`, `=`, `!=`, `<`, `<=`,
440
441
  and `matches`, `OR` has precedence over `AND`. `NOT` can only be used as infix
441
442
  operator regarding a single attribute.
442
443
 
443
- Hash based queries support `:and => [...]` and `:or => [...]`, which take an array
444
- of `:not => {...}`, `:matches => {...}`, `:eq => {...}`, `:not_eq => {...}`,
445
- `:lt => {...}`, `:lteq => {...}`, `:gt => {...}`, `:gteq => {...}` and `:query => "..."`
446
- arguments. Moreover, `:query => "..."` makes it possible to create sub-queries.
444
+ Hash based queries support `and: [...]` and `or: [...]`, which take an array
445
+ of `not: {...}`, `matches: {...}`, `eq: {...}`, `not_eq: {...}`,
446
+ `lt: {...}`, `lteq: {...}`, `gt: {...}`, `gteq: {...}` and `query: "..."`
447
+ arguments. Moreover, `query: "..."` makes it possible to create sub-queries.
447
448
  The other rules for query string queries apply to hash based queries as well.
448
449
 
450
+ ### Custom operators (Hash based queries)
451
+
452
+ SearchCop also provides the ability to define custom operators by defining a
453
+ `generator` in `search_scope`. They can then be used with the hash based query
454
+ search. This is useful when you want to use database operators that are not
455
+ supported by SearchCop.
456
+
457
+ For example, if you wanted to perform a `LIKE` query where a book title starts
458
+ with a string, you can define the search scope like so:
459
+
460
+ ```ruby
461
+ search_scope :search do
462
+ attributes :title
463
+
464
+ generator :starts_with do |column_name, raw_value|
465
+ pattern = "#{raw_value}%"
466
+ "#{column_name} LIKE #{quote pattern}"
467
+ end
468
+ end
469
+ ```
470
+
471
+ When you want to perform the search you use it like this:
472
+
473
+ ```ruby
474
+ Book.search(title: { starts_with: "The Great" })
475
+ ```
476
+
477
+ Security Note: The query returned from the generator will be interpolated
478
+ directly into the query that goes to your database. This opens up a potential
479
+ SQL Injection point in your app. If you use this feature you'll want to make
480
+ sure the query you're returning is safe to execute.
481
+
449
482
  ## Mapping
450
483
 
451
484
  When searching in boolean, datetime, timestamp, etc. fields, SearchCop
@@ -530,7 +563,7 @@ class Product < ActiveRecord::Base
530
563
  search_scope :search do
531
564
  attributes :title, :description
532
565
 
533
- options :title, :default => true
566
+ options :title, default: true
534
567
  end
535
568
  end
536
569
 
@@ -31,9 +31,13 @@ class SearchCop::HashParser
31
31
  collection = SearchCopGrammar::Attributes::Collection.new(query_info, key.to_s)
32
32
 
33
33
  if value.is_a?(Hash)
34
- raise(SearchCop::ParseError, "Unknown operator #{value.keys.first}") unless [:matches, :eq, :not_eq, :gt, :gteq, :lt, :lteq].include?(value.keys.first)
34
+ raise(SearchCop::ParseError, "Unknown operator #{value.keys.first}") unless collection.valid_operator?(value.keys.first)
35
35
 
36
- collection.send value.keys.first, value.values.first.to_s
36
+ if generator = collection.generator_for(value.keys.first)
37
+ collection.generator generator, value.values.first
38
+ else
39
+ collection.send value.keys.first, value.values.first.to_s
40
+ end
37
41
  else
38
42
  collection.send :matches, value.to_s
39
43
  end
@@ -1,12 +1,13 @@
1
1
 
2
2
  module SearchCop
3
3
  class Reflection
4
- attr_accessor :attributes, :options, :aliases, :scope
4
+ attr_accessor :attributes, :options, :aliases, :scope, :generators
5
5
 
6
6
  def initialize
7
7
  self.attributes = {}
8
8
  self.options = {}
9
9
  self.aliases = {}
10
+ self.generators = {}
10
11
  end
11
12
 
12
13
  def default_attributes
@@ -46,6 +47,10 @@ module SearchCop
46
47
  reflection.scope = block
47
48
  end
48
49
 
50
+ def generator(name, &block)
51
+ reflection.generators[name] = block
52
+ end
53
+
49
54
  private
50
55
 
51
56
  def attributes_hash(hash)
@@ -1,3 +1,3 @@
1
1
  module SearchCop
2
- VERSION = "1.0.8"
2
+ VERSION = "1.0.9"
3
3
  end
@@ -55,6 +55,10 @@ module SearchCop
55
55
  "NOT (#{visit node.object})"
56
56
  end
57
57
 
58
+ def visit_SearchCopGrammar_Nodes_Generator(node)
59
+ instance_exec visit(node.left), node.right[:value], &node.right[:generator]
60
+ end
61
+
58
62
  def quote_table_name(name)
59
63
  connection.quote_table_name name
60
64
  end
@@ -90,6 +94,7 @@ module SearchCop
90
94
  alias :visit_Float :quote
91
95
  alias :visit_Fixnum :quote
92
96
  alias :visit_Symbol :quote
97
+ alias :visit_Integer :quote
93
98
  end
94
99
  end
95
100
  end
@@ -6,6 +6,8 @@ module SearchCopGrammar
6
6
  class Collection
7
7
  attr_reader :query_info, :key
8
8
 
9
+ INCLUDED_OPERATORS = [:matches, :eq, :not_eq, :gt, :gteq, :lt, :lteq].freeze
10
+
9
11
  def initialize(query_info, key)
10
12
  raise(SearchCop::UnknownColumn, "Unknown column #{key}") unless query_info.scope.reflection.attributes[key]
11
13
 
@@ -31,6 +33,12 @@ module SearchCopGrammar
31
33
  end
32
34
  end
33
35
 
36
+ def generator(generator, value)
37
+ attributes.collect! do |attribute|
38
+ SearchCopGrammar::Nodes::Generator.new(attribute, generator: generator, value: value)
39
+ end.inject(:or)
40
+ end
41
+
34
42
  def matches(value)
35
43
  if fulltext?
36
44
  SearchCopGrammar::Nodes::MatchesFulltext.new self, value.to_s
@@ -88,6 +96,18 @@ module SearchCopGrammar
88
96
 
89
97
  Attributes.const_get(klass.columns_hash[column].type.to_s.classify).new(klass, alias_for(table), column, options)
90
98
  end
99
+
100
+ def generator_for(name)
101
+ generators[name]
102
+ end
103
+
104
+ def valid_operator?(operator)
105
+ (INCLUDED_OPERATORS + generators.keys).include?(operator)
106
+ end
107
+
108
+ def generators
109
+ query_info.scope.reflection.generators
110
+ end
91
111
  end
92
112
 
93
113
  class Base
@@ -73,6 +73,7 @@ module SearchCopGrammar
73
73
  class LessThan < Binary; end
74
74
  class LessThanOrEqual < Binary; end
75
75
  class Matches < Binary; end
76
+ class Generator < Binary; end
76
77
 
77
78
  class Not
78
79
  include Base
data/test/hash_test.rb CHANGED
@@ -93,5 +93,15 @@ class HashTest < SearchCop::TestCase
93
93
  assert_includes results, expected
94
94
  refute_includes results, rejected
95
95
  end
96
+
97
+ def test_custom_matcher
98
+ expected = create(:product, :title => "Expected")
99
+ rejected = create(:product, :title => "Rejected")
100
+
101
+ results = Product.search(:title => { :custom_eq => "Expected" })
102
+
103
+ assert_includes results, expected
104
+ refute_includes results, rejected
105
+ end
96
106
  end
97
107
 
data/test/test_helper.rb CHANGED
@@ -52,6 +52,10 @@ class Product < ActiveRecord::Base
52
52
  if DATABASE == "postgres"
53
53
  options :title, :dictionary => "english"
54
54
  end
55
+
56
+ generator :custom_eq do |column_name, raw_value|
57
+ "#{column_name} = #{quote raw_value}"
58
+ end
55
59
  end
56
60
 
57
61
  search_scope :user_search do
data/test/visitor_test.rb CHANGED
@@ -97,5 +97,14 @@ class VisitorTest < SearchCop::TestCase
97
97
  assert_equal("(MATCH(`products`.`title`) AGAINST('(Query1) (Query2)' IN BOOLEAN MODE))", SearchCop::Visitors::Visitor.new(ActiveRecord::Base.connection).visit(node)) if ENV["DATABASE"] == "mysql"
98
98
  assert_equal("(to_tsvector('english', COALESCE(\"products\".\"title\", '')) @@ to_tsquery('english', '(''Query1'') | (''Query2'')'))", SearchCop::Visitors::Visitor.new(ActiveRecord::Base.connection).visit(node)) if ENV["DATABASE"] == "postgres"
99
99
  end
100
+
101
+ def test_generator
102
+ generator = ->(column_name, value) do
103
+ "#{column_name} = #{quote value}"
104
+ end
105
+ node = SearchCopGrammar::Attributes::Collection.new(SearchCop::QueryInfo.new(Product, Product.search_scopes[:search]), "title").generator(generator, "value").optimize!
106
+
107
+ assert_equal "#{quote_table_name "products"}.#{quote_column_name "title"} = 'value'", SearchCop::Visitors::Visitor.new(ActiveRecord::Base.connection).visit(node)
108
+ end
100
109
  end
101
110
 
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.8
4
+ version: 1.0.9
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-03-23 00:00:00.000000000 Z
11
+ date: 2017-07-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: treetop