search_cop 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,4 @@
1
+
2
+ require "arel"
3
+ require "search_cop/arel/visitors"
4
+
@@ -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,13 @@
1
+
2
+ module SearchCop
3
+ class QueryInfo
4
+ attr_accessor :model, :scope, :references
5
+
6
+ def initialize(model, scope)
7
+ self.model = model
8
+ self.scope = scope
9
+ self.references = []
10
+ end
11
+ end
12
+ end
13
+
@@ -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
+
@@ -0,0 +1,3 @@
1
+ module SearchCop
2
+ VERSION = "1.0.0"
3
+ end
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
+