q3 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +42 -0
- data/LICENSE.txt +20 -0
- data/README.md +106 -0
- data/Rakefile +40 -0
- data/bin/q3 +2 -0
- data/config.ru +1 -0
- data/lib/q3.rb +257 -0
- data/spec/q3_spec.rb +341 -0
- data/spec/spec_helper.rb +32 -0
- metadata +175 -0
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem "rspec", "~> 2.3.0"
|
10
|
+
gem "bundler", "~> 1.0.0"
|
11
|
+
gem "jeweler", "~> 1.6.4"
|
12
|
+
#gem "rcov", ">= 0"
|
13
|
+
end
|
14
|
+
|
15
|
+
gem 'sinatra'
|
16
|
+
gem 'builder'
|
17
|
+
gem 'redis'
|
18
|
+
gem 'redis-namespace'
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
builder (3.2.2)
|
5
|
+
diff-lcs (1.1.3)
|
6
|
+
git (1.2.7)
|
7
|
+
jeweler (1.6.4)
|
8
|
+
bundler (~> 1.0)
|
9
|
+
git (>= 1.2.5)
|
10
|
+
rake
|
11
|
+
rack (1.5.2)
|
12
|
+
rack-protection (1.5.3)
|
13
|
+
rack
|
14
|
+
rake (10.3.2)
|
15
|
+
redis (3.1.0)
|
16
|
+
redis-namespace (1.5.0)
|
17
|
+
redis (~> 3.0, >= 3.0.4)
|
18
|
+
rspec (2.3.0)
|
19
|
+
rspec-core (~> 2.3.0)
|
20
|
+
rspec-expectations (~> 2.3.0)
|
21
|
+
rspec-mocks (~> 2.3.0)
|
22
|
+
rspec-core (2.3.1)
|
23
|
+
rspec-expectations (2.3.0)
|
24
|
+
diff-lcs (~> 1.1.2)
|
25
|
+
rspec-mocks (2.3.0)
|
26
|
+
sinatra (1.4.5)
|
27
|
+
rack (~> 1.4)
|
28
|
+
rack-protection (~> 1.4)
|
29
|
+
tilt (~> 1.3, >= 1.3.4)
|
30
|
+
tilt (1.4.1)
|
31
|
+
|
32
|
+
PLATFORMS
|
33
|
+
ruby
|
34
|
+
|
35
|
+
DEPENDENCIES
|
36
|
+
builder
|
37
|
+
bundler (~> 1.0.0)
|
38
|
+
jeweler (~> 1.6.4)
|
39
|
+
redis
|
40
|
+
redis-namespace
|
41
|
+
rspec (~> 2.3.0)
|
42
|
+
sinatra
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2014 tily
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
# Q3
|
2
|
+
|
3
|
+
## Summary
|
4
|
+
|
5
|
+
Simple Amazon SQS compatible API implementation with sinatra and redis.
|
6
|
+
|
7
|
+
## Concept
|
8
|
+
|
9
|
+
* simplicity
|
10
|
+
* no authentication, open like wiki
|
11
|
+
* no validation, just works as proxy to redis functions
|
12
|
+
|
13
|
+
## Usage
|
14
|
+
|
15
|
+
### global service
|
16
|
+
|
17
|
+
You can try q3 easily with global endpoint <http://q3-global.herokuapp.com/>.
|
18
|
+
|
19
|
+
$ curl "http://q3-global.herokuapp.com/?Action=CreateQueue&QueueName=MyQueue001"
|
20
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
21
|
+
<CreateQueueResponse>
|
22
|
+
<CreateQueueResult>
|
23
|
+
<QueueUrl>http://q3-global.herokuapp.com/*/MyQueue001</QueueUrl>
|
24
|
+
</CreateQueueResult>
|
25
|
+
<ResponseMetadata>
|
26
|
+
<RequestId>9bcf7831-59a3-456c-b5fe-e0674f322b79</RequestId>
|
27
|
+
</ResponseMetadata>
|
28
|
+
</CreateQueueResponse>
|
29
|
+
|
30
|
+
You can use any Amazon SQS client.
|
31
|
+
|
32
|
+
require 'aws-sdk'
|
33
|
+
q3 = AWS::SQS.new(
|
34
|
+
:sqs_endpoint => 'q3-global.herokuapp.com',
|
35
|
+
:access_key_id => 'dummy',
|
36
|
+
:secret_access_key => 'dummy',
|
37
|
+
:use_ssl => false
|
38
|
+
)
|
39
|
+
|
40
|
+
queue = q3.queues.create('MyQueue001')
|
41
|
+
queue.send_message('hello!')
|
42
|
+
p queue.receive_message.body # => "hello!"
|
43
|
+
|
44
|
+
### as rack app
|
45
|
+
|
46
|
+
Install gem and you can invoke `q3` command to run api.
|
47
|
+
|
48
|
+
$ gem install q3
|
49
|
+
$ ruby bin/q3
|
50
|
+
[2014-07-06 19:54:47] INFO WEBrick 1.3.1
|
51
|
+
[2014-07-06 19:54:47] INFO ruby 2.0.0 (2014-02-24) [universal.x86_64-darwin13]
|
52
|
+
== Sinatra/1.4.5 has taken the stage on 4567 for development with backup from WEBrick
|
53
|
+
[2014-07-06 19:54:47] INFO WEBrick::HTTPServer#start: pid=29495 port=4567
|
54
|
+
|
55
|
+
Or you can integrate q3 into your config.ru like this:
|
56
|
+
|
57
|
+
require 'q3.rb'
|
58
|
+
run Q3
|
59
|
+
|
60
|
+
## Actions
|
61
|
+
|
62
|
+
See for complete action list: [Welcome - Amazon Simple Queue Service](http://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/Welcome.html).
|
63
|
+
|
64
|
+
### implemented
|
65
|
+
|
66
|
+
* Queue Operation
|
67
|
+
* CreateQueue
|
68
|
+
* ListQueues
|
69
|
+
* GetQueueAttributes
|
70
|
+
* GetQueueUrl
|
71
|
+
* SetQueueAttributes
|
72
|
+
* DeleteQueue
|
73
|
+
* Message Operation
|
74
|
+
* SendMessage
|
75
|
+
* ReceiveMessage
|
76
|
+
* ChangeMessageVisibility
|
77
|
+
* DeleteMessage
|
78
|
+
|
79
|
+
### will be implemented
|
80
|
+
|
81
|
+
* SendMessageBatch
|
82
|
+
* ChangeMessageVisibilityBatch
|
83
|
+
* DeleteMessageBatch
|
84
|
+
* ListDeadLetterSourceQueues
|
85
|
+
|
86
|
+
### will not be implemented
|
87
|
+
|
88
|
+
* AddPermission
|
89
|
+
* RemovePermission
|
90
|
+
|
91
|
+
## Data Schema
|
92
|
+
|
93
|
+
| Key Name | Type | Content | Delete Timing |
|
94
|
+
| ------------------------------------------------------- | ---------- | ------------------------------ | --------------------------------------------- |
|
95
|
+
| Queues | List | Queue Names | - |
|
96
|
+
| Queues:${QueueName} | Hash | Queue Attributes | DeleteQueue action |
|
97
|
+
| Queues:${QueueName}:Messages | List | Message Ids | DeleteQueue action |
|
98
|
+
| Queues:${QueueName}:Messages:${MessageId} | Hash | Message Attributes | expires due to MessageRetentionPeriod |
|
99
|
+
| Queues:${QueueName}:Messages:${MessageId}:Delayed | String | Message Id | expires due to DelaySeconds |
|
100
|
+
| Queues:${QueueName}:Messages:${MessageId}:ReceiptHandle | String | ReceiptHandle | expires due to VisibilityTimeout |
|
101
|
+
| Queues:${QueueName}:ReceiptHandles:${ReceiptHandle} | String | Message Id | expires due to VisibilityTimeout |
|
102
|
+
|
103
|
+
## TODO
|
104
|
+
|
105
|
+
* https
|
106
|
+
* mount on subdirectory
|
data/Rakefile
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "q3"
|
18
|
+
gem.homepage = "http://github.com/tily/q3"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{Simple Amazon SQS compatible API implementation with sinatra and redis}
|
21
|
+
gem.description = %Q{Simple Amazon SQS compatible API implementation with sinatra and redis}
|
22
|
+
gem.email = "tily05@gmail.com"
|
23
|
+
gem.authors = ["tily"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
gem.executables = ['q3']
|
26
|
+
end
|
27
|
+
Jeweler::RubygemsDotOrgTasks.new
|
28
|
+
|
29
|
+
require 'rspec/core'
|
30
|
+
require 'rspec/core/rake_task'
|
31
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
32
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
33
|
+
end
|
34
|
+
|
35
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
36
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
37
|
+
spec.rcov = true
|
38
|
+
end
|
39
|
+
|
40
|
+
task :default => :spec
|
data/bin/q3
ADDED
data/config.ru
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
(require './lib/q3.rb') && (run Q3)
|
data/lib/q3.rb
ADDED
@@ -0,0 +1,257 @@
|
|
1
|
+
%w(digest/md5 sinatra/base builder redis redis-namespace).each {|x| require x }
|
2
|
+
|
3
|
+
class Q3 < Sinatra::Base
|
4
|
+
configure do
|
5
|
+
enable :logging
|
6
|
+
end
|
7
|
+
|
8
|
+
DEFAULTS = {
|
9
|
+
'VisibilityTimeout' => 30,
|
10
|
+
'MaximumMessageSize' => 262144,
|
11
|
+
'MessageRetentionPeriod' => 345600,
|
12
|
+
'DelaySeconds' => 0,
|
13
|
+
'ReceiveMessageWaitTimeSeconds' => 0,
|
14
|
+
}
|
15
|
+
CREATE_QUEUE = %w(
|
16
|
+
VisibilityTimeout MessageRetentionPeriod MaximumMessageSize DelaySeconds ReceiveMessageWaitTimeSeconds
|
17
|
+
)
|
18
|
+
SET_QUEUE_ATTRIBUTES = %w(
|
19
|
+
DelaySeconds MaximumMessageSize MessageRetentionPeriod Policy ReceiveMessageWaitTimeSeconds
|
20
|
+
VisibilityTimeout RedrivePolicy
|
21
|
+
)
|
22
|
+
|
23
|
+
def self.dispatch!
|
24
|
+
@paths.each do |path, opts|
|
25
|
+
[:get, :post].each do |x|
|
26
|
+
send(x, path) do
|
27
|
+
if opt = opts.find {|opt| opt[:action] == params['Action'] }
|
28
|
+
instance_eval(&opt[:block])
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.action(action, path='/', &block)
|
36
|
+
@paths ||= Hash.new {|h, k| h[k] = [] }
|
37
|
+
@paths[path] << {action: action, block: block}
|
38
|
+
end
|
39
|
+
|
40
|
+
before do
|
41
|
+
logger.info "#{request_id}: request start with path = #{request.path_info}, params = #{params}"
|
42
|
+
content_type 'application/xml'
|
43
|
+
end
|
44
|
+
|
45
|
+
after do
|
46
|
+
logger.info "#{request_id}: request end with #{response.body}"
|
47
|
+
end
|
48
|
+
|
49
|
+
action('CreateQueue') do
|
50
|
+
halt 400, return_error_xml('Sender', 'MissingParameter', '') if params['QueueName'].nil?
|
51
|
+
timestamp = now
|
52
|
+
hash = CREATE_QUEUE.inject({'CreateTimestamp' => timestamp, 'LastModifiedTimestamp' => timestamp}) do |hash, attribute|
|
53
|
+
hash[attribute] = attributes[attribute] || DEFAULTS[attribute]
|
54
|
+
hash
|
55
|
+
end
|
56
|
+
redis.rpush("Queues", params[:QueueName])
|
57
|
+
redis.hmset("Queues:#{params[:QueueName]}", *hash.to_a)
|
58
|
+
return_xml {|xml| xml.QueueUrl queue_url("#{params[:QueueName]}") }
|
59
|
+
end
|
60
|
+
|
61
|
+
action('ListQueues') do
|
62
|
+
return_xml do |xml|
|
63
|
+
redis.lrange('Queues', 0, -1).each {|queue_name| xml.QueueUrl queue_url(queue_name) }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
action('GetQueueUrl') do
|
68
|
+
validate_queue_existence
|
69
|
+
return_xml {|xml| xml.QueueUrl queue_url(params[:QueueName]) }
|
70
|
+
end
|
71
|
+
|
72
|
+
action('GetQueueAttributes', '/*/:QueueName') do
|
73
|
+
validate_queue_existence
|
74
|
+
delayed = redis.keys("Queues:#{params[:QueueName]}:Messages:*:Delayed").size
|
75
|
+
not_visible = redis.keys("Queues:#{params[:QueueName]}:Messages:*:ReceiptHandle").size
|
76
|
+
messages = redis.llen("Queues:#{params[:QueueName]}:Messages") - delayed - not_visible
|
77
|
+
return_xml do |xml|
|
78
|
+
queue.each do |name, value|
|
79
|
+
xml.Attribute { xml.Name name; xml.Value value }
|
80
|
+
end
|
81
|
+
xml.Attribute { xml.Name 'ApproximateNumberOfMessages' ; xml.Value messages }
|
82
|
+
xml.Attribute { xml.Name 'ApproximateNumberOfMessagesNotVisible' ; xml.Value not_visible }
|
83
|
+
xml.Attribute { xml.Name 'ApproximateNumberOfMessagesDelayed' ; xml.Value delayed }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
action('SetQueueAttributes', '/*/:QueueName') do
|
88
|
+
validate_queue_existence
|
89
|
+
hash = SET_QUEUE_ATTRIBUTES.inject({'LastModifiedTimestamp' => now}) do |hash, attribute|
|
90
|
+
hash[attribute] = attributes[attribute] if attributes[attribute]
|
91
|
+
hash
|
92
|
+
end
|
93
|
+
redis.hmset("Queues:#{params[:QueueName]}", hash.to_a.flatten)
|
94
|
+
return_xml {}
|
95
|
+
end
|
96
|
+
|
97
|
+
action('DeleteQueue', '/*/:QueueName') do
|
98
|
+
validate_queue_existence
|
99
|
+
redis.keys("Queues:#{params[:QueueName]}*").each {|key| redis.del(key) }
|
100
|
+
redis.lrem("Queues", 0, params[:QueueName])
|
101
|
+
return_xml {}
|
102
|
+
end
|
103
|
+
|
104
|
+
action('SendMessage', '/*/:QueueName') do
|
105
|
+
validate_queue_existence
|
106
|
+
delay_seconds = params['DelaySeconds'] || queue['DelaySeconds']
|
107
|
+
message_id = SecureRandom.uuid
|
108
|
+
redis.rpush("Queues:#{params[:QueueName]}:Messages", message_id)
|
109
|
+
redis.hmset("Queues:#{params[:QueueName]}:Messages:#{message_id}",
|
110
|
+
'MessageId', message_id,
|
111
|
+
'MessageBody', params[:MessageBody],
|
112
|
+
'SenderId', '*',
|
113
|
+
'SentTimestamp', now,
|
114
|
+
'ApproximateReceiveCount', 0
|
115
|
+
)
|
116
|
+
redis.expire("Queues:#{params[:QueueName]}:Messages:#{message_id}", queue['MessageRetentionPeriod'])
|
117
|
+
if delay_seconds.to_i > 0
|
118
|
+
redis.set("Queues:#{params[:QueueName]}:Messages:#{message_id}:Delayed", message_id)
|
119
|
+
redis.expire("Queues:#{params[:QueueName]}:Messages:#{message_id}:Delayed", delay_seconds)
|
120
|
+
end
|
121
|
+
return_xml do |xml|
|
122
|
+
xml.MD5OfMessageBody Digest::MD5.hexdigest(params[:MessageBody])
|
123
|
+
xml.MessageId message_id
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
action('ReceiveMessage', '/*/:QueueName') do
|
128
|
+
validate_queue_existence
|
129
|
+
wait_time_seconds = params['WaitTimeSeconds'] || queue['ReceiveMessageWaitTimeSeconds']
|
130
|
+
max_number_of_messages = params['MaxNumberOfMessages'] ? params['MaxNumberOfMessages'].to_i : 1
|
131
|
+
visibility_timeout = params['VisibilityTimeout'] || queue['VisibilityTimeout']
|
132
|
+
visible_messages = []
|
133
|
+
message_ids = redis.lrange("Queues:#{params[:QueueName]}:Messages", 0, -1)
|
134
|
+
message_ids.each do |message_id|
|
135
|
+
next if redis.exists("Queues:#{params[:QueueName]}:Messages:#{message_id}:ReceiptHandle")
|
136
|
+
next if redis.exists("Queues:#{params[:QueueName]}:Messages:#{message_id}:Delayed")
|
137
|
+
message = redis.hgetall("Queues:#{params[:QueueName]}:Messages:#{message_id}")
|
138
|
+
message['ApproximateFirstReceiveTimestamp'] ||= now
|
139
|
+
message['ApproximateReceiveCount'] = (message['ApproximateReceiveCount'].to_i + 1).to_s
|
140
|
+
redis.hmset("Queues:#{params[:QueueName]}:Messages:#{message_id}", message.to_a.flatten)
|
141
|
+
receipt_handle = SecureRandom.uuid
|
142
|
+
redis.set("Queues:#{params[:QueueName]}:Messages:#{message_id}:ReceiptHandle", receipt_handle)
|
143
|
+
redis.expire("Queues:#{params[:QueueName]}:Messages:#{message_id}:ReceiptHandle", visibility_timeout)
|
144
|
+
redis.set("Queues:#{params[:QueueName]}:ReceiptHandles:#{receipt_handle}", message_id)
|
145
|
+
redis.expire("Queues:#{params[:QueueName]}:ReceiptHandles:#{receipt_handle}", visibility_timeout)
|
146
|
+
visible_messages << {:MessageId => message_id, :MessageBody => message['MessageBody'], :ReceiptHandle => receipt_handle, :Attributes => message}
|
147
|
+
break if visible_messages.size >= max_number_of_messages
|
148
|
+
end
|
149
|
+
return_xml do |xml|
|
150
|
+
visible_messages.each do |message|
|
151
|
+
xml.Message do
|
152
|
+
xml.MessageId message[:MessageId]
|
153
|
+
xml.ReceiptHandle message[:ReceiptHandle]
|
154
|
+
xml.MD5OfBody Digest::MD5.hexdigest(message[:MessageBody])
|
155
|
+
xml.Body message[:MessageBody]
|
156
|
+
%w(SenderId SentTimestamp ApproximateFirstReceiveTimestamp ApproximateReceiveCount).each do |name|
|
157
|
+
xml.Attribute { xml.Name name; xml.Value message[:Attributes][name] }
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
action('ChangeMessageVisibility', '/*/:QueueName') do
|
165
|
+
validate_queue_existence
|
166
|
+
message_id = redis.get("Queues:#{params[:QueueName]}:ReceiptHandles:#{params[:ReceiptHandle]}")
|
167
|
+
redis.expire("Queues:#{params[:QueueName]}:Messages:#{message_id}:ReceiptHandle", params['VisibilityTimeout'])
|
168
|
+
redis.expire("Queues:#{params[:QueueName]}:ReceiptHandles:#{params[:ReceiptHandle]}", params['VisibilityTimeout'])
|
169
|
+
return_xml {}
|
170
|
+
end
|
171
|
+
|
172
|
+
action('DeleteMessage', '/*/:QueueName') do
|
173
|
+
validate_queue_existence
|
174
|
+
message_id = redis.get("Queues:#{params[:QueueName]}:ReceiptHandles:#{params[:ReceiptHandle]}")
|
175
|
+
redis.lrem("Queues:#{params[:QueueName]}:Messages", 0, message_id)
|
176
|
+
redis.del("Queues:#{params[:QueueName]}:ReceiptHandles:#{params[:ReceiptHandle]}")
|
177
|
+
return_xml {}
|
178
|
+
end
|
179
|
+
|
180
|
+
action('SendMessageBatch', '/*/:QueueName') do
|
181
|
+
validate_queue_existence
|
182
|
+
# not implemented yet
|
183
|
+
end
|
184
|
+
|
185
|
+
action('ChangeMessageVisibilityBatch', '/*/:QueueName') do
|
186
|
+
validate_queue_existence
|
187
|
+
# not implemented yet
|
188
|
+
end
|
189
|
+
|
190
|
+
action('DeleteMessageBatch', '/*/:QueueName') do
|
191
|
+
validate_queue_existence
|
192
|
+
# not implemented yet
|
193
|
+
end
|
194
|
+
|
195
|
+
helpers do
|
196
|
+
def redis
|
197
|
+
@redis ||= Redis::Namespace.new(:Q3, redis: Redis.new(url: ENV['REDISTOGO_URL'] || 'redis://localhost:6379/15'))
|
198
|
+
end
|
199
|
+
|
200
|
+
def request_id
|
201
|
+
@request_id ||= SecureRandom.uuid
|
202
|
+
end
|
203
|
+
|
204
|
+
def queue
|
205
|
+
@queue ||= redis.hgetall("Queues:#{params[:QueueName]}")
|
206
|
+
end
|
207
|
+
|
208
|
+
def queue_url(queue_name)
|
209
|
+
"http://#{request.host}/*/#{queue_name}"
|
210
|
+
end
|
211
|
+
|
212
|
+
def attributes
|
213
|
+
@attributes ||= (1..10).to_a.inject({}) do |attributes, i|
|
214
|
+
if (name = params["Attribute.#{i.to_s}.Name"]) && (value = params["Attribute.#{i.to_s}.Value"])
|
215
|
+
attributes[name] = value
|
216
|
+
end
|
217
|
+
attributes
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def return_xml(&block)
|
222
|
+
builder do |xml|
|
223
|
+
xml.instruct!
|
224
|
+
xml.tag!("#{params['Action']}Response") do
|
225
|
+
xml.tag!("#{params['Action']}Result") do
|
226
|
+
block.call(xml)
|
227
|
+
end
|
228
|
+
xml.ResponseMetadata { xml.RequestId request_id }
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def return_error_xml(type, code, message)
|
234
|
+
builder do |xml|
|
235
|
+
xml.instruct!
|
236
|
+
xml.ErrorResponse do
|
237
|
+
xml.Error do
|
238
|
+
xml.Type type
|
239
|
+
xml.Code code
|
240
|
+
xml.Message message
|
241
|
+
end
|
242
|
+
xml.ResponseMetadata { xml.RequestId request_id }
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def validate_queue_existence
|
248
|
+
halt 400, return_error_xml('Sender', 'NonExistentQueue', 'The specified queue does not exist for this wsdl version.') if queue.empty?
|
249
|
+
end
|
250
|
+
|
251
|
+
def now
|
252
|
+
(Time.now.to_f * 1000.0).to_i
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
dispatch!
|
257
|
+
end
|
data/spec/q3_spec.rb
ADDED
@@ -0,0 +1,341 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe "Q3" do
|
4
|
+
before do
|
5
|
+
client.list_queues[:queue_urls].each do |queue_url|
|
6
|
+
client.delete_queue(:queue_url => queue_url)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
after do
|
11
|
+
client.list_queues[:queue_urls].each do |queue_url|
|
12
|
+
client.delete_queue(:queue_url => queue_url)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'Actions' do
|
17
|
+
context 'Queue' do
|
18
|
+
context "CreateQueue" do
|
19
|
+
it 'should create queue' do
|
20
|
+
client.create_queue(:queue_name => 'myqueue001')
|
21
|
+
expect(client.list_queues[:queue_urls]).to include('http://localhost/*/myqueue001')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context "ListQueues" do
|
26
|
+
it 'should list queues' do
|
27
|
+
1.upto(5) do |i|
|
28
|
+
client.create_queue(:queue_name => "myqueue00#{i}")
|
29
|
+
end
|
30
|
+
expect(client.list_queues[:queue_urls]).to eq %w(
|
31
|
+
http://localhost/*/myqueue001
|
32
|
+
http://localhost/*/myqueue002
|
33
|
+
http://localhost/*/myqueue003
|
34
|
+
http://localhost/*/myqueue004
|
35
|
+
http://localhost/*/myqueue005
|
36
|
+
)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "GetQueueUrl" do
|
41
|
+
it 'should get queue url' do
|
42
|
+
client.create_queue(queue_name: 'myqueue001')
|
43
|
+
res = client.get_queue_url(queue_name: 'myqueue001')
|
44
|
+
expect(res[:queue_url]).to eq('http://localhost/*/myqueue001')
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should raise error when non existent queue is specified' do
|
48
|
+
expect {
|
49
|
+
client.get_queue_url(queue_name: 'myqueue002')
|
50
|
+
}.to raise_error(AWS::SQS::Errors::NonExistentQueue)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context "GetQueueAttributes" do
|
55
|
+
it 'should get queue attributes' do
|
56
|
+
now = (Time.now.to_f * 1000.0).to_i
|
57
|
+
client.create_queue(queue_name: 'myqueue001')
|
58
|
+
res = client.get_queue_attributes(queue_url: 'http://localhost/*/myqueue001')
|
59
|
+
expect(res[:attributes]["CreateTimestamp"].to_i).to be_within(5000).of(now)
|
60
|
+
expect(res[:attributes]["LastModifiedTimestamp"].to_i).to be_within(5000).of(now)
|
61
|
+
expect(res[:attributes]["VisibilityTimeout"]).to eq("30")
|
62
|
+
expect(res[:attributes]["MessageRetentionPeriod"]).to eq("345600")
|
63
|
+
expect(res[:attributes]["MaximumMessageSize"]).to eq("262144")
|
64
|
+
expect(res[:attributes]["DelaySeconds"]).to eq("0")
|
65
|
+
expect(res[:attributes]["ReceiveMessageWaitTimeSeconds"]).to eq("0")
|
66
|
+
expect(res[:attributes]["ApproximateNumberOfMessages"]).to eq("0")
|
67
|
+
expect(res[:attributes]["ApproximateNumberOfMessagesNotVisible"]).to eq("0")
|
68
|
+
expect(res[:attributes]["ApproximateNumberOfMessagesDelayed"]).to eq("0")
|
69
|
+
end
|
70
|
+
|
71
|
+
it 'should get updated LastModifiedTimestamp' do
|
72
|
+
past = (Time.now.to_f * 1000.0).to_i
|
73
|
+
queue = q3.queues.create('myqueue001')
|
74
|
+
sleep 3
|
75
|
+
now = (Time.now.to_f * 1000.0).to_i
|
76
|
+
queue.visibility_timeout = 1
|
77
|
+
expect(queue.last_modified_timestamp.to_i).not_to be_within(2000).of(past)
|
78
|
+
expect(queue.last_modified_timestamp.to_i).to be_within(2000).of(now)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'should get precise ApproximateNumberOfMessages*' do
|
82
|
+
queue = q3.queues.create('myqueue001')
|
83
|
+
|
84
|
+
queue.send_message('hello', delay_seconds: 5)
|
85
|
+
queue.send_message('hello')
|
86
|
+
queue.send_message('hello')
|
87
|
+
queue.receive_message
|
88
|
+
|
89
|
+
res = client.get_queue_attributes(queue_url: 'http://localhost/*/myqueue001')
|
90
|
+
expect(res[:attributes]["ApproximateNumberOfMessages"]).to eq("1")
|
91
|
+
expect(res[:attributes]["ApproximateNumberOfMessagesNotVisible"]).to eq("1")
|
92
|
+
expect(res[:attributes]["ApproximateNumberOfMessagesDelayed"]).to eq("1")
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context "SetQueueAttributes" do
|
97
|
+
it 'should set queue attributes' do
|
98
|
+
client.create_queue(queue_name: 'myqueue001')
|
99
|
+
client.set_queue_attributes(
|
100
|
+
queue_url: 'http://localhost/*/myqueue001',
|
101
|
+
attributes: {
|
102
|
+
'VisibilityTimeout' => '1',
|
103
|
+
'MessageRetentionPeriod' => '2',
|
104
|
+
'MaximumMessageSize' => '3',
|
105
|
+
'DelaySeconds' => '4',
|
106
|
+
'ReceiveMessageWaitTimeSeconds' => '5',
|
107
|
+
}
|
108
|
+
)
|
109
|
+
res = client.get_queue_attributes(queue_url: 'http://localhost/*/myqueue001')
|
110
|
+
expect(res[:attributes]["VisibilityTimeout"]).to eq("1")
|
111
|
+
expect(res[:attributes]["MessageRetentionPeriod"]).to eq("2")
|
112
|
+
expect(res[:attributes]["MaximumMessageSize"]).to eq("3")
|
113
|
+
expect(res[:attributes]["DelaySeconds"]).to eq("4")
|
114
|
+
expect(res[:attributes]["ReceiveMessageWaitTimeSeconds"]).to eq("5")
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
context "DeleteQueue" do
|
119
|
+
it 'should delete queue' do
|
120
|
+
client.create_queue(queue_name: 'myqueue001')
|
121
|
+
client.delete_queue(queue_url: 'http://localhost/*/myqueue001')
|
122
|
+
expect(client.list_queues[:queue_urls]).not_to include('http://localhost/*/myqueue001')
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
context 'Message' do
|
128
|
+
context "SendMessage" do
|
129
|
+
it 'should send message' do
|
130
|
+
client.create_queue(queue_name: 'myqueue001')
|
131
|
+
client.send_message(
|
132
|
+
queue_url: 'http://localhost/*/myqueue001',
|
133
|
+
message_body: 'hello'
|
134
|
+
)
|
135
|
+
res = client.receive_message(queue_url: 'http://localhost/*/myqueue001')
|
136
|
+
expect(res[:messages].first[:body]).to eq('hello')
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
context "ReceiveMessage" do
|
141
|
+
it 'should receive message' do
|
142
|
+
client.create_queue(queue_name: 'myqueue001')
|
143
|
+
client.send_message(
|
144
|
+
queue_url: 'http://localhost/*/myqueue001',
|
145
|
+
message_body: 'hello'
|
146
|
+
)
|
147
|
+
res = client.receive_message(queue_url: 'http://localhost/*/myqueue001')
|
148
|
+
expect(res[:messages].first[:body]).to eq('hello')
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'should receive message attributes' do
|
152
|
+
queue = q3.queues.create('myqueue001')
|
153
|
+
sent_at = Time.now.to_i
|
154
|
+
queue.send_message('hello')
|
155
|
+
|
156
|
+
sleep 3
|
157
|
+
now = Time.now.to_i
|
158
|
+
message = queue.receive_message(visibility_timeout: 1)
|
159
|
+
expect(message.sender_id).to eq('*')
|
160
|
+
expect(message.sent_timestamp.to_i).to be_within(1).of(sent_at)
|
161
|
+
expect(message.approximate_first_receive_timestamp.to_i).to be_within(1).of(now)
|
162
|
+
expect(message.approximate_receive_count.to_i).to eq(1)
|
163
|
+
|
164
|
+
sleep 2
|
165
|
+
message = queue.receive_message(visibility_timeout: 1)
|
166
|
+
expect(message.approximate_receive_count.to_i).to eq(2)
|
167
|
+
end
|
168
|
+
|
169
|
+
it 'should receive message attributes' do
|
170
|
+
queue = q3.queues.create('myqueue001')
|
171
|
+
1.upto(10) do |i|
|
172
|
+
queue.send_message("hello #{i}")
|
173
|
+
end
|
174
|
+
res = q3.client.receive_message(
|
175
|
+
queue_url: 'http://localhost/*/myqueue001',
|
176
|
+
max_number_of_messages: 5
|
177
|
+
)
|
178
|
+
expect(res[:messages].size).to eq(5)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
context "ChangeMessageVisibility" do
|
183
|
+
it 'should change message visibility' do
|
184
|
+
client.create_queue(queue_name: 'myqueue001')
|
185
|
+
client.send_message(
|
186
|
+
queue_url: 'http://localhost/*/myqueue001',
|
187
|
+
message_body: 'hello'
|
188
|
+
)
|
189
|
+
res = client.receive_message(queue_url: 'http://localhost/*/myqueue001')
|
190
|
+
receipt_handle = res[:messages].first[:receipt_handle]
|
191
|
+
client.change_message_visibility(
|
192
|
+
queue_url: 'http://localhost/*/myqueue001',
|
193
|
+
receipt_handle: receipt_handle,
|
194
|
+
visibility_timeout: 3,
|
195
|
+
)
|
196
|
+
sleep 5
|
197
|
+
res = client.receive_message(queue_url: 'http://localhost/*/myqueue001')
|
198
|
+
expect(res[:messages].first[:body]).to eq('hello')
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
context "DeleteMessage" do
|
203
|
+
it 'should delete message' do
|
204
|
+
client.create_queue(queue_name: 'myqueue001')
|
205
|
+
client.send_message(
|
206
|
+
queue_url: 'http://localhost/*/myqueue001',
|
207
|
+
message_body: 'hello'
|
208
|
+
)
|
209
|
+
res = client.receive_message(queue_url: 'http://localhost/*/myqueue001')
|
210
|
+
receipt_handle = res[:messages].first[:receipt_handle]
|
211
|
+
client.delete_message(
|
212
|
+
queue_url: 'http://localhost/*/myqueue001',
|
213
|
+
receipt_handle: receipt_handle
|
214
|
+
)
|
215
|
+
res = client.receive_message(queue_url: 'http://localhost/*/myqueue001')
|
216
|
+
expect(res[:messages]).to be_empty
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
context 'Features' do
|
223
|
+
context 'Message Extention Period' do
|
224
|
+
it 'with CreateQueue MessageExtentionPeriod' do
|
225
|
+
queue = q3.queues.create('myqueue001', :message_retention_period => 3)
|
226
|
+
|
227
|
+
queue.send_message('hello')
|
228
|
+
message = queue.receive_message
|
229
|
+
expect(message.body).to eq('hello')
|
230
|
+
|
231
|
+
queue.send_message('hello')
|
232
|
+
sleep 4
|
233
|
+
message = queue.receive_message
|
234
|
+
expect(message).to be_nil
|
235
|
+
end
|
236
|
+
|
237
|
+
it 'with SetQueueAttributes MessageExtentionPeriod' do
|
238
|
+
queue = q3.queues.create('myqueue001')
|
239
|
+
queue.message_retention_period = 3
|
240
|
+
|
241
|
+
queue.send_message('hello')
|
242
|
+
message = queue.receive_message
|
243
|
+
expect(message.body).to eq('hello')
|
244
|
+
|
245
|
+
queue.send_message('hello')
|
246
|
+
sleep 4
|
247
|
+
message = queue.receive_message
|
248
|
+
expect(message).to be_nil
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
context 'First In First Out' do
|
253
|
+
it 'First in first out' do
|
254
|
+
queue = q3.queues.create('myqueue001', :visibility_timeout => 3)
|
255
|
+
|
256
|
+
1.upto(10) do |i|
|
257
|
+
queue.send_message("hello #{i}")
|
258
|
+
end
|
259
|
+
|
260
|
+
1.upto(10) do |i|
|
261
|
+
message = queue.receive_message
|
262
|
+
expect(message.body).to eq("hello #{i}")
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
context 'Long Polling' do
|
268
|
+
pending 'not implemented yet'
|
269
|
+
end
|
270
|
+
|
271
|
+
context 'Visibility Timeout' do
|
272
|
+
it 'with CreateQueue VisibilityTimeout' do
|
273
|
+
queue = q3.queues.create('myqueue001', :visibility_timeout => 3)
|
274
|
+
|
275
|
+
queue.send_message('hello')
|
276
|
+
message = queue.receive_message
|
277
|
+
expect(message.body).to eq('hello')
|
278
|
+
|
279
|
+
message = queue.receive_message
|
280
|
+
expect(message).to be_nil
|
281
|
+
|
282
|
+
sleep 4
|
283
|
+
message = queue.receive_message
|
284
|
+
expect(message.body).to eq('hello')
|
285
|
+
end
|
286
|
+
|
287
|
+
it 'with SetQueueAttributes VisibilityTimeout' do
|
288
|
+
queue = q3.queues.create('myqueue001')
|
289
|
+
queue.visibility_timeout = 3
|
290
|
+
|
291
|
+
queue.send_message('hello')
|
292
|
+
message = queue.receive_message
|
293
|
+
expect(message.body).to eq('hello')
|
294
|
+
|
295
|
+
message = queue.receive_message
|
296
|
+
expect(message).to be_nil
|
297
|
+
|
298
|
+
sleep 4
|
299
|
+
message = queue.receive_message
|
300
|
+
expect(message.body).to eq('hello')
|
301
|
+
end
|
302
|
+
|
303
|
+
it 'with ReceiveMessage VisibilityTimeout' do
|
304
|
+
queue = q3.queues.create('myqueue001')
|
305
|
+
|
306
|
+
queue.send_message('hello')
|
307
|
+
message = queue.receive_message(visibility_timeout: 3)
|
308
|
+
expect(message.body).to eq('hello')
|
309
|
+
|
310
|
+
message = queue.receive_message
|
311
|
+
expect(message).to be_nil
|
312
|
+
|
313
|
+
sleep 4
|
314
|
+
message = queue.receive_message
|
315
|
+
expect(message.body).to eq('hello')
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
context 'Delayed Message' do
|
320
|
+
it 'with CreateQueue DelaySeconds' do
|
321
|
+
queue = q3.queues.create('myqueue001', :delay_seconds => 3)
|
322
|
+
queue.send_message('hello')
|
323
|
+
message = queue.receive_message
|
324
|
+
expect(message).to be_nil
|
325
|
+
sleep 4
|
326
|
+
message = queue.receive_message
|
327
|
+
expect(message.body).to eq('hello')
|
328
|
+
end
|
329
|
+
|
330
|
+
it 'with SendMessage DelaySeconds' do
|
331
|
+
queue = q3.queues.create('myqueue001')
|
332
|
+
queue.send_message('hello', :delay_seconds => 3)
|
333
|
+
message = queue.receive_message
|
334
|
+
expect(message).to be_nil
|
335
|
+
sleep 4
|
336
|
+
message = queue.receive_message
|
337
|
+
expect(message.body).to eq('hello')
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'rspec'
|
4
|
+
require 'q3'
|
5
|
+
require 'aws-sdk'
|
6
|
+
|
7
|
+
# Requires supporting files with custom matchers and macros, etc,
|
8
|
+
# in ./support/ and its subdirectories.
|
9
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
10
|
+
|
11
|
+
RSpec.configure do |config|
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
#AWS.config(
|
16
|
+
# logger: Logger.new($stdout),
|
17
|
+
# log_level: :debug,
|
18
|
+
# http_wire_trace: true
|
19
|
+
#)
|
20
|
+
|
21
|
+
def q3
|
22
|
+
@q3 ||= AWS::SQS.new(
|
23
|
+
:sqs_endpoint => 'localhost',
|
24
|
+
:access_key_id => 'dummy',
|
25
|
+
:secret_access_key => 'dummy',
|
26
|
+
:use_ssl => false
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def client
|
31
|
+
q3.client
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: q3
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- tily
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-07-06 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: sinatra
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: builder
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: redis
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: redis-namespace
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rspec
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 2.3.0
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 2.3.0
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: bundler
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ~>
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 1.0.0
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: 1.0.0
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: jeweler
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ~>
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 1.6.4
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ~>
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 1.6.4
|
126
|
+
description: Simple Amazon SQS compatible API implementation with sinatra and redis
|
127
|
+
email: tily05@gmail.com
|
128
|
+
executables:
|
129
|
+
- q3
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files:
|
132
|
+
- LICENSE.txt
|
133
|
+
- README.md
|
134
|
+
files:
|
135
|
+
- .document
|
136
|
+
- .rspec
|
137
|
+
- Gemfile
|
138
|
+
- Gemfile.lock
|
139
|
+
- LICENSE.txt
|
140
|
+
- README.md
|
141
|
+
- Rakefile
|
142
|
+
- bin/q3
|
143
|
+
- config.ru
|
144
|
+
- lib/q3.rb
|
145
|
+
- spec/q3_spec.rb
|
146
|
+
- spec/spec_helper.rb
|
147
|
+
homepage: http://github.com/tily/q3
|
148
|
+
licenses:
|
149
|
+
- MIT
|
150
|
+
post_install_message:
|
151
|
+
rdoc_options: []
|
152
|
+
require_paths:
|
153
|
+
- lib
|
154
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
155
|
+
none: false
|
156
|
+
requirements:
|
157
|
+
- - ! '>='
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
segments:
|
161
|
+
- 0
|
162
|
+
hash: 1071069083813068665
|
163
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
164
|
+
none: false
|
165
|
+
requirements:
|
166
|
+
- - ! '>='
|
167
|
+
- !ruby/object:Gem::Version
|
168
|
+
version: '0'
|
169
|
+
requirements: []
|
170
|
+
rubyforge_project:
|
171
|
+
rubygems_version: 1.8.23
|
172
|
+
signing_key:
|
173
|
+
specification_version: 3
|
174
|
+
summary: Simple Amazon SQS compatible API implementation with sinatra and redis
|
175
|
+
test_files: []
|