nervion 0.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/.gitignore +4 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +46 -0
- data/LICENSE +22 -0
- data/README.md +197 -0
- data/Rakefile +22 -0
- data/features/step_definitions/streaming_steps.rb +55 -0
- data/features/streaming.feature +16 -0
- data/features/support/env.rb +9 -0
- data/features/support/streaming_api_double.rb +45 -0
- data/fixtures/responses.rb +53 -0
- data/fixtures/stream.txt +100 -0
- data/lib/nervion.rb +4 -0
- data/lib/nervion/callback_table.rb +30 -0
- data/lib/nervion/client.rb +23 -0
- data/lib/nervion/configuration.rb +50 -0
- data/lib/nervion/facade.rb +54 -0
- data/lib/nervion/http_parser.rb +62 -0
- data/lib/nervion/oauth_header.rb +88 -0
- data/lib/nervion/oauth_signature.rb +54 -0
- data/lib/nervion/percent_encoder.rb +11 -0
- data/lib/nervion/reconnection_scheduler.rb +96 -0
- data/lib/nervion/request.rb +99 -0
- data/lib/nervion/stream.rb +64 -0
- data/lib/nervion/stream_handler.rb +42 -0
- data/lib/nervion/version.rb +3 -0
- data/nervion.gemspec +24 -0
- data/spec/nervion/callback_table_spec.rb +18 -0
- data/spec/nervion/client_spec.rb +51 -0
- data/spec/nervion/configuration_spec.rb +58 -0
- data/spec/nervion/facade_spec.rb +90 -0
- data/spec/nervion/http_parser_spec.rb +26 -0
- data/spec/nervion/oauth_header_spec.rb +115 -0
- data/spec/nervion/oauth_signature_spec.rb +66 -0
- data/spec/nervion/percent_encoder_spec.rb +20 -0
- data/spec/nervion/reconnection_scheduler_spec.rb +84 -0
- data/spec/nervion/request_spec.rb +90 -0
- data/spec/nervion/stream_handler_spec.rb +67 -0
- data/spec/nervion/stream_spec.rb +97 -0
- data/spec/spec_helper.rb +4 -0
- metadata +200 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.3@nervion --create
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
nervion (0.0.1)
|
5
|
+
eventmachine (~> 1.0.0.beta.4)
|
6
|
+
http_parser.rb (~> 0.5.3)
|
7
|
+
yajl-ruby (~> 1.1.0)
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
builder (3.0.0)
|
13
|
+
cucumber (1.2.1)
|
14
|
+
builder (>= 2.1.2)
|
15
|
+
diff-lcs (>= 1.1.3)
|
16
|
+
gherkin (~> 2.11.0)
|
17
|
+
json (>= 1.4.6)
|
18
|
+
diff-lcs (1.1.3)
|
19
|
+
eventmachine (1.0.0.beta.4)
|
20
|
+
gherkin (2.11.0)
|
21
|
+
json (>= 1.4.6)
|
22
|
+
http_parser.rb (0.5.3)
|
23
|
+
json (1.7.3)
|
24
|
+
multi_json (1.3.6)
|
25
|
+
rspec (2.9.0)
|
26
|
+
rspec-core (~> 2.9.0)
|
27
|
+
rspec-expectations (~> 2.9.0)
|
28
|
+
rspec-mocks (~> 2.9.0)
|
29
|
+
rspec-core (2.9.0)
|
30
|
+
rspec-expectations (2.9.1)
|
31
|
+
diff-lcs (~> 1.1.3)
|
32
|
+
rspec-mocks (2.9.0)
|
33
|
+
simplecov (0.6.4)
|
34
|
+
multi_json (~> 1.0)
|
35
|
+
simplecov-html (~> 0.5.3)
|
36
|
+
simplecov-html (0.5.3)
|
37
|
+
yajl-ruby (1.1.0)
|
38
|
+
|
39
|
+
PLATFORMS
|
40
|
+
ruby
|
41
|
+
|
42
|
+
DEPENDENCIES
|
43
|
+
cucumber
|
44
|
+
nervion!
|
45
|
+
rspec
|
46
|
+
simplecov
|
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Javier Acero
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
# Nervion
|
2
|
+
|
3
|
+
**A minimalistic Twitter Stream API Ruby client**.
|
4
|
+
|
5
|
+
|
6
|
+
## Motivation
|
7
|
+
|
8
|
+
In our current project we had the need to consume the stream provided by twitter.
|
9
|
+
Although there are a few gems available we had to suffer the pain of poor
|
10
|
+
documentation and error swallowing, which made us lose a lot of time.
|
11
|
+
|
12
|
+
At that point I decided to build one on my own, and that's why you are reading
|
13
|
+
this.
|
14
|
+
|
15
|
+
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
**WARNING: This project hasn't been released as a Gem yet**. This is here for
|
20
|
+
future reference.
|
21
|
+
|
22
|
+
Add this line to your application's Gemfile:
|
23
|
+
|
24
|
+
gem 'nervion'
|
25
|
+
|
26
|
+
And then execute:
|
27
|
+
|
28
|
+
$ bundle
|
29
|
+
|
30
|
+
Or install it yourself as:
|
31
|
+
|
32
|
+
$ gem install nervion
|
33
|
+
|
34
|
+
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
Nervion mimics the endpoints provided by the
|
39
|
+
[Twitter Stream API](https://dev.twitter.com/docs/streaming-apis).
|
40
|
+
Currently it supports the
|
41
|
+
[Public Streams](https://dev.twitter.com/docs/streaming-apis/streams/public).
|
42
|
+
In the future we will add support for the
|
43
|
+
[User Streams](https://dev.twitter.com/docs/streaming-apis/streams/user)
|
44
|
+
and the
|
45
|
+
[Site Streams](Use://dev.twitter.com/docs/streaming-apis/streams/site).
|
46
|
+
|
47
|
+
Specifically the two calls that are that are available to the broad audience:
|
48
|
+
|
49
|
+
- [`follow`](https://dev.twitter.com/docs/api/1/post/statuses/filter)
|
50
|
+
- [`sample`](https://dev.twitter.com/docs/api/1/get/statuses/sample)
|
51
|
+
|
52
|
+
[`firehose`](https://dev.twitter.com/docs/api/1/get/statuses/firehose)
|
53
|
+
is not supported yet since requires a special access level.
|
54
|
+
|
55
|
+
Checkout the docs of both endpoints to know what tweets you can query the
|
56
|
+
Streaming API for.
|
57
|
+
|
58
|
+
You can specify any of the parameters supported by the endpoints by passing them
|
59
|
+
as named parameters to the provided methods:
|
60
|
+
|
61
|
+
```ruby
|
62
|
+
require 'nervion'
|
63
|
+
|
64
|
+
Nervion.filter(delimited: 1953, track: 'ruby', stall_warnings: true) do |parsed_status|
|
65
|
+
#do something with the parsed status
|
66
|
+
end
|
67
|
+
```
|
68
|
+
|
69
|
+
If the API adds support for more parameters in the future they will be supported
|
70
|
+
straight away since Nervion does no work on them: they are just submitted to
|
71
|
+
Twitter.
|
72
|
+
|
73
|
+
|
74
|
+
|
75
|
+
###Authentication
|
76
|
+
|
77
|
+
Since Twitter plans to remove support for basic auth eventually, **Nervion only
|
78
|
+
supports OAuth authentication**.
|
79
|
+
|
80
|
+
You can provide the tokens and secrets in a configuration flavour:
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
Nervion.configure do |config|
|
84
|
+
config.consumer_key = the_consumer_key
|
85
|
+
config.consumer_secret = the_consumer_secret
|
86
|
+
config.access_token = the_access_token
|
87
|
+
config.access_token_secret = the_access_token_secret
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
|
92
|
+
###Parsing JSON
|
93
|
+
|
94
|
+
**Nervion will parse the JSON returned by twitter for you**. It uses
|
95
|
+
[Yajl](https://github.com/brianmario/yajl-ruby) as JSON parser for its out of
|
96
|
+
the box support for JSON streams.
|
97
|
+
|
98
|
+
**The hash keys are symbolized in the process of parsing**. You will always have
|
99
|
+
to use symbols to fetch data in the callbacks.
|
100
|
+
|
101
|
+
|
102
|
+
|
103
|
+
###Callbacks
|
104
|
+
|
105
|
+
Nowdays Nervion only has one callback that acts upon the received statuses. It
|
106
|
+
**will support callbacks on specific types of tweets and errors** in future
|
107
|
+
versions.
|
108
|
+
|
109
|
+
The callbacks will receive only one parameter: the hash with the symbolized keys
|
110
|
+
resultant of the JSON parsing. You get to choose what to do with the hash:
|
111
|
+
[mash](https://github.com/intridea/hashie) it before working with it or even
|
112
|
+
wrap it in some object that specializes on querying the information that is
|
113
|
+
relevant to you.
|
114
|
+
|
115
|
+
To know what keys to expect you should browse the
|
116
|
+
[*Platform Objects Documentation*](https://dev.twitter.com/docs/platform-objects/tweets).
|
117
|
+
|
118
|
+
|
119
|
+
####Status Callback
|
120
|
+
|
121
|
+
You can setup a callback that **acts on all the received statuses** by simply
|
122
|
+
passing in a block to the API call you are making:
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
Nervion.sample { |status| puts status[:text] if status.has_key? :text }
|
126
|
+
```
|
127
|
+
|
128
|
+
Be aware that **the callback will be called with any type of timeline update**
|
129
|
+
(or even with warnings if the `stall_warnings` parameter is set to `true`. Keep
|
130
|
+
this in mind when querying the hash.
|
131
|
+
|
132
|
+
|
133
|
+
#### HTTP Error Callback
|
134
|
+
|
135
|
+
This callback will be executed when the Streaming API sends a response with a
|
136
|
+
status code above 200. After the callback has been executed a retry will be
|
137
|
+
scheduled adhering to the
|
138
|
+
[connection Guidelines](https://dev.twitter.com/docs/streaming-api/concepts#connecting)
|
139
|
+
provided by twitter.
|
140
|
+
|
141
|
+
You can setup the callback like this:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
Nervion.on_http_error do |status, body|
|
145
|
+
puts "Response status was: #{status}"
|
146
|
+
puts "Response body was: #{body}"
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
If no callback is set, Nervion's default behaviour will be to output the an
|
151
|
+
error message to `STDERR` that contains both the status and the body of Twitter
|
152
|
+
Streaming API's response.
|
153
|
+
|
154
|
+
|
155
|
+
#### Network Error callback
|
156
|
+
|
157
|
+
This callback will be executed when the connection with the Twitter Stream API
|
158
|
+
is unexpectedly closed.
|
159
|
+
|
160
|
+
```ruby
|
161
|
+
Nervion.on_network_error do
|
162
|
+
puts 'There was a connection error but Nervion will automatically reconnect'
|
163
|
+
end
|
164
|
+
```
|
165
|
+
|
166
|
+
**Nervion will do nothing by default when network errors occurr** because it is
|
167
|
+
unlikely that they are provoked by the client itself.
|
168
|
+
|
169
|
+
|
170
|
+
## EventMachine Integration
|
171
|
+
|
172
|
+
Nervion runs on the top of EventMachine.
|
173
|
+
|
174
|
+
In the near future this `README` will provide a guideline to take advantage of
|
175
|
+
the benefits that EventMachine can provide when used correctly.
|
176
|
+
|
177
|
+
## Roadmap
|
178
|
+
|
179
|
+
There are some features that are needed and that will be developed before the first
|
180
|
+
release of the gem:
|
181
|
+
|
182
|
+
- <del>Provide an HTTP error callback</del> *done!*
|
183
|
+
- <del>Provide a network error callback</del> *done!*
|
184
|
+
- <del>Adhere to the
|
185
|
+
[Twitter Connection guidelines](https://dev.twitter.com/docs/streaming-api/concepts#connecting)</del>
|
186
|
+
*done!*
|
187
|
+
- Take advantage of EventMachine deferrables on callbacks
|
188
|
+
- Rewrite and improve the DSL provided to setup Nervion
|
189
|
+
|
190
|
+
Once those basic features are provided there are a few more that will be very
|
191
|
+
interesting to have:
|
192
|
+
|
193
|
+
- Use a gzip compressed stream
|
194
|
+
- Add callbacks to act on specific types of tweets: i.e. `on_retweet`,
|
195
|
+
`on_deleted_status`
|
196
|
+
|
197
|
+
If people start using the client more features will be added.
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'cucumber/rake/task'
|
6
|
+
|
7
|
+
task :default => [:test]
|
8
|
+
task :test => [:rspec, :cucumber]
|
9
|
+
|
10
|
+
RSpec::Core::RakeTask.new(:rspec) do |t|
|
11
|
+
t.rspec_opts = %w{ --color --format=progress --require spec/spec_helper.rb }
|
12
|
+
end
|
13
|
+
|
14
|
+
Cucumber::Rake::Task.new(:cucumber) do |t|
|
15
|
+
t.cucumber_opts = '--format progress'
|
16
|
+
end
|
17
|
+
|
18
|
+
task :cov do
|
19
|
+
ENV['COVERAGE'] = 'true'
|
20
|
+
Rake::Task[:rspec].execute
|
21
|
+
Rake::Task[:cucumber].execute
|
22
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
TEST_HOST = '0.0.0.0'
|
2
|
+
TEST_PORT = '9000'
|
3
|
+
|
4
|
+
def point_client_to_fake_server
|
5
|
+
Nervion.send :remove_const, 'STREAM_API_HOST'
|
6
|
+
Nervion.send :remove_const, 'STREAM_API_PORT'
|
7
|
+
Nervion.const_set 'STREAM_API_HOST', TEST_HOST
|
8
|
+
Nervion.const_set 'STREAM_API_PORT', TEST_PORT
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_client_with(server_version)
|
12
|
+
EM.run do
|
13
|
+
EM.start_server(TEST_HOST, TEST_PORT, server_version)
|
14
|
+
EM.add_timer(0) { Nervion.sample { |status| @statuses << status } }
|
15
|
+
EM.add_timer(0.1) { EM.stop }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
Given /^Nervion is connected to Twitter Streaming API$/ do
|
20
|
+
point_client_to_fake_server
|
21
|
+
end
|
22
|
+
|
23
|
+
When /^a status update is sent by Twitter$/ do
|
24
|
+
@statuses = []
|
25
|
+
test_client_with WorkingStreamingApiDouble
|
26
|
+
end
|
27
|
+
|
28
|
+
When /^an HTTP error occurs$/ do
|
29
|
+
Nervion.on_http_error do |status, body|
|
30
|
+
@status, @body = status, body
|
31
|
+
Nervion.stop
|
32
|
+
end
|
33
|
+
test_client_with HttpErrorStreamingApiDouble
|
34
|
+
end
|
35
|
+
|
36
|
+
When /^a network error occurs$/ do
|
37
|
+
Nervion.on_network_error do
|
38
|
+
@network_error_detected = true
|
39
|
+
Nervion.stop
|
40
|
+
end
|
41
|
+
test_client_with NetworkErrorStreamingApiDouble
|
42
|
+
end
|
43
|
+
|
44
|
+
Then /^Nervion calls the status callback with it$/ do
|
45
|
+
@statuses.count.should eq 100
|
46
|
+
end
|
47
|
+
|
48
|
+
Then /^Nervion calls the HTTP error callback$/ do
|
49
|
+
@status.should eq 401
|
50
|
+
@body.should match /Unauthorized/
|
51
|
+
end
|
52
|
+
|
53
|
+
Then /^Nervion calls the network error callback$/ do
|
54
|
+
@network_error_detected.should be_true
|
55
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
Feature: Callbacks
|
2
|
+
|
3
|
+
Background:
|
4
|
+
Given Nervion is connected to Twitter Streaming API
|
5
|
+
|
6
|
+
Scenario: Calling the status callback
|
7
|
+
When a status update is sent by Twitter
|
8
|
+
Then Nervion calls the status callback with it
|
9
|
+
|
10
|
+
Scenario: Calling the http error callback
|
11
|
+
When an HTTP error occurs
|
12
|
+
Then Nervion calls the HTTP error callback
|
13
|
+
|
14
|
+
Scenario: Calling the network error callback
|
15
|
+
When a network error occurs
|
16
|
+
Then Nervion calls the network error callback
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'fixtures/responses'
|
2
|
+
|
3
|
+
STREAM_FILE_PATH = 'fixtures/stream.txt'
|
4
|
+
|
5
|
+
class WorkingStreamingApiDouble < EM::Connection
|
6
|
+
def post_init
|
7
|
+
start_tls
|
8
|
+
end
|
9
|
+
|
10
|
+
def receive_data(data)
|
11
|
+
send_response_ok
|
12
|
+
stream_sample
|
13
|
+
end
|
14
|
+
|
15
|
+
def send_response_ok
|
16
|
+
send_data RESPONSE_200_HEADERS
|
17
|
+
end
|
18
|
+
|
19
|
+
def stream_sample
|
20
|
+
EM::FileStreamer.new(self, STREAM_FILE_PATH, http_chunks: true).callback do
|
21
|
+
close_connection_after_writing
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class HttpErrorStreamingApiDouble < EM::Connection
|
27
|
+
def post_init
|
28
|
+
start_tls
|
29
|
+
end
|
30
|
+
|
31
|
+
def receive_data(data)
|
32
|
+
send_data RESPONSE_401
|
33
|
+
close_connection_after_writing
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class NetworkErrorStreamingApiDouble < EM::Connection
|
38
|
+
def post_init
|
39
|
+
start_tls
|
40
|
+
end
|
41
|
+
|
42
|
+
def receive_data(data)
|
43
|
+
close_connection
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
RESPONSE_200_HEADERS = <<RESPONSE_200_HEADERS
|
2
|
+
HTTP/1.1 200 OK\r
|
3
|
+
Content-Type: application/json\r
|
4
|
+
Transfer-Encoding: chunked\r\n\r
|
5
|
+
RESPONSE_200_HEADERS
|
6
|
+
|
7
|
+
BODY_200 = <<BODY_200
|
8
|
+
{"delete":{"status":{"user_id_str":"482755917","id":182856546533908480,"user_id":482755917,"id_str":"182856546533908480"}}}\r
|
9
|
+
BODY_200
|
10
|
+
|
11
|
+
RESPONSE_200 = <<RESPONSE_200
|
12
|
+
HTTP/1.1 200 OK\r
|
13
|
+
Content-Type: application/json\r
|
14
|
+
Transfer-Encoding: chunked\r
|
15
|
+
\r
|
16
|
+
7d\r
|
17
|
+
{"delete":{"status":{"user_id_str":"482755917","id":182856546533908480,"user_id":482755917,"id_str":"182856546533908480"}}}\r
|
18
|
+
RESPONSE_200
|
19
|
+
|
20
|
+
BODY_401 = <<BODY_401
|
21
|
+
<html>
|
22
|
+
<head>
|
23
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
24
|
+
<title>Error 401 Unauthorized</title>
|
25
|
+
</head>
|
26
|
+
<body>
|
27
|
+
<h2>HTTP ERROR: 401</h2>
|
28
|
+
<p>Problem accessing '/1/statuses/sample.json'. Reason:
|
29
|
+
<pre>Unauthorized</pre>
|
30
|
+
</body>
|
31
|
+
</html>\r
|
32
|
+
BODY_401
|
33
|
+
|
34
|
+
RESPONSE_401 = <<RESPONSE_401
|
35
|
+
HTTP/1.1 401 Unauthorized\r
|
36
|
+
Content-Type: text/html\r
|
37
|
+
WWW-Authenticate: Basic realm="Firehose"\r
|
38
|
+
Cache-Control: must-revalidate,no-cache,no-store\r
|
39
|
+
Content-Length: 1241\r
|
40
|
+
Connection: close\r
|
41
|
+
\r
|
42
|
+
<html>
|
43
|
+
<head>
|
44
|
+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
45
|
+
<title>Error 401 Unauthorized</title>
|
46
|
+
</head>
|
47
|
+
<body>
|
48
|
+
<h2>HTTP ERROR: 401</h2>
|
49
|
+
<p>Problem accessing '/1/statuses/sample.json'. Reason:
|
50
|
+
<pre>Unauthorized</pre>
|
51
|
+
</body>
|
52
|
+
</html>\r
|
53
|
+
RESPONSE_401
|