ruby-druid 0.1.9 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,37 @@
1
+ module Druid
2
+ class Context
3
+ include ActiveModel::Model
4
+
5
+ attr_accessor :timeout
6
+ validates :timeout, allow_nil: true, numericality: true
7
+
8
+ attr_accessor :priority
9
+ validates :priority, allow_nil: true, numericality: true
10
+
11
+ attr_accessor :queryId
12
+
13
+ attr_accessor :useCache
14
+ validates :useCache, allow_nil: true, inclusion: { in: [true, false] }
15
+
16
+ attr_accessor :populateCache
17
+ validates :populateCache, allow_nil: true, inclusion: { in: [true, false] }
18
+
19
+ attr_accessor :bySegment
20
+ validates :bySegment, allow_nil: true, inclusion: { in: [true, false] }
21
+
22
+ attr_accessor :finalize
23
+ validates :finalize, allow_nil: true, inclusion: { in: [true, false] }
24
+
25
+ attr_accessor :chunkPeriod
26
+
27
+ def initialize(attributes = {})
28
+ super
29
+ @queryId ||= SecureRandom.uuid
30
+ end
31
+
32
+ def as_json(options = {})
33
+ super(options.merge(except: %w(errors validation_context)))
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,95 @@
1
+ require 'multi_json'
2
+ require 'iso8601'
3
+
4
+ module Druid
5
+ class DataSource
6
+
7
+ attr_reader :name, :uri, :metrics, :dimensions
8
+
9
+ def initialize(name, uri)
10
+ @name = name.split('/').last
11
+ uri = uri.sample if uri.is_a?(Array)
12
+ if uri.is_a?(String)
13
+ @uri = URI(uri)
14
+ else
15
+ @uri = uri
16
+ end
17
+ end
18
+
19
+ def metadata
20
+ @metadata ||= metadata!
21
+ end
22
+
23
+ def metadata!(opts = {})
24
+ meta_path = "#{@uri.path}datasources/#{name}"
25
+
26
+ if opts[:interval]
27
+ from, to = opts[:interval]
28
+ from = from.respond_to?(:iso8601) ? from.iso8601 : ISO8601::DateTime.new(from).to_s
29
+ to = to.respond_to?(:iso8601) ? to.iso8601 : ISO8601::DateTime.new(to).to_s
30
+
31
+ meta_path += "?interval=#{from}/#{to}"
32
+ end
33
+
34
+ req = Net::HTTP::Get.new(meta_path)
35
+ response = Net::HTTP.new(uri.host, uri.port).start do |http|
36
+ http.open_timeout = 10 # if druid is down fail fast
37
+ http.read_timeout = nil # we wait until druid is finished
38
+ http.request(req)
39
+ end
40
+
41
+ if response.code != '200'
42
+ raise "Request failed: #{response.code}: #{response.body}"
43
+ end
44
+
45
+ MultiJson.load(response.body)
46
+ end
47
+
48
+ def metrics
49
+ @metrics ||= metadata['metrics']
50
+ end
51
+
52
+ def dimensions
53
+ @dimensions ||= metadata['dimensions']
54
+ end
55
+
56
+ def post(query)
57
+ query = query.query if query.is_a?(Druid::Query::Builder)
58
+ query = Query.new(MultiJson.load(query)) if query.is_a?(String)
59
+ query.dataSource = name
60
+
61
+ req = Net::HTTP::Post.new(uri.path, { 'Content-Type' => 'application/json' })
62
+ req.body = query.to_json
63
+
64
+ response = Net::HTTP.new(uri.host, uri.port).start do |http|
65
+ http.open_timeout = 10 # if druid is down fail fast
66
+ http.read_timeout = nil # we wait until druid is finished
67
+ http.request(req)
68
+ end
69
+
70
+ if response.code != '200'
71
+ # ignore GroupBy cache issues and try again without cached results
72
+ if query.context.useCache != false && response.code == "500" && response.body =~ /Cannot have a null result!/
73
+ query.context.useCache = false
74
+ return self.post(query)
75
+ end
76
+
77
+ raise Error.new(response), "request failed"
78
+ end
79
+
80
+ MultiJson.load(response.body)
81
+ end
82
+
83
+ class Error < StandardError
84
+ attr_reader :response
85
+ def initialize(response)
86
+ @response = response
87
+ end
88
+
89
+ def message
90
+ MultiJson.load(response.body)["error"]
91
+ end
92
+ end
93
+
94
+ end
95
+ end
@@ -1,269 +1,325 @@
1
1
  module Druid
