ruby-druid 0.1.1

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,32 @@
1
+ module Druid
2
+
3
+ class ResponseRow
4
+ (instance_methods + private_instance_methods).each do |method|
5
+ unless method.to_s =~ /^(__|object_id|initialize)/
6
+ undef_method method
7
+ end
8
+ end
9
+
10
+ attr_reader :timestamp
11
+ attr_reader :row
12
+
13
+ def initialize(row)
14
+ @timestamp = row['timestamp']
15
+ @row = row['event'] || row['result']
16
+ end
17
+
18
+ def method_missing(name, *args, &block)
19
+ @row.send name, *args, &block
20
+ end
21
+
22
+ def to_s
23
+ "#{@timestamp.to_s}:#{@row.to_s}"
24
+ end
25
+
26
+ def inspect
27
+ "#{@timestamp.inspect}:#{@row.inspect}"
28
+ end
29
+
30
+ end
31
+
32
+ end
@@ -0,0 +1,129 @@
1
+ require 'zk'
2
+ require 'json'
3
+ require 'rest_client'
4
+
5
+ module Druid
6
+
7
+ class ZooHandler
8
+ def initialize(uri, opts = {})
9
+ @zk = ZK.new uri, :chroot => :check
10
+ @registry = Hash.new {|hash,key| hash[key] = Array.new }
11
+ @discovery_path = opts[:discovery_path] || '/discoveryPath'
12
+ @watched_services = Hash.new
13
+
14
+ init_zookeeper
15
+ end
16
+
17
+ def init_zookeeper
18
+ @zk.on_expired_session do
19
+ init_zookeeper
20
+ end
21
+
22
+ @zk.register(@discovery_path, :only => :child) do |event|
23
+ check_services
24
+ end
25
+
26
+ check_services
27
+ end
28
+
29
+ def close!
30
+ @zk.close!
31
+ end
32
+
33
+ def check_services
34
+ zk_services = @zk.children @discovery_path, :watch => true
35
+
36
+ #remove deprecated services
37
+ (services - zk_services).each do |old_service|
38
+ @registry.delete old_service
39
+ if @watched_services.include? old_service
40
+ @watched_services.delete(old_service).unregister
41
+ end
42
+ end
43
+
44
+ zk_services.each do |service|
45
+ check_service service unless @watched_services.include? service
46
+ end
47
+ end
48
+
49
+ def check_service(service)
50
+ unless @watched_services.include? service
51
+ watchPath = "#{@discovery_path}/#{service}"
52
+ @watched_services[service] = @zk.register(watchPath, :only => :child) do |event|
53
+ old_handler = @watched_services.delete(service)
54
+ if old_handler
55
+ old_handler.unregister
56
+ end
57
+ check_service service
58
+ end
59
+
60
+ known = @registry[service].map{ |node| node[:name] } rescue []
61
+ live = @zk.children(watchPath, :watch => true)
62
+
63
+ # copy the unchanged entries
64
+ new_list = @registry[service].select{ |node| live.include? node[:name] } rescue []
65
+
66
+ # verify the new entries to be living brokers
67
+ (live - known).each do |name|
68
+ info = @zk.get "#{watchPath}/#{name}"
69
+ node = JSON.parse(info[0])
70
+ uri = "http://#{node['address']}:#{node['port']}/druid/v2/"
71
+
72
+ begin
73
+ check_uri = "#{uri}datasources/"
74
+
75
+ check = RestClient::Request.execute({
76
+ :method => :get,
77
+ :url => check_uri,
78
+ :timeout => 5,
79
+ :open_timeout => 5
80
+ })
81
+
82
+ if check.code == 200
83
+ new_list.push({
84
+ :name => name,
85
+ :uri => uri,
86
+ :data_sources => JSON.parse(check.to_str)
87
+ })
88
+ else
89
+ end
90
+ rescue
91
+ end
92
+ end
93
+
94
+ if !new_list.empty?
95
+ # poor mans load balancing
96
+ @registry[service] = new_list.shuffle
97
+ else
98
+ # don't show services w/o active brokers
99
+ @registry.delete service
100
+ end
101
+ end
102
+ end
103
+
104
+ def services
105
+ @registry.keys
106
+ end
107
+
108
+ def data_sources
109
+ result = Hash.new { |hash, key| hash[key] = [] }
110
+
111
+ @registry.each do |service, brokers|
112
+ brokers.each do |broker|
113
+ broker[:data_sources].each do |data_source|
114
+ result["#{service}/#{data_source}"] << broker[:uri]
115
+ end
116
+ end
117
+ end
118
+ result.each do |source, uris|
119
+ result[source] = uris.sample if uris.respond_to?(:sample)
120
+ end
121
+
122
+ result
123
+ end
124
+
125
+ def to_s
126
+ @registry.to_s
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,19 @@
1
+ require 'rake'
2
+
3
+ Gem::Specification.new do |gem|
4
+ gem.name = 'ruby-druid'
5
+ gem.version = '0.1.1'
6
+ gem.date = '2013-08-01'
7
+ gem.summary = 'Ruby client for druid'
8
+ gem.description = 'Ruby client for metamx druid'
9
+ gem.authors = 'The LiquidM Team'
10
+ gem.email = 'tech@liquidm.com'
11
+ gem.homepage = 'https://github.com/madvertise/ruby-druid'
12
+
13
+ gem.files = `git ls-files`.split("\n")
14
+ gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
15
+ gem.require_paths = ['lib']
16
+
17
+ gem.add_dependency 'zk'
18
+ gem.add_dependency 'rest-client'
19
+ end
@@ -0,0 +1,69 @@
1
+ require 'spec_helper'
2
+
3
+ describe Druid::Client do
4
+
5
+ it 'calls zookeeper on intialize' do
6
+ Druid::ZooHandler.should_receive(:new)
7
+ Druid::Client.new('test_uri', zk_keepalive: true)
8
+ end
9
+
10
+ it 'creates a query' do
11
+ Druid::ZooHandler.stub!(:new).and_return(mock(Druid::ZooHandler, :data_sources => {'test/test' => 'http://www.example.com'}, :close! => true))
12
+ Druid::Client.new('test_uri', zk_keepalive: true).query('test/test').should be_a Druid::Query
13
+ end
14
+
15
+ it 'sends query if block is given' do
16
+ Druid::ZooHandler.stub!(:new).and_return(mock(Druid::ZooHandler, :data_sources => {'test/test' => 'http://www.example.com'}, :close! => true))
17
+ client = Druid::Client.new('test_uri', zk_keepalive: true)
18
+ client.should_receive(:send)
19
+ client.query('test/test') do
20
+ group(:group1)
21
+ end
22
+ end
23
+
24
+ it 'parses response on 200' do
25
+ stub_request(:post, "http://www.example.com/druid/v2").
26
+ with(:body => "{\"dataSource\":\"test\",\"granularity\":\"all\",\"intervals\":[\"2013-04-04T00:00:00+00:00/2013-04-04T00:00:00+00:00\"]}",
27
+ :headers => {'Accept'=>'*/*', 'Content-Type'=>'application/json', 'User-Agent'=>'Ruby'}).
28
+ to_return(:status => 200, :body => "[]", :headers => {})
29
+ Druid::ZooHandler.stub!(:new).and_return(mock(Druid::ZooHandler, :data_sources => {'test/test' => 'http://www.example.com/druid/v2'}, :close! => true))
30
+ client = Druid::Client.new('test_uri', zk_keepalive: true)
31
+ JSON.should_receive(:parse).and_return([])
32
+ client.send(client.query('test/test').interval("2013-04-04", "2013-04-04"))
33
+ end
34
+
35
+ it 'raises on request failure' do
36
+ stub_request(:post, "http://www.example.com/druid/v2").
37
+ with(:body => "{\"dataSource\":\"test\",\"granularity\":\"all\",\"intervals\":[\"2013-04-04T00:00:00+00:00/2013-04-04T00:00:00+00:00\"]}",
38
+ :headers => {'Accept'=>'*/*', 'Content-Type'=>'application/json', 'User-Agent'=>'Ruby'}).
39
+ to_return(:status => 666, :body => "Strange server error", :headers => {})
40
+ Druid::ZooHandler.stub!(:new).and_return(mock(Druid::ZooHandler, :data_sources => {'test/test' => 'http://www.example.com/druid/v2'}, :close! => true))
41
+ client = Druid::Client.new('test_uri', zk_keepalive: true)
42
+ expect { client.send(client.query('test/test').interval("2013-04-04", "2013-04-04")) }.to raise_error(RuntimeError, /Request failed: 666: Strange server error/)
43
+ end
44
+
45
+ it 'should have a static setup' do
46
+ client = Druid::Client.new('test_uri', :static_setup => {'madvertise/mock' => 'mock_uri'})
47
+ client.data_sources.should == ['madvertise/mock']
48
+ client.data_source_uri('madvertise/mock').should == URI('mock_uri')
49
+ end
50
+
51
+ it 'should report dimensions of a data source correctly' do
52
+ stub_request(:get, "http://www.example.com/druid/v2/datasources/mock").
53
+ with(:headers =>{'Accept'=>'*/*', 'User-Agent'=>'Ruby'}).
54
+ to_return(:status => 200, :body => '{"dimensions":["d1","d2","d3"],"metrics":["m1", "m2"]}')
55
+
56
+ client = Druid::Client.new('test_uri', :static_setup => {'madvertise/mock' => 'http://www.example.com/druid/v2/'})
57
+ client.data_source('madvertise/mock').dimensions.should == ["d1","d2","d3"]
58
+ end
59
+
60
+ it 'should report metrics of a data source correctly' do
61
+ stub_request(:get, "http://www.example.com/druid/v2/datasources/mock").
62
+ with(:headers =>{'Accept'=>'*/*', 'User-Agent'=>'Ruby'}).
63
+ to_return(:status => 200, :body => '{"dimensions":["d1","d2","d3"],"metrics":["m1", "m2"]}')
64
+
65
+ client = Druid::Client.new('test_uri', :static_setup => {'madvertise/mock' => 'http://www.example.com/druid/v2/'})
66
+ client.data_source('madvertise/mock').metrics.should == ["m1","m2"]
67
+ end
68
+
69
+ end
@@ -0,0 +1,377 @@
1
+ require "spec_helper"
2
+
3
+ describe Druid::Query do
4
+
5
+ before :each do
6
+ @query = Druid::Query.new('test')
7
+ end
8
+
9
+ it 'takes a datasource in the constructor' do
10
+ query = Druid::Query.new('test')
11
+ JSON.parse(query.to_json)['dataSource'].should == 'test'
12
+ end
13
+
14
+ it 'takes a query type' do
15
+ @query.query_type('query_type')
16
+ JSON.parse(@query.to_json)['queryType'].should == 'query_type'
17
+ end
18
+
19
+ it 'sets query type by group_by' do
20
+ @query.group_by()
21
+ JSON.parse(@query.to_json)['queryType'].should == 'groupBy'
22
+ end
23
+
24
+ it 'sets query type to timeseries' do
25
+ @query.time_series()
26
+ JSON.parse(@query.to_json)['queryType'].should == 'timeseries'
27
+ end
28
+
29
+ it 'takes dimensions from group_by method' do
30
+ @query.group_by(:a, :b, :c)
31
+ JSON.parse(@query.to_json)['dimensions'].should == ['a', 'b', 'c']
32
+ end
33
+
34
+ it 'build a post aggregation with a constant right' do
35
+ @query.postagg{(a + 1).as ctr }
36
+
37
+ JSON.parse(@query.to_json)['postAggregations'].should == [{"type"=>"arithmetic",
38
+ "fn"=>"+",
39
+ "fields"=>
40
+ [{"type"=>"fieldAccess", "name"=>"a", "fieldName"=>"a"},
41
+ {"type"=>"constant", "value"=>1}],
42
+ "name"=>"ctr"}]
43
+ end
44
+
45
+ it 'build a + post aggregation' do
46
+ @query.postagg{(a + b).as ctr }
47
+ JSON.parse(@query.to_json)['postAggregations'].should == [{"type"=>"arithmetic",
48
+ "fn"=>"+",
49
+ "fields"=>
50
+ [{"type"=>"fieldAccess","name"=>"a", "fieldName"=>"a"},
51
+ {"type"=>"fieldAccess", "name"=>"b", "fieldName"=>"b"}],
52
+ "name"=>"ctr"}]
53
+ end
54
+
55
+ it 'build a - post aggregation' do
56
+ @query.postagg{(a - b).as ctr }
57
+ JSON.parse(@query.to_json)['postAggregations'].should == [{"type"=>"arithmetic",
58
+ "fn"=>"-",
59
+ "fields"=>
60
+ [{"type"=>"fieldAccess", "name"=>"a", "fieldName"=>"a"},
61
+ {"type"=>"fieldAccess", "name"=>"b", "fieldName"=>"b"}],
62
+ "name"=>"ctr"}]
63
+ end
64
+
65
+ it 'build a * post aggregation' do
66
+ @query.postagg{(a * b).as ctr }
67
+ JSON.parse(@query.to_json)['postAggregations'].should == [{"type"=>"arithmetic",
68
+ "fn"=>"*",
69
+ "fields"=>
70
+ [{"type"=>"fieldAccess", "name"=>"a", "fieldName"=>"a"},
71
+ {"type"=>"fieldAccess", "name"=>"b", "fieldName"=>"b"}],
72
+ "name"=>"ctr"}]
73
+ end
74
+
75
+ it 'build a / post aggregation' do
76
+ @query.postagg{(a / b).as ctr }
77
+ JSON.parse(@query.to_json)['postAggregations'].should == [{"type"=>"arithmetic",
78
+ "fn"=>"/",
79
+ "fields"=>
80
+ [{"type"=>"fieldAccess", "name"=>"a", "fieldName"=>"a"},
81
+ {"type"=>"fieldAccess", "name"=>"b", "fieldName"=>"b"}],
82
+ "name"=>"ctr"}]
83
+ end
84
+
85
+ it 'build a complex post aggregation' do
86
+ @query.postagg{((a / b) * 1000).as ctr }
87
+ JSON.parse(@query.to_json)['postAggregations'].should == [{"type"=>"arithmetic",
88
+ "fn"=>"*",
89
+ "fields"=>
90
+ [{"type"=>"arithmetic", "fn"=>"/", "fields"=>
91
+ [{"type"=>"fieldAccess", "name"=>"a", "fieldName"=>"a"},
92
+ {"type"=>"fieldAccess", "name"=>"b", "fieldName"=>"b"}]},
93
+ {"type"=>"constant", "value"=>1000}],
94
+ "name"=>"ctr"}]
95
+ end
96
+
97
+ it 'adds fields required by the postagg operation to longsum' do
98
+ @query.postagg{ (a/b).as c }
99
+ JSON.parse(@query.to_json)['aggregations'].should == [{"type"=>"longSum", "name"=>"a", "fieldName"=>"a"},
100
+ {"type"=>"longSum", "name"=>"b", "fieldName"=>"b"}]
101
+ end
102
+
103
+ it 'chains aggregations' do
104
+ @query.postagg{(a / b).as ctr }.postagg{(b / a).as rtc }
105
+
106
+ JSON.parse(@query.to_json)['postAggregations'].should == [{"type"=>"arithmetic",
107
+ "fn"=>"/",
108
+ "fields"=>
109
+ [{"type"=>"fieldAccess", "name"=>"a", "fieldName"=>"a"},
110
+ {"type"=>"fieldAccess", "name"=>"b", "fieldName"=>"b"}],
111
+ "name"=>"ctr"},
112
+ {"type"=>"arithmetic",
113
+ "fn"=>"/",
114
+ "fields"=>
115
+ [{"type"=>"fieldAccess", "name"=>"b", "fieldName"=>"b"},
116
+ {"type"=>"fieldAccess", "name"=>"a", "fieldName"=>"a"}],
117
+ "name"=>"rtc"}
118
+ ]
119
+ end
120
+
121
+ it 'builds aggregations on long_sum' do
122
+ @query.long_sum(:a, :b, :c)
123
+ JSON.parse(@query.to_json)['aggregations'].should == [
124
+ { 'type' => 'longSum', 'name' => 'a', 'fieldName' => 'a'},
125
+ { 'type' => 'longSum', 'name' => 'b', 'fieldName' => 'b'},
126
+ { 'type' => 'longSum', 'name' => 'c', 'fieldName' => 'c'}
127
+ ]
128
+ end
129
+
130
+
131
+ it 'appends long_sum properties from aggregations on calling long_sum again' do
132
+ @query.long_sum(:a, :b, :c)
133
+ @query.double_sum(:x,:y)
134
+ @query.long_sum(:d, :e, :f)
135
+ JSON.parse(@query.to_json)['aggregations'].sort{|x,y| x['name'] <=> y['name']}.should == [
136
+ { 'type' => 'longSum', 'name' => 'a', 'fieldName' => 'a'},
137
+ { 'type' => 'longSum', 'name' => 'b', 'fieldName' => 'b'},
138
+ { 'type' => 'longSum', 'name' => 'c', 'fieldName' => 'c'},
139
+ { 'type' => 'longSum', 'name' => 'd', 'fieldName' => 'd'},
140
+ { 'type' => 'longSum', 'name' => 'e', 'fieldName' => 'e'},
141
+ { 'type' => 'longSum', 'name' => 'f', 'fieldName' => 'f'},
142
+ { 'type' => 'doubleSum', 'name' => 'x', 'fieldName' => 'x'},
143
+ { 'type' => 'doubleSum', 'name' => 'y', 'fieldName' => 'y'}
144
+ ]
145
+ end
146
+
147
+ it 'removes duplicate aggregation fields' do
148
+ @query.long_sum(:a, :b)
149
+ @query.long_sum(:b)
150
+
151
+ JSON.parse(@query.to_json)['aggregations'].should == [
152
+ { 'type' => 'longSum', 'name' => 'a', 'fieldName' => 'a'},
153
+ { 'type' => 'longSum', 'name' => 'b', 'fieldName' => 'b'},
154
+ ]
155
+ end
156
+
157
+ it 'must be chainable' do
158
+ q = [Druid::Query.new('test')]
159
+ q.push q[-1].query_type('a')
160
+ q.push q[-1].data_source('b')
161
+ q.push q[-1].group_by('c')
162
+ q.push q[-1].long_sum('d')
163
+ q.push q[-1].double_sum('e')
164
+ q.push q[-1].filter{a.eq 1}
165
+ q.push q[-1].interval("2013-01-26T00", "2020-01-26T00:15")
166
+ q.push q[-1].granularity(:day)
167
+
168
+ q.each do |instance|
169
+ instance.should == q[0]
170
+ end
171
+ end
172
+
173
+ it 'parses intervals from strings' do
174
+ @query.interval('2013-01-26T0', '2020-01-26T00:15')
175
+ JSON.parse(@query.to_json)['intervals'].should == ['2013-01-26T00:00:00+00:00/2020-01-26T00:15:00+00:00']
176
+ end
177
+
178
+ it 'takes multiple intervals' do
179
+ @query.intervals([['2013-01-26T0', '2020-01-26T00:15'],['2013-04-23T0', '2013-04-23T15:00']])
180
+ JSON.parse(@query.to_json)['intervals'].should == ["2013-01-26T00:00:00+00:00/2020-01-26T00:15:00+00:00", "2013-04-23T00:00:00+00:00/2013-04-23T15:00:00+00:00"]
181
+ end
182
+
183
+ it 'accepts Time objects for intervals' do
184
+ @query.interval(a = Time.now, b = Time.now + 1)
185
+ JSON.parse(@query.to_json)['intervals'].should == ["#{a.iso8601}/#{b.iso8601}"]
186
+ end
187
+
188
+ it 'takes a granularity from string' do
189
+ @query.granularity('all')
190
+ JSON.parse(@query.to_json)['granularity'].should == 'all'
191
+ end
192
+
193
+ it 'should take a period' do
194
+ @query.granularity(:day, 'CEST')
195
+ @query.properties[:granularity].should == {
196
+ :type => "period",
197
+ :period => "P1D",
198
+ :timeZone => "Europe/Berlin"
199
+ }
200
+ end
201
+
202
+ it 'creates an equals filter' do
203
+ @query.filter{a.eq 1}
204
+ JSON.parse(@query.to_json)['filter'].should == {"type"=>"selector", "dimension"=>"a", "value"=>1}
205
+ end
206
+
207
+ it 'creates an equals filter with ==' do
208
+ @query.filter{a == 1}
209
+ JSON.parse(@query.to_json)['filter'].should == {"type"=>"selector", "dimension"=>"a", "value"=>1}
210
+ end
211
+
212
+
213
+ it 'creates a not filter' do
214
+ @query.filter{!a.eq 1}
215
+ JSON.parse(@query.to_json)['filter'].should == {"field" =>
216
+ {"type"=>"selector", "dimension"=>"a", "value"=>1},
217
+ "type" => "not"}
218
+ end
219
+
220
+ it 'creates a not filter with neq' do
221
+ @query.filter{a.neq 1}
222
+ JSON.parse(@query.to_json)['filter'].should == {"field" =>
223
+ {"type"=>"selector", "dimension"=>"a", "value"=>1},
224
+ "type" => "not"}
225
+ end
226
+
227
+ it 'creates a not filter with !=' do
228
+ @query.filter{a != 1}
229
+ JSON.parse(@query.to_json)['filter'].should == {"field" =>
230
+ {"type"=>"selector", "dimension"=>"a", "value"=>1},
231
+ "type" => "not"}
232
+ end
233
+
234
+
235
+ it 'creates an and filter' do
236
+ @query.filter{a.neq(1) & b.eq(2) & c.eq('foo')}
237
+ JSON.parse(@query.to_json)['filter'].should == {"fields" => [
238
+ {"type"=>"not", "field"=>{"type"=>"selector", "dimension"=>"a", "value"=>1}},
239
+ {"type"=>"selector", "dimension"=>"b", "value"=>2},
240
+ {"type"=>"selector", "dimension"=>"c", "value"=>"foo"}
241
+ ],
242
+ "type" => "and"}
243
+ end
244
+
245
+ it 'creates an or filter' do
246
+ @query.filter{a.neq(1) | b.eq(2) | c.eq('foo')}
247
+ JSON.parse(@query.to_json)['filter'].should == {"fields" => [
248
+ {"type"=>"not", "field"=> {"type"=>"selector", "dimension"=>"a", "value"=>1}},
249
+ {"type"=>"selector", "dimension"=>"b", "value"=>2},
250
+ {"type"=>"selector", "dimension"=>"c", "value"=>"foo"}
251
+ ],
252
+ "type" => "or"}
253
+ end
254
+
255
+ it 'chains filters' do
256
+ @query.filter{a.eq(1)}.filter{b.eq(2)}
257
+ JSON.parse(@query.to_json)['filter'].should == {"fields" => [
258
+ {"type"=>"selector", "dimension"=>"a", "value"=>1},
259
+ {"type"=>"selector", "dimension"=>"b", "value"=>2}
260
+ ],
261
+ "type" => "and"}
262
+ end
263
+
264
+ it 'creates filter from hash' do
265
+ @query.filter a:1, b:2
266
+ JSON.parse(@query.to_json)['filter'].should == {"fields" => [
267
+ {"type"=>"selector", "dimension"=>"a", "value"=>1},
268
+ {"type"=>"selector", "dimension"=>"b", "value"=>2}
269
+ ],
270
+ "type" => "and"}
271
+
272
+ end
273
+
274
+ it 'creates an in statement with or filter' do
275
+ @query.filter{a.in [1,2,3]}
276
+ JSON.parse(@query.to_json)['filter'].should == {"fields" => [
277
+ {"type"=>"selector", "dimension"=>"a", "value"=>1},
278
+ {"type"=>"selector", "dimension"=>"a", "value"=>2},
279
+ {"type"=>"selector", "dimension"=>"a", "value"=>3}
280
+ ],
281
+ "type" => "or"}
282
+ end
283
+
284
+ it 'creates a javascript with > filter' do
285
+ @query.filter{a > 100}
286
+ JSON.parse(@query.to_json)['filter'].should == {
287
+ "type" => "javascript",
288
+ "dimension" => "a",
289
+ "function" => "function(a) { return(a > 100); }"
290
+ }
291
+ end
292
+
293
+ it 'creates a mixed javascript filter' do
294
+ @query.filter{(a >= 128) & (a != 256)}
295
+ JSON.parse(@query.to_json)['filter'].should == {"fields" => [
296
+ {"type" => "javascript", "dimension" => "a", "function" => "function(a) { return(a >= 128); }"},
297
+ {"field" => {"type" => "selector", "dimension" => "a", "value" => 256}, "type" => "not"}
298
+ ],
299
+ "type" => "and"}
300
+ end
301
+
302
+ it 'creates a complex javascript filter' do
303
+ @query.filter{(a >= 4) & (a <= '128')}
304
+ JSON.parse(@query.to_json)['filter'].should == {"fields" => [
305
+ {"type" => "javascript", "dimension" => "a", "function" => "function(a) { return(a >= 4); }"},
306
+ {"type" => "javascript", "dimension" => "a", "function" => "function(a) { return(a <= '128'); }"}
307
+ ],
308
+ "type" => "and"}
309
+ end
310
+
311
+ it 'can chain two in statements' do
312
+ @query.filter{a.in([1,2,3]) & b.in([1,2,3])}
313
+ JSON.parse(@query.to_json)['filter'].should == {"type"=>"and", "fields"=>[
314
+ {"type"=>"or", "fields"=>[
315
+ {"type"=>"selector", "dimension"=>"a", "value"=>1},
316
+ {"type"=>"selector", "dimension"=>"a", "value"=>2},
317
+ {"type"=>"selector", "dimension"=>"a", "value"=>3}
318
+ ]},
319
+ {"type"=>"or", "fields"=>[
320
+ {"type"=>"selector", "dimension"=>"b", "value"=>1},
321
+ {"type"=>"selector", "dimension"=>"b", "value"=>2},
322
+ {"type"=>"selector", "dimension"=>"b", "value"=>3}
323
+ ]}
324
+ ]}
325
+ end
326
+
327
+ it 'creates a greater than having clause' do
328
+ @query.having{a > 100}
329
+ JSON.parse(@query.to_json)['having'].should == {
330
+ "type"=>"greaterThan", "aggregation"=>"a", "value"=>100
331
+ }
332
+ end
333
+
334
+ it 'does not accept in with empty array' do
335
+ expect { @query.filter{a.in []} }.to raise_error "Must provide non-empty array in in()"
336
+ end
337
+
338
+ it 'does raise on invalid filter statement' do
339
+ expect { @query.filter{:a} }.to raise_error 'Not a valid filter'
340
+ end
341
+
342
+ it 'raises if no value is passed to a filter operator' do
343
+ expect { @query.filter{a.eq a}.to_json}.to raise_error 'no value assigned'
344
+ end
345
+
346
+ it 'raises wrong number of arguments if filter operator is called without param' do
347
+ expect { @query.filter{a.eq}.to_json}.to raise_error 'wrong number of arguments (0 for 1)'
348
+ end
349
+
350
+ it 'should query regexp using .regexp(string)' do
351
+ JSON.parse(@query.filter{a.regexp('[1-9].*')}.to_json)['filter'].should == {
352
+ "dimension"=>"a",
353
+ "type"=>"regex",
354
+ "pattern"=>"[1-9].*"
355
+ }
356
+ end
357
+
358
+ it 'should query regexp using .eq(regexp)' do
359
+ JSON.parse(@query.filter{a.in(/abc.*/)}.to_json)['filter'].should == {
360
+ "dimension"=>"a",
361
+ "type"=>"regex",
362
+ "pattern"=>"abc.*"
363
+ }
364
+ end
365
+
366
+ it 'should query regexp using .in([regexp])' do
367
+ JSON.parse(@query.filter{ a.in(['b', /[a-z].*/, 'c']) }.to_json)['filter'].should == {
368
+ "type"=>"or",
369
+ "fields"=>[
370
+ {"dimension"=>"a", "type"=>"selector", "value"=>"b"},
371
+ {"dimension"=>"a", "type"=>"regex", "pattern"=>"[a-z].*"},
372
+ {"dimension"=>"a", "type"=>"selector", "value"=>"c"}
373
+ ]
374
+ }
375
+ end
376
+
377
+ end