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