2
2
  class Filter
3
- (instance_methods + private_instance_methods).each do |method|
4
- unless method.to_s =~ /^(__|instance_eval|instance_exec|initialize|object_id|raise|puts|inspect|send)/ || method.to_s =~ /\?/
5
- undef_method method
3
+ include ActiveModel::Model
4
+
5
+ attr_accessor :type
6
+ validates :type, inclusion: { in: %w(selector regex and or not javascript) }
7
+
8
+ class DimensionValidator < ActiveModel::EachValidator
9
+ TYPES = %w(selector regex javascript)
10
+ def validate_each(record, attribute, value)
11
+ if TYPES.include?(record.type)
12
+ record.errors.add(attribute, 'may not be blank') if value.blank?
13
+ else
14
+ record.errors.add(attribute, "is not supported by type=#{record.type}") if value
15
+ end
6
16
  end
7
17
  end
8
18
 
9
- def method_missing(method_id, *args)
10
- FilterDimension.new(method_id)
19
+ attr_accessor :dimension
20
+ validates :dimension, dimension: true
21
+
22
+ class ValueValidator < ActiveModel::EachValidator
23
+ TYPES = %w(selector)
24
+ def validate_each(record, attribute, value)
25
+ if TYPES.include?(record.type)
26
+ record.errors.add(attribute, 'may not be blank') if value.blank?
27
+ else
28
+ record.errors.add(attribute, "is not supported by type=#{record.type}") if value
29
+ end
30
+ end
11
31
  end
12
- end
13
32
 
14
- class FilterParameter
15
- (instance_methods + private_instance_methods).each do |method|
16
- unless method.to_s =~ /^(__|instance_eval|instance_exec|initialize|object_id|raise|puts|inspect|class|send)/ || method.to_s =~ /\?/
17
- undef_method method
33
+ attr_accessor :value
34
+ validates :value, value: true
35
+
36
+ class PatternValidator < ActiveModel::EachValidator
37
+ TYPES = %w(regex)
38
+ def validate_each(record, attribute, value)
39
+ if TYPES.include?(record.type)
40
+ record.errors.add(attribute, 'may not be blank') if value.blank?
41
+ else
42
+ record.errors.add(attribute, "is not supported by type=#{record.type}") if value
43
+ end
44
+ end
45
+ end
46
+
47
+ attr_accessor :pattern
48
+ validates :pattern, pattern: true
49
+
50
+ class FieldsValidator < ActiveModel::EachValidator
51
+ TYPES = %w(and or)
52
+ def validate_each(record, attribute, value)
53
+ if TYPES.include?(record.type)
54
+ value.each(&:valid?) # trigger validation
55
+ value.each do |fvalue|
56
+ fvalue.errors.messages.each do |k, v|
57
+ record.errors.add(attribute, { k => v })
58
+ end
59
+ end
60
+ else
61
+ record.errors.add(attribute, "is not supported by type=#{record.type}") unless value.blank?
62
+ end
63
+ end
64
+ end
65
+
66
+ attr_accessor :fields
67
+ validates :fields, fields: true
68
+
69
+ def fields
70
+ @fields ||= []
71
+ end
72
+
73
+ def fields=(value)
74
+ if value.is_a?(Array)
75
+ @fields = value.map do |x|
76
+ x.is_a?(Filter) ? x : Filter.new(x)
77
+ end
78
+ else
79
+ @fields = [value]
80
+ end
81
+ end
82
+
83
+ class FieldValidator < ActiveModel::EachValidator
84
+ TYPES = %w(not)
85
+ def validate_each(record, attribute, value)
86
+ if TYPES.include?(record.type)
87
+ if value
88
+ value.valid? # trigger validation
89
+ value.errors.messages.each do |k, v|
90
+ record.errors.add(attribute, { k => v })
91
+ end
92
+ else
93
+ record.errors.add(attribute, "may not be blank")
94
+ end
95
+ else
96
+ record.errors.add(attribute, "is not supported by type=#{record.type}") if value
97
+ end
98
+ end
99
+ end
100
+
101
+ attr_accessor :field
102
+ validates :field, field: true
103
+
104
+ def field=(value)
105
+ if value.is_a?(Hash)
106
+ @field = Filter.new(value)
107
+ else
108
+ @field = value
18
109
  end
