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.
@@ -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
+