search_cop 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +18 -0
- data/.travis.yml +33 -0
- data/Appraisals +14 -0
- data/Gemfile +23 -0
- data/LICENSE.txt +22 -0
- data/MIGRATION.md +66 -0
- data/README.md +530 -0
- data/Rakefile +9 -0
- data/gemfiles/3.2.gemfile +26 -0
- data/gemfiles/4.0.gemfile +26 -0
- data/gemfiles/4.1.gemfile +26 -0
- data/lib/search_cop/arel/visitors.rb +223 -0
- data/lib/search_cop/arel.rb +4 -0
- data/lib/search_cop/grammar_parser.rb +22 -0
- data/lib/search_cop/hash_parser.rb +42 -0
- data/lib/search_cop/query_builder.rb +26 -0
- data/lib/search_cop/query_info.rb +13 -0
- data/lib/search_cop/search_scope.rb +62 -0
- data/lib/search_cop/version.rb +3 -0
- data/lib/search_cop.rb +71 -0
- data/lib/search_cop_grammar/attributes.rb +229 -0
- data/lib/search_cop_grammar/nodes.rb +183 -0
- data/lib/search_cop_grammar.rb +133 -0
- data/lib/search_cop_grammar.treetop +55 -0
- data/search_cop.gemspec +29 -0
- data/test/and_test.rb +27 -0
- data/test/boolean_test.rb +53 -0
- data/test/database.yml +17 -0
- data/test/date_test.rb +75 -0
- data/test/datetime_test.rb +76 -0
- data/test/error_test.rb +17 -0
- data/test/float_test.rb +67 -0
- data/test/fulltext_test.rb +27 -0
- data/test/hash_test.rb +97 -0
- data/test/integer_test.rb +67 -0
- data/test/not_test.rb +27 -0
- data/test/or_test.rb +29 -0
- data/test/scope_test.rb +35 -0
- data/test/search_cop_test.rb +131 -0
- data/test/string_test.rb +84 -0
- data/test/test_helper.rb +150 -0
- metadata +216 -0
@@ -0,0 +1,223 @@
|
|
1
|
+
|
2
|
+
module SearchCop
|
3
|
+
module Arel
|
4
|
+
module Visitors
|
5
|
+
module ToSql
|
6
|
+
if ::Arel::VERSION >= "4.0.1"
|
7
|
+
def visit_SearchCopGrammar_Nodes_And(o, a)
|
8
|
+
visit ::Arel::Nodes::Grouping.new(o.nodes.inject { |res, cur| ::Arel::Nodes::And.new [res, cur] }), a
|
9
|
+
end
|
10
|
+
|
11
|
+
def visit_SearchCopGrammar_Nodes_Or(o, a)
|
12
|
+
visit ::Arel::Nodes::Grouping.new(o.nodes.inject { |res, cur| ::Arel::Nodes::Or.new res, cur }), a
|
13
|
+
end
|
14
|
+
|
15
|
+
def visit_SearchCopGrammar_Nodes_Equality(o, a)
|
16
|
+
visit ::Arel::Nodes::Equality.new(o.left, o.right), a
|
17
|
+
end
|
18
|
+
|
19
|
+
def visit_SearchCopGrammar_Nodes_NotEqual(o, a)
|
20
|
+
visit ::Arel::Nodes::NotEqual.new(o.left, o.right), a
|
21
|
+
end
|
22
|
+
|
23
|
+
def visit_SearchCopGrammar_Nodes_LessThan(o, a)
|
24
|
+
visit ::Arel::Nodes::LessThan.new(o.left, o.right), a
|
25
|
+
end
|
26
|
+
|
27
|
+
def visit_SearchCopGrammar_Nodes_LessThanOrEqual(o, a)
|
28
|
+
visit ::Arel::Nodes::LessThanOrEqual.new(o.left, o.right), a
|
29
|
+
end
|
30
|
+
|
31
|
+
def visit_SearchCopGrammar_Nodes_GreaterThan(o, a)
|
32
|
+
visit ::Arel::Nodes::GreaterThan.new(o.left, o.right), a
|
33
|
+
end
|
34
|
+
|
35
|
+
def visit_SearchCopGrammar_Nodes_GreaterThanOrEqual(o, a)
|
36
|
+
visit ::Arel::Nodes::GreaterThanOrEqual.new(o.left, o.right), a
|
37
|
+
end
|
38
|
+
|
39
|
+
def visit_SearchCopGrammar_Nodes_Not(o, a)
|
40
|
+
visit ::Arel::Nodes::Not.new(o.object), a
|
41
|
+
end
|
42
|
+
|
43
|
+
def visit_SearchCopGrammar_Nodes_Matches(o, a)
|
44
|
+
visit ::Arel::Nodes::Matches.new(o.left, o.right), a
|
45
|
+
end
|
46
|
+
else
|
47
|
+
def visit_SearchCopGrammar_Nodes_And(o)
|
48
|
+
visit ::Arel::Nodes::Grouping.new(o.nodes.inject { |res, cur| ::Arel::Nodes::And.new [res, cur] })
|
49
|
+
end
|
50
|
+
|
51
|
+
def visit_SearchCopGrammar_Nodes_Or(o)
|
52
|
+
visit ::Arel::Nodes::Grouping.new(o.nodes.inject { |res, cur| ::Arel::Nodes::Or.new res, cur })
|
53
|
+
end
|
54
|
+
|
55
|
+
def visit_SearchCopGrammar_Nodes_Equality(o)
|
56
|
+
visit ::Arel::Nodes::Equality.new(o.left, o.right)
|
57
|
+
end
|
58
|
+
|
59
|
+
def visit_SearchCopGrammar_Nodes_NotEqual(o)
|
60
|
+
visit ::Arel::Nodes::NotEqual.new(o.left, o.right)
|
61
|
+
end
|
62
|
+
|
63
|
+
def visit_SearchCopGrammar_Nodes_LessThan(o)
|
64
|
+
visit ::Arel::Nodes::LessThan.new(o.left, o.right)
|
65
|
+
end
|
66
|
+
|
67
|
+
def visit_SearchCopGrammar_Nodes_LessThanOrEqual(o)
|
68
|
+
visit ::Arel::Nodes::LessThanOrEqual.new(o.left, o.right)
|
69
|
+
end
|
70
|
+
|
71
|
+
def visit_SearchCopGrammar_Nodes_GreaterThan(o)
|
72
|
+
visit ::Arel::Nodes::GreaterThan.new(o.left, o.right)
|
73
|
+
end
|
74
|
+
|
75
|
+
def visit_SearchCopGrammar_Nodes_GreaterThanOrEqual(o)
|
76
|
+
visit ::Arel::Nodes::GreaterThanOrEqual.new(o.left, o.right)
|
77
|
+
end
|
78
|
+
|
79
|
+
def visit_SearchCopGrammar_Nodes_Not(o)
|
80
|
+
visit ::Arel::Nodes::Not.new(o.object)
|
81
|
+
end
|
82
|
+
|
83
|
+
def visit_SearchCopGrammar_Nodes_Matches(o)
|
84
|
+
visit ::Arel::Nodes::Matches.new(o.left, o.right)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
module MySQL
|
90
|
+
if ::Arel::VERSION >= "4.0.1"
|
91
|
+
def visit_SearchCopGrammar_Attributes_Collection(o, a)
|
92
|
+
o.attributes.collect { |attribute| visit attribute.attribute, a }.join(", ")
|
93
|
+
end
|
94
|
+
|
95
|
+
def visit_SearchCopGrammar_Nodes_FulltextExpression(o, a)
|
96
|
+
"MATCH(#{visit o.collection, a}) AGAINST(#{visit visit(o.node, a), a} IN BOOLEAN MODE)"
|
97
|
+
end
|
98
|
+
|
99
|
+
def visit_SearchCopGrammar_Nodes_MatchesFulltextNot(o, a)
|
100
|
+
o.right.split(/[\s+'"<>()~-]+/).collect { |word| "-#{word}" }.join(" ")
|
101
|
+
end
|
102
|
+
|
103
|
+
def visit_SearchCopGrammar_Nodes_MatchesFulltext(o, a)
|
104
|
+
words = o.right.split(/[\s+'"<>()~-]+/)
|
105
|
+
|
106
|
+
words.size > 1 ? "\"#{words.join " "}\"" : words.first
|
107
|
+
end
|
108
|
+
|
109
|
+
def visit_SearchCopGrammar_Nodes_And_Fulltext(o, a)
|
110
|
+
res = o.nodes.collect do |node|
|
111
|
+
if node.is_a?(SearchCopGrammar::Nodes::MatchesFulltextNot)
|
112
|
+
visit node, a
|
113
|
+
else
|
114
|
+
node.nodes.size > 1 ? "+(#{visit node, a})" : "+#{visit node, a}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
res.join " "
|
119
|
+
end
|
120
|
+
|
121
|
+
def visit_SearchCopGrammar_Nodes_Or_Fulltext(o, a)
|
122
|
+
o.nodes.collect { |node| "(#{visit node, a})" }.join(" ")
|
123
|
+
end
|
124
|
+
else
|
125
|
+
def visit_SearchCopGrammar_Attributes_Collection(o)
|
126
|
+
o.attributes.collect { |attribute| visit attribute.attribute }.join(", ")
|
127
|
+
end
|
128
|
+
|
129
|
+
def visit_SearchCopGrammar_Nodes_FulltextExpression(o)
|
130
|
+
"MATCH(#{visit o.collection}) AGAINST(#{visit visit(o.node)} IN BOOLEAN MODE)"
|
131
|
+
end
|
132
|
+
|
133
|
+
def visit_SearchCopGrammar_Nodes_MatchesFulltextNot(o)
|
134
|
+
o.right.split(/[\s+'"<>()~-]+/).collect { |word| "-#{word}" }.join(" ")
|
135
|
+
end
|
136
|
+
|
137
|
+
def visit_SearchCopGrammar_Nodes_MatchesFulltext(o)
|
138
|
+
words = o.right.split(/[\s+'"<>()~-]+/)
|
139
|
+
|
140
|
+
words.size > 1 ? "\"#{words.join " "}\"" : words.first
|
141
|
+
end
|
142
|
+
|
143
|
+
def visit_SearchCopGrammar_Nodes_And_Fulltext(o)
|
144
|
+
res = o.nodes.collect do |node|
|
145
|
+
if node.is_a?(SearchCopGrammar::Nodes::MatchesFulltextNot)
|
146
|
+
visit node
|
147
|
+
else
|
148
|
+
node.nodes.size > 1 ? "+(#{visit node})" : "+#{visit node}"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
res.join " "
|
153
|
+
end
|
154
|
+
|
155
|
+
def visit_SearchCopGrammar_Nodes_Or_Fulltext(o)
|
156
|
+
o.nodes.collect { |node| "(#{visit node})" }.join(" ")
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
module PostgreSQL
|
162
|
+
if ::Arel::VERSION >= "4.0.1"
|
163
|
+
def visit_SearchCopGrammar_Attributes_Collection(o, a)
|
164
|
+
o.attributes.collect { |attribute| visit attribute.attribute, a }.join(" || ' ' || ")
|
165
|
+
end
|
166
|
+
|
167
|
+
def visit_SearchCopGrammar_Nodes_FulltextExpression(o, a)
|
168
|
+
dictionary = o.collection.options[:dictionary] || "simple"
|
169
|
+
|
170
|
+
"to_tsvector(#{visit dictionary, a}, #{visit o.collection, a}) @@ to_tsquery(#{visit dictionary, a}, #{visit visit(o.node, a), a})"
|
171
|
+
end
|
172
|
+
|
173
|
+
def visit_SearchCopGrammar_Nodes_MatchesFulltextNot(o, a)
|
174
|
+
"!'#{o.right}'"
|
175
|
+
end
|
176
|
+
|
177
|
+
def visit_SearchCopGrammar_Nodes_MatchesFulltext(o, a)
|
178
|
+
"'#{o.right.gsub /[\s&|!:'"]+/, " "}'"
|
179
|
+
end
|
180
|
+
|
181
|
+
def visit_SearchCopGrammar_Nodes_And_Fulltext(o, a)
|
182
|
+
o.nodes.collect { |node| "(#{visit node, a})" }.join(" & ")
|
183
|
+
end
|
184
|
+
|
185
|
+
def visit_SearchCopGrammar_Nodes_Or_Fulltext(o, a)
|
186
|
+
o.nodes.collect { |node| "(#{visit node, a})" }.join(" | ")
|
187
|
+
end
|
188
|
+
else
|
189
|
+
def visit_SearchCopGrammar_Attributes_Collection(o)
|
190
|
+
o.attributes.collect { |attribute| visit attribute.attribute }.join(" || ' ' || ")
|
191
|
+
end
|
192
|
+
|
193
|
+
def visit_SearchCopGrammar_Nodes_FulltextExpression(o)
|
194
|
+
dictionary = o.collection.options[:dictionary] || "simple"
|
195
|
+
|
196
|
+
"to_tsvector(#{visit dictionary.to_sym}, #{visit o.collection}) @@ to_tsquery(#{visit dictionary.to_sym}, #{visit visit(o.node)})" # to_sym fixes a 3.2 + postgres bug
|
197
|
+
end
|
198
|
+
|
199
|
+
def visit_SearchCopGrammar_Nodes_MatchesFulltextNot(o)
|
200
|
+
"!'#{o.right}'"
|
201
|
+
end
|
202
|
+
|
203
|
+
def visit_SearchCopGrammar_Nodes_MatchesFulltext(o)
|
204
|
+
"'#{o.right.gsub /[\s&|!:'"]+/, " "}'"
|
205
|
+
end
|
206
|
+
|
207
|
+
def visit_SearchCopGrammar_Nodes_And_Fulltext(o)
|
208
|
+
o.nodes.collect { |node| "(#{visit node})" }.join(" & ")
|
209
|
+
end
|
210
|
+
|
211
|
+
def visit_SearchCopGrammar_Nodes_Or_Fulltext(o)
|
212
|
+
o.nodes.collect { |node| "(#{visit node})" }.join(" | ")
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
Arel::Visitors::PostgreSQL.send :include, SearchCop::Arel::Visitors::PostgreSQL
|
221
|
+
Arel::Visitors::MySQL.send :include, SearchCop::Arel::Visitors::MySQL
|
222
|
+
Arel::Visitors::ToSql.send :include, SearchCop::Arel::Visitors::ToSql
|
223
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
|
2
|
+
require "search_cop_grammar"
|
3
|
+
require "treetop"
|
4
|
+
|
5
|
+
Treetop.load File.expand_path("../../search_cop_grammar.treetop", __FILE__)
|
6
|
+
|
7
|
+
module SearchCop
|
8
|
+
class GrammarParser
|
9
|
+
attr_reader :query_info
|
10
|
+
|
11
|
+
def initialize(query_info)
|
12
|
+
@query_info = query_info
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse(string)
|
16
|
+
node = SearchCopGrammarParser.new.parse(string) || raise(ParseError)
|
17
|
+
node.query_info = query_info
|
18
|
+
node.evaluate
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,42 @@
|
|
1
|
+
|
2
|
+
class SearchCop::HashParser
|
3
|
+
attr_reader :query_info
|
4
|
+
|
5
|
+
def initialize(query_info)
|
6
|
+
@query_info = query_info
|
7
|
+
end
|
8
|
+
|
9
|
+
def parse(hash)
|
10
|
+
res = hash.collect do |key, value|
|
11
|
+
case key
|
12
|
+
when :and
|
13
|
+
value.collect { |val| parse val }.inject(:and)
|
14
|
+
when :or
|
15
|
+
value.collect { |val| parse val }.inject(:or)
|
16
|
+
when :not
|
17
|
+
parse(value).not
|
18
|
+
when :query
|
19
|
+
SearchCop::Parser.parse value, query_info
|
20
|
+
else
|
21
|
+
parse_attribute key, value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
res.inject :and
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def parse_attribute(key, value)
|
31
|
+
collection = SearchCopGrammar::Attributes::Collection.new(query_info, key.to_s)
|
32
|
+
|
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)
|
35
|
+
|
36
|
+
collection.send value.keys.first, value.values.first.to_s
|
37
|
+
else
|
38
|
+
collection.send :matches, value.to_s
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
|
2
|
+
module SearchCop
|
3
|
+
class QueryBuilder
|
4
|
+
attr_accessor :query_info, :scope, :sql
|
5
|
+
|
6
|
+
def initialize(model, query, scope)
|
7
|
+
self.scope = scope
|
8
|
+
self.query_info = QueryInfo.new(model, scope)
|
9
|
+
|
10
|
+
arel = SearchCop::Parser.parse(query, query_info).optimize!
|
11
|
+
|
12
|
+
self.sql = model.connection.visitor.accept(arel)
|
13
|
+
end
|
14
|
+
|
15
|
+
def associations
|
16
|
+
all_associations - [query_info.model.name.tableize.to_sym]
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def all_associations
|
22
|
+
scope.reflection.attributes.values.flatten.uniq.collect { |column| column.split(".").first }.collect { |column| scope.reflection.aliases[column] || column.to_sym }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
@@ -0,0 +1,62 @@
|
|
1
|
+
|
2
|
+
module SearchCop
|
3
|
+
class Reflection
|
4
|
+
attr_accessor :attributes, :options, :aliases, :scope
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
self.attributes = {}
|
8
|
+
self.options = {}
|
9
|
+
self.aliases = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
def default_attributes
|
13
|
+
keys = options.select { |key, value| value[:default] == true }.keys
|
14
|
+
keys = attributes.keys.reject { |key| options[key] && options[key][:default] == false } if keys.empty?
|
15
|
+
keys = keys.to_set
|
16
|
+
|
17
|
+
attributes.select { |key, value| keys.include? key }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class SearchScope
|
22
|
+
attr_accessor :name, :model, :reflection
|
23
|
+
|
24
|
+
def initialize(name, model)
|
25
|
+
self.model = model
|
26
|
+
self.reflection = Reflection.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def attributes(*args)
|
30
|
+
args.each do |arg|
|
31
|
+
attributes_hash arg.is_a?(Hash) ? arg : { arg => arg }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def options(key, options = {})
|
36
|
+
reflection.options[key.to_s] = (reflection.options[key.to_s] || {}).merge(options)
|
37
|
+
end
|
38
|
+
|
39
|
+
def aliases(hash)
|
40
|
+
hash.each do |key, value|
|
41
|
+
reflection.aliases[key.to_s] = value.respond_to?(:table_name) ? value.table_name : value.to_s
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def scope(&block)
|
46
|
+
reflection.scope = block
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def attributes_hash(hash)
|
52
|
+
hash.each do |key, value|
|
53
|
+
reflection.attributes[key.to_s] = Array(value).collect do |column|
|
54
|
+
table, attribute = column.to_s =~ /\./ ? column.to_s.split(".") : [model.name.tableize, column]
|
55
|
+
|
56
|
+
"#{table}.#{attribute}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
data/lib/search_cop.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
|
2
|
+
require "search_cop/version"
|
3
|
+
require "search_cop/arel"
|
4
|
+
require "search_cop/search_scope"
|
5
|
+
require "search_cop/query_info"
|
6
|
+
require "search_cop/query_builder"
|
7
|
+
require "search_cop/grammar_parser"
|
8
|
+
require "search_cop/hash_parser"
|
9
|
+
|
10
|
+
module SearchCop
|
11
|
+
class SpecificationError < StandardError; end
|
12
|
+
class UnknownAttribute < SpecificationError; end
|
13
|
+
|
14
|
+
class RuntimeError < StandardError; end
|
15
|
+
class UnknownColumn < RuntimeError; end
|
16
|
+
class NoSearchableAttributes < RuntimeError; end
|
17
|
+
class IncompatibleDatatype < RuntimeError; end
|
18
|
+
class ParseError < RuntimeError; end
|
19
|
+
|
20
|
+
module Parser
|
21
|
+
def self.parse(query, query_info)
|
22
|
+
if query.is_a?(Hash)
|
23
|
+
SearchCop::HashParser.new(query_info).parse(query)
|
24
|
+
else
|
25
|
+
SearchCop::GrammarParser.new(query_info).parse(query)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.included(base)
|
31
|
+
base.extend ClassMethods
|
32
|
+
|
33
|
+
base.class_attribute :search_scopes
|
34
|
+
base.search_scopes = {}
|
35
|
+
|
36
|
+
base.search_scopes[:search] = SearchScope.new(:search, base)
|
37
|
+
end
|
38
|
+
|
39
|
+
module ClassMethods
|
40
|
+
def search_scope(name, &block)
|
41
|
+
search_scope = search_scopes[name] || SearchScope.new(name, self)
|
42
|
+
search_scope.instance_exec(&block)
|
43
|
+
|
44
|
+
search_scopes[name] = search_scope
|
45
|
+
|
46
|
+
self.class.send(:define_method, name) { |query| search_cop query, name }
|
47
|
+
self.class.send(:define_method, "unsafe_#{name}") { |query| unsafe_search_cop query, name }
|
48
|
+
end
|
49
|
+
|
50
|
+
def search_reflection(scope_name)
|
51
|
+
search_scopes[scope_name].reflection
|
52
|
+
end
|
53
|
+
|
54
|
+
def search_cop(query, scope_name)
|
55
|
+
unsafe_search_cop query, scope_name
|
56
|
+
rescue SearchCop::RuntimeError
|
57
|
+
respond_to?(:none) ? none : where("1 = 0")
|
58
|
+
end
|
59
|
+
|
60
|
+
def unsafe_search_cop(query, scope_name)
|
61
|
+
return respond_to?(:scoped) ? scoped : all if query.blank?
|
62
|
+
|
63
|
+
query_builder = QueryBuilder.new(self, query, search_scopes[scope_name])
|
64
|
+
|
65
|
+
scope = search_scopes[scope_name].reflection.scope ? instance_exec(&search_scopes[scope_name].reflection.scope) : eager_load(query_builder.associations)
|
66
|
+
|
67
|
+
scope.where query_builder.sql
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
@@ -0,0 +1,229 @@
|
|
1
|
+
|
2
|
+
require "treetop"
|
3
|
+
|
4
|
+
module SearchCopGrammar
|
5
|
+
module Attributes
|
6
|
+
class Collection
|
7
|
+
attr_reader :query_info, :key
|
8
|
+
|
9
|
+
def initialize(query_info, key)
|
10
|
+
raise(SearchCop::UnknownColumn, "Unknown column #{key}") unless query_info.scope.reflection.attributes[key]
|
11
|
+
|
12
|
+
@query_info = query_info
|
13
|
+
@key = key
|
14
|
+
end
|
15
|
+
|
16
|
+
def eql?(other)
|
17
|
+
self == other
|
18
|
+
end
|
19
|
+
|
20
|
+
def ==(other)
|
21
|
+
other.is_a?(self.class) && [query_info.model, key] == [query_info.model, other.key]
|
22
|
+
end
|
23
|
+
|
24
|
+
def hash
|
25
|
+
[query_info.model, key].hash
|
26
|
+
end
|
27
|
+
|
28
|
+
[:eq, :not_eq, :lt, :lteq, :gt, :gteq].each do |method|
|
29
|
+
define_method method do |value|
|
30
|
+
attributes.collect! { |attribute| attribute.send method, value }.inject(:or)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def matches(value)
|
35
|
+
if fulltext?
|
36
|
+
SearchCopGrammar::Nodes::MatchesFulltext.new self, value.to_s
|
37
|
+
else
|
38
|
+
attributes.collect! { |attribute| attribute.matches value }.inject(:or)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def fulltext?
|
43
|
+
(query_info.scope.reflection.options[key] || {})[:type] == :fulltext
|
44
|
+
end
|
45
|
+
|
46
|
+
def compatible?(value)
|
47
|
+
attributes.all? { |attribute| attribute.compatible? value }
|
48
|
+
end
|
49
|
+
|
50
|
+
def options
|
51
|
+
query_info.scope.reflection.options[key]
|
52
|
+
end
|
53
|
+
|
54
|
+
def attributes
|
55
|
+
@attributes ||= query_info.scope.reflection.attributes[key].collect { |attribute_definition| attribute_for attribute_definition }
|
56
|
+
end
|
57
|
+
|
58
|
+
def klass_for(table)
|
59
|
+
klass = query_info.scope.reflection.aliases[table]
|
60
|
+
klass ||= table
|
61
|
+
|
62
|
+
query_info.model.reflections[klass.to_sym] ? query_info.model.reflections[klass.to_sym].klass : klass.classify.constantize
|
63
|
+
end
|
64
|
+
|
65
|
+
def alias_for(table)
|
66
|
+
(query_info.scope.reflection.aliases[table] && table) || klass_for(table).table_name
|
67
|
+
end
|
68
|
+
|
69
|
+
def attribute_for(attribute_definition)
|
70
|
+
query_info.references.push attribute_definition
|
71
|
+
|
72
|
+
table, column = attribute_definition.split(".")
|
73
|
+
klass = klass_for(table)
|
74
|
+
|
75
|
+
raise(SearchCop::UnknownAttribute, "Unknown attribute #{attribute_definition}") unless klass.columns_hash[column]
|
76
|
+
|
77
|
+
Attributes.const_get(klass.columns_hash[column].type.to_s.classify).new(klass.arel_table.alias(alias_for(table))[column], klass, options)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class Base
|
82
|
+
attr_reader :attribute, :options
|
83
|
+
|
84
|
+
def initialize(attribute, klass, options = {})
|
85
|
+
@attribute = attribute
|
86
|
+
@klass = klass
|
87
|
+
@options = (options || {})
|
88
|
+
end
|
89
|
+
|
90
|
+
def map(value)
|
91
|
+
value
|
92
|
+
end
|
93
|
+
|
94
|
+
def compatible?(value)
|
95
|
+
map value
|
96
|
+
|
97
|
+
true
|
98
|
+
rescue SearchCop::IncompatibleDatatype
|
99
|
+
false
|
100
|
+
end
|
101
|
+
|
102
|
+
def fulltext?
|
103
|
+
false
|
104
|
+
end
|
105
|
+
|
106
|
+
{ :eq => "Equality", :not_eq => "NotEqual", :lt => "LessThan", :lteq => "LessThanOrEqual", :gt => "GreaterThan", :gteq => "GreaterThanOrEqual", :matches => "Matches" }.each do |method, class_name|
|
107
|
+
define_method method do |value|
|
108
|
+
raise(SearchCop::IncompatibleDatatype, "Incompatible datatype for #{value}") unless compatible?(value)
|
109
|
+
|
110
|
+
SearchCopGrammar::Nodes.const_get(class_name).new(@attribute, map(value))
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def method_missing(name, *args, &block)
|
115
|
+
@attribute.send name, *args, &block
|
116
|
+
end
|
117
|
+
|
118
|
+
def respond_to?(*args)
|
119
|
+
@attribute.respond_to? *args
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
class String < Base
|
124
|
+
def matches_value(value)
|
125
|
+
return value.gsub(/\*/, "%") if (options[:left_wildcard] != false && value.strip =~ /^[^*]+\*$|^\*[^*]+$/) || value.strip =~ /^[^*]+\*$/
|
126
|
+
|
127
|
+
options[:left_wildcard] != false ? "%#{value}%" : "#{value}%"
|
128
|
+
end
|
129
|
+
|
130
|
+
def matches(value)
|
131
|
+
super matches_value(value)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class Text < String; end
|
136
|
+
|
137
|
+
class WithoutMatches < Base
|
138
|
+
def matches(value)
|
139
|
+
eq value
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
class Float < WithoutMatches
|
144
|
+
def compatible?(value)
|
145
|
+
return true if value.to_s =~ /^[0-9]+(\.[0-9]+)?$/
|
146
|
+
|
147
|
+
false
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
class Integer < Float; end
|
152
|
+
class Decimal < Float; end
|
153
|
+
|
154
|
+
class Datetime < WithoutMatches
|
155
|
+
def parse(value)
|
156
|
+
return value .. value unless value.is_a?(::String)
|
157
|
+
|
158
|
+
if value =~ /^[0-9]{4,}$/
|
159
|
+
::Time.new(value).beginning_of_year .. ::Time.new(value).end_of_year
|
160
|
+
elsif value =~ /^([0-9]{4,})(\.|-|\/)([0-9]{1,2})$/
|
161
|
+
::Time.new($1, $3, 15).beginning_of_month .. ::Time.new($1, $3, 15).end_of_month
|
162
|
+
elsif value =~ /^([0-9]{1,2})(\.|-|\/)([0-9]{4,})$/
|
163
|
+
::Time.new($3, $1, 15).beginning_of_month .. ::Time.new($3, $1, 15).end_of_month
|
164
|
+
elsif value !~ /:/
|
165
|
+
time = ::Time.parse(value)
|
166
|
+
time.beginning_of_day .. time.end_of_day
|
167
|
+
else
|
168
|
+
time = ::Time.parse(value)
|
169
|
+
time .. time
|
170
|
+
end
|
171
|
+
rescue ArgumentError
|
172
|
+
raise SearchCop::IncompatibleDatatype, "Incompatible datatype for #{value}"
|
173
|
+
end
|
174
|
+
|
175
|
+
def map(value)
|
176
|
+
parse(value).first
|
177
|
+
end
|
178
|
+
|
179
|
+
def eq(value)
|
180
|
+
between parse(value)
|
181
|
+
end
|
182
|
+
|
183
|
+
def not_eq(value)
|
184
|
+
between(parse(value)).not
|
185
|
+
end
|
186
|
+
|
187
|
+
def gt(value)
|
188
|
+
super parse(value).last
|
189
|
+
end
|
190
|
+
|
191
|
+
def between(range)
|
192
|
+
gteq(range.first).and(lteq(range.last))
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
class Timestamp < Datetime; end
|
197
|
+
|
198
|
+
class Date < Datetime
|
199
|
+
def parse(value)
|
200
|
+
return value .. value unless value.is_a?(::String)
|
201
|
+
|
202
|
+
if value =~ /^[0-9]{4,}$/
|
203
|
+
::Date.new(value.to_i).beginning_of_year .. ::Date.new(value.to_i).end_of_year
|
204
|
+
elsif value =~ /^([0-9]{4,})(\.|-|\/)([0-9]{1,2})$/
|
205
|
+
::Date.new($1.to_i, $3.to_i, 15).beginning_of_month .. ::Date.new($1.to_i, $3.to_i, 15).end_of_month
|
206
|
+
elsif value =~ /^([0-9]{1,2})(\.|-|\/)([0-9]{4,})$/
|
207
|
+
::Date.new($3.to_i, $1.to_i, 15).beginning_of_month .. ::Date.new($3.to_i, $1.to_i, 15).end_of_month
|
208
|
+
else
|
209
|
+
date = ::Date.parse(value)
|
210
|
+
date .. date
|
211
|
+
end
|
212
|
+
rescue ArgumentError
|
213
|
+
raise SearchCop::IncompatibleDatatype, "Incompatible datatype for #{value}"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
class Time < Datetime; end
|
218
|
+
|
219
|
+
class Boolean < WithoutMatches
|
220
|
+
def map(value)
|
221
|
+
return true if value.to_s =~ /^(1|true|yes)$/i
|
222
|
+
return false if value.to_s =~ /^(0|false|no)$/i
|
223
|
+
|
224
|
+
raise SearchCop::IncompatibleDatatype, "Incompatible datatype for #{value}"
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|