19
110
  end
20
111
 
21
- def to_s
22
- to_hash.to_s
112
+ class FunctionValidator < ActiveModel::EachValidator
113
+ TYPES = %w(javascript)
114
+ def validate_each(record, attribute, value)
115
+ if TYPES.include?(record.type)
116
+ record.errors.add(attribute, 'may not be blank') if value.blank?
117
+ else
118
+ record.errors.add(attribute, "is not supported by type=#{record.type}") if value
119
+ end
120
+ end
23
121
  end
24
122
 
25
- def as_json(*a)
26
- to_hash
123
+ attr_accessor :function
124
+ validates :function, function: true
125
+
126
+ def as_json(options = {})
127
+ super(options.merge(except: %w(errors validation_context)))
27
128
  end
28
129
 
29
- def to_json(*a)
30
- to_hash.to_json(*a)
130
+ def method_missing(name, *args)
131
+ DimensionFilter.new(dimension: name)
31
132
  end
32
133
  end
33
134
 
34
- class FilterDimension < FilterParameter
35
- def initialize(name)
36
- @name = name
37
- @value = nil
38
- @regexp = nil
135
+ module BooleanOperators
136
+ def &(other)
137
+ BooleanFilter.new({
138
+ type: 'and',
139
+ fields: [self, other],
140
+ })
141
+ end
142
+
143
+ def |(other)
144
+ BooleanFilter.new({
145
+ type: 'or',
146
+ fields: [self, other],
147
+ })
39
148
  end
40
149
 
150
+ def !()
151
+ BooleanFilter.new({
152
+ type: 'not',
153
+ field: self,
154
+ })
155
+ end
156
+ end
157
+
158
+ class DimensionFilter < Filter
159
+ include BooleanOperators
160
+
41
161
  def in_rec(bounds)
42
- RecFilter.new(@name, bounds)
43
- end
162
+ RecFilter.new(@dimension, bounds)
163
+ end
44
164
 
45
165
  def in_circ(bounds)
46
- CircFilter.new(@name, bounds)
47
- end
166
+ CircFilter.new(@dimension, bounds)
167
+ end
48
168
 
49
169
  def eq(value)
50
- return self.in(value) if value.is_a? Array
51
- return self.regexp(value) if value.is_a? Regexp
52
- @value = value
170
+ case value
171
+ when ::Array
172
+ self.in(value)
173
+ when ::Regexp
174
+ self.regexp(value)
175
+ else
176
+ @type = 'selector'
177
+ @value = value
178
+ end
53
179
  self
54
180
  end
55
181
 
56
182
  alias :'==' :eq
57
183
 
58
184
  def neq(value)
59
- return !self.in(value)
185
+ return !self.eq(value)
60
186
  end
61
187
 
62
188
  alias :'!=' :neq
63
189
 
64
190
  def in(*args)
65
- values = args.flatten
66
- filter_multiple(values, 'or', :eq)
191
+ filter_multiple(args.flatten, 'or', :eq)
67
192
  end
68
193
 
69
194
  def nin(*args)
70
- values = args.flatten
71
- filter_multiple(values, 'and', :neq)
195
+ filter_multiple(args.flatten, 'and', :neq)
72
196
  end
73
197
 
