search_cop 1.0.8 → 1.0.9

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
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