rubberband 0.0.2

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,3 @@
1
+ require "transport/base_protocol"
2
+ require "transport/base"
3
+ require "transport/http"
@@ -0,0 +1,32 @@
1
+ module ElasticSearch
2
+ module Transport
3
+ class RetryableError < StandardError; end
4
+ class FatalError < StandardError; end
5
+
6
+ DEFAULTS = {
7
+ :encoder => ElasticSearch::Encoding::JSON
8
+ }.freeze
9
+
10
+ class Base
11
+ include BaseProtocol
12
+
13
+ attr_accessor :server, :options
14
+
15
+ def initialize(server, options={})
16
+ @server = server
17
+ @options = DEFAULTS.merge(options)
18
+ end
19
+
20
+ def connect!
21
+ raise NotImplementedError
22
+ end
23
+
24
+ def close
25
+ end
26
+
27
+ def encoder
28
+ @encoder ||= @options[:encoder].new
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,98 @@
1
+ module ElasticSearch
2
+ module Transport
3
+ module BaseProtocol
4
+
5
+ # index api
6
+
7
+ def index(index, type, id, document, options={})
8
+ raise NotImplementedError
9
+ end
10
+
11
+ def get(index, type, id, options={})
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def delete(options)
16
+ raise NotImplementedError
17
+ end
18
+
19
+ def search(index, type, query, options={})
20
+ raise NotImplementedError
21
+ end
22
+
23
+ def count(index, type, query, options={})
24
+ raise NotImplementedError
25
+ end
26
+
27
+ # admin index api
28
+ def index_status(index_list, options={})
29
+ raise NotImplementedError
30
+ end
31
+
32
+ def create_index(index, create_options={}, options={})
33
+ raise NotImplementedError
34
+ end
35
+
36
+ def delete_index(index, options={})
37
+ raise NotImplementedError
38
+ end
39
+
40
+ def alias_index(operations, options={})
41
+ raise NotImplementedError
42
+ end
43
+
44
+ def update_mapping(index, type, mapping, options)
45
+ raise NotImplementedError
46
+ end
47
+
48
+ def flush(index_list, options={})
49
+ raise NotImplementedError
50
+ end
51
+
52
+ def refresh(index_list, options={})
53
+ raise NotImplementedError
54
+ end
55
+
56
+ def snapshot(index_list, options={})
57
+ raise NotImplementedError
58
+ end
59
+
60
+ def optimize(index_list, options={})
61
+ raise NotImplementedError
62
+ end
63
+
64
+
65
+ # admin cluster api
66
+ def cluster_health(index_list, options={})
67
+ raise NotImplementedError
68
+ end
69
+
70
+ def cluster_state(options={})
71
+ raise NotImplementedError
72
+ end
73
+
74
+ def nodes_info(node_list, options={})
75
+ raise NotImplementedError
76
+ end
77
+
78
+ def nodes_stats(node_list, options={})
79
+ raise NotImplementedError
80
+ end
81
+
82
+ def shutdown_nodes(node_list, options={})
83
+ raise NotImplementedError
84
+ end
85
+
86
+ def restart_nodes(node_list, options={})
87
+ raise NotImplementedError
88
+ end
89
+
90
+ # misc
91
+ def all_nodes
92
+ raise NotImplementedError
93
+ end
94
+
95
+ end
96
+ end
97
+ end
98
+
@@ -0,0 +1,263 @@
1
+ require 'patron'
2
+ require 'cgi'
3
+
4
+ module ElasticSearch
5
+ module Transport
6
+ class HTTP < Base
7
+
8
+ class ConnectionFailed < RetryableError; end
9
+ class HostResolutionError < RetryableError; end
10
+ class TimeoutError < RetryableError; end
11
+ class RequestError < FatalError; end
12
+
13
+ DEFAULTS = {
14
+ :timeout => 5
15
+ }.freeze
16
+
17
+ def initialize(server, options={})
18
+ super
19
+ @options = DEFAULTS.merge(@options)
20
+ end
21
+
22
+ def connect!
23
+ @session = Patron::Session.new
24
+ @session.base_url = @server
25
+ @session.timeout = @options[:timeout]
26
+ @session.headers['User-Agent'] = 'ElasticSearch.rb v0.1'
27
+ request(:get, "/") # try a get to see if the server responds
28
+ end
29
+
30
+ # index api (TODO modulize)
31
+
32
+ def index(index, type, id, document, options={})
33
+ body = encoder.is_encoded?(document) ? document : encoder.encode(document)
34
+ if id.nil?
35
+ response = request(:post, generate_path(:index => index, :type => type), body)
36
+ else
37
+ response = request(:put, generate_path(:index => index, :type => type, :id => id), body)
38
+ end
39
+ handle_error(response) unless response.status == 200
40
+ encoder.decode(response.body)
41
+ end
42
+
43
+ def get(index, type, id, options={})
44
+ response = request(:get, generate_path(:index => index, :type => type, :id => id))
45
+ return nil if response.status == 404
46
+
47
+ handle_error(response) unless response.status == 200
48
+ hit = encoder.decode(response.body)
49
+ unescape_id!(hit) #TODO extract these two calls from here and search
50
+ set_encoding!(hit)
51
+ hit # { "_id", "_index", "_type", "_source" }
52
+ end
53
+
54
+ def delete(index, type, id, options={})
55
+ response = request(:delete, generate_path(:index => index, :type => type, :id => id))
56
+ handle_error(response) unless response.status == 200 # ElasticSearch always returns 200 on delete, even if the object doesn't exist
57
+ encoder.decode(response.body)
58
+ end
59
+
60
+ def search(index, type, query, options={})
61
+ if query.is_a?(Hash)
62
+ # patron cannot submit get requests with content, so if query is a hash, post it instead (assume a query hash is using the query dsl)
63
+ response = request(:post, generate_path(:index => index, :type => type, :op => "_search", :params => options), encoder.encode(query))
64
+ else
65
+ response = request(:get, generate_path(:index => index, :type => type, :op => "_search", :params => options.merge(:q => query)))
66
+ end
67
+ handle_error(response) unless response.status == 200
68
+ results = encoder.decode(response.body)
69
+ # unescape ids
70
+ results["hits"]["hits"].each do |hit|
71
+ unescape_id!(hit)
72
+ set_encoding!(hit)
73
+ end
74
+ results # {"hits"=>{"hits"=>[{"_id", "_type", "_source", "_index"}], "total"}, "_shards"=>{"failed", "total", "successful"}}
75
+ end
76
+
77
+ def count(index, type, query, options={})
78
+ if query.is_a?(Hash)
79
+ # patron cannot submit get requests with content, so if query is a hash, post it instead (assume a query hash is using the query dsl)
80
+ response = request(:post, generate_path(:index => index, :type => type, :op => "_count", :params => options), encoder.encode(query))
81
+ else
82
+ response = request(:get, generate_path(:index => index, :type => type, :op => "_count", :params => options.merge(:q => query)))
83
+ end
84
+ handle_error(response) unless response.status == 200
85
+ encoder.decode(response.body) # {"count", "_shards"=>{"failed", "total", "successful"}}
86
+ end
87
+
88
+ # admin index api (TODO modulize)
89
+ #
90
+ def index_status(index_list, options={})
91
+ standard_request(:get, generate_path(:index => index_list, :op => "_status"))
92
+ end
93
+
94
+ def create_index(index, create_options={}, options={})
95
+ standard_request(:put, generate_path(:index => index), encoder.encode(create_options))
96
+ end
97
+
98
+ def delete_index(index, options={})
99
+ standard_request(:delete, generate_path(:index => index))
100
+ end
101
+
102
+ def alias_index(operations, options={})
103
+ standard_request(:post, generate_path(:op => "_aliases"), encoder.encode(operations))
104
+ end
105
+
106
+ def update_mapping(index, type, mapping, options)
107
+ standard_request(:put, generate_path(:index => index, :type => type, :op => "_mapping", :params => options), encoder.encode(mapping))
108
+ end
109
+
110
+ def flush(index_list, options={})
111
+ standard_request(:post, generate_path(:index => index_list, :op => "_flush", :params => options), "")
112
+ end
113
+
114
+ def refresh(index_list, options={})
115
+ standard_request(:post, generate_path(:index => index_list, :op => "_refresh"), "")
116
+ end
117
+
118
+ def snapshot(index_list, options={})
119
+ standard_request(:post, generate_path(:index => index_list, :type => "_gateway", :op => "snapshot"), "")
120
+ end
121
+
122
+ def optimize(index_list, options={})
123
+ standard_request(:post, generate_path(:index => index_list, :op => "_optimize", :params => options), "")
124
+ end
125
+
126
+ # admin cluster api (TODO modulize)
127
+
128
+ def cluster_health(index_list, options={})
129
+ standard_request(:get, generate_path(:index => "_cluster", :type => "health", :id => index_list, :params => options))
130
+ end
131
+
132
+ def cluster_state(options={})
133
+ standard_request(:get, generate_path(:index => "_cluster", :op => "state"))
134
+ end
135
+
136
+ def nodes_info(node_list, options={})
137
+ standard_request(:get, generate_path(:index => "_cluster", :type => "nodes", :id => node_list))
138
+ end
139
+
140
+ def nodes_stats(node_list, options={})
141
+ standard_request(:get, generate_path(:index => "_cluster", :type => "nodes", :id => node_list, :op => "stats"))
142
+ end
143
+
144
+ def shutdown_nodes(node_list, options={})
145
+ standard_request(:post, generate_path(:index => "_cluster", :type => "nodes", :id => node_list, :op => "_shutdown", :params => options), "")
146
+ end
147
+
148
+ def restart_nodes(node_list, options={})
149
+ standard_request(:post, generate_path(:index => "_cluster", :type => "nodes", :id => node_list, :op => "_restart", :params => options), "")
150
+ end
151
+
152
+ # misc helper methods (TODO modulize)
153
+ def all_nodes
154
+ http_addresses = nodes_info([])["nodes"].collect { |id, node| node["http_address"] }
155
+ http_addresses.collect! do |a|
156
+ if a =~ /inet\[.*\/([\d.:]+)\]/
157
+ $1
158
+ end
159
+ end.compact!
160
+ http_addresses
161
+ end
162
+
163
+ private
164
+
165
+ def standard_request(*args)
166
+ response = request(*args)
167
+ handle_error(response) unless response.status == 200
168
+ encoder.decode(response.body)
169
+ end
170
+
171
+ # :index - one or many index names
172
+ # :type - one or many types
173
+ # :id - one id
174
+ # :params - hash of query params
175
+ def generate_path(options)
176
+ path = ""
177
+ path << "/#{Array(options[:index]).collect { |i| escape(i.downcase) }.join(",")}" if options[:index] && !options[:index].empty?
178
+ path << "/#{Array(options[:type]).collect { |t| escape(t) }.join(",")}" if options[:type] && !options[:type].empty?
179
+ path << "/#{Array(options[:id]).collect { |id| escape(id) }.join(",")}" if options[:id] && !options[:id].empty?
180
+ path << "/#{options[:op]}" if options[:op]
181
+ path << "?" << query_string(options[:params]) if options[:params] && !options[:params].empty?
182
+ path
183
+ end
184
+
185
+ def unescape_id!(hit)
186
+ hit["_id"] = unescape(hit["_id"])
187
+ nil
188
+ end
189
+
190
+ def set_encoding!(hit)
191
+ encode_utf8(hit["_source"])
192
+ nil
193
+ end
194
+
195
+ # faster than CGI.escape
196
+ # stolen from RSolr, which stole it from Rack
197
+ def escape(string)
198
+ string.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n) {
199
+ #'%'+$1.unpack('H2'*$1.size).join('%').upcase
200
+ '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
201
+ }.tr(' ', '+')
202
+ end
203
+
204
+ def unescape(string)
205
+ CGI.unescape(string)
206
+ end
207
+
208
+ if ''.respond_to?(:force_encoding) && ''.respond_to?(:encoding)
209
+ # encodes the string as utf-8 in Ruby 1.9
210
+ def encode_utf8(string)
211
+ # ElasticSearch only ever returns json in UTF-8 (per the JSON spec) so we can use force_encoding here (#TODO what about ids? can we assume those are always ascii?)
212
+ string.force_encoding(Encoding::UTF_8)
213
+ end
214
+ else
215
+ # returns the unaltered string in Ruby 1.8
216
+ def encode_utf8(string)
217
+ string
218
+ end
219
+ end
220
+
221
+ # Return the bytesize of String; uses String#size under Ruby 1.8 and
222
+ # String#bytesize under 1.9.
223
+ if ''.respond_to?(:bytesize)
224
+ def bytesize(string)
225
+ string.bytesize
226
+ end
227
+ else
228
+ def bytesize(string)
229
+ string.size
230
+ end
231
+ end
232
+
233
+ #doesn't handle arrays or hashes or what have you
234
+ def query_string(params)
235
+ params.collect { |k,v| "#{escape(k.to_s)}=#{escape(v.to_s)}" }.join("&")
236
+ end
237
+
238
+ def request(method, path, body=nil, headers={})
239
+ begin
240
+ #puts "request: #{@session.base_url}#{path} body:#{body}"
241
+ response = @session.request(method, path, headers, :data => body)
242
+ handle_error(response) if response.status >= 500
243
+ response
244
+ rescue Exception => e
245
+ case e
246
+ when Patron::ConnectionFailed
247
+ raise ConnectionFailed
248
+ when Patron::HostResolutionError
249
+ raise HostResolutionError
250
+ when TimeoutError
251
+ raise TimeoutError
252
+ else
253
+ raise e
254
+ end
255
+ end
256
+ end
257
+
258
+ def handle_error(response)
259
+ raise RequestError, "(#{response.status}) #{response.body}"
260
+ end
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,7 @@
1
+ require 'test_helper'
2
+
3
+ class ElasticSearchTest < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
@@ -0,0 +1,34 @@
1
+ require 'test_helper'
2
+
3
+ class HitsTest < Test::Unit::TestCase
4
+ context "A Hit instance" do
5
+ setup do
6
+ @response = {"_source" => {"foo" => "bar"}, "_id" => "1"}
7
+ @hit = ElasticSearch::Api::Hit.new(@response)
8
+ end
9
+
10
+ should "set id" do
11
+ assert_equal @response["_id"], @hit.id
12
+ end
13
+
14
+ should "set hit attributes" do
15
+ assert_equal @response["_source"]["foo"], @hit.foo
16
+ end
17
+
18
+ should "be frozen" do
19
+ assert @hit.attributes.frozen?
20
+ end
21
+
22
+
23
+ end
24
+
25
+ context "A Hits instance" do
26
+ setup do
27
+ @response1 = {"_source" => {"foo" => "bar"}, "_id" => "1"}
28
+ @hit = ElasticSearch::Api::Hit.new(@response)
29
+
30
+ end
31
+
32
+ should "exist"
33
+ end
34
+ end
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'elasticsearch'
8
+
9
+ class Test::Unit::TestCase
10
+ end