nervion 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/.gitignore +4 -0
  2. data/.rspec +2 -0
  3. data/.rvmrc +1 -0
  4. data/Gemfile +3 -0
  5. data/Gemfile.lock +46 -0
  6. data/LICENSE +22 -0
  7. data/README.md +197 -0
  8. data/Rakefile +22 -0
  9. data/features/step_definitions/streaming_steps.rb +55 -0
  10. data/features/streaming.feature +16 -0
  11. data/features/support/env.rb +9 -0
  12. data/features/support/streaming_api_double.rb +45 -0
  13. data/fixtures/responses.rb +53 -0
  14. data/fixtures/stream.txt +100 -0
  15. data/lib/nervion.rb +4 -0
  16. data/lib/nervion/callback_table.rb +30 -0
  17. data/lib/nervion/client.rb +23 -0
  18. data/lib/nervion/configuration.rb +50 -0
  19. data/lib/nervion/facade.rb +54 -0
  20. data/lib/nervion/http_parser.rb +62 -0
  21. data/lib/nervion/oauth_header.rb +88 -0
  22. data/lib/nervion/oauth_signature.rb +54 -0
  23. data/lib/nervion/percent_encoder.rb +11 -0
  24. data/lib/nervion/reconnection_scheduler.rb +96 -0
  25. data/lib/nervion/request.rb +99 -0
  26. data/lib/nervion/stream.rb +64 -0
  27. data/lib/nervion/stream_handler.rb +42 -0
  28. data/lib/nervion/version.rb +3 -0
  29. data/nervion.gemspec +24 -0
  30. data/spec/nervion/callback_table_spec.rb +18 -0
  31. data/spec/nervion/client_spec.rb +51 -0
  32. data/spec/nervion/configuration_spec.rb +58 -0
  33. data/spec/nervion/facade_spec.rb +90 -0
  34. data/spec/nervion/http_parser_spec.rb +26 -0
  35. data/spec/nervion/oauth_header_spec.rb +115 -0
  36. data/spec/nervion/oauth_signature_spec.rb +66 -0
  37. data/spec/nervion/percent_encoder_spec.rb +20 -0
  38. data/spec/nervion/reconnection_scheduler_spec.rb +84 -0
  39. data/spec/nervion/request_spec.rb +90 -0
  40. data/spec/nervion/stream_handler_spec.rb +67 -0
  41. data/spec/nervion/stream_spec.rb +97 -0
  42. data/spec/spec_helper.rb +4 -0
  43. metadata +200 -0
@@ -0,0 +1,4 @@
1
+ TODOs
2
+ .DS_Store
3
+ examples
4
+ coverage
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ -I .
2
+ -I lib/
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.3@nervion --create
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -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.
@@ -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.
@@ -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,9 @@
1
+ $: << File.join(File.dirname(__FILE__), '..', '..', 'lib')
2
+
3
+ require 'nervion'
4
+ require 'eventmachine'
5
+
6
+ if ENV['COVERAGE']
7
+ require 'simplecov'
8
+ SimpleCov.start
9
+ end
@@ -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