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,11 @@
1
+ require 'uri'
2
+
3
+ module Nervion
4
+ module PercentEncoder
5
+ RESERVED_CHARACTERS = /[^a-zA-Z0-9\-\.\_\~]/
6
+
7
+ def self.encode(value)
8
+ URI.escape value.to_s, RESERVED_CHARACTERS
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,96 @@
1
+ require 'eventmachine'
2
+
3
+ module Nervion
4
+ class ReconnectionScheduler
5
+ HTTP_ERROR_LIMIT = 10
6
+ NETWORK_ERROR_LIMIT = 65
7
+
8
+ def initialize
9
+ @http_errors = ErrorCounter.new(HTTP_ERROR_LIMIT)
10
+ @network_errors = ErrorCounter.new(NETWORK_ERROR_LIMIT)
11
+ @http_wait_calculator = HttpWaitCalculator.new
12
+ @network_wait_calculator = NetworkWaitCalculator.new
13
+ end
14
+
15
+ def reconnect_after_http_error_in(stream)
16
+ reconnect_after_error stream, @http_errors, @http_wait_calculator
17
+ end
18
+
19
+ def reconnect_after_network_error_in(stream)
20
+ reconnect_after_error stream, @network_errors, @network_wait_calculator
21
+ end
22
+
23
+ private
24
+
25
+ def reconnect_after_error(stream, errors, wait_calculator)
26
+ errors.notify_error
27
+ schedule_reconnect stream, wait_calculator.wait_after(errors.count)
28
+ end
29
+
30
+ def schedule_reconnect(stream, seconds)
31
+ EM.add_timer(seconds) { stream.retry }
32
+ end
33
+ end
34
+
35
+ class ErrorCounter
36
+ attr_reader :count
37
+
38
+ def initialize(limit)
39
+ @count = 0
40
+ @limit = limit
41
+ end
42
+
43
+ def notify_error
44
+ @count += 1
45
+ raise TooManyConnectionErrors if too_many_errors?
46
+ end
47
+
48
+ private
49
+
50
+ def too_many_errors?
51
+ @count >= @limit
52
+ end
53
+ end
54
+
55
+ class WaitCalculator
56
+ def initialize(max_wait, &calculator)
57
+ @max_wait = max_wait
58
+ @calculator = calculator
59
+ end
60
+
61
+ def wait_after(error_count)
62
+ cap_wait @max_wait, calculate_wait_after(error_count)
63
+ end
64
+
65
+ private
66
+
67
+ def cap_wait(cap_value, current_wait)
68
+ [current_wait, cap_value].min
69
+ end
70
+
71
+ def calculate_wait_after(error_count)
72
+ @calculator.call(error_count)
73
+ end
74
+ end
75
+
76
+ class HttpWaitCalculator < WaitCalculator
77
+ MIN_WAIT = 10
78
+ MAX_WAIT = 240
79
+
80
+ def initialize
81
+ super(MAX_WAIT) { |error_count| MIN_WAIT * 2**(error_count - 1) }
82
+ end
83
+ end
84
+
85
+ class NetworkWaitCalculator < WaitCalculator
86
+ MIN_WAIT = 0.25
87
+ MAX_WAIT = 16
88
+
89
+ def initialize
90
+ super(MAX_WAIT) { |error_count| MIN_WAIT * error_count }
91
+ end
92
+ end
93
+
94
+ class TooManyConnectionErrors < Exception
95
+ end
96
+ end
@@ -0,0 +1,99 @@
1
+ require_relative 'oauth_header'
2
+
3
+ module Nervion
4
+ def self.get(uri, params = {}, oauth_params)
5
+ Get.new uri, params, oauth_params
6
+ end
7
+
8
+ def self.post(uri, params = {}, oauth_params)
9
+ Post.new uri, params, oauth_params
10
+ end
11
+
12
+ module Request
13
+ attr_reader :params, :oauth_params
14
+
15
+ def initialize(uri, params, oauth_params)
16
+ @uri = URI.parse(uri)
17
+ @params = params
18
+ @oauth_params = oauth_params
19
+ end
20
+
21
+ def uri
22
+ @uri.to_s
23
+ end
24
+
25
+ def path
26
+ @uri.request_uri
27
+ end
28
+
29
+ def host
30
+ @uri.host
31
+ end
32
+
33
+ def port
34
+ @uri.port
35
+ end
36
+
37
+ private
38
+
39
+ def request_line
40
+ "#{http_method} #{path} HTTP/1.1"
41
+ end
42
+
43
+ def headers
44
+ [ "Host: #{host}", "Authorization: #{OAuthHeader.for(self)}" ]
45
+ end
46
+
47
+ def percent_encode(params)
48
+ params.map do |name, value|
49
+ "#{name.to_s}=#{PercentEncoder.encode(value.to_s)}"
50
+ end.join '&'
51
+ end
52
+ end
53
+
54
+ class Get
55
+ include Request
56
+
57
+ def path
58
+ if params.any?
59
+ "#{super}?#{percent_encode(params)}"
60
+ else
61
+ super
62
+ end
63
+ end
64
+
65
+ def to_s
66
+ "#{request_line}\r\n#{headers.join("\r\n")}\r\n\r\n"
67
+ end
68
+
69
+ def http_method
70
+ 'GET'
71
+ end
72
+ end
73
+
74
+ class Post
75
+ include Request
76
+ include PercentEncoder
77
+
78
+ def to_s
79
+ "#{request_line}\r\n#{headers.join("\r\n")}\r\n\r\n#{body}\r\n"
80
+ end
81
+
82
+ def http_method
83
+ 'POST'
84
+ end
85
+
86
+ private
87
+
88
+ def headers
89
+ super << [
90
+ 'Content-Type: application/x-www-form-urlencoded',
91
+ "Content-Length: #{body.length}"
92
+ ]
93
+ end
94
+
95
+ def body
96
+ percent_encode params
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,64 @@
1
+ require 'eventmachine'
2
+ require 'nervion/http_parser'
3
+ require 'nervion/reconnection_scheduler'
4
+
5
+ module Nervion
6
+ class Stream < EM::Connection
7
+
8
+ attr_reader :http_error
9
+
10
+ def initialize(*args)
11
+ @request = args[0]
12
+ @handler = args[1]
13
+ end
14
+
15
+ def post_init
16
+ @scheduler = ReconnectionScheduler.new
17
+ end
18
+
19
+ def connection_completed
20
+ start_tls
21
+ send_data @request
22
+ end
23
+
24
+ def receive_data(data)
25
+ @handler << data
26
+ rescue HttpError => error
27
+ @http_error = error
28
+ end
29
+
30
+ def retry
31
+ @http_error = nil
32
+ reconnect @request.host, @request.port
33
+ end
34
+
35
+ def unbind
36
+ handle_closed_stream unless @handler.stream_close_requested?
37
+ end
38
+
39
+ def http_error_occurred?
40
+ not http_error.nil?
41
+ end
42
+
43
+ private
44
+
45
+ def handle_closed_stream
46
+ if http_error_occurred?
47
+ handle_http_error_and_reopen_stream
48
+ else
49
+ handle_network_error_and_reopen_stream
50
+ end
51
+ end
52
+
53
+ def handle_http_error_and_reopen_stream
54
+ @handler.handle_http_error http_error
55
+ @scheduler.reconnect_after_http_error_in self
56
+ end
57
+
58
+ def handle_network_error_and_reopen_stream
59
+ @handler.handle_network_error
60
+ @scheduler.reconnect_after_network_error_in self
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,42 @@
1
+ require 'yajl'
2
+ require 'nervion/http_parser'
3
+
4
+ module Nervion
5
+ class StreamHandler
6
+
7
+ def initialize(callbacks)
8
+ @callbacks = callbacks
9
+ @http_parser = HttpParser.new(setup_json_parser)
10
+ end
11
+
12
+ def <<(data)
13
+ @http_parser << data
14
+ end
15
+
16
+ def handle_http_error(error)
17
+ @http_parser.reset!
18
+ @callbacks[:http_error].call(error.status, error.body)
19
+ end
20
+
21
+ def handle_network_error
22
+ @http_parser.reset!
23
+ @callbacks[:network_error].call
24
+ end
25
+
26
+ def stream_close_requested?
27
+ @close_stream ||= false
28
+ end
29
+
30
+ def close_stream
31
+ @close_stream = true
32
+ end
33
+
34
+ private
35
+
36
+ def setup_json_parser
37
+ Yajl::Parser.new(symbolize_keys: true).tap do |json_parser|
38
+ json_parser.on_parse_complete = @callbacks[:status]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,3 @@
1
+ module Nervion
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/nervion/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Javier Acero"]
6
+ gem.email = ["j4cegu@gmail.com"]
7
+ gem.description = %q{A minimalistic Twitter Stream API Ruby client}
8
+ gem.summary = %q{}
9
+ gem.homepage = "https://github.com/jacegu/nervion"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(spec|features)/})
14
+ gem.name = "nervion"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Nervion::VERSION
17
+
18
+ gem.add_runtime_dependency 'eventmachine', '~> 1.0.0.beta.4'
19
+ gem.add_runtime_dependency 'http_parser.rb', '~> 0.5.3'
20
+ gem.add_runtime_dependency 'yajl-ruby', '~> 1.1.0'
21
+ gem.add_development_dependency 'rspec'
22
+ gem.add_development_dependency 'cucumber'
23
+ gem.add_development_dependency 'simplecov'
24
+ end
@@ -0,0 +1,18 @@
1
+ require 'nervion/callback_table'
2
+
3
+ describe Nervion::CallbackTable do
4
+ it 'can be setup with a callback' do
5
+ callback = stub(:callback)
6
+ subject[:name]= callback
7
+ subject[:name].should be callback
8
+ end
9
+
10
+ it 'has a callback for network errors by default' do
11
+ subject[:network_error].should_not be_nil
12
+ end
13
+
14
+ it 'has a callback for http errors by default' do
15
+ STDERR.should_receive(:puts).with(/500|error/)
16
+ subject[:http_error].call(500, 'error')
17
+ end
18
+ end
@@ -0,0 +1,51 @@
1
+ require 'eventmachine'
2
+ require 'nervion/client'
3
+
4
+ describe Nervion::Client do
5
+ subject { described_class.new('http://twitter.com', 443) }
6
+ let(:request) { stub :request }
7
+ let(:callbacks) { stub :callbacks }
8
+ let(:stream_handler) { stub :stream_handler }
9
+
10
+ before(:all) do
11
+ module EventMachine
12
+ class << self
13
+ alias old_run run
14
+ alias old_connect connect
15
+ end
16
+ def self.run; yield; end
17
+ def self.connect(*args); end
18
+ end
19
+ end
20
+
21
+ after(:all) do
22
+ module EM
23
+ class << self
24
+ alias run old_run
25
+ alias connect old_connect
26
+ end
27
+ end
28
+ end
29
+
30
+ before do
31
+ Nervion::StreamHandler.stub(:new).with(callbacks).and_return(stream_handler)
32
+ end
33
+
34
+ it 'starts treaming' do
35
+ EM.should_receive(:connect).with(
36
+ 'http://twitter.com',
37
+ 443,
38
+ Nervion::Stream,
39
+ request,
40
+ stream_handler
41
+ )
42
+ subject.stream(request, callbacks)
43
+ end
44
+
45
+ it 'stops streaming' do
46
+ stream_handler.should_receive(:close_stream).ordered
47
+ EM.should_receive(:stop).ordered
48
+ subject.stream(request, callbacks)
49
+ subject.stop
50
+ end
51
+ end
@@ -0,0 +1,58 @@
1
+ require 'nervion/configuration'
2
+
3
+ describe Nervion::Configuration do
4
+
5
+ it 'allows configuration from the top level' do
6
+ Nervion::Configuration.should_receive(:access_key=).with 'access_key'
7
+ Nervion.configure { |config| config.access_key = 'access_key' }
8
+ end
9
+
10
+ context 'when it has not been configured' do
11
+ it 'has an empty string as consumer_key' do
12
+ described_class.consumer_key.should eq ''
13
+ end
14
+
15
+ it 'has an empty string as consumer secret' do
16
+ described_class.consumer_secret.should eq ''
17
+ end
18
+
19
+ it 'has an empty string as access token' do
20
+ described_class.access_token.should eq ''
21
+ end
22
+
23
+ it 'has an empty string as access token secret' do
24
+ described_class.access_token_secret.should eq ''
25
+ end
26
+ end
27
+
28
+ context 'configuration' do
29
+ it 'can be set with the consumer key' do
30
+ described_class.consumer_key = 'consumer_key'
31
+ described_class.consumer_key.should eq 'consumer_key'
32
+ end
33
+
34
+ it 'can be set with the consumer secret' do
35
+ described_class.consumer_secret = 'consumer_secret'
36
+ described_class.consumer_secret.should eq 'consumer_secret'
37
+ end
38
+
39
+ it 'can be set with the access token' do
40
+ described_class.access_token = 'access token'
41
+ described_class.access_token.should eq 'access token'
42
+ end
43
+
44
+ it 'can be set with the access token secret' do
45
+ described_class.access_token_secret = 'access token secret'
46
+ described_class.access_token_secret.should eq 'access token secret'
47
+ end
48
+ end
49
+
50
+ context 'accessing configuration' do
51
+ it 'settings can de accessed as if it was hash' do
52
+ described_class.consumer_secret = 'consumer_secret'
53
+ described_class[:consumer_secret].should eq 'consumer_secret'
54
+ described_class.fetch(:consumer_secret).should eq 'consumer_secret'
55
+ end
56
+ end
57
+
58
+ end