amaze_sns 1.0.1

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/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