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