search_cop 1.0.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.
- 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
|
+
|