74
- def &(other)
75
- filter_and = FilterOperator.new('and', true)
76
- filter_and.add(self)
77
- filter_and.add(other)
78
- filter_and
198
+ def filter_multiple(values, operator, method)
199
+ ::Kernel.raise 'Values cannot be empty' if values.empty?
200
+ return self.__send__(method, values[0]) if values.length == 1
201
+ BooleanFilter.new({
202
+ type: operator,
203
+ fields: values.map do |value|
204
+ DimensionFilter.new(dimension: @dimension).__send__(method, value)
205
+ end
206
+ })
79
207
  end
80
208
 
81
- def |(other)
82
- filter_or = FilterOperator.new('or', true)
83
- filter_or.add(self)
84
- filter_or.add(other)
85
- filter_or
86
- end
209
+ alias_method :not_in, :nin
87
210
 
88
- def !()
89
- filter_not = FilterOperator.new('not', false)
90
- filter_not.add(self)
91
- filter_not
211
+ def regexp(r)
212
+ r = ::Regexp.new(r) unless r.is_a?(::Regexp)
213
+ @pattern = r.inspect[1...-1] #to_s doesn't work
214
+ @type = 'regex'
215
+ self
92
216
  end
93
217
 
94
218
  def >(value)
95
- filter_js = FilterJavascript.new_comparison(@name, '>', value)
96
- filter_js
219
+ JavascriptFilter.new_comparison(@dimension, '>', value)
97
220
  end
98
221
 
99
222
  def <(value)
100
- filter_js = FilterJavascript.new_comparison(@name, '<', value)
101
- filter_js
223
+ JavascriptFilter.new_comparison(@dimension, '<', value)
102
224
  end
103
225
 
104
226
  def >=(value)
105
- filter_js = FilterJavascript.new_comparison(@name, '>=', value)
106
- filter_js
227
+ JavascriptFilter.new_comparison(@dimension, '>=', value)
107
228
  end
108
229
 
109
230
  def <=(value)
110
- filter_js = FilterJavascript.new_comparison(@name, '<=', value)
111
- filter_js
231
+ JavascriptFilter.new_comparison(@dimension, '<=', value)
112
232
  end
113
233
 
114
234
  def javascript(js)
115
- filter_js = FilterJavascript.new(@name, js)
116
- filter_js
117
- end
118
-
119
- def regexp(r)
120
- r = Regexp.new(r) unless r.is_a? Regexp
121
- @regexp = r.inspect[1...-1] #to_s doesn't work
122
- self
123
- end
124
-
125
- def to_hash
126
- raise 'no value assigned' unless @value.nil? ^ @regexp.nil?
127
- hash = {
128
- :dimension => @name
129
- }
130
- if @value
131
- hash['type'] = 'selector'
132
- hash['value'] = @value
133
- elsif @regexp
134
- hash['type'] = 'regex'
135
- hash['pattern'] = @regexp
136
- end
137
- hash
138
- end
139
-
140
- private
141
-
142
- def filter_multiple(values, operator, method)
143
- raise 'Values cannot be empty' if values.empty?
144
- return self.send(method, values[0]) if values.length == 1
145
-
146
- filter = FilterOperator.new(operator, true)
147
- values.each do |value|
148
- raise 'Value cannot be a parameter' if value.is_a?(FilterParameter)
149
- filter.add(FilterDimension.new(@name).send(method, value))
150
- end
151
- filter
235
+ JavascriptFilter.new(@dimension, js)
152
236
  end
153
237
  end
154
238
 
155
- class FilterOperator < FilterParameter
156
- def initialize(name, takes_many)
157
- @name = name
158
- @takes_many = takes_many
159
- @elements = []
160
- end
161
-
162
- def add(element)
163
- @elements.push element
164
- end
165
-
239
+ class BooleanFilter < Filter
166
240
  def &(other)
167
- if @name == 'and'
168
- filter_and = self
241
+ if @type.to_s == 'and'
242
+ self.fields << other
243
+ self
169
244
  else
170
- filter_and = FilterOperator.new('and', true)
171
- filter_and.add(self)
245
+ BooleanFilter.new({
246
+ type: 'and',
247
+ fields: [self, other],
248
+ })
172
249
  end
