pusher 0.10.0 → 0.11.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/Gemfile.lock +1 -1
- data/README.md +109 -38
- data/lib/pusher.rb +9 -21
- data/lib/pusher/channel.rb +17 -29
- data/lib/pusher/client.rb +186 -30
- data/lib/pusher/query_encoder.rb +47 -0
- data/lib/pusher/request.rb +30 -85
- data/lib/pusher/resource.rb +36 -0
- data/pusher.gemspec +1 -1
- data/spec/channel_spec.rb +12 -143
- data/spec/client_spec.rb +208 -14
- data/spec/spec_helper.rb +8 -1
- data/spec/web_hook_spec.rb +7 -5
- metadata +112 -114
@@ -0,0 +1,47 @@
|
|
1
|
+
module Pusher
|
2
|
+
# Query string encoding extracted with thanks from em-http-request
|
3
|
+
module QueryEncoder
|
4
|
+
def encode_query(uri, query)
|
5
|
+
encoded_query = if query.kind_of?(Hash)
|
6
|
+
query.map { |k, v| encode_param(k, v) }.join('&')
|
7
|
+
else
|
8
|
+
query.to_s
|
9
|
+
end
|
10
|
+
|
11
|
+
if uri && !uri.query.to_s.empty?
|
12
|
+
encoded_query = [encoded_query, uri.query].reject {|part| part.empty?}.join("&")
|
13
|
+
end
|
14
|
+
encoded_query.to_s.empty? ? uri.path : "#{uri.path}?#{encoded_query}"
|
15
|
+
end
|
16
|
+
|
17
|
+
# URL encodes query parameters:
|
18
|
+
# single k=v, or a URL encoded array, if v is an array of values
|
19
|
+
def encode_param(k, v)
|
20
|
+
if v.is_a?(Array)
|
21
|
+
v.map { |e| escape(k) + "[]=" + escape(e) }.join("&")
|
22
|
+
else
|
23
|
+
escape(k) + "=" + escape(v)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def escape(s)
|
28
|
+
if defined?(EscapeUtils)
|
29
|
+
EscapeUtils.escape_url(s.to_s)
|
30
|
+
else
|
31
|
+
s.to_s.gsub(/([^a-zA-Z0-9_.-]+)/n) {
|
32
|
+
'%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
|
33
|
+
}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
if ''.respond_to?(:bytesize)
|
38
|
+
def bytesize(string)
|
39
|
+
string.bytesize
|
40
|
+
end
|
41
|
+
else
|
42
|
+
def bytesize(string)
|
43
|
+
string.size
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/pusher/request.rb
CHANGED
@@ -4,59 +4,12 @@ require 'multi_json'
|
|
4
4
|
|
5
5
|
module Pusher
|
6
6
|
class Request
|
7
|
-
# Query string encoding extracted with thanks from em-http-request
|
8
|
-
module QueryEncoder
|
9
|
-
def encode_query(uri, query)
|
10
|
-
encoded_query = if query.kind_of?(Hash)
|
11
|
-
query.map { |k, v| encode_param(k, v) }.join('&')
|
12
|
-
else
|
13
|
-
query.to_s
|
14
|
-
end
|
15
|
-
|
16
|
-
if uri && !uri.query.to_s.empty?
|
17
|
-
encoded_query = [encoded_query, uri.query].reject {|part| part.empty?}.join("&")
|
18
|
-
end
|
19
|
-
encoded_query.to_s.empty? ? uri.path : "#{uri.path}?#{encoded_query}"
|
20
|
-
end
|
21
|
-
|
22
|
-
# URL encodes query parameters:
|
23
|
-
# single k=v, or a URL encoded array, if v is an array of values
|
24
|
-
def encode_param(k, v)
|
25
|
-
if v.is_a?(Array)
|
26
|
-
v.map { |e| escape(k) + "[]=" + escape(e) }.join("&")
|
27
|
-
else
|
28
|
-
escape(k) + "=" + escape(v)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def escape(s)
|
33
|
-
if defined?(EscapeUtils)
|
34
|
-
EscapeUtils.escape_url(s.to_s)
|
35
|
-
else
|
36
|
-
s.to_s.gsub(/([^a-zA-Z0-9_.-]+)/n) {
|
37
|
-
'%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
|
38
|
-
}
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
if ''.respond_to?(:bytesize)
|
43
|
-
def bytesize(string)
|
44
|
-
string.bytesize
|
45
|
-
end
|
46
|
-
else
|
47
|
-
def bytesize(string)
|
48
|
-
string.size
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
7
|
include QueryEncoder
|
54
8
|
|
55
9
|
attr_reader :body, :params
|
56
10
|
|
57
|
-
def initialize(verb, uri, params, body = nil
|
58
|
-
@verb = verb
|
59
|
-
@uri = uri
|
11
|
+
def initialize(client, verb, uri, params, body = nil)
|
12
|
+
@client, @verb, @uri = client, verb, uri
|
60
13
|
|
61
14
|
if body
|
62
15
|
@body = body
|
@@ -64,36 +17,25 @@ module Pusher
|
|
64
17
|
end
|
65
18
|
|
66
19
|
request = Signature::Request.new(verb.to_s.upcase, uri.path, params)
|
67
|
-
request.sign(
|
20
|
+
request.sign(client.authentication_token)
|
68
21
|
@params = request.signed_params
|
69
22
|
end
|
70
23
|
|
71
24
|
def send_sync
|
72
|
-
|
73
|
-
require 'net/https' unless defined?(Net::HTTPS)
|
74
|
-
else
|
75
|
-
require 'net/http' unless defined?(Net::HTTP)
|
76
|
-
end
|
77
|
-
|
78
|
-
@http_sync ||= begin
|
79
|
-
http = Net::HTTP.new(@uri.host, @uri.port)
|
80
|
-
http.use_ssl = true if ssl?
|
81
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if ssl?
|
82
|
-
http
|
83
|
-
end
|
25
|
+
http = @client.net_http_client
|
84
26
|
|
85
27
|
begin
|
86
28
|
case @verb
|
87
29
|
when :post
|
88
|
-
response =
|
30
|
+
response = http.post(encode_query(@uri, @params), @body, {
|
89
31
|
'Content-Type'=> 'application/json'
|
90
32
|
})
|
91
33
|
when :get
|
92
|
-
response =
|
34
|
+
response = http.get(encode_query(@uri, @params), {
|
93
35
|
'Content-Type'=> 'application/json'
|
94
36
|
})
|
95
37
|
else
|
96
|
-
raise "
|
38
|
+
raise "Unsuported verb"
|
97
39
|
end
|
98
40
|
rescue Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED,
|
99
41
|
Errno::ETIMEDOUT, Errno::EHOSTUNREACH,
|
@@ -111,31 +53,36 @@ module Pusher
|
|
111
53
|
end
|
112
54
|
|
113
55
|
def send_async
|
114
|
-
|
115
|
-
|
56
|
+
df = EM::DefaultDeferrable.new
|
57
|
+
|
58
|
+
http_client = @client.em_http_client(@uri)
|
59
|
+
http = case @verb
|
60
|
+
when :post
|
61
|
+
http_client.post({
|
62
|
+
:query => @params, :timeout => 5, :body => @body,
|
63
|
+
:head => {'Content-Type'=> 'application/json'}
|
64
|
+
})
|
65
|
+
when :get
|
66
|
+
http_client.get({
|
67
|
+
:query => @params, :timeout => 5,
|
68
|
+
:head => {'Content-Type'=> 'application/json'}
|
69
|
+
})
|
70
|
+
else
|
71
|
+
raise "Unsuported verb"
|
116
72
|
end
|
117
|
-
require 'em-http' unless defined?(EventMachine::HttpRequest)
|
118
|
-
|
119
|
-
deferrable = EM::DefaultDeferrable.new
|
120
|
-
|
121
|
-
http = EventMachine::HttpRequest.new(@uri).post({
|
122
|
-
:query => @params, :timeout => 5, :body => @body,
|
123
|
-
:head => {'Content-Type'=> 'application/json'}
|
124
|
-
})
|
125
73
|
http.callback {
|
126
74
|
begin
|
127
|
-
handle_response(http.response_header.status, http.response.chomp)
|
128
|
-
deferrable.succeed
|
75
|
+
df.succeed(handle_response(http.response_header.status, http.response.chomp))
|
129
76
|
rescue => e
|
130
|
-
|
77
|
+
df.fail(e)
|
131
78
|
end
|
132
79
|
}
|
133
80
|
http.errback {
|
134
81
|
Pusher.logger.debug("Network error connecting to pusher: #{http.inspect}")
|
135
|
-
|
82
|
+
df.fail(Error.new("Network error connecting to pusher"))
|
136
83
|
}
|
137
84
|
|
138
|
-
|
85
|
+
df
|
139
86
|
end
|
140
87
|
|
141
88
|
private
|
@@ -151,16 +98,14 @@ module Pusher
|
|
151
98
|
when 401
|
152
99
|
raise AuthenticationError, body
|
153
100
|
when 404
|
154
|
-
raise Error, "
|
101
|
+
raise Error, "404 Not found (#{@uri.path})"
|
102
|
+
when 407
|
103
|
+
raise Error, "Proxy Authentication Required"
|
155
104
|
else
|
156
105
|
raise Error, "Unknown error (status code #{status_code}): #{body}"
|
157
106
|
end
|
158
107
|
end
|
159
108
|
|
160
|
-
def ssl?
|
161
|
-
@uri.scheme == 'https'
|
162
|
-
end
|
163
|
-
|
164
109
|
def symbolize_first_level(hash)
|
165
110
|
hash.inject({}) do |result, (key, value)|
|
166
111
|
result[key.to_sym] = value
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Pusher
|
2
|
+
class Resource
|
3
|
+
def initialize(client, path)
|
4
|
+
@client = client
|
5
|
+
@path = path
|
6
|
+
end
|
7
|
+
|
8
|
+
def get(params)
|
9
|
+
create_request(:get, params).send_sync
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_async(params)
|
13
|
+
create_request(:get, params).send_async
|
14
|
+
end
|
15
|
+
|
16
|
+
def post(params)
|
17
|
+
body = MultiJson.encode(params)
|
18
|
+
create_request(:post, {}, body).send_sync
|
19
|
+
end
|
20
|
+
|
21
|
+
def post_async(params)
|
22
|
+
body = MultiJson.encode(params)
|
23
|
+
create_request(:post, {}, body).send_async
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def create_request(verb, params, body = nil)
|
29
|
+
Request.new(@client, verb, url, params, body)
|
30
|
+
end
|
31
|
+
|
32
|
+
def url
|
33
|
+
@_url ||= @client.url(@path)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/pusher.gemspec
CHANGED
data/spec/channel_spec.rb
CHANGED
@@ -9,108 +9,26 @@ describe Pusher::Channel do
|
|
9
9
|
:host => 'api.pusherapp.com',
|
10
10
|
:port => 80,
|
11
11
|
})
|
12
|
-
@client.encrypted = false
|
13
12
|
@channel = @client['test_channel']
|
14
|
-
|
15
|
-
WebMock.reset!
|
16
|
-
WebMock.disable_net_connect!
|
17
13
|
end
|
18
14
|
|
19
|
-
let(:pusher_url_regexp) { %r{/apps/20/
|
15
|
+
let(:pusher_url_regexp) { %r{/apps/20/events} }
|
20
16
|
|
21
17
|
def stub_post(status, body = nil)
|
22
18
|
options = {:status => status}
|
23
19
|
options.merge!({:body => body}) if body
|
24
20
|
|
25
|
-
|
21
|
+
stub_request(:post, pusher_url_regexp).to_return(options)
|
26
22
|
end
|
27
23
|
|
28
24
|
def stub_post_to_raise(e)
|
29
|
-
|
25
|
+
stub_request(:post, pusher_url_regexp).to_raise(e)
|
30
26
|
end
|
31
27
|
|
32
|
-
describe 'trigger!' do
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
it 'should configure HTTP library to talk to pusher API' do
|
38
|
-
@channel.trigger!('new_event', 'Some data')
|
39
|
-
WebMock.should have_requested(:post, %r{http://api.pusherapp.com})
|
40
|
-
end
|
41
|
-
|
42
|
-
it "should POST to https api if ssl enabled" do
|
43
|
-
@client.encrypted = true
|
44
|
-
encrypted_channel = Pusher::Channel.new(@client.url, 'test_channel', @client)
|
45
|
-
encrypted_channel.trigger('new_event', 'Some data')
|
46
|
-
WebMock.should have_requested(:post, %r{https://api.pusherapp.com})
|
47
|
-
end
|
48
|
-
|
49
|
-
it 'should POST hashes by encoding as JSON in the request body' do
|
50
|
-
@channel.trigger!('new_event', {
|
51
|
-
:name => 'Pusher',
|
52
|
-
:last_name => 'App'
|
53
|
-
})
|
54
|
-
WebMock.should have_requested(:post, pusher_url_regexp).with { |req|
|
55
|
-
query_hash = req.uri.query_values
|
56
|
-
query_hash["name"].should == 'new_event'
|
57
|
-
query_hash["auth_key"].should == @client.key
|
58
|
-
query_hash["auth_timestamp"].should_not be_nil
|
59
|
-
|
60
|
-
parsed = MultiJson.decode(req.body)
|
61
|
-
parsed.should == {
|
62
|
-
"name" => 'Pusher',
|
63
|
-
"last_name" => 'App'
|
64
|
-
}
|
65
|
-
|
66
|
-
req.headers['Content-Type'].should == 'application/json'
|
67
|
-
}
|
68
|
-
end
|
69
|
-
|
70
|
-
it "should POST string data unmodified in request body" do
|
71
|
-
string = "foo\nbar\""
|
72
|
-
@channel.trigger!('new_event', string)
|
73
|
-
WebMock.should have_requested(:post, pusher_url_regexp).with { |req| req.body.should == "foo\nbar\"" }
|
74
|
-
end
|
75
|
-
|
76
|
-
def trigger
|
77
|
-
lambda { @channel.trigger!('new_event', 'Some data') }
|
78
|
-
end
|
79
|
-
|
80
|
-
it "should catch all Net::HTTP exceptions and raise a Pusher::HTTPError, exposing the original error if required" do
|
81
|
-
stub_post_to_raise Timeout::Error
|
82
|
-
error_raised = nil
|
83
|
-
|
84
|
-
begin
|
85
|
-
trigger.call
|
86
|
-
rescue => e
|
87
|
-
error_raised = e
|
88
|
-
end
|
89
|
-
|
90
|
-
error_raised.class.should == Pusher::HTTPError
|
91
|
-
error_raised.message.should == 'Exception from WebMock (Timeout::Error)'
|
92
|
-
error_raised.original_error.class.should == Timeout::Error
|
93
|
-
end
|
94
|
-
|
95
|
-
|
96
|
-
it "should raise Pusher::Error if pusher returns 400" do
|
97
|
-
stub_post 400
|
98
|
-
trigger.should raise_error(Pusher::Error)
|
99
|
-
end
|
100
|
-
|
101
|
-
it "should raise AuthenticationError if pusher returns 401" do
|
102
|
-
stub_post 401
|
103
|
-
trigger.should raise_error(Pusher::AuthenticationError)
|
104
|
-
end
|
105
|
-
|
106
|
-
it "should raise Pusher::Error if pusher returns 404" do
|
107
|
-
stub_post 404
|
108
|
-
trigger.should raise_error(Pusher::Error, 'Resource not found: app_id is probably invalid')
|
109
|
-
end
|
110
|
-
|
111
|
-
it "should raise Pusher::Error if pusher returns 500" do
|
112
|
-
stub_post 500, "some error"
|
113
|
-
trigger.should raise_error(Pusher::Error, 'Unknown error (status code 500): some error')
|
28
|
+
describe '#trigger!' do
|
29
|
+
it "should use @client.trigger internally" do
|
30
|
+
@client.should_receive(:trigger)
|
31
|
+
@channel.trigger('new_event', 'Some data')
|
114
32
|
end
|
115
33
|
end
|
116
34
|
|
@@ -134,70 +52,21 @@ describe Pusher::Channel do
|
|
134
52
|
end
|
135
53
|
|
136
54
|
describe "#trigger_async" do
|
137
|
-
it "should
|
138
|
-
|
139
|
-
|
140
|
-
@channel.trigger_async('new_event', 'Some data').callback {
|
141
|
-
WebMock.should have_requested(:post, %r{http://api.pusherapp.com})
|
142
|
-
EM.stop
|
143
|
-
}
|
144
|
-
}
|
145
|
-
end
|
146
|
-
|
147
|
-
it "should POST to https api if ssl enabled" do
|
148
|
-
@client.encrypted = true
|
149
|
-
EM.run {
|
150
|
-
stub_post 202
|
151
|
-
channel = Pusher::Channel.new(@client.url, 'test_channel', @client)
|
152
|
-
channel.trigger_async('new_event', 'Some data').callback {
|
153
|
-
WebMock.should have_requested(:post, %r{https://api.pusherapp.com})
|
154
|
-
EM.stop
|
155
|
-
}
|
156
|
-
}
|
157
|
-
end
|
158
|
-
|
159
|
-
it "should return a deferrable which succeeds in success case" do
|
160
|
-
stub_post 202
|
161
|
-
|
162
|
-
EM.run {
|
163
|
-
d = @channel.trigger_async('new_event', 'Some data')
|
164
|
-
d.callback {
|
165
|
-
WebMock.should have_requested(:post, pusher_url_regexp)
|
166
|
-
EM.stop
|
167
|
-
}
|
168
|
-
d.errback {
|
169
|
-
fail
|
170
|
-
EM.stop
|
171
|
-
}
|
172
|
-
}
|
173
|
-
end
|
174
|
-
|
175
|
-
it "should return a deferrable which fails (with exception) in fail case" do
|
176
|
-
stub_post 401
|
177
|
-
|
178
|
-
EM.run {
|
179
|
-
d = @channel.trigger_async('new_event', 'Some data')
|
180
|
-
d.callback {
|
181
|
-
fail
|
182
|
-
}
|
183
|
-
d.errback { |error|
|
184
|
-
WebMock.should have_requested(:post, pusher_url_regexp)
|
185
|
-
error.should be_kind_of(Pusher::AuthenticationError)
|
186
|
-
EM.stop
|
187
|
-
}
|
188
|
-
}
|
55
|
+
it "should use @client.trigger_async internally" do
|
56
|
+
@client.should_receive(:trigger_async)
|
57
|
+
@channel.trigger_async('new_event', 'Some data')
|
189
58
|
end
|
190
59
|
end
|
191
60
|
|
192
61
|
describe '#info' do
|
193
62
|
it "should call the Client#channel_info" do
|
194
|
-
@client.should_receive(:
|
63
|
+
@client.should_receive(:get).with("/channels/mychannel", anything)
|
195
64
|
@channel = @client['mychannel']
|
196
65
|
@channel.info
|
197
66
|
end
|
198
67
|
|
199
68
|
it "should assemble the requested attribes into the info option" do
|
200
|
-
@client.should_receive(:
|
69
|
+
@client.should_receive(:get).with(anything, {
|
201
70
|
:info => "user_count,connection_count"
|
202
71
|
})
|
203
72
|
@channel = @client['presence-foo']
|