amaze_sns 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ Amazon SNS gem
2
+ ==============
3
+
4
+ Introduction
5
+ ---------
6
+ A Ruby gem for use with the Amazon Simple Notification service (http://aws.amazon.com/sns/).
7
+
8
+ Usage
9
+ ---------------
10
+
11
+ AmazeSNS.skey = <your amazon aws secret key>
12
+
13
+ AmazeSNS.akey = <your amazon aws access key>
14
+
15
+ AmazeSNS['your_topic_name'] # creates a new Topic Object but not yet published
16
+ AmazeSNS['your_topic_name'].create # add new topic to local hash and to SNS
17
+ AmazeSNS['your_topic_name'].delete # removes it from both SNS and local hash
18
+
19
+ AmazeSNS.logger = my_logger # set a logger for the response
20
+
21
+ Dependencies
22
+ ---------------
23
+ Require the CrackXML gem for parsing XML responses back from SNS; EventMachine
24
+ and EM-Http request gem for the requests; and ruby hmac gem for authenticating with Amazon Web Services
25
+
26
+
27
+ Tests
28
+ ---------------
29
+ The specs are not fully working at the moment as the gem is undergoing a serious revamp.
30
+
31
+ The gem itself has been tested on systems running ruby 1.8.6 and ruby 1.8.7
32
+
33
+
34
+
35
+ Copyright
36
+ ---------
37
+
38
+ Copyright (c) 2010 29 Steps UK. See LICENSE for details.
@@ -0,0 +1,34 @@
1
+ class AmazeSNSRuntimeError < RuntimeError; end
2
+ class AmazeSNSError < StandardError; end
3
+ class AuthenticationError < AmazeSNSRuntimeError; end
4
+
5
+ class InvalidOptions < AmazeSNSError
6
+ def message
7
+ 'Please supply valid options'
8
+ end
9
+ end
10
+
11
+ class AuthorizationError < AmazeSNSError
12
+ def message
13
+ 'You do not have permission to access the resource'
14
+ end
15
+ end
16
+
17
+ class InternalError < AmazeSNSError
18
+ def message
19
+ 'An internal service error has occured on the Simple Notification Service'
20
+ end
21
+ end
22
+
23
+ class InvalidParameterError < AmazeSNSError
24
+ def message
25
+ 'An invalid parameter is in the request'
26
+ end
27
+ end
28
+
29
+
30
+ class NotFoundError < AmazeSNSError
31
+ def message
32
+ 'The requested resource is not found'
33
+ end
34
+ end
@@ -0,0 +1,31 @@
1
+
2
+ require "hmac"
3
+ require "hmac-sha2"
4
+ require "cgi"
5
+ require "time"
6
+ require "base64"
7
+
8
+
9
+ # HELPER METHODS
10
+ def url_encode(string)
11
+ string = string.to_s
12
+ # It's kinda like CGI.escape, except CGI.escape is encoding a tilde when
13
+ # it ought not to be, so we turn it back. Also space NEEDS to be %20 not +.
14
+ return CGI.escape(string).gsub("%7E", "~").gsub("+", "%20")
15
+ end
16
+
17
+ def canonical_querystring(params)
18
+ # I hope this built-in sort sorts by byte order, that's what's required.
19
+ values = params.keys.sort.collect {|key| [url_encode(key), url_encode(params[key])].join("=") }
20
+
21
+ return values.join("&")
22
+ end
23
+
24
+
25
+ def hash_to_query(hash)
26
+ hash.collect do |key, value|
27
+
28
+ url_encode(key) + "=" + url_encode(value)
29
+
30
+ end.join("&")
31
+ end
@@ -0,0 +1,94 @@
1
+
2
+ require 'crack/xml'
3
+
4
+ require File.dirname(__FILE__) + "/helpers"
5
+ require File.dirname(__FILE__) + "/exceptions"
6
+
7
+ require 'em-http'
8
+
9
+
10
+ class Request
11
+
12
+ attr_accessor :params, :options, :httpresponse
13
+
14
+ def initialize(params, options={})
15
+ @params = params
16
+ @options = options
17
+ end
18
+
19
+ def process2
20
+ query_string = canonical_querystring(@params)
21
+ string_to_sign = "GET
22
+ #{AmazeSNS.host}
23
+ /
24
+ #{query_string}"
25
+
26
+ hmac = HMAC::SHA256.new(AmazeSNS.skey)
27
+ hmac.update( string_to_sign )
28
+ signature = Base64.encode64(hmac.digest).chomp
29
+
30
+ params['Signature'] = signature
31
+ querystring2 = params.collect { |key, value| [url_encode(key), url_encode(value)].join("=") }.join('&') # order doesn't matter for the actual request
32
+ response = HttpClient.get "#{AmazeSNS.host}?#{querystring2}"
33
+ parsed_response = Crack::XML.parse(response)
34
+ return parsed_response
35
+ end
36
+
37
+ def process
38
+ query_string = canonical_querystring(@params)
39
+
40
+ string_to_sign = "GET
41
+ #{AmazeSNS.host}
42
+ /
43
+ #{query_string}"
44
+
45
+ hmac = HMAC::SHA256.new(AmazeSNS.skey)
46
+ hmac.update( string_to_sign )
47
+ signature = Base64.encode64(hmac.digest).chomp
48
+
49
+ params['Signature'] = signature
50
+ querystring2 = params.collect { |key, value| [url_encode(key), url_encode(value)].join("=") }.join('&') # order doesn't matter for the actual request
51
+
52
+ unless defined?(EventMachine) && EventMachine.reactor_running?
53
+ raise AmazeSNSRuntimeError, "In order to use this you must be running inside an eventmachine loop"
54
+ end
55
+
56
+ require 'em-http' unless defined?(EventMachine::HttpRequest)
57
+
58
+ @httpresponse ||= http_class.new("http://#{AmazeSNS.host}/?#{querystring2}").send(:get)
59
+ @httpresponse.callback{ success_callback }
60
+ @httpresponse.errback{ error_callback }
61
+ nil
62
+ end
63
+
64
+ def http_class
65
+ EventMachine::HttpRequest
66
+ end
67
+
68
+
69
+ def success_callback
70
+ case @httpresponse.response_header.status
71
+ when 403
72
+ raise AuthorizationError
73
+ when 500
74
+ raise InternalError
75
+ when 400
76
+ raise InvalidParameterError
77
+ when 404
78
+ raise NotFoundError
79
+ else
80
+ call_user_success_handler
81
+ end #end case
82
+ end
83
+
84
+ def call_user_success_handler
85
+ @options[:on_success].call(httpresponse) if options[:on_success].respond_to?(:call)
86
+ end
87
+
88
+ def error_callback
89
+ EventMachine.stop
90
+ raise AmazeSNSRuntimeError.new("A runtime error has occured: status code: #{@httpresponse.response_header.status}")
91
+ end
92
+
93
+
94
+ end
@@ -0,0 +1,17 @@
1
+ class Subscription
2
+
3
+ attr_accessor :owner, :protocol, :topicarn, :endpoint, :subarn
4
+
5
+ def initialize(args)
6
+ @owner = args["Owner"]
7
+ @protocol = args["Protocol"]
8
+ @topicarn = args["TopicArn"]
9
+ @endpoint = args["Endpoint"]
10
+ @subarn = args["SubscriptionArn"]
11
+ end
12
+
13
+ def to_s
14
+ "Subscription: Owner - #{@owner} : Protocol - #{@protocol} : TopicARN - #{@topicarn} : EndPoint - #{@endpoint} : SubscriptionARN - #{@subarn}"
15
+ end
16
+
17
+ end
@@ -0,0 +1,342 @@
1
+ require File.dirname(__FILE__) + "/request"
2
+ require File.dirname(__FILE__) + "/exceptions"
3
+ require "eventmachine"
4
+ require "json"
5
+
6
+ class Topic
7
+
8
+ attr_accessor :topic, :arn, :attrs
9
+
10
+ def initialize(topic, arn='')
11
+ @topic = topic
12
+ @arn = arn
13
+ @attrs ||= {}
14
+ end
15
+
16
+ def generate_request(params,&blk)
17
+ req_options={}
18
+ req_options[:on_success] = blk if blk
19
+ Request.new(params, req_options).process
20
+ end
21
+
22
+ # for running th EM loop w/o repetitions
23
+ def reactor(&blk)
24
+ EM.run do
25
+ instance_eval(&blk)
26
+ end
27
+ end
28
+
29
+ def create
30
+ params = {
31
+ 'Name' => "#{@topic}",
32
+ 'Action' => 'CreateTopic',
33
+ 'SignatureMethod' => 'HmacSHA256',
34
+ 'SignatureVersion' => 2,
35
+ 'Timestamp' => Time.now.iso8601,
36
+ 'AWSAccessKeyId' => AmazeSNS.akey
37
+ }
38
+
39
+ reactor{
40
+ generate_request(params) do |response|
41
+ parsed_response = Crack::XML.parse(response.response)
42
+ @arn = parsed_response["CreateTopicResponse"]["CreateTopicResult"]["TopicArn"]
43
+ AmazeSNS.topics[@topic.to_s] = self # add to hash
44
+ AmazeSNS.topics.rehash
45
+ EM.stop
46
+ end
47
+ }
48
+ @arn
49
+ end
50
+
51
+ # delete topic
52
+ def delete
53
+ parsed_response = ''
54
+ params = {
55
+ 'TopicArn' => "#{arn}",
56
+ 'Action' => 'DeleteTopic',
57
+ 'SignatureMethod' => 'HmacSHA256',
58
+ 'SignatureVersion' => 2,
59
+ 'Timestamp' => Time.now.iso8601,
60
+ 'AWSAccessKeyId' => AmazeSNS.akey
61
+ }
62
+
63
+ reactor{
64
+ generate_request(params) do |response|
65
+ parsed_response = Crack::XML.parse(response.response)
66
+ AmazeSNS.topics.delete("#{@topic}")
67
+ AmazeSNS.topics.rehash
68
+ EM.stop
69
+ end
70
+ }
71
+ parsed_response
72
+ end
73
+
74
+ # get attributes for topic from remote sns server
75
+ # TopicArn -- the topic's ARN
76
+ # Owner -- the AWS account ID of the topic's owner
77
+ # Policy -- the JSON serialization of the topic's access control policy
78
+ # DisplayName -- the human-readable name used in the "From" field for notifications to email and email-json endpoints
79
+
80
+ def attrs
81
+ outcome = nil
82
+ params = {
83
+ 'TopicArn' => "#{arn}",
84
+ 'Action' => 'GetTopicAttributes',
85
+ 'SignatureMethod' => 'HmacSHA256',
86
+ 'SignatureVersion' => 2,
87
+ 'Timestamp' => Time.now.iso8601,
88
+ 'AWSAccessKeyId' => AmazeSNS.akey
89
+ }
90
+
91
+ reactor{
92
+ generate_request(params) do |response|
93
+ parsed_response = Crack::XML.parse(response.response)
94
+ res = parsed_response['GetTopicAttributesResponse']['GetTopicAttributesResult']['Attributes']["entry"]
95
+ outcome = make_hash(res) #res["entry"] is an array of hashes - need to turn it into hash with key value
96
+ EM.stop
97
+ end
98
+ }
99
+ return outcome
100
+ end
101
+
102
+ # The SetTopicAttributes action allows a topic owner to set an attribute of the topic to a new value.
103
+ # only following attributes can be set:
104
+ # TopicArn -- the topic's ARN
105
+ # Owner -- the AWS account ID of the topic's owner
106
+ # Policy -- the JSON serialization of the topic's access control policy
107
+ # DisplayName -- the human-readable name used in the "From" field for notifications to email and email-json endpoints
108
+
109
+ def set_attrs(opts)
110
+ outcome = nil
111
+ params = {
112
+ 'AttributeName' => "#{opts[:name]}",
113
+ 'AttributeValue' => "#{opts[:value]}",
114
+ 'TopicArn' => "#{arn}",
115
+ 'Action' => 'SetTopicAttributes',
116
+ 'SignatureMethod' => 'HmacSHA256',
117
+ 'SignatureVersion' => 2,
118
+ 'Timestamp' => Time.now.iso8601,
119
+ 'AWSAccessKeyId' => AmazeSNS.akey
120
+ }
121
+
122
+ reactor{
123
+ generate_request(params) do |response|
124
+ parsed_response = Crack::XML.parse(response.response)
125
+ outcome = parsed_response['SetTopicAttributesResponse']['ResponseMetadata']['RequestId']
126
+ EM.stop
127
+ end
128
+ }
129
+ return outcome
130
+ end
131
+
132
+ # subscribe method
133
+ def subscribe(opts)
134
+ raise InvalidOptions unless ( !(opts.empty?) && opts.instance_of?(Hash) )
135
+ res=''
136
+ params = {
137
+ 'TopicArn' => "#{arn}",
138
+ 'Endpoint' => "#{opts[:endpoint]}",
139
+ 'Protocol' => "#{opts[:protocol]}",
140
+ 'Action' => 'Subscribe',
141
+ 'SignatureMethod' => 'HmacSHA256',
142
+ 'SignatureVersion' => 2,
143
+ 'Timestamp' => Time.now.iso8601,
144
+ 'AWSAccessKeyId' => AmazeSNS.akey
145
+ }
146
+
147
+ reactor{
148
+ generate_request(params) do |response|
149
+ parsed_response = Crack::XML.parse(response.response)
150
+ res = parsed_response['SubscribeResponse']['SubscribeResult']['SubscriptionArn']
151
+ return res
152
+ EM.stop
153
+ end
154
+ }
155
+ res
156
+ end
157
+
158
+ def unsubscribe(id)
159
+ raise InvalidOptions unless ( !(id.empty?) && id.instance_of?(String) )
160
+ res=''
161
+ params = {
162
+ 'SubscriptionArn' => "#{id}",
163
+ 'Action' => 'Unsubscribe',
164
+ 'SignatureMethod' => 'HmacSHA256',
165
+ 'SignatureVersion' => 2,
166
+ 'Timestamp' => Time.now.iso8601,
167
+ 'AWSAccessKeyId' => AmazeSNS.akey
168
+ }
169
+
170
+ reactor{
171
+ generate_request(params) do |response|
172
+ parsed_response = Crack::XML.parse(response.response)
173
+ res = parsed_response['UnsubscribeResponse']['ResponseMetadata']['RequestId']
174
+ return res
175
+ EM.stop
176
+ end
177
+ }
178
+ res
179
+ end
180
+
181
+
182
+ # grabs list of subscriptions for this topic only
183
+ def subscriptions
184
+ nh={}
185
+ params = {
186
+ 'TopicArn' => "#{arn}",
187
+ 'Action' => 'ListSubscriptionsByTopic',
188
+ 'SignatureMethod' => 'HmacSHA256',
189
+ 'SignatureVersion' => 2,
190
+ 'Timestamp' => Time.now.iso8601,
191
+ 'AWSAccessKeyId' => AmazeSNS.akey
192
+ }
193
+
194
+ reactor{
195
+ generate_request(params) do |response|
196
+ parsed_response = Crack::XML.parse(response.response)
197
+ arr = parsed_response['ListSubscriptionsByTopicResponse']['ListSubscriptionsByTopicResult']['Subscriptions']['member'] unless (parsed_response['ListSubscriptionsByTopicResponse']['ListSubscriptionsByTopicResult']['Subscriptions'].nil?)
198
+
199
+ if !(arr.nil?) && (arr.instance_of?(Array))
200
+ #temp fix for now
201
+ nh = arr.inject({}) do |h,v|
202
+ key = v["SubscriptionArn"].to_s
203
+ value = v
204
+ h[key.to_s] = value
205
+ h
206
+ end
207
+ elsif !(arr.nil?) && (arr.instance_of?(Hash))
208
+ # to deal with one subscription issue
209
+ key = arr["SubscriptionArn"]
210
+ arr.delete("SubscriptionArn")
211
+ nh[key.to_s] = arr
212
+ end
213
+ return nh
214
+ EM.stop
215
+ end
216
+ }
217
+ nh
218
+ end
219
+
220
+ # The AddPermission action adds a statement to a topic's access control policy, granting access for the
221
+ # specified AWS accounts to the specified actions.
222
+
223
+ def add_permission(opts)
224
+ raise InvalidOptions unless ( !(opts.empty?) && opts.instance_of?(Hash) )
225
+ res=''
226
+ params = {
227
+ 'TopicArn' => "#{arn}",
228
+ 'Label' => "#{opts[:label]}",
229
+ 'ActionName.member.1' => "#{opts[:action_name]}",
230
+ 'AWSAccountId.member.1' => "#{opts[:account_id]}",
231
+ 'Action' => 'AddPermission',
232
+ 'SignatureMethod' => 'HmacSHA256',
233
+ 'SignatureVersion' => 2,
234
+ 'Timestamp' => Time.now.iso8601,
235
+ 'AWSAccessKeyId' => AmazeSNS.akey
236
+ }
237
+
238
+ reactor{
239
+ generate_request(params) do |response|
240
+ parsed_response = Crack::XML.parse(response.response)
241
+ res = parsed_response['AddPermissionResponse']['ResponseMetadata']['RequestId']
242
+ return res
243
+ EM.stop
244
+ end
245
+ }
246
+ res
247
+ end
248
+
249
+ # The RemovePermission action removes a statement from a topic's access control policy.
250
+ def remove_permission(label)
251
+ raise InvalidOptions unless ( !(label.empty?) && label.instance_of?(String) )
252
+ res=''
253
+ params = {
254
+ 'TopicArn' => "#{arn}",
255
+ 'Label' => "#{label}",
256
+ 'Action' => 'RemovePermission',
257
+ 'SignatureMethod' => 'HmacSHA256',
258
+ 'SignatureVersion' => 2,
259
+ 'Timestamp' => Time.now.iso8601,
260
+ 'AWSAccessKeyId' => AmazeSNS.akey
261
+ }
262
+
263
+ reactor{
264
+ generate_request(params) do |response|
265
+ parsed_response = Crack::XML.parse(response.response)
266
+ res = parsed_response['RemovePermissionResponse']['ResponseMetadata']['RequestId']
267
+ return res
268
+ EM.stop
269
+ end
270
+ }
271
+ res
272
+ end
273
+
274
+
275
+ def publish!(msg, subject='')
276
+ raise InvalidOptions unless ( !(msg.empty?) && msg.instance_of?(String) )
277
+ res=''
278
+ params = {
279
+ 'Subject' => "My First Message",
280
+ 'TopicArn' => "#{arn}",
281
+ "Message" => "#{msg}",
282
+ 'Action' => 'Publish',
283
+ 'SignatureMethod' => 'HmacSHA256',
284
+ 'SignatureVersion' => 2,
285
+ 'Timestamp' => Time.now.iso8601,
286
+ 'AWSAccessKeyId' => AmazeSNS.akey
287
+ }
288
+
289
+ reactor{
290
+ generate_request(params) do |response|
291
+ parsed_response = Crack::XML.parse(response.response)
292
+ res = parsed_response['PublishResponse']['PublishResult']['MessageId']
293
+ return res
294
+ EM.stop
295
+ end
296
+ }
297
+ res
298
+ end
299
+
300
+ def confirm_subscription(token)
301
+ raise InvalidOptions unless ( !(token.empty?) && token.instance_of?(String) )
302
+ arr=[]
303
+ params = {
304
+ 'TopicArn' => "#{arn}",
305
+ 'Token' => "#{token}",
306
+ 'Action' => 'ConfirmSubscription',
307
+ 'SignatureMethod' => 'HmacSHA256',
308
+ 'SignatureVersion' => 2,
309
+ 'Timestamp' => Time.now.iso8601,
310
+ 'AWSAccessKeyId' => AmazeSNS.akey
311
+ }
312
+
313
+ reactor{
314
+ generate_request(params) do |response|
315
+ parsed_response = Crack::XML.parse(response.response)
316
+ resp = parsed_response['ConfirmSubscriptionResponse']['ConfirmSubscriptionResult']['SubscriptionArn']
317
+ id = parsed_response['ConfirmSubscriptionResponse']['ResponseMetadata']['RequestId']
318
+ arr = [resp,id]
319
+ return arr
320
+ EM.stop
321
+ end
322
+ }
323
+ arr
324
+ end
325
+
326
+
327
+
328
+
329
+ def make_hash(arr)
330
+ hash = arr.inject({}) do |h, v|
331
+ (v["key"] == "Policy")? value = JSON.parse(v["value"]) : value = v["value"]
332
+ key = v["key"].to_s
333
+ h[key] = value
334
+ h
335
+ end
336
+
337
+ hash
338
+ end
339
+
340
+
341
+
342
+ end
data/lib/amaze_sns.rb ADDED
@@ -0,0 +1,147 @@
1
+ autoload 'Logger', 'logger'
2
+
3
+
4
+ require File.dirname(__FILE__) + "/amaze/topic"
5
+ require File.dirname(__FILE__) + "/amaze/subscription"
6
+ require File.dirname(__FILE__) + "/amaze/helpers"
7
+ require File.dirname(__FILE__) + "/amaze/request"
8
+ require File.dirname(__FILE__) + "/amaze/exceptions"
9
+ require "eventmachine"
10
+ require 'crack/xml'
11
+
12
+ class AmazeSNS
13
+
14
+ class CredentialError < ::ArgumentError
15
+ def message
16
+ 'Please provide your Amazon keys to use the service'
17
+ end
18
+ end
19
+
20
+ class << self
21
+ attr_accessor :host, :logger, :topics, :skey, :akey, :subscriptions
22
+ #attr_writer :skey, :akey
23
+ end
24
+
25
+
26
+ self.host = 'sns.us-east-1.amazonaws.com'
27
+ self.logger = Logger.new($STDOUT)
28
+ self.skey = ''
29
+ self.akey=''
30
+ self.topics ||= {}
31
+ self.subscriptions ||= {}
32
+
33
+ def self.[](topic)
34
+ raise CredentialError unless (!(@skey.empty?) && !(@akey.empty?))
35
+ @topics[topic.to_s] = Topic.new(topic) unless @topics.has_key?(topic)
36
+ @topics[topic.to_s]
37
+ end
38
+
39
+
40
+ def self.method_missing(id, *args, &blk)
41
+ case(id.to_s)
42
+ when /^list_(.*)/
43
+ send(:process_query, $1, &blk)
44
+ when /^refresh_(.*)/
45
+ send(:process_query, $1, &blk)
46
+ else
47
+ #super
48
+ raise NoMethodError
49
+ end
50
+ end
51
+
52
+ def self.process_query(type, &blk)
53
+ # p "INSIDE PROCESS query"
54
+ type = type.capitalize
55
+ params = {
56
+ 'Action' => "List#{type}",
57
+ 'SignatureMethod' => 'HmacSHA256',
58
+ 'SignatureVersion' => 2,
59
+ 'Timestamp' => Time.now.iso8601,
60
+ 'AWSAccessKeyId' => @akey
61
+ }
62
+
63
+ req_options={}
64
+
65
+ if (blk)
66
+ prc = blk
67
+ else
68
+ prc = default_prc
69
+ end
70
+
71
+ req_options[:on_success] = prc
72
+ #req = Request.new(params, req_options).process
73
+
74
+ EM.run{
75
+ Request.new(params, req_options).process
76
+ }
77
+
78
+ end
79
+
80
+ def self.default_prc
81
+ prc = Proc.new do |resp|
82
+ parsed_response = Crack::XML.parse(resp.response)
83
+ #p "SUB RESPONSE: #{parsed_response.inspect}"
84
+ self.process_response(parsed_response)
85
+ EM.stop
86
+ end
87
+ end
88
+
89
+ def self.process_response(resp)
90
+ kind = (resp.has_key?("ListTopicsResponse"))? "Topics" : "Subscriptions"
91
+ cla = (resp.has_key?("ListTopicsResponse"))? "Topic" : "Subscription"
92
+ #p "KIND IS #{kind}"
93
+
94
+ result = resp["List#{kind}Response"]["List#{kind}Result"]["#{kind}"]
95
+ if result.nil?
96
+ p "NO DATA FOUND"
97
+ return nil
98
+ else
99
+ results = result["member"]
100
+ end
101
+
102
+ @collection = self.send(kind.downcase)
103
+
104
+ if !(results.nil?)
105
+ if (results.instance_of?(Array))
106
+ results.each do |t|
107
+ label = t["TopicArn"].split(':').last.to_s
108
+ unless @collection.has_key?(label)
109
+ case cla
110
+ when "Topic"
111
+ @collection[label] = Topic.new(label,t["TopicArn"])
112
+ when "Subscription"
113
+ @collection[label] = Array.new
114
+ @collection[label] << Subscription.new(t)
115
+ end
116
+
117
+ #@collection[label] = Kernel.const_get("#{cla}").new(t) # t is a hash
118
+ else
119
+ case cla
120
+ when "Topic"
121
+ @collection[label].arn = t["TopicArn"]
122
+ when "Subscription"
123
+ #@collection[label] = t["TopicArn"]
124
+ sub = Subscription.new(t)
125
+ @collection[label] << sub unless @collection[label].detect{|x| x.subarn == sub.subarn}
126
+ end
127
+ #@collection[label].arn = t["TopicArn"]
128
+ end
129
+ end
130
+ elsif (results.instance_of?(Hash))
131
+ # lone entry results in a hash so parse it that way ...
132
+ label = results["TopicArn"].split(':').last.to_s
133
+ case cla
134
+ when "Topic"
135
+ @collection[label] = Topic.new(label, results["TopicArn"])
136
+ when "Subscription"
137
+ @collection[label] = Subscription.new(results)
138
+ end
139
+
140
+ #@collection[label] = Kernel.const_get("#{cla}").new(results)
141
+ end
142
+ else
143
+ return nil
144
+ end # end outer if
145
+ end
146
+
147
+ end
@@ -0,0 +1,138 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe AmazeSNS do
4
+
5
+ before(:each) do
6
+ EventMachine::MockHttpRequest.reset_counts!
7
+ EventMachine::MockHttpRequest.reset_registry!
8
+ end
9
+
10
+ describe 'in its initial state' do
11
+
12
+ it "should return the preconfigured host endpoint" do
13
+ AmazeSNS.host.should == 'sns.us-east-1.amazonaws.com'
14
+ end
15
+
16
+ it "should return a blank Topics hash" do
17
+ AmazeSNS.topics.should == {}
18
+ end
19
+
20
+ end
21
+
22
+ describe 'without the keys' do
23
+ it 'should raise an ArgumentError if no keys are present' do
24
+ lambda{
25
+ AmazeSNS['test']
26
+ }.should raise_error(AmazeSNS::CredentialError)
27
+ end
28
+ end
29
+
30
+ describe 'with the keys configured' do
31
+
32
+ before do
33
+ AmazeSNS.akey = '123456'
34
+ AmazeSNS.skey = '123456'
35
+ @topic = AmazeSNS['Test']
36
+ end
37
+
38
+ it 'should return a new topic object' do
39
+ @topic.should be_kind_of(Topic)
40
+ @topic.topic.should == 'Test'
41
+ end
42
+
43
+ end
44
+
45
+
46
+ describe 'Request#process' do
47
+ module EventMachine
48
+ module HttpEncoding
49
+ def encode_query(path, query, uri_query)
50
+ encoded_query = if query.kind_of?(Hash)
51
+ query.sort{|a, b| a.to_s <=> b.to_s}.
52
+ map { |k, v| encode_param(k, v) }.
53
+ join('&')
54
+ else
55
+ query.to_s
56
+ end
57
+ if !uri_query.to_s.empty?
58
+ encoded_query = [encoded_query, uri_query].reject {|part| part.empty?}.join("&")
59
+ end
60
+ return path if encoded_query.to_s.empty?
61
+ "#{path}?#{encoded_query}"
62
+ end
63
+ end
64
+ end
65
+
66
+ before :each do
67
+ EM::HttpRequest = EM::MockHttpRequest
68
+ EM::HttpRequest.reset_registry!
69
+ EM::HttpRequest.reset_counts!
70
+ EM::HttpRequest.pass_through_requests = false
71
+
72
+ @params = {
73
+ 'Action' => 'ListTopics',
74
+ 'SignatureMethod' => 'HmacSHA256',
75
+ 'SignatureVersion' => 2,
76
+ 'Timestamp' => Time.now.iso8601,
77
+ 'AWSAccessKeyId' => AmazeSNS.akey
78
+ }
79
+
80
+ @query_string = canonical_querystring(@params)
81
+
82
+ string_to_sign = "GET
83
+ #{AmazeSNS.host}
84
+ /
85
+ #{@query_string}"
86
+
87
+ hmac = HMAC::SHA256.new(AmazeSNS.skey)
88
+ hmac.update( string_to_sign )
89
+ signature = Base64.encode64(hmac.digest).chomp
90
+
91
+ @params['Signature'] = signature
92
+ @querystring2 = @params.collect { |key, value| [url_encode(key), url_encode(value)].join("=") }.join('&')
93
+
94
+ end
95
+
96
+ it "should return a deferrable which succeeds in success case" do
97
+ #Time.stub(:now).and_return(123)
98
+
99
+ data = <<-RESPONSE.gsub(/^ +/, '')
100
+ HTTP/1.1 202 Accepted
101
+ Content-Type: text/html
102
+ Content-Length: 13
103
+ Connection: keep-alive
104
+ Server: thin 1.2.7 codename No Hup
105
+
106
+ 202 ACCEPTED
107
+ RESPONSE
108
+
109
+ url = "http://#{AmazeSNS.host}/?#{@querystring2}"
110
+
111
+ EM::HttpRequest.register(url, :get, data)
112
+
113
+ EM.run {
114
+ d = AmazeSNS.list_topics
115
+ d.callback{
116
+ @raw_resp = Crack::XML.parse(resp.response)
117
+ AmazeSNS.process_response(@raw_resp)
118
+ EM.stop
119
+ }
120
+ }
121
+
122
+
123
+ end
124
+
125
+
126
+ end
127
+
128
+
129
+
130
+
131
+ after do
132
+ AmazeSNS.topics={}
133
+ end
134
+
135
+
136
+ end # end outer describe
137
+
138
+
data/spec/spec.opts ADDED
@@ -0,0 +1 @@
1
+ --colour
@@ -0,0 +1,47 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ require 'spec'
3
+ require 'spec/autorun'
4
+ require 'request'
5
+ require 'amaze_sns'
6
+ require 'eventmachine'
7
+ require 'em-http'
8
+ require 'em-http/mock'
9
+
10
+ Spec::Runner.configure do |config|
11
+ # EventMachine.instance_eval do
12
+ # # Switching out EM's defer since it makes tests just a tad more unreliable
13
+ # alias :defer_original :defer
14
+ # def defer
15
+ # yield
16
+ # end
17
+ # end unless EM.respond_to?(:defer_original)
18
+ #
19
+ #
20
+ # def run_in_em_loop
21
+ # EM.run {
22
+ # yield
23
+ # }
24
+ # end
25
+ #
26
+ # class Request
27
+ # self.module_eval do
28
+ # def http_class
29
+ # EventMachine::MockHttpRequest
30
+ # end
31
+ # end
32
+ # end
33
+
34
+
35
+ def fake_response
36
+ <<-HEREDOC
37
+ <ListTopicsResponse xmlns=\"http://sns.amazonaws.com/doc/2010-03-31/\">\n<ListTopicsResult>\n<Topics>\n<member>\n<TopicArn>arn:aws:sns:us-east-1:365155214602:cars</TopicArn>\n</member>\n<member>\n<TopicArn>arn:aws:sns:us-east-1:365155214602:luckypooz</TopicArn>\n</member>\n<member>\n<TopicArn>arn:aws:sns:us-east-1:365155214602:29steps_products</TopicArn>\n</member>\n</Topics>\n</ListTopicsResult>\n <ResponseMetadata>\n<RequestId>d4a2ff9b-56dc-11df-b6e7-a7864eff589e</RequestId>\n</ResponseMetadata>\n</ListTopicsResponse>
38
+ HEREDOC
39
+ end
40
+
41
+
42
+
43
+
44
+ end #end configure
45
+
46
+
47
+
@@ -0,0 +1,23 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ describe Topic do
4
+ before do
5
+ @topic = Topic.new('my_test_topic', 'arn:123456')
6
+ end
7
+
8
+ describe 'in its initial state' do
9
+ it 'should be an instance of Topic class' do
10
+ @topic.should be_kind_of(Topic)
11
+ end
12
+
13
+ it 'should return the topic name and arn when requested' do
14
+ @topic.topic.should == 'my_test_topic'
15
+ @topic.arn.should == 'arn:123456'
16
+ end
17
+
18
+ end
19
+
20
+ describe 'when creating a topic' do
21
+ end
22
+
23
+ end
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: amaze_sns
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 1
7
+ - 0
8
+ - 1
9
+ version: 1.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Chee Yeo
13
+ - 29 Steps UK
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-07-08 00:00:00 +01:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: eventmachine
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ - 12
31
+ - 9
32
+ version: 0.12.9
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: em-http-request
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ segments:
43
+ - 0
44
+ - 2
45
+ - 8
46
+ version: 0.2.8
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: crack
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 0
58
+ - 1
59
+ - 6
60
+ version: 0.1.6
61
+ type: :runtime
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: ruby-hmac
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ segments:
71
+ - 0
72
+ - 4
73
+ - 0
74
+ version: 0.4.0
75
+ type: :runtime
76
+ version_requirements: *id004
77
+ - !ruby/object:Gem::Dependency
78
+ name: json
79
+ prerelease: false
80
+ requirement: &id005 !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ segments:
85
+ - 1
86
+ - 4
87
+ - 3
88
+ version: 1.4.3
89
+ type: :runtime
90
+ version_requirements: *id005
91
+ - !ruby/object:Gem::Dependency
92
+ name: rspec
93
+ prerelease: false
94
+ requirement: &id006 !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ segments:
99
+ - 1
100
+ - 2
101
+ - 9
102
+ version: 1.2.9
103
+ type: :development
104
+ version_requirements: *id006
105
+ description: Ruby gem to interface with the Amazon Simple Notification Service
106
+ email: info@29steps.co.uk
107
+ executables: []
108
+
109
+ extensions: []
110
+
111
+ extra_rdoc_files:
112
+ - README.md
113
+ files:
114
+ - lib/amaze/exceptions.rb
115
+ - lib/amaze/helpers.rb
116
+ - lib/amaze/request.rb
117
+ - lib/amaze/subscription.rb
118
+ - lib/amaze/topic.rb
119
+ - lib/amaze_sns.rb
120
+ - README.md
121
+ has_rdoc: true
122
+ homepage: http://29steps.co.uk
123
+ licenses: []
124
+
125
+ post_install_message:
126
+ rdoc_options:
127
+ - --charset=UTF-8
128
+ require_paths:
129
+ - lib
130
+ required_ruby_version: !ruby/object:Gem::Requirement
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ segments:
135
+ - 1
136
+ - 8
137
+ - 6
138
+ version: 1.8.6
139
+ required_rubygems_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ segments:
144
+ - 0
145
+ version: "0"
146
+ requirements: []
147
+
148
+ rubyforge_project:
149
+ rubygems_version: 1.3.6
150
+ signing_key:
151
+ specification_version: 3
152
+ summary: Ruby gem for Amazon Simple Notification Service SNS
153
+ test_files:
154
+ - spec/amaze_sns_spec.rb
155
+ - spec/spec_helper.rb
156
+ - spec/topic_spec.rb
157
+ - spec/spec.opts