jungle 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ = 0.1.0
2
+
3
+ * initial release
@@ -0,0 +1,12 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/jungle.rb
6
+ lib/jungle/client.rb
7
+ lib/jungle/support.rb
8
+ lib/jungle/version.rb
9
+ test/example.rb
10
+ test/jungle_test.rb
11
+ test/test.rb
12
+ test/test_helper.rb
@@ -0,0 +1,63 @@
1
+ Jungle
2
+ http://jungle.rubyforge.org/
3
+ by Tom Preston-Werner
4
+
5
+ == DESCRIPTION:
6
+
7
+ Jungle is a client library for the Amazon SQS (Simple Queue Service). It provides an intuitive, well documented API and a fully tested codebase.
8
+
9
+ == INSTALLATION:
10
+
11
+ Jungle can be installed via RubyGems:
12
+
13
+ $ sudo gem install jungle
14
+
15
+ == USAGE:
16
+
17
+ In order to use Jungle to interface with Amazon web services, you must first sign up for a web services account at http://amazonaws.com. Once you have an account, you'll need to locate your access key (20 bytes long) and secret access key (40 bytes long). It is important to keep your secret access key secure (take necessary precautions when deploying code that contains your AWS credentials). With your credentials handy, you can create a Jungle client.
18
+
19
+ require 'rubygems'
20
+ require 'jungle'
21
+
22
+ client = Jungle::Client.new('1234567890ABCDEFGHIJ', '1234567890abcdefghijklmnopqrstuvwxyz1234')
23
+
24
+ From the client, you can get access to an SQS client, on which you can execute SQS commands.
25
+
26
+ The first thing you'll need to do is create a queue:
27
+
28
+ client.sqs.create_queue('foo')
29
+
30
+ Once you've created the queue, you can send messages to it:
31
+
32
+ client.sqs.send_message('foo', "Help, I'm trapped in a REPL!")
33
+
34
+ Messages are always placed on the back of the queue. To retrieve a message from the front of the queue:
35
+
36
+ message = client.sqs.receive_message('foo')
37
+
38
+ The call to Jungle::SQSClient#receive_message will return a single Jungle::SQSMessage by default. From this you can get the message's id and body.
39
+
40
+ message.id #=> "1AM3Q5BE7ZZRNQD8MEY6|3H4AA8J7EJKM0DQZR7E1|9CBMKVD6TTQX44QJ1S30"
41
+ message.body #=> "Help, I'm trapped in a REPL!"
42
+
43
+ When you make a call to Jungle::SQSClient#receive_message, the message(s) that are returned will be invisible to other receive_message calls for 30 seconds. This prevents two clients from receiving the same message, but still allows the message to be handled after the timeout. You can specify a different visibility timeout when you create the queue:
44
+
45
+ client.sqs.create_queue('bar', { :visibility_timeout => '60' })
46
+
47
+ Alternatively, you can specify a visibility timeout for a specific message when you place it on the queue.
48
+
49
+ client.sqs.send_message('foo', { :visibility_timeout => '15' })
50
+
51
+ If you have the id of a message handy and you'd like to get that message without locking it, you can peek at it:
52
+
53
+ unlocked_message = client.sqs.peek_message('foo', message.id)
54
+
55
+ unlocked_message.id #=> "1AM3Q5BE7ZZRNQD8MEY6|3H4AA8J7EJKM0DQZR7E1|9CBMKVD6TTQX44QJ1S30"
56
+ unlocked_message.body #=> "Help, I'm trapped in a REPL!"
57
+
58
+ Once you've handled the message, you'll want to remove it permanently from the queue:
59
+
60
+ client.sqs.delete_message('foo', message.id)
61
+
62
+ You need to specify both the queue and the message id in order to delete the message.
63
+
@@ -0,0 +1,15 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/jungle.rb'
6
+
7
+ Hoe.new('jungle', Jungle::VERSION) do |p|
8
+ p.rubyforge_name = 'jungle'
9
+ p.summary = 'A pure Ruby client library for Amazon\'s SQS business web services.'
10
+ p.description = p.paragraphs_of('README.txt', 2).join("\n\n")
11
+ p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
12
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
13
+ p.need_tar = false
14
+ p.extra_deps = []
15
+ end
@@ -0,0 +1,9 @@
1
+ require 'base64'
2
+ require 'openssl'
3
+ require 'digest/sha1'
4
+ require 'net/https'
5
+ require 'delegate'
6
+ require 'rexml/document'
7
+ require 'yaml'
8
+
9
+ Dir[File.join(File.dirname(__FILE__), 'jungle/**/*.rb')].sort.each { |lib| require lib }
@@ -0,0 +1,591 @@
1
+ module Jungle
2
+ class Client
3
+ # Return a new Jungle client on which you get SQS or S3 clients and make requests to Amazon AWS
4
+ # +access_key_id+ is an AWS access key (20 bytes long)
5
+ # +secret_access_key+ is an AWS secret access key used to compute HMAC:SHA1 (40 bytes)
6
+ def initialize(access_key_id = nil, secret_access_key = nil)
7
+ @auth = Auth.new(access_key_id, secret_access_key)
8
+ end
9
+
10
+ # Return an SQSClient with the credentials of this Client. The returned object is
11
+ # a singleton, and as such the same instance will be returned for all calls to this
12
+ # method on this client
13
+ def sqs
14
+ @sqs ||= SQSClient.new(@auth)
15
+ end
16
+ end
17
+
18
+ class AWSClient
19
+ protected
20
+
21
+ # Return a connection from the connection pool (or block until one is available)
22
+ # This method must be overridden by AWSClient subclasses
23
+ def get_connection_from_pool
24
+ raise(Exception, "AWS#get_connection_from_pool must be overridden in subclasses")
25
+ end
26
+
27
+ def camelize(lower_case_and_underscored_word, first_letter_in_uppercase = true)
28
+ if first_letter_in_uppercase
29
+ lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase }
30
+ else
31
+ lower_case_and_underscored_word.first + camelize(lower_case_and_underscored_word)[1..-1]
32
+ end
33
+ end
34
+
35
+ # Construct a url from the given path and query parameters
36
+ def construct_url(path, query_params = {})
37
+ query_part = query_params.keys.map { |key| "#{camelize(key)}=#{query_params[key]}" }.join('&')
38
+
39
+ base = query_part.size != 0 ? (path == '' ? '/?' : '?') : ''
40
+
41
+ path << base << query_part
42
+ end
43
+
44
+ # Make a REST call
45
+ def rest_call(host, path, query, security_params, data = '', headers = {})
46
+ connection = get_connection_from_pool()
47
+
48
+ connection.set_debug_output($stdout) if debug()
49
+
50
+ connection.start do
51
+ request = method_to_request_class(security_params['method']).new(construct_url(path, query))
52
+
53
+ security_headers = {'Authorization' => security_params['auth_string'],
54
+ 'Date' => security_params['date'],
55
+ 'AWS-Version' => '2006-04-01',
56
+ 'Content-Type' => 'text/plain'}
57
+
58
+ set_headers(request, headers)
59
+ set_headers(request, security_headers)
60
+
61
+ if request.request_body_permitted?
62
+ return connection.request(request, data)
63
+ else
64
+ return connection.request(request)
65
+ end
66
+ end
67
+ end
68
+
69
+ # Return the corresponding Net:HTTP class for the given method
70
+ # +method+ is one of "get", "post", put", or "delete"
71
+ def method_to_request_class(method)
72
+ method =~ /(get|post|put|delete)/i || raise(InvalidArgument, "method must be a valid HTTP method")
73
+ Net::HTTP.const_get(method.capitalize.intern)
74
+ end
75
+
76
+ # Add the given headers to the request
77
+ # +request+ is the request
78
+ # +headers+ is a hash containing key, value pairs
79
+ # +prefix+ is a string that will be appended to each key
80
+ def set_headers(request, headers, prefix='')
81
+ headers.each do |key, value|
82
+ request[prefix + key] = value
83
+ end
84
+ end
85
+ end
86
+
87
+ class SQSClient < AWSClient
88
+ attr_accessor :service_url
89
+ attr_accessor :debug
90
+ attr_reader :mode
91
+
92
+ # Return a new SQSClient
93
+ # +auth+ is a Jungle::Auth object with knowledge of authentication credentials
94
+ def initialize(auth)
95
+ @auth = auth
96
+ @debug = false
97
+
98
+ @mode = :lazy
99
+ @queue_lookup = {}
100
+
101
+ self.service_url = "queue.amazonaws.com"
102
+ end
103
+
104
+ # An SQSClient may be in one of two modes: lazy or strict. In strict mode,
105
+ # calls to send_message and receive_message must specify the full queue path
106
+ # (e.g. A8SVKWT62YDQI/foo). Lazy mode allows you to specify just the queue
107
+ # name (e.g. foo) and will make a call to create_queue to look up the queue
108
+ # path for a given queue name. As queue paths are cached by the client, this
109
+ # process only happens once.
110
+ # +mode+ is a symbol representing the mode to be set, either :lazy or :strict
111
+ # (default :lazy)
112
+ def mode=(mode)
113
+ [:lazy, :strict].include?(mode) || raise(InvalidArgument, "Mode must be either :lazy or :strict")
114
+ @mode = mode
115
+ end
116
+
117
+ # Create a new queue with the given name. If the queue already exists, the
118
+ # operation returns the same response as if it had just been created.
119
+ # +name+ is the name of the queue you wish to create, e.g. Foo
120
+ # +options+ is a hash that may specify the following options:
121
+ # +visibility_timeout+ is the visibility timeout to set as the default for
122
+ # new messages on this queue
123
+ #
124
+ # The response has the following readers:
125
+ # #operation - :create_queue
126
+ # #code - the http response code (200 on success)
127
+ # #xml - the full xml body that was returned with the request
128
+ # #queue_url - the queue path that was created
129
+ def create_queue(name, options = {})
130
+ validate_queue_name(name)
131
+
132
+ # construct request url string
133
+ path = ''
134
+ query = {'QueueName' => name}
135
+ query.merge! options
136
+
137
+ # get security params as a hash
138
+ security_params = @auth.security_params('POST', path)
139
+
140
+ # make the call
141
+ http_response = rest_call(self.service_url, path, query, security_params)
142
+
143
+ # create the sqs response
144
+ sqs_response = SQSResponseFactory.manufacture(:create_queue, http_response)
145
+
146
+ # add to queue lookup if response was success and mode is lazy
147
+ if @mode == :lazy && sqs_response.code == 200
148
+ @queue_lookup[name] ||= sqs_response.queue_url
149
+ end
150
+
151
+ sqs_response
152
+ end
153
+
154
+ # List the available queues on this account (up to 10,000).
155
+ # +options+ is a hash that may specify the following options:
156
+ # +queue_name_prefix+ is a string that, if specified, will cause
157
+ # only queues that start with the given string
158
+ # to be returned.
159
+ #
160
+ # The response has the following readers:
161
+ # #operation - :list_queues
162
+ # #code - the http response code (200 on success)
163
+ # #xml - the full xml body that was returned with the request
164
+ # #queue_urls - an array of queue urls
165
+ def list_queues(options = {})
166
+ # construct request url string
167
+ path = ''
168
+ query = options
169
+
170
+ # get security params as a hash
171
+ security_params = @auth.security_params('GET', path)
172
+
173
+ # make the call
174
+ http_response = rest_call(self.service_url, path, query, security_params)
175
+
176
+ # create the sqs response
177
+ SQSResponseFactory.manufacture(:list_queues, http_response)
178
+ end
179
+
180
+ # Put the specified message on the given queue
181
+ # +queue+ is the name of the queue on which to add the message
182
+ # +message+ is the data that this message should contain. Must be between 1 and 256k bytes
183
+ #
184
+ # The response has the following readers:
185
+ # #operation - :send_message
186
+ # #code - the http response code (200 on success)
187
+ # #xml - the full xml body that was returned with the request
188
+ # #message_id - the id of the just created message
189
+ def send_message(queue, message)
190
+ validate_queue_name(queue)
191
+ validate_message(message)
192
+
193
+ # if in lazy mode, find queue path
194
+ queue_path = resolve_queue_path(queue)
195
+
196
+ # construct request url string
197
+ path = '/' << queue_path << '/back'
198
+ query = {}
199
+
200
+ # get security params as a hash
201
+ security_params = @auth.security_params('PUT', path)
202
+
203
+ # prepare the message
204
+ payload = message
205
+
206
+ # make the call
207
+ response = rest_call(self.service_url, path, query, security_params, payload)
208
+
209
+ # return the response
210
+ SQSResponseFactory.manufacture(:send_message, response)
211
+ end
212
+
213
+ # Receive a message from the given queue
214
+ # +queue+ is the name (lazy mode) or path (strict mode) of the queue
215
+ # +options+ is a hash that may specify the following options:
216
+ # +number_of_messages+ is the maximum number of messages to return. If the number
217
+ # of messages in the queue is less than the value specified
218
+ # then all of the remaining messages will be received (default 1)
219
+ # +visibility_timeout+ is the duration, in seconds, that the received message or messages
220
+ # will not be visible to other receive_message calls.
221
+ #
222
+ # The response has the following readers:
223
+ # #operation - :receive_message
224
+ # #code - the http response code (200 on success)
225
+ # #xml - the full xml body that was returned with the request
226
+ # #messages - an array of SQSMessage objects
227
+ #
228
+ # An SQSMessage has the following readers:
229
+ # #id - the id of the message
230
+ # #body - the body of the message
231
+ def receive_messages(queue, options = {})
232
+ validate_queue_name(queue)
233
+
234
+ queue_path = resolve_queue_path(queue)
235
+
236
+ # construct request url string
237
+ path = '/' << queue_path << '/front'
238
+ query = options
239
+
240
+ # get security params as a hash
241
+ security_params = @auth.security_params('GET', path)
242
+
243
+ # make the call
244
+ response = rest_call(self.service_url, path, query, security_params)
245
+
246
+ # return the response
247
+ SQSResponseFactory.manufacture(:receive_message, response, {:queue_path => queue_path})
248
+ end
249
+
250
+ # Receive a single message from the given queue
251
+ # +queue+ is the name (lazy mode) or path (strict mode) of the queue
252
+ # +options+ is a hash that may specify the following options:
253
+ # +visibility_timeout+ is the duration, in seconds, that the received message
254
+ # will not be visible to other receive_message or
255
+ # receive messages calls.
256
+ #
257
+ # The response has the following readers:
258
+ # #operation - :receive_message
259
+ # #code - the http response code (200 on success)
260
+ # #xml - the full xml body that was returned with the request
261
+ # #message - the SQSMessage object or nil if none were returned
262
+ #
263
+ # An SQSMessage has the following readers:
264
+ # #id - the id of the message
265
+ # #body - the body of the message
266
+ def receive_message(queue, options = {})
267
+ local_options = { :number_of_messages => 1 }
268
+ receive_messages(queue, options.merge(local_options))
269
+ end
270
+
271
+ # Delete a message
272
+ # +queue+ is the name (lazy mode) or path (strict mode) of the queue
273
+ # +message_id+ is the message id of the message
274
+ #
275
+ # The response has the following readers:
276
+ # #operation - :delete_message
277
+ # #code - the http response code (200 on success)
278
+ # #xml - the full xml body that was returned with the request
279
+ def delete_message(message_or_queue, message_id = nil)
280
+ message_values = {}
281
+
282
+ if message_or_queue.instance_of? SQSMessage
283
+ message = message_or_queue
284
+ message_values[:queue_path] = message.queue_path
285
+ message_values[:id] = message.id
286
+ else
287
+ queue = message_or_queue
288
+ validate_queue_name(queue)
289
+ message_values[:queue_path] = resolve_queue_path(queue)
290
+ message_values[:id] = message_id
291
+ end
292
+
293
+ # construct request url string
294
+ path = '/' << message_values[:queue_path] << '/' << message_values[:id]
295
+ query = {}
296
+
297
+ # get security params as a hash
298
+ security_params = @auth.security_params('DELETE', path)
299
+
300
+ # make the call
301
+ response = rest_call(self.service_url, path, query, security_params)
302
+
303
+ # return the response
304
+ SQSResponseFactory.manufacture(:delete_message, response)
305
+ end
306
+
307
+ # Look at a message without deleting it from the queue or changing its
308
+ # visibility.
309
+ # +queue+ is the name (lazy mode) or path (strict mode) of the queue (see mode=)
310
+ # +message_id+ is the message id of the message
311
+ #
312
+ # The response has the following readers:
313
+ # #operation - :peek_message
314
+ # #code - the http response code (200 on success)
315
+ # #xml - the full xml body that was returned with the request
316
+ # #message - the SQSMessage
317
+ #
318
+ # An SQSMessage has the following readers:
319
+ # #id - the id of the message
320
+ # #body - the body of the message
321
+ def peek_message(queue, message_id)
322
+ validate_queue_name(queue)
323
+
324
+ queue_path = resolve_queue_path(queue)
325
+
326
+ # construct request url string
327
+ path = '/' << queue_path << '/' << message_id
328
+ query = {}
329
+
330
+ # get security params as a hash
331
+ security_params = @auth.security_params('GET', path)
332
+
333
+ # make the call
334
+ response = rest_call(self.service_url, path, query, security_params)
335
+
336
+ # return the response
337
+ SQSResponseFactory.manufacture(:peek_message, response)
338
+ end
339
+
340
+ protected
341
+
342
+ # Return a pooled http connection for this service. If none are available,
343
+ # block until one becomes available, then return it
344
+ def get_connection_from_pool
345
+ http = Net::HTTP.new(self.service_url, 443)
346
+ http.use_ssl = true
347
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
348
+ http
349
+ end
350
+
351
+ private
352
+
353
+ # Validate the queue. The queue should not begin with a protocol specifier or a slash.
354
+ # +queue+ is the queue string
355
+ def validate_queue_name(queue)
356
+ queue !~ /(http|https|ftp|ssh):\/\// || raise(InvalidArgument, "name should be JUST queue name, not a full URL")
357
+ queue !~ /^\// || raise(InvalidArgument, "name must not start with a slash")
358
+ end
359
+
360
+ # Validate the given message. The message must be between 1 and 256k bytes
361
+ # +message+ is the message string
362
+ def validate_message(message)
363
+ message.size < (256 * 1024) || raise(InvalidArgument, "message must be less than 256Kb")
364
+ end
365
+
366
+ def resolve_queue_path(queue)
367
+ # if in lazy mode, find real queue name
368
+ if @mode == :lazy
369
+ if queue_path = @queue_lookup[queue]
370
+ # queue name is in lookup
371
+ return queue_path
372
+ else
373
+ # queue name not in lookup, make a service request to get it
374
+ res = create_queue(queue)
375
+ res.code == 200 || raise(InternalError, "Request to CreateQueue failed (looking up queue name)")
376
+ return res.queue_url
377
+ end
378
+ else
379
+ return queue
380
+ end
381
+ end
382
+ end
383
+
384
+ class Auth
385
+
386
+ def self.config_file=(file)
387
+ @config_file = file
388
+ end
389
+
390
+ def self.config_file
391
+ @config_file
392
+ end
393
+
394
+ # Return a new Auth
395
+ # +access_key_id+ is an AWS access key (20 bytes long)
396
+ # +secret_access_key+ is an AWS secret access key used to compute HMAC:SHA1 (40 bytes)
397
+ def initialize(access_key_id = nil, secret_access_key = nil)
398
+ if access_key_id.nil? && secret_access_key.nil?
399
+ # get credentials from config file
400
+ config_file = Auth.config_file || "jungle.yml"
401
+ config_file_contents = File.open(config_file).read
402
+ config = YAML::load(config_file_contents)
403
+
404
+ access_key_id = config['jungle']['access_key_id']
405
+ secret_access_key = config['jungle']['secret_access_key']
406
+ else
407
+ raise(InvalidArgument, 'access_key_id must be 20 bytes') unless access_key_id.size == 20
408
+ raise(InvalidArgument, 'secret_access_key must be 40 bytes') unless secret_access_key.size == 40
409
+ end
410
+
411
+ @access_key_id = access_key_id
412
+ @secret_access_key = secret_access_key
413
+ end
414
+
415
+ # Return the security params for the given request
416
+ # +method+ is the HTTP method. One of GET, POST, PUT, DELETE
417
+ # +path+ is the full REST url, e.g. http://example.com/foo?bar=baz
418
+ def security_params(method, path)
419
+ params = base_params(method, path)
420
+
421
+ signed_canonical_string = hmac_signature(canonical_string(params), @secret_access_key)
422
+
423
+ params.merge({'signed_canonical_string' => signed_canonical_string,
424
+ 'auth_string' => "AWS #{@access_key_id}:#{signed_canonical_string}"})
425
+ end
426
+
427
+ private
428
+
429
+ # Compute an HMAC:SHA1 signature of the given string with the given key by
430
+ # taking the hmac-sha1 sum, and then base64 encoding it.Optionally, it
431
+ # will also url encode the result of that to protect the string if it's
432
+ # going to be used as a query string parameter.
433
+ def hmac_signature(string, key, urlencode = false)
434
+ digest = OpenSSL::Digest::Digest.new('sha1')
435
+ b64_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, key, string)).strip
436
+
437
+ if urlencode
438
+ return CGI::escape(b64_hmac)
439
+ else
440
+ return b64_hmac
441
+ end
442
+ end
443
+
444
+ # Construct the base params for use in a request
445
+ # +method+ is the HTTP method. One of GET, POST, PUT, DELETE
446
+ # +path+ is the full REST url, e.g. http://example.com/foo?bar=baz
447
+ def base_params(method, path)
448
+ # ensure path is acceptable
449
+ path !~ /http:\/\// || raise(InvalidArgument, "path must not include protocol or domain")
450
+
451
+ # create the resource path
452
+ path.chomp!
453
+ resource = path
454
+ resource << '/' if resource == ''
455
+
456
+ security_params = {'method' => method,
457
+ 'resource' => resource,
458
+ 'md5_signature' => '',
459
+ 'date' => Time.now.httpdate,
460
+ 'content_type' => 'text/plain',
461
+ 'access_key_id' => @access_key_id,
462
+ 'secret_access_key_id' => @secret_access_key
463
+ }
464
+ end
465
+
466
+ # Construct the canonical string used in the HMAC:SHA1 to authorize AWS requests
467
+ # +params+ is the result of a call to #base_params
468
+ def canonical_string(params)
469
+ buf = []
470
+ buf << params['method']
471
+ buf << params['md5_signature']
472
+ buf << params['content_type']
473
+ buf << params['date']
474
+ buf << params['resource']
475
+ buf.join("\n")
476
+ end
477
+ end
478
+
479
+ # Contains a message that has been received from a queue.
480
+ class SQSMessage
481
+ attr_accessor :id, :body, :queue_path
482
+
483
+ def initialize(queue_path, xml)
484
+ @queue_path = queue_path
485
+ m, @id, @body = *xml.match(/<MessageId>(.*?)<\/MessageId><MessageBody>(.*?)<\/MessageBody>/m)
486
+ end
487
+
488
+ def inspect
489
+ puts "#<Message id:#{@id} body:#{@body}>"
490
+ end
491
+ end
492
+
493
+ class SQSResponseFactory
494
+ Struct.new("SQSErrorResponse", :operation, :code, :xml)
495
+
496
+ Struct.new("SQSCreateQueueResponse", :operation, :code, :xml, :queue_url)
497
+ Struct.new("SQSListQueuesResponse", :operation, :code, :xml, :queue_urls)
498
+ Struct.new("SQSSendMessageResponse", :operation, :code, :xml, :message_id)
499
+ Struct.new("SQSReceiveMessageResponse", :operation, :code, :xml, :messages)
500
+ Struct.new("SQSDeleteMessageResponse", :operation, :code, :xml)
501
+ Struct.new("SQSPeekMessageResponse", :operation, :code, :xml, :message)
502
+
503
+ class Struct::SQSReceiveMessageResponse
504
+ def message
505
+ messages.first
506
+ end
507
+ end
508
+
509
+ def self.manufacture(operation, response, options = nil)
510
+ begin
511
+ if options
512
+ self.send(operation.to_s + "_response", operation, response, options)
513
+ else
514
+ self.send(operation.to_s + "_response", operation, response)
515
+ end
516
+ rescue
517
+ raise
518
+ raise(InternalError, "operation #{operation} not implemented")
519
+ end
520
+
521
+ # case operation
522
+ # when :create_queue: self.create_queue_response(operation, response)
523
+ # when :list_queues: self.list_queues_response(operation, response)
524
+ # when :send_message: self.send_message_response(operation, response)
525
+ # when :receive_message: self.receive_message_response(operation, response)
526
+ # else raise(InternalError, "operation #{operation} not implemented")
527
+ # end
528
+ end
529
+
530
+ def self.create_queue_response(operation, response)
531
+ if response.is_a? Net::HTTPSuccess
532
+ xml = response.read_body
533
+ m, queue_url = *xml.match(/<QueueUrl>http:\/\/.*?\/(.*?)<\/QueueUrl>/)
534
+ Struct::SQSCreateQueueResponse.new(operation, response.code.to_i, xml, queue_url)
535
+ else
536
+ Struct::SQSErrorResponse.new(operation, response.code.to_i, response.read_body)
537
+ end
538
+ end
539
+
540
+ def self.list_queues_response(operation, response)
541
+ if response.is_a? Net::HTTPSuccess
542
+ xml = response.read_body
543
+ queue_urls = xml.scan(/<QueueUrl>(.+?)<\/QueueUrl>/m).flatten
544
+ Struct::SQSListQueuesResponse.new(operation, response.code.to_i, xml, queue_urls)
545
+ else
546
+ Struct::SQSErrorResponse.new(operation, response.code.to_i, response.read_body)
547
+ end
548
+ end
549
+
550
+ def self.send_message_response(operation, response)
551
+ if response.is_a? Net::HTTPSuccess
552
+ xml = response.read_body
553
+ m, message_id = *xml.match(/<MessageId>(.*?)<\/MessageId>/)
554
+ Struct::SQSCreateQueueResponse.new(operation, response.code.to_i, xml, message_id)
555
+ else
556
+ Struct::SQSErrorResponse.new(operation, response.code.to_i, response.read_body)
557
+ end
558
+ end
559
+
560
+ def self.receive_message_response(operation, response, options)
561
+ if response.is_a? Net::HTTPSuccess
562
+ xml = response.read_body
563
+ messages = xml.scan(/<Message>.*?<\/Message>/m).map { |message_xml| SQSMessage.new(options[:queue_path], message_xml) }
564
+ Struct::SQSReceiveMessageResponse.new(operation, response.code.to_i, xml, messages)
565
+ else
566
+ Struct::SQSErrorResponse.new(operation, response.code.to_i, response.read_body)
567
+ end
568
+ end
569
+
570
+ def self.delete_message_response(operation, response)
571
+ if response.is_a? Net::HTTPSuccess
572
+ xml = response.read_body
573
+ Struct::SQSDeleteMessageResponse.new(operation, response.code.to_i, xml)
574
+ else
575
+ Struct::SQSErrorResponse.new(operation, response.code.to_i, response.read_body)
576
+ end
577
+ end
578
+
579
+ def self.peek_message_response(operation, response)
580
+ if response.is_a? Net::HTTPSuccess
581
+ xml = response.read_body
582
+ m, message_xml = *xml.match(/<Message>.*?<\/Message>/)
583
+ message = SQSMessage.new(message_xml)
584
+ Struct::SQSPeekMessageResponse.new(operation, response.code.to_i, xml, message)
585
+ else
586
+ Struct::SQSErrorResponse.new(operation, response.code.to_i, response.read_body)
587
+ end
588
+ end
589
+
590
+ end
591
+ end
@@ -0,0 +1,9 @@
1
+ module Jungle
2
+ class InvalidArgument < Exception
3
+
4
+ end
5
+
6
+ class InternalError < Exception
7
+
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module Jungle #:nodoc:
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,25 @@
1
+ require 'jungle'
2
+
3
+ jungle_client = Jungle::Client.new.sqs
4
+
5
+ 1.upto(4) do |i|
6
+ puts 'put message'
7
+ jungle_client.send_message('numbers', "#{i} bottles of beer on the wall")
8
+ end
9
+
10
+ threads = []
11
+
12
+ 2.times do
13
+ threads << Thread.new(jungle_client) do |client|
14
+ while true
15
+ res = client.receive_message('numbers')
16
+ break unless res.code == 200 && res && res.message
17
+ puts res.message.body
18
+ client.delete_message(res.message)
19
+ end
20
+ end
21
+ end
22
+
23
+ threads.each do |thread|
24
+ thread.join
25
+ end
@@ -0,0 +1,370 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ context "The void" do
4
+ specify "should allow the creation of a Jungle::Client" do
5
+ lambda { Jungle::Client.new }.should_not_raise
6
+ end
7
+ end
8
+
9
+
10
+ context "A Jungle::Client" do
11
+ setup do
12
+ @client = Jungle::Client.new
13
+ end
14
+
15
+ specify "should have an sqs method" do
16
+ @client.should_not_be nil
17
+ lambda { @client.sqs }.should_not_raise
18
+ @client.sqs.should.be.instance_of? Jungle::SQSClient
19
+ end
20
+
21
+ specify "should return the same SQSClient for each call to #sqs" do
22
+ sqs1 = @client.sqs
23
+ sqs2 = @client.sqs
24
+ sqs1.object_id.should.be.equal sqs2.object_id
25
+ end
26
+ end
27
+
28
+
29
+ context "A Jungle::AWSClient construct_url call" do
30
+ setup do
31
+ @client = Jungle::AWSClient.new
32
+ end
33
+
34
+ specify "should generate the proper url" do
35
+ @client.bypass.construct_url("").should == ""
36
+ @client.bypass.construct_url("", {'FooBar' => 'baz'}).should == "/?FooBar=baz"
37
+
38
+ @client.bypass.construct_url("/foo").should == "/foo"
39
+ @client.bypass.construct_url("/foo", {'FooBar' => 'baz'}).should == "/foo?FooBar=baz"
40
+ end
41
+
42
+ specify "should throw an exception on bad input data" do
43
+
44
+ end
45
+ end
46
+
47
+
48
+ # context "A Jungle::AWSClient make_request call" do
49
+ # setup do
50
+ # @client = Jungle::AWSClient.new
51
+ # end
52
+ #
53
+ # specify "should " do
54
+ # @client.bypass.make_request('POST', 'foo')
55
+ # end
56
+ # end
57
+
58
+
59
+ context "A Jungle::AWSClient get_connection_from_pool call" do
60
+ setup do
61
+ @client = Jungle::AWSClient.new
62
+ end
63
+
64
+ specify "should raise an exception" do
65
+ lambda { @client.get_connection_from_pool }.should_raise
66
+ end
67
+ end
68
+
69
+
70
+ context "A Jungle::AWSClient method_to_request_class call" do
71
+ setup do
72
+ @client = Jungle::AWSClient.new
73
+ end
74
+
75
+ specify "should return a request class for valid HTTP methods" do
76
+ @client.bypass.method_to_request_class('GET').should == Net::HTTP::Get
77
+ @client.bypass.method_to_request_class('Post').should == Net::HTTP::Post
78
+ @client.bypass.method_to_request_class('put').should == Net::HTTP::Put
79
+ @client.bypass.method_to_request_class('DeLeTe').should == Net::HTTP::Delete
80
+ end
81
+
82
+ specify "should raise an exception on invalid method" do
83
+ lambda { @client.bypass.method_to_request_class('Foo') }.should_raise
84
+ end
85
+ end
86
+
87
+
88
+ context "A Jungle::Client::SQSClient" do
89
+ setup do
90
+ @client = Jungle::Client.new
91
+ end
92
+
93
+ specify "should respond to create_queue" do
94
+ lambda { @client.sqs.create_queue('foo') }.should_not_raise
95
+ end
96
+ end
97
+
98
+
99
+ context "A Jungle::Client::SQSClient get_connection_from_pool call" do
100
+ setup do
101
+ @client = Jungle::Client.new
102
+ end
103
+
104
+ specify "should return a Net::HTTP object" do
105
+ @client.sqs.bypass.get_connection_from_pool.should_be_an_instance_of Net::HTTP
106
+ end
107
+ end
108
+
109
+
110
+ context "A Jungle::Client::SQSClient validate_message call" do
111
+ setup do
112
+ @client = Jungle::Client.new
113
+ end
114
+
115
+ specify "should not raise on messages less than 256Kb in length" do
116
+ lambda { @client.sqs.bypass.validate_message('foo') }.should_not_raise
117
+ end
118
+
119
+ specify "should raise on messages more than 256Kb in length" do
120
+ big_message = ''
121
+ (257 * 1024).times { big_message << 'a' }
122
+ big_message.size.should == 257 * 1024
123
+ lambda { @client.sqs.bypass.validate_message(big_message) }.should_raise
124
+ end
125
+ end
126
+
127
+
128
+ context "A Jungle::Client::SQSClient create_queue call" do
129
+ setup do
130
+ @client = Jungle::Client.new
131
+ end
132
+
133
+ specify "should not accept full URL queue names" do
134
+ lambda { @client.sqs.create_queue('http://example.com/foo') }.should_raise
135
+ end
136
+
137
+ specify "should not accept queue names that start with a slash" do
138
+ lambda { @client.sqs.create_queue('/foo') }.should_raise
139
+ end
140
+
141
+ specify "should accept simple string names" do
142
+ lambda { @client.sqs.create_queue('foo') }.should_not_raise
143
+ end
144
+
145
+ specify "should return success" do
146
+ #@client.sqs.debug = true
147
+ res = @client.sqs.create_queue('foo')
148
+ res.code.should == 200
149
+ end
150
+
151
+ specify "should return the queue name part of the new queue" do
152
+ res = @client.sqs.create_queue('foo')
153
+ res.queue_url.should.match(/.*?\/foo/)
154
+ end
155
+ end
156
+
157
+
158
+ context "A Jungle::Client::SQSClient send_message call" do
159
+ setup do
160
+ @client = Jungle::Client.new
161
+ end
162
+
163
+ specify "should accept a message on an existing queue in strict mode" do
164
+ #@client.sqs.debug = true
165
+ @client.sqs.mode = :strict
166
+
167
+ res = @client.sqs.create_queue('foo')
168
+ qurl = res.queue_url
169
+
170
+ lambda { @client.sqs.send_message(qurl, 'ping') }.should_not_raise
171
+ end
172
+
173
+ specify "should return error on non existing queue in strict mode" do
174
+ #@client.sqs.debug = true
175
+ @client.sqs.mode = :strict
176
+
177
+ res = @client.sqs.send_message('foo', 'ping')
178
+ res.code.should_not == 200
179
+ end
180
+
181
+ specify "should accept a message on an existing queue in lazy mode" do
182
+ #@client.sqs.debug = true
183
+
184
+ @client.sqs.create_queue('foo')
185
+
186
+ lambda { @client.sqs.send_message('foo', 'ping') }.should_not_raise
187
+ end
188
+
189
+ specify "should accept a message on a non existing queue in lazy mode" do
190
+ #@client.sqs.debug = true
191
+
192
+ lambda { @client.sqs.send_message('foo', 'ping') }.should_not_raise
193
+ end
194
+
195
+ specify "should return success" do
196
+ #@client.sqs.debug = true
197
+ @client.sqs.mode = :strict
198
+
199
+ res = @client.sqs.create_queue('foo')
200
+ qurl = res.queue_url
201
+
202
+ res = @client.sqs.send_message(qurl, 'ping')
203
+ res.code.should == 200
204
+ end
205
+ end
206
+
207
+
208
+ context "A Jungle::Client::SQSClient receive_message call" do
209
+ setup do
210
+ @client = Jungle::Client.new
211
+ end
212
+
213
+ specify "should return success" do
214
+ #@client.sqs.debug = true
215
+
216
+ res = @client.sqs.receive_message('foo')
217
+ res.code.should == 200
218
+ end
219
+
220
+ specify "should return a response containing one SQSMessage" do
221
+ #@client.sqs.debug = true
222
+
223
+ @client.sqs.send_message('foo', 'ping')
224
+
225
+ res = @client.sqs.receive_message('foo')
226
+ res.messages.size.should == 1
227
+ end
228
+
229
+ specify "should return return n response containing one SQSMessage with the message" do
230
+ #@client.sqs.debug = true
231
+
232
+ @client.sqs.send_message('foo', 'ping')
233
+
234
+ res = @client.sqs.receive_message('foo')
235
+
236
+ p res.messages.first
237
+
238
+ #res.messages.first.body.should == 'ping'
239
+ end
240
+ end
241
+
242
+
243
+ context "A Jungle::Client::SQSClient receive_messages call" do
244
+ setup do
245
+ @client = Jungle::Client.new
246
+ end
247
+
248
+ specify "should return success" do
249
+ #@client.sqs.debug = true
250
+
251
+ res = @client.sqs.receive_messages('foo')
252
+ res.code.should == 200
253
+ end
254
+
255
+ specify "should return a response containing several SQSMessages" do
256
+ #@client.sqs.debug = true
257
+
258
+ @client.sqs.send_message('foo', 'ping')
259
+ @client.sqs.send_message('foo', 'ping')
260
+
261
+ res = @client.sqs.receive_messages('foo', :number_of_messages => 2)
262
+
263
+ res.messages.size.should == 2
264
+ end
265
+ end
266
+
267
+
268
+ context "The response from Jungle::Client.SQSClient list_queues" do
269
+ setup do
270
+ @client = Jungle::Client.new
271
+ end
272
+
273
+ specify "should return success" do
274
+ res = @client.sqs.list_queues
275
+ res.code.should == 200
276
+ end
277
+
278
+ specify "should return an array of strings" do
279
+ res = @client.sqs.list_queues
280
+ res.queue_urls.size.should_be > 1
281
+ res.queue_urls.each do |name|
282
+ name.should_be_an_instance_of String
283
+ end
284
+ end
285
+ end
286
+
287
+
288
+ context "The response from Jungle::Auth base_params" do
289
+ setup do
290
+ @auth = Jungle::Auth.new
291
+ end
292
+
293
+ specify "should contain 'method' matching method" do
294
+ @auth.bypass.base_params("GET", '/foo')['method'].should == 'GET'
295
+ end
296
+
297
+ specify "should contain 'resource' matching resource substring of path" do
298
+ @auth.bypass.base_params("GET", '/foo')['resource'].should == '/foo'
299
+ end
300
+ end
301
+
302
+
303
+ context "The response from Jungle::Auth canonical_string" do
304
+ setup do
305
+ @auth = Jungle::Auth.new
306
+ end
307
+
308
+ specify "should be a non-empty string" do
309
+ canonical = @auth.bypass.canonical_string(@auth.bypass.base_params('GET', '/foo'))
310
+ canonical.should_not_be nil
311
+ canonical.should_be_instance_of String
312
+ canonical.should_not == ""
313
+ end
314
+
315
+ specify "should always contain the method as the first line" do
316
+ %w{GET POST PUT DELETE}.each do |method|
317
+ canonical = @auth.bypass.canonical_string(@auth.bypass.base_params(method, '/foo'))
318
+ canonical.split("\n")[0].should == method
319
+ end
320
+ end
321
+
322
+ specify "should always have an empty second line" do
323
+ canonical = @auth.bypass.canonical_string(@auth.bypass.base_params('GET', '/foo'))
324
+ canonical.split("\n")[1].should == ''
325
+ end
326
+
327
+ specify "should always have text/plain as the third line" do
328
+ canonical = @auth.bypass.canonical_string(@auth.bypass.base_params('GET', '/foo'))
329
+ canonical.split("\n")[2].should == 'text/plain'
330
+ end
331
+
332
+ specify "should always have the current date as the fourth line" do
333
+ canonical = @auth.bypass.canonical_string(@auth.bypass.base_params('GET', '/foo'))
334
+ # Time.stubs(:httpdate).returns('Thu, 17 Nov 2005 18:49:58 GMT')
335
+ # canonical.split("\n")[3].should == 'Thu, 17 Nov 2005 18:49:58 GMT'
336
+ end
337
+
338
+ specify "should always have the resource address as the fifth line" do
339
+ canonical = @auth.bypass.canonical_string(@auth.bypass.base_params('GET', ''))
340
+ canonical.split("\n")[4].should == '/'
341
+
342
+ canonical = @auth.bypass.canonical_string(@auth.bypass.base_params('GET', '/'))
343
+ canonical.split("\n")[4].should == '/'
344
+ end
345
+ end
346
+
347
+ context "The response from Jungle::Auth hmac_signature" do
348
+ setup do
349
+ @auth = Jungle::Auth.new
350
+ end
351
+
352
+ specify "should not be blank" do
353
+ @auth.bypass.hmac_signature("foo", "bar").should_not == ''
354
+ end
355
+
356
+ specify "should return an sha1" do
357
+ hmac = @auth.bypass.hmac_signature("foo", "bar")
358
+ hmac.size.should == 28
359
+ end
360
+ end
361
+
362
+ context "The response from Jungle::Auth security_params" do
363
+ setup do
364
+ @auth = Jungle::Auth.new
365
+ end
366
+
367
+ specify "should have plausible auth_string" do
368
+ @auth.bypass.security_params('GET', '')['auth_string'].should =~ /^AWS .{20}:.{28}$/
369
+ end
370
+ end
@@ -0,0 +1,17 @@
1
+ require 'benchmark'
2
+ require 'rexml/document'
3
+ require 'xmlsimple'
4
+ require 'xml/libxml'
5
+ include Benchmark
6
+
7
+ xml = "<?xml version=\"1.0\"?>\n<CreateQueueResponse xmlns=\"http://queue.amazonaws.com/doc/2006-04-01/\"><QueueUrl>http://queue.amazonaws.com/A8SVKWT62YDQI/foo</QueueUrl><ResponseStatus><StatusCode>Success</StatusCode><RequestId>cba8efdf-f348-4335-8277-f756af3789eb</RequestId></ResponseStatus></CreateQueueResponse>"
8
+
9
+ n = 50000
10
+
11
+ Benchmark.bm(7) do |x|
12
+ x.report("regex:") { n.times { xml =~ /<QueueUrl>(.*?)<\/QueueUrl>/; val = $1 } }
13
+
14
+ #x.report("rexml:") { n.times { doc = REXML::Document.new(xml); val = doc.elements['CreateQueueResponse'].elements['QueueUrl'].text } }
15
+
16
+ x.report("xmlsimple:") { n.times { doc = XmlSimple.xml_in(xml, 'ForceArray' => false); val = doc['QueueUrl'] } }
17
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec'
2
+
3
+ require File.dirname(__FILE__) + '/../lib/jungle'
4
+
5
+ # This allows you to be a good OOP citizen and honor encapsulation, but
6
+ # still make calls to private methods (for testing) by doing
7
+ #
8
+ # obj.bypass.private_thingie(arg1, arg2)
9
+ #
10
+ # Which is easier on the eye than
11
+ #
12
+ # obj.send(:private_thingie, arg1, arg2)
13
+ #
14
+ class Object
15
+ class Bypass
16
+ instance_methods.each { |m| undef_method m unless m =~ /^__/ }
17
+
18
+ def initialize(ref)
19
+ @ref = ref
20
+ end
21
+
22
+ def method_missing(sym, *args)
23
+ @ref.__send__(sym, *args)
24
+ end
25
+ end
26
+
27
+ def bypass
28
+ Bypass.new(self)
29
+ end
30
+ end
31
+
metadata ADDED
@@ -0,0 +1,56 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: jungle
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.1.0
7
+ date: 2007-01-06 00:00:00 -08:00
8
+ summary: A pure Ruby client library for Amazon's SQS business web services.
9
+ require_paths:
10
+ - lib
11
+ email: ryand-ruby@zenspider.com
12
+ homepage: "\thttp://jungle.rubyforge.org/"
13
+ rubyforge_project: jungle
14
+ description: Jungle is a client library for the Amazon SQS (Simple Queue Service). It provides an intuitive, well documented API and a fully tested codebase.
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ authors:
29
+ - Ryan Davis
30
+ files:
31
+ - History.txt
32
+ - Manifest.txt
33
+ - README.txt
34
+ - Rakefile
35
+ - lib/jungle.rb
36
+ - lib/jungle/client.rb
37
+ - lib/jungle/support.rb
38
+ - lib/jungle/version.rb
39
+ - test/example.rb
40
+ - test/jungle_test.rb
41
+ - test/test.rb
42
+ - test/test_helper.rb
43
+ test_files:
44
+ - test/test_helper.rb
45
+ rdoc_options: []
46
+
47
+ extra_rdoc_files: []
48
+
49
+ executables: []
50
+
51
+ extensions: []
52
+
53
+ requirements: []
54
+
55
+ dependencies: []
56
+