jungle 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +3 -0
- data/Manifest.txt +12 -0
- data/README.txt +63 -0
- data/Rakefile +15 -0
- data/lib/jungle.rb +9 -0
- data/lib/jungle/client.rb +591 -0
- data/lib/jungle/support.rb +9 -0
- data/lib/jungle/version.rb +3 -0
- data/test/example.rb +25 -0
- data/test/jungle_test.rb +370 -0
- data/test/test.rb +17 -0
- data/test/test_helper.rb +31 -0
- metadata +56 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
@@ -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
|
data/lib/jungle.rb
ADDED
@@ -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
|
data/test/example.rb
ADDED
@@ -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
|
data/test/jungle_test.rb
ADDED
@@ -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
|
data/test/test.rb
ADDED
@@ -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
|
data/test/test_helper.rb
ADDED
@@ -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
|
+
|