songkick-transport 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,83 @@
1
+ module Songkick
2
+ module Transport
3
+
4
+ class Request
5
+ attr_accessor :response,
6
+ :error
7
+
8
+ attr_reader :endpoint,
9
+ :verb,
10
+ :path,
11
+ :params,
12
+ :headers,
13
+ :timeout,
14
+ :start_time,
15
+ :duration
16
+
17
+ alias :http_method :verb
18
+
19
+ def initialize(endpoint, verb, path, params, headers = {}, timeout = DEFAULT_TIMEOUT, start_time = nil, response = nil, error = nil)
20
+ @endpoint = endpoint
21
+ @verb = verb.to_s.downcase
22
+ @path = path
23
+ @headers = headers
24
+ @params = params
25
+ @timeout = timeout
26
+ @response = response
27
+ @error = error
28
+ @start_time = start_time || Time.now
29
+ @duration = (Time.now.to_f - start_time.to_f) * 1000
30
+ @multipart = Serialization.multipart?(params)
31
+ end
32
+
33
+ def use_body?
34
+ USE_BODY.include?(@verb)
35
+ end
36
+
37
+ def multipart?
38
+ @multipart
39
+ end
40
+
41
+ def content_type
42
+ return nil unless use_body?
43
+ if @multipart
44
+ multipart_request[:content_type]
45
+ else
46
+ 'application/x-www-form-urlencoded'
47
+ end
48
+ end
49
+
50
+ def body
51
+ return nil unless use_body?
52
+ if @multipart
53
+ multipart_request[:body]
54
+ else
55
+ Serialization.build_query_string(params)
56
+ end
57
+ end
58
+
59
+ def url
60
+ Serialization.build_url(@verb, @endpoint, @path, @params)
61
+ end
62
+
63
+ def to_s
64
+ url = Serialization.build_url(@verb, @endpoint, @path, @params, true)
65
+ command = "#{@verb.upcase} '#{url}'"
66
+ return command unless use_body?
67
+ query = Serialization.build_query_string(params, true, true)
68
+ command << " -H 'Content-Type: #{content_type}'"
69
+ command << " -d '#{query}'"
70
+ command
71
+ end
72
+
73
+ private
74
+
75
+ def multipart_request
76
+ return nil unless @multipart
77
+ @multipart_request ||= Serialization.serialize_multipart(params)
78
+ end
79
+ end
80
+
81
+ end
82
+ end
83
+
@@ -0,0 +1,45 @@
1
+ module Songkick
2
+ module Transport
3
+
4
+ class Response
5
+ def self.process(request, status, headers, body)
6
+ case status.to_i
7
+ when 200 then OK.new(status, headers, body)
8
+ when 201 then Created.new(status, headers, body)
9
+ when 204 then NoContent.new(status, headers, body)
10
+ when 409 then UserError.new(status, headers, body)
11
+ else
12
+ Transport.logger.warn "Received error code: #{status} -- #{request}"
13
+ raise HttpError.new(request, status, headers, body)
14
+ end
15
+ rescue Yajl::ParseError
16
+ Transport.logger.warn "Request returned invalid JSON: #{request}"
17
+ raise Transport::InvalidJSONError, request
18
+ end
19
+
20
+ attr_reader :data, :headers, :status
21
+
22
+ def initialize(status, headers, body)
23
+ @data = if body.is_a?(String)
24
+ body.strip == '' ? nil : Yajl::Parser.parse(body)
25
+ else
26
+ body
27
+ end
28
+
29
+ @headers = Headers.new(headers)
30
+ @status = status.to_i
31
+ end
32
+
33
+ def errors
34
+ data && data['errors']
35
+ end
36
+
37
+ class OK < Response ; end
38
+ class Created < Response ; end
39
+ class NoContent < Response ; end
40
+ class UserError < Response ; end
41
+ end
42
+
43
+ end
44
+ end
45
+
@@ -0,0 +1,79 @@
1
+ module Songkick
2
+ module Transport
3
+ module Serialization
4
+
5
+ extend self
6
+
7
+ SANITIZED_VALUE = '[REMOVED]'
8
+
9
+ def build_url(verb, host, path, params, scrub=false)
10
+ url = host + path
11
+ return url if USE_BODY.include?(verb)
12
+ qs = build_query_string(params, true, scrub)
13
+ url + (qs == '' ? '' : '?' + qs)
14
+ end
15
+
16
+ def build_query_string(params, fully_encode = true, sanitize = false)
17
+ pairs = []
18
+ each_qs_param('', params) do |key, value|
19
+ if sanitize and sanitize?(key)
20
+ value = SANITIZED_VALUE
21
+ end
22
+ pairs << [key, value]
23
+ end
24
+ if fully_encode
25
+ pairs.map { |p| p.join('=') }.join('&')
26
+ else
27
+ pairs.inject({}) do |hash, pair|
28
+ hash[pair.first] = pair.last
29
+ hash
30
+ end
31
+ end
32
+ end
33
+
34
+ def each_qs_param(prefix, value, &block)
35
+ case value
36
+ when Array
37
+ value.each { |e| each_qs_param(prefix + "[]", e, &block) }
38
+ when Hash
39
+ value.each do |k,v|
40
+ key = (prefix == '') ? CGI.escape(k.to_s) : prefix + "[#{CGI.escape k.to_s}]"
41
+ each_qs_param(key, v, &block)
42
+ end
43
+ when Transport::IO
44
+ block.call(prefix, value)
45
+ else
46
+ block.call(prefix, CGI.escape(value.to_s))
47
+ end
48
+ end
49
+
50
+ def multipart?(params)
51
+ case params
52
+ when Hash then params.any? { |k,v| multipart? v }
53
+ when Array then params.any? { |e| multipart? e }
54
+ else Transport::IO === params
55
+ end
56
+ end
57
+
58
+ def sanitize?(key)
59
+ Transport.sanitized_params.any? { |param| param === key }
60
+ end
61
+
62
+ def serialize_multipart(params, boundary = Multipartable::DEFAULT_BOUNDARY)
63
+ params = build_query_string(params, false)
64
+
65
+ parts = params.map { |k,v| Parts::Part.new(boundary, k, v) }
66
+ parts << Parts::EpiloguePart.new(boundary)
67
+ ios = parts.map { |p| p.to_io }
68
+
69
+ {
70
+ :content_type => "multipart/form-data; boundary=#{boundary}",
71
+ :content_length => parts.inject(0) { |sum,i| sum + i.length }.to_s,
72
+ :body => CompositeReadIO.new(*ios).read
73
+ }
74
+ end
75
+
76
+ end
77
+ end
78
+ end
79
+
@@ -0,0 +1,27 @@
1
+ module Songkick
2
+ module Transport
3
+
4
+ class TimeoutDecorator
5
+ def initialize(client, timeout)
6
+ @client = client
7
+ @timeout = timeout
8
+ end
9
+
10
+ HTTP_VERBS.each do |verb|
11
+ class_eval %{
12
+ def #{verb}(path, params = {}, headers ={}, timeout = nil)
13
+ @client.__send__(:#{verb}, path, params, headers, timeout || @timeout)
14
+ end
15
+ }
16
+ end
17
+
18
+ private
19
+
20
+ def method_missing(*args, &block)
21
+ @client.__send__(*args, &block)
22
+ end
23
+ end
24
+
25
+ end
26
+ end
27
+
@@ -0,0 +1,28 @@
1
+ module Songkick
2
+ module Transport
3
+ class UpstreamError < RuntimeError
4
+ attr_reader :request
5
+
6
+ def initialize(request)
7
+ @request = request
8
+ end
9
+
10
+ def message
11
+ "#{self.class}: #{@request}"
12
+ end
13
+ alias :to_s :message
14
+ end
15
+
16
+ class HostResolutionError < UpstreamError
17
+ end
18
+
19
+ class ConnectionFailedError < UpstreamError
20
+ end
21
+
22
+ class TimeoutError < UpstreamError
23
+ end
24
+
25
+ class InvalidJSONError < UpstreamError
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,60 @@
1
+ require 'spec_helper'
2
+
3
+ module Songkick
4
+ module Transport
5
+
6
+ describe Curb do
7
+ after do
8
+ Songkick::Transport::Curb.clear_thread_connection
9
+ end
10
+
11
+ class FakeCurl
12
+ attr_writer :url, :timeout
13
+ attr_reader :on_header, :response_code, :body_str, :headers
14
+
15
+ def initialize(options)
16
+ @error = options[:error]
17
+ @headers = {}
18
+ end
19
+
20
+ def http_get
21
+ raise(@error, "bang") if @error
22
+ end
23
+
24
+ def reset
25
+ end
26
+ end
27
+
28
+ subject{ Curb.new('localhost', :connection => @fake_curl) }
29
+ let(:request){ Request.new('http://localhost', 'get', '/', {}) }
30
+
31
+ def self.it_should_raise(exception)
32
+ it "should raise error #{exception}" do
33
+ begin
34
+ subject.execute_request(request)
35
+ rescue => e
36
+ e.class.should == exception
37
+ end
38
+ end
39
+ end
40
+
41
+ def self.when_request_raises_the_exception(raised_exception, &block)
42
+ describe "when request raises a #{raised_exception}" do
43
+ before(:each) do
44
+ @fake_curl = FakeCurl.new(:error => raised_exception)
45
+ end
46
+
47
+ class_exec(&block)
48
+ end
49
+ end
50
+
51
+ describe "handling errors" do
52
+ when_request_raises_the_exception(Curl::Err::HostResolutionError) { it_should_raise(Transport::HostResolutionError) }
53
+ when_request_raises_the_exception(Curl::Err::ConnectionFailedError){ it_should_raise(Transport::ConnectionFailedError) }
54
+ when_request_raises_the_exception(Curl::Err::TimeoutError) { it_should_raise(Transport::TimeoutError) }
55
+ when_request_raises_the_exception(Curl::Err::GotNothingError) { it_should_raise(Transport::UpstreamError) }
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ module Songkick
4
+ module Transport
5
+
6
+ describe HttParty do
7
+ class FakeJSONException < Exception; end
8
+
9
+ let(:request){ Request.new('http://localhost', 'get', '/', {}) }
10
+
11
+ describe "handling errors" do
12
+ class FakeHttparty < Songkick::Transport::HttParty::Adapter
13
+ class << self
14
+ attr_accessor :error
15
+ end
16
+
17
+ def self.get(path, args)
18
+ raise(error, "bang") if error
19
+ end
20
+
21
+ end
22
+
23
+ def self.it_should_raise(exception)
24
+ it "should raise error #{exception}" do
25
+ begin
26
+ @httparty.execute_request(request)
27
+ rescue => e
28
+ e.class.should == exception
29
+ end
30
+ end
31
+ end
32
+
33
+ def self.when_request_raises_the_exception(raised_exception, &block)
34
+ describe "when request raises a #{raised_exception}" do
35
+ before(:each) do
36
+ FakeHttparty.error = raised_exception
37
+ @httparty = Songkick::Transport::HttParty.new('localhost', {:adapter => FakeHttparty})
38
+ end
39
+
40
+ class_exec(&block)
41
+ end
42
+ end
43
+
44
+ describe "handling errors" do
45
+ when_request_raises_the_exception(FakeJSONException) { it_should_raise(Transport::InvalidJSONError) }
46
+ when_request_raises_the_exception(SocketError) { it_should_raise(Transport::ConnectionFailedError) }
47
+ when_request_raises_the_exception(Timeout::Error) { it_should_raise(Transport::TimeoutError) }
48
+ when_request_raises_the_exception(UpstreamError) { it_should_raise(Transport::UpstreamError) }
49
+ when_request_raises_the_exception(Exception) { it_should_raise(Transport::UpstreamError) }
50
+ end
51
+ end
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,60 @@
1
+ require "spec_helper"
2
+
3
+ describe Songkick::Transport::Request do
4
+ let :params do
5
+ {:username => "Louis", :password => "CK", :access => {:token => "foo"}}
6
+ end
7
+
8
+ let :get_request do
9
+ Songkick::Transport::Request.new("www.example.com", "GET", "/", params)
10
+ end
11
+
12
+ let :post_request do
13
+ Songkick::Transport::Request.new("www.example.com", "POST", "/", params)
14
+ end
15
+
16
+ def query(request, pattern)
17
+ request.to_s.scan(pattern).flatten.first.split("&").sort
18
+ end
19
+
20
+ describe :to_s do
21
+ context "with a get request" do
22
+ it "returns the request as a curl command" do
23
+ pattern = %r{^GET 'www.example.com/\?([^']+)'$}
24
+ get_request.to_s.should =~ pattern
25
+ query(get_request, pattern).should == ["access[token]=foo", "password=CK", "username=Louis"]
26
+ end
27
+ end
28
+
29
+ context "with a post request" do
30
+ it "returns the request as a curl command" do
31
+ pattern = %r{^POST 'www.example.com/' -H 'Content-Type: application/x-www-form-urlencoded' -d '([^']+)'$}
32
+ post_request.to_s.should =~ pattern
33
+ query(post_request, pattern).should == ["access[token]=foo", "password=CK", "username=Louis"]
34
+ end
35
+ end
36
+
37
+ describe "with query sanitization" do
38
+ before do
39
+ Songkick::Transport.stub(:sanitized_params).and_return [/password/, "access[token]"]
40
+ end
41
+
42
+ context "with a get request" do
43
+ it "removes the parameter values from the request" do
44
+ pattern = %r{^GET 'www.example.com/\?([^']+)'$}
45
+ get_request.to_s.should =~ pattern
46
+ query(get_request, pattern).should == ["access[token]=[REMOVED]", "password=[REMOVED]", "username=Louis"]
47
+ end
48
+ end
49
+
50
+ context "with a post request" do
51
+ it "removes the parameter values from the request" do
52
+ pattern = %r{^POST 'www.example.com/' -H 'Content-Type: application/x-www-form-urlencoded' -d '([^']+)'$}
53
+ post_request.to_s.should =~ pattern
54
+ query(post_request, pattern).should == ["access[token]=[REMOVED]", "password=[REMOVED]", "username=Louis"]
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+