173
- filter_and.add(other)
174
- filter_and
175
250
  end
176
251
 
177
252
  def |(other)
178
- if @name == 'or'
179
- filter_or = self
253
+ if @type.to_s == 'or'
254
+ self.fields << other
255
+ self
180
256
  else
181
- filter_or = FilterOperator.new('or', true)
182
- filter_or.add(self)
257
+ BooleanFilter.new({
258
+ type: 'or',
259
+ fields: [self, other],
260
+ })
183
261
  end
184
- filter_or.add(other)
185
- filter_or
186
262
  end
187
263
 
188
264
  def !()
189
- if @name == 'not'
190
- @elements[0]
191
- else
192
- filter_not = FilterOperator.new('not', false)
193
- filter_not.add(self)
194
- filter_not
195
- end
196
- end
197
-
198
- def to_hash
199
- result = {
200
- :type => @name
201
- }
202
- if @takes_many
203
- result[:fields] = @elements.map(&:to_hash)
265
+ if @type.to_s == 'not'
266
+ self.field
267
+ self
204
268
  else
205
- result[:field] = @elements[0].to_hash
269
+ BooleanFilter.new({
270
+ type: 'not',
271
+ field: self,
272
+ })
206
273
  end
207
- result
208
274
  end
209
275
  end
210
-
211
- class RecFilter < FilterDimension
276
+
277
+ class RecFilter < Filter
278
+ include BooleanOperators
212
279
 
213
280
  def initialize(dimension, bounds)
281
+ super()
282
+ @type = 'spatial'
214
283
  @dimension = dimension
215
- @bounds = bounds
216
- end
217
-
218
- def to_hash
219
- {
220
- :type => "spatial",
221
- :dimension => @dimension,
222
- :bound =>{
223
- :type => "rectangular",
224
- :minCoords => @bounds.first,
225
- :maxCoords => @bounds.last
226
- }
284
+ @bound = {
285
+ type: 'rectangular',
286
+ minCoords: bounds.first,
287
+ maxCoords: bounds.last,
227
288
  }
228
289
  end
229
290
  end
230
291
 
231
- class CircFilter < FilterDimension
292
+ class CircFilter < Filter
293
+ include BooleanOperators
232
294
 
233
295
  def initialize(dimension, bounds)
296
+ super()
297
+ @type = 'spatial'
234
298
  @dimension = dimension
235
- @bounds = bounds
236
- end
237
-
238
- def to_hash
239
- {
240
- :type => "spatial",
241
- :dimension => @dimension,
242
- :bound =>{
243
- :type => "radius",
244
- :coords => @bounds.first,
245
- :radius => @bounds.last
246
- }
299
+ @bound = {
300
+ type: 'radius',
301
+ coords: bounds.first,
302
+ radius: bounds.last,
247
303
  }
248
304
  end
249
305
  end
250
306
 
251
- class FilterJavascript < FilterDimension
252
- def initialize(dimension, expression)
307
+ class JavascriptFilter < Filter
308
+ include BooleanOperators
309
+
310
+ def initialize(dimension, function)
311
+ super()
312
+ @type = 'javascript'
253
313
  @dimension = dimension
254
- @expression = expression
314
+ @function = function
255
315
  end
256
316
 
257
- def self.new_comparison(dimension, operator, value)
258
- self.new(dimension, "#{dimension} #{operator} #{value.is_a?(String) ? "'#{value}'" : value}")
317
+ def self.new_expression(dimension, expression)
318
+ self.new(dimension, "function(#{dimension}) { return(#{expression}); }")
259
319
  end
260
320
 
261
- def to_hash
262
- {
263
- :type => 'javascript',
264
- :dimension => @dimension,
265
- :function => "function(#{@dimension}) { return(#{@expression}); }"
266
- }
321
+ def self.new_comparison(dimension, operator, value)
322
+ self.new_expression(dimension, "#{dimension} #{operator} #{value.to_json}")
267
323
  end
268
324
  end
269
- end
325
+ end