ruby-druid 0.1.9 → 0.9.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,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