songkick-transport 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +313 -0
- data/examples/example.rb +38 -0
- data/examples/loop.rb +17 -0
- data/examples/server.rb +15 -0
- data/examples/tcp_server.rb +42 -0
- data/examples/thread_safety.rb +16 -0
- data/lib/songkick/transport.rb +83 -0
- data/lib/songkick/transport/base.rb +53 -0
- data/lib/songkick/transport/curb.rb +76 -0
- data/lib/songkick/transport/header_decorator.rb +27 -0
- data/lib/songkick/transport/headers.rb +36 -0
- data/lib/songkick/transport/http_error.rb +24 -0
- data/lib/songkick/transport/httparty.rb +66 -0
- data/lib/songkick/transport/rack_test.rb +52 -0
- data/lib/songkick/transport/reporting.rb +60 -0
- data/lib/songkick/transport/request.rb +83 -0
- data/lib/songkick/transport/response.rb +45 -0
- data/lib/songkick/transport/serialization.rb +79 -0
- data/lib/songkick/transport/timeout_decorator.rb +27 -0
- data/lib/songkick/transport/upstream_error.rb +28 -0
- data/spec/songkick/transport/curb_spec.rb +60 -0
- data/spec/songkick/transport/httparty_spec.rb +55 -0
- data/spec/songkick/transport/request_spec.rb +60 -0
- data/spec/songkick/transport/response_spec.rb +76 -0
- data/spec/songkick/transport_spec.rb +189 -0
- data/spec/spec_helper.rb +72 -0
- metadata +217 -0
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'net/http'
|
3
|
+
require 'net/http/post/multipart'
|
4
|
+
require 'uri'
|
5
|
+
require 'yajl'
|
6
|
+
|
7
|
+
module Songkick
|
8
|
+
module Transport
|
9
|
+
DEFAULT_TIMEOUT = 5
|
10
|
+
DEFAULT_FORMAT = :json
|
11
|
+
|
12
|
+
HTTP_VERBS = %w[get post put delete head]
|
13
|
+
USE_BODY = %w[post put]
|
14
|
+
|
15
|
+
ROOT = File.expand_path('..', __FILE__)
|
16
|
+
|
17
|
+
autoload :Serialization, ROOT + '/transport/serialization'
|
18
|
+
autoload :Base, ROOT + '/transport/base'
|
19
|
+
autoload :Curb, ROOT + '/transport/curb'
|
20
|
+
autoload :Headers, ROOT + '/transport/headers'
|
21
|
+
autoload :HeaderDecorator, ROOT + '/transport/header_decorator'
|
22
|
+
autoload :HttParty, ROOT + '/transport/httparty'
|
23
|
+
autoload :RackTest, ROOT + '/transport/rack_test'
|
24
|
+
autoload :Reporting, ROOT + '/transport/reporting'
|
25
|
+
autoload :Request, ROOT + '/transport/request'
|
26
|
+
autoload :Response, ROOT + '/transport/response'
|
27
|
+
autoload :TimeoutDecorator, ROOT + '/transport/timeout_decorator'
|
28
|
+
|
29
|
+
autoload :UpstreamError, ROOT + '/transport/upstream_error'
|
30
|
+
autoload :HostResolutionError, ROOT + '/transport/upstream_error'
|
31
|
+
autoload :TimeoutError, ROOT + '/transport/upstream_error'
|
32
|
+
autoload :ConnectionFailedError,ROOT + '/transport/upstream_error'
|
33
|
+
autoload :InvalidJSONError, ROOT + '/transport/upstream_error'
|
34
|
+
autoload :HttpError, ROOT + '/transport/http_error'
|
35
|
+
|
36
|
+
IO = UploadIO
|
37
|
+
|
38
|
+
def self.io(object)
|
39
|
+
if Hash === object and [:tempfile, :type, :filename].all? { |k| object.has_key? k } # Rack upload
|
40
|
+
Transport::IO.new(object[:tempfile], object[:type], object[:filename])
|
41
|
+
|
42
|
+
elsif object.respond_to?(:content_type) and object.respond_to?(:original_filename) # Rails upload
|
43
|
+
Transport::IO.new(object, object.content_type, object.original_filename)
|
44
|
+
|
45
|
+
else
|
46
|
+
raise ArgumentError, "Could not generate a Transport::IO from #{object.inspect}"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.logger
|
51
|
+
@logger ||= begin
|
52
|
+
require 'logger'
|
53
|
+
Logger.new(STDOUT)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.logger=(logger)
|
58
|
+
@logger = logger
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.verbose=(verbose)
|
62
|
+
@verbose = verbose
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.verbose?
|
66
|
+
@verbose
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.report
|
70
|
+
Reporting.report
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.sanitize(*params)
|
74
|
+
sanitized_params.concat(params)
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.sanitized_params
|
78
|
+
@sanitized_params ||= []
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Songkick
|
2
|
+
module Transport
|
3
|
+
|
4
|
+
class Base
|
5
|
+
attr_accessor :user_agent
|
6
|
+
|
7
|
+
HTTP_VERBS.each do |verb|
|
8
|
+
class_eval %{
|
9
|
+
def #{verb}(path, params = {}, head = {}, timeout = nil)
|
10
|
+
req = Request.new(endpoint, '#{verb}', path, params, headers.merge(head), timeout)
|
11
|
+
Reporting.log_request(req)
|
12
|
+
|
13
|
+
response = execute_request(req)
|
14
|
+
|
15
|
+
Reporting.log_response(response, req)
|
16
|
+
Reporting.record(req, response)
|
17
|
+
response
|
18
|
+
rescue => error
|
19
|
+
Reporting.record(req, nil, error)
|
20
|
+
raise error
|
21
|
+
end
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
def with_headers(headers = {})
|
26
|
+
HeaderDecorator.new(self, headers)
|
27
|
+
end
|
28
|
+
|
29
|
+
def with_timeout(timeout = DEFAULT_TIMEOUT)
|
30
|
+
TimeoutDecorator.new(self, timeout)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def process(url, status, headers, body)
|
36
|
+
Response.process(url, status, headers, body)
|
37
|
+
end
|
38
|
+
|
39
|
+
def headers
|
40
|
+
{
|
41
|
+
'Connection' => 'close',
|
42
|
+
'User-Agent' => user_agent || ''
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
def logger
|
47
|
+
Transport.logger
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'curb'
|
3
|
+
|
4
|
+
module Songkick
|
5
|
+
module Transport
|
6
|
+
|
7
|
+
class Curb < Base
|
8
|
+
def self.clear_thread_connection
|
9
|
+
Thread.current[:transport_curb_easy] = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(host, options = {})
|
13
|
+
@host = host
|
14
|
+
@timeout = options[:timeout] || DEFAULT_TIMEOUT
|
15
|
+
@user_agent = options[:user_agent]
|
16
|
+
if c = options[:connection]
|
17
|
+
Thread.current[:transport_curb_easy] = c
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def connection
|
22
|
+
Thread.current[:transport_curb_easy] ||= Curl::Easy.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def endpoint
|
26
|
+
@host
|
27
|
+
end
|
28
|
+
|
29
|
+
def execute_request(req)
|
30
|
+
connection.reset
|
31
|
+
|
32
|
+
connection.url = req.url
|
33
|
+
connection.timeout = req.timeout || @timeout
|
34
|
+
connection.headers.update(req.headers)
|
35
|
+
|
36
|
+
response_headers = {}
|
37
|
+
|
38
|
+
connection.on_header do |header_line|
|
39
|
+
line = header_line.sub(/\r\n$/, '')
|
40
|
+
parts = line.split(/:\s*/)
|
41
|
+
if parts.size >= 2
|
42
|
+
response_headers[parts.shift] = parts * ':'
|
43
|
+
end
|
44
|
+
header_line.bytesize
|
45
|
+
end
|
46
|
+
|
47
|
+
if req.use_body?
|
48
|
+
connection.headers['Content-Type'] = req.content_type
|
49
|
+
connection.__send__("http_#{req.verb}", req.body)
|
50
|
+
else
|
51
|
+
connection.__send__("http_#{req.verb}")
|
52
|
+
end
|
53
|
+
|
54
|
+
process(req, connection.response_code, response_headers, connection.body_str)
|
55
|
+
|
56
|
+
rescue Curl::Err::HostResolutionError => error
|
57
|
+
logger.warn "Could not resolve host: #{@host}"
|
58
|
+
raise Transport::HostResolutionError, req
|
59
|
+
|
60
|
+
rescue Curl::Err::ConnectionFailedError => error
|
61
|
+
logger.warn "Could not connect to host: #{@host}"
|
62
|
+
raise Transport::ConnectionFailedError, req
|
63
|
+
|
64
|
+
rescue Curl::Err::TimeoutError => error
|
65
|
+
logger.warn "Request timed out after #{@timeout}s : #{req}"
|
66
|
+
raise Transport::TimeoutError, req
|
67
|
+
|
68
|
+
rescue Curl::Err::GotNothingError => error
|
69
|
+
logger.warn "Got nothing: #{req}"
|
70
|
+
raise Transport::UpstreamError, req
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Songkick
|
2
|
+
module Transport
|
3
|
+
|
4
|
+
class HeaderDecorator
|
5
|
+
def initialize(client, headers)
|
6
|
+
@client = client
|
7
|
+
@headers = Headers.new(headers)
|
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.merge(headers), 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,36 @@
|
|
1
|
+
module Songkick
|
2
|
+
module Transport
|
3
|
+
|
4
|
+
class Headers
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
def initialize(hash = {})
|
8
|
+
@hash = {}
|
9
|
+
hash.each do |key, value|
|
10
|
+
@hash[self.class.normalize(key)] = value
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def each(&block)
|
15
|
+
@hash.each(&block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](header_name)
|
19
|
+
@hash[self.class.normalize(header_name)]
|
20
|
+
end
|
21
|
+
|
22
|
+
def merge(hash)
|
23
|
+
@hash.merge(hash)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.normalize(header_name)
|
27
|
+
header_name.
|
28
|
+
gsub(/^HTTP_/, '').gsub('_', '-').
|
29
|
+
downcase.
|
30
|
+
gsub(/(^|-)([a-z])/) { $1 + $2.upcase }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Songkick
|
2
|
+
module Transport
|
3
|
+
class HttpError < UpstreamError
|
4
|
+
attr_reader :request, :data, :headers, :status
|
5
|
+
|
6
|
+
def initialize(request, status, headers, body)
|
7
|
+
@request = request
|
8
|
+
|
9
|
+
@data = if body.is_a?(String)
|
10
|
+
body.strip == '' ? nil : (Yajl::Parser.parse(body) rescue body)
|
11
|
+
else
|
12
|
+
body
|
13
|
+
end
|
14
|
+
|
15
|
+
@headers = Headers.new(headers)
|
16
|
+
@status = status.to_i
|
17
|
+
end
|
18
|
+
|
19
|
+
def message
|
20
|
+
"#{self.class}: status code: #{@status} from: #{@request}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'httparty'
|
2
|
+
|
3
|
+
module Songkick
|
4
|
+
module Transport
|
5
|
+
|
6
|
+
class HttParty
|
7
|
+
def self.new(host, options = {})
|
8
|
+
klass = options[:adapter] || Class.new(Adapter)
|
9
|
+
klass.base_uri(host)
|
10
|
+
klass.default_timeout(options[:timeout] || DEFAULT_TIMEOUT)
|
11
|
+
klass.format(options[:format] || DEFAULT_FORMAT)
|
12
|
+
|
13
|
+
transport = klass.new
|
14
|
+
transport.user_agent = options[:user_agent]
|
15
|
+
transport
|
16
|
+
end
|
17
|
+
|
18
|
+
class Adapter < Base
|
19
|
+
include HTTParty
|
20
|
+
|
21
|
+
def endpoint
|
22
|
+
self.class.base_uri
|
23
|
+
end
|
24
|
+
|
25
|
+
def execute_request(req)
|
26
|
+
timeout = req.timeout || self.class.default_options[:timeout]
|
27
|
+
|
28
|
+
response = if req.use_body?
|
29
|
+
if req.multipart?
|
30
|
+
head = req.headers.merge('Content-Type' => req.content_type)
|
31
|
+
self.class.__send__(req.verb, req.path, :body => req.body, :headers => head, :timeout => timeout)
|
32
|
+
else
|
33
|
+
self.class.__send__(req.verb, req.path, :body => req.body, :headers => req.headers, :timeout => timeout)
|
34
|
+
end
|
35
|
+
else
|
36
|
+
self.class.__send__(req.verb, req.url, :headers => req.headers, :timeout => timeout)
|
37
|
+
end
|
38
|
+
|
39
|
+
process(req, response.code, response.headers, response.parsed_response)
|
40
|
+
|
41
|
+
rescue SocketError => error
|
42
|
+
logger.warn "Could not connect to host: #{self.class.base_uri}"
|
43
|
+
raise ConnectionFailedError, req
|
44
|
+
|
45
|
+
rescue Timeout::Error => error
|
46
|
+
logger.warn "Request timed out: #{req}"
|
47
|
+
raise Transport::TimeoutError, req
|
48
|
+
|
49
|
+
rescue UpstreamError => error
|
50
|
+
raise error
|
51
|
+
|
52
|
+
rescue Object => error
|
53
|
+
if error.class.name =~ /json/i or error.message =~ /json/i
|
54
|
+
logger.warn("Request returned invalid JSON: #{req}")
|
55
|
+
raise Transport::InvalidJSONError, req
|
56
|
+
else
|
57
|
+
logger.warn("Error trying to call #{req}: #{error.class}: #{error.message}")
|
58
|
+
raise UpstreamError, req
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'rack/test'
|
2
|
+
require 'timeout'
|
3
|
+
|
4
|
+
module Songkick
|
5
|
+
module Transport
|
6
|
+
|
7
|
+
class RackTest < Base
|
8
|
+
class Client
|
9
|
+
attr_reader :app
|
10
|
+
include Rack::Test::Methods
|
11
|
+
|
12
|
+
def initialize(app)
|
13
|
+
@app = app
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(app, options = {})
|
18
|
+
@app = app
|
19
|
+
@timeout = options[:timeout] || DEFAULT_TIMEOUT
|
20
|
+
end
|
21
|
+
|
22
|
+
HTTP_VERBS.each do |verb|
|
23
|
+
class_eval %{
|
24
|
+
def #{verb}(path, params = {}, head = {}, timeout = nil)
|
25
|
+
client = Client.new(@app)
|
26
|
+
start = Time.now
|
27
|
+
request = Request.new(@app, '#{verb}', path, params, headers.merge(head), timeout, start)
|
28
|
+
result = nil
|
29
|
+
|
30
|
+
Timeout.timeout(timeout || @timeout) do
|
31
|
+
request.headers.each { |key, value| client.header(key, value) }
|
32
|
+
response = client.#{verb}(path, params)
|
33
|
+
result = process("\#{path}, \#{params.inspect}", response.status, response.headers, response.body)
|
34
|
+
Reporting.record(request, result)
|
35
|
+
result
|
36
|
+
end
|
37
|
+
|
38
|
+
rescue UpstreamError => error
|
39
|
+
Reporting.record(request, nil, error)
|
40
|
+
raise error
|
41
|
+
|
42
|
+
rescue Object => error
|
43
|
+
logger.warn(error.message)
|
44
|
+
raise UpstreamError, request
|
45
|
+
end
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
@@ -0,0 +1,60 @@
|
|
1
|
+
module Songkick
|
2
|
+
module Transport
|
3
|
+
|
4
|
+
module Reporting
|
5
|
+
def self.report
|
6
|
+
Report.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.record(request, response, error = nil)
|
10
|
+
return unless report = Thread.current[:songkick_transport_report]
|
11
|
+
report.record(request, response, error)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.log_request(request)
|
15
|
+
return unless Transport.verbose?
|
16
|
+
logger.info(request.to_s)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.log_response(response, request)
|
20
|
+
return unless Transport.verbose?
|
21
|
+
duration = (Time.now.to_f - request.start_time.to_f) * 1000
|
22
|
+
logger.info "Response status: #{response.status}, duration: #{duration.ceil}ms"
|
23
|
+
logger.debug "Response data: #{response.data.inspect}"
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.logger
|
27
|
+
Transport.logger
|
28
|
+
end
|
29
|
+
|
30
|
+
class Report
|
31
|
+
include Enumerable
|
32
|
+
extend Forwardable
|
33
|
+
def_delegators :@requests, :each, :first, :last, :length, :size, :[]
|
34
|
+
|
35
|
+
def initialize
|
36
|
+
@requests = []
|
37
|
+
end
|
38
|
+
|
39
|
+
def execute
|
40
|
+
Thread.current[:songkick_transport_report] = self
|
41
|
+
yield
|
42
|
+
ensure
|
43
|
+
Thread.current[:songkick_transport_report] = nil
|
44
|
+
end
|
45
|
+
|
46
|
+
def record(request, response, error)
|
47
|
+
request.response = response
|
48
|
+
request.error = error
|
49
|
+
@requests << request
|
50
|
+
end
|
51
|
+
|
52
|
+
def total_duration
|
53
|
+
inject(0) { |s,r| s + r.duration }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|