-
1
require 'active_support/core_ext/benchmark'
-
1
require 'rest_client'
-
1
require 'net/https'
-
1
require 'date'
-
1
require 'time'
-
1
require 'uri'
-
-
1
module ApiResource
-
# Class to handle connections to remote web services.
-
# This class is used by ActiveResource::Base to interface with REST
-
# services.
-
1
class Connection
-
-
1
HTTP_FORMAT_HEADER_NAMES = {
-
:get => 'Accept',
-
:put => 'Content-Type',
-
:post => 'Content-Type',
-
:delete => 'Accept',
-
:head => 'Accept'
-
}
-
-
1
attr_reader :site, :user, :password, :auth_type, :timeout, :proxy, :ssl_options
-
1
attr_accessor :format, :headers
-
-
1
class << self
-
1
def requests
-
@@requests ||= []
-
end
-
end
-
-
# The +site+ parameter is required and will set the +site+
-
# attribute to the URI for the remote resource service.
-
1
def initialize(site, format = ApiResource::Formats::JsonFormat, headers)
-
403
raise ArgumentError, 'Missing site URI' unless site
-
403
@user = @password = nil
-
403
@uri_parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
-
403
self.site = site
-
403
self.format = format
-
403
self.headers = headers
-
end
-
-
# Set URI for remote service.
-
1
def site=(site)
-
403
@site = site.is_a?(URI) ? site : @uri_parser.parse(site)
-
403
@user = @uri_parser.unescape(@site.user) if @site.user
-
403
@password = @uri_parser.unescape(@site.password) if @site.password
-
end
-
-
# Sets the number of seconds after which HTTP requests to the remote service should time out.
-
1
def timeout=(timeout)
-
667
@timeout = timeout
-
end
-
-
1
def get(path, headers = self.headers)
-
# our site and headers for this request
-
210
site = self.site.merge(path)
-
210
headers = build_request_headers(headers, :get, site)
-
-
210
self.with_caching(path, headers) do
-
207
format.decode(request(:get, path, headers))
-
end
-
end
-
-
1
def delete(path, headers = self.headers)
-
4
request(:delete, path, build_request_headers(headers, :delete, self.site.merge(path)))
-
4
return true
-
end
-
-
1
def head(path, headers = self.headers)
-
1
request(:head, path, build_request_headers(headers, :head, self.site.merge(path)))
-
end
-
-
-
1
def put(path, body = {}, headers = self.headers)
-
# If there's a file to send then we can't use JSON or XML
-
2
if !body.is_a?(String) && RestClient::Payload.has_file?(body)
-
format.decode(request(:put, path, body, build_request_headers(headers, :put, self.site.merge(path))))
-
else
-
2
format.decode(request(:put, path, body, build_request_headers(headers, :put, self.site.merge(path))))
-
end
-
end
-
-
1
def post(path, body = {}, headers = self.headers)
-
5
if !body.is_a?(String) && RestClient::Payload.has_file?(body)
-
format.decode(request(:post, path, body, build_request_headers(headers, :post, self.site.merge(path))))
-
else
-
5
format.decode(request(:post, path, body, build_request_headers(headers, :post, self.site.merge(path))))
-
end
-
end
-
-
1
protected
-
-
1
def cache_key(path, headers)
-
5
key = Digest::MD5.hexdigest([path, headers].to_s)
-
5
return "a-#{key}-#{ApiResource::Base.ttl}"
-
end
-
-
1
def with_caching(path, data = {}, &block)
-
210
if ApiResource::Base.ttl.to_f > 0.0
-
5
key = self.cache_key(path, data)
-
5
ApiResource.cache.fetch(key, :expires_in => ApiResource::Base.ttl) do
-
2
yield
-
end
-
else
-
205
yield
-
end
-
end
-
-
1
private
-
# Makes a request to the remote service.
-
1
def request(method, path, *arguments)
-
219
handle_response(path) do
-
219
ActiveSupport::Notifications.instrument("request.api_resource") do |payload|
-
-
# debug logging
-
219
ApiResource.logger.debug("#{method.to_s.upcase} #{site.scheme}://#{site.host}:#{site.port}#{path}")
-
-
219
payload[:method] = method
-
219
payload[:request_uri] = "#{site.scheme}://#{site.host}:#{site.port}#{path}"
-
219
payload[:result] = http(path).send(method, *arguments)
-
end
-
end
-
end
-
-
# Handles response and error codes from the remote service.
-
1
def handle_response(path, &block)
-
219
begin
-
219
result = yield
-
rescue RestClient::RequestTimeout
-
1
raise ApiResource::RequestTimeout.new("Request Time Out - Accessing #{path}}")
-
rescue Exception => error
-
3
if error.respond_to?(:http_code)
-
3
ApiResource.logger.error("#{self} accessing #{path}")
-
3
ApiResource.logger.error(error.message)
-
3
result = error.response
-
else
-
raise ApiResource::ConnectionError.new(nil, :message => "Unknown error #{error}")
-
end
-
end
-
218
return propogate_response_or_error(result, result.code)
-
end
-
-
1
def propogate_response_or_error(response, code)
-
218
case code.to_i
-
when 301,302
-
raise ApiResource::Redirection.new(response)
-
when 200..400
-
213
response.body
-
when 400
-
raise ApiResource::BadRequest.new(response)
-
when 401
-
raise ApiResource::UnauthorizedAccess.new(response)
-
when 403
-
raise ApiResource::ForbiddenAccess.new(response)
-
when 404
-
3
raise ApiResource::ResourceNotFound.new(response)
-
when 405
-
raise ApiResource::MethodNotAllowed.new(response)
-
when 406
-
raise ApiResource::NotAccepatable.new(response)
-
when 409
-
raise ApiResource::ResourceNotFound.new(response)
-
when 410
-
raise ApiResource::ResourceGone.new(response)
-
when 422
-
2
raise ApiResource::UnprocessableEntity.new(response)
-
when 401..499
-
raise ApiResource::ClientError.new(response)
-
when 500..600
-
raise ApiResource::ServerError.new(response)
-
else
-
raise ApiResource::ConnectionError.new(response, :message => "Unknown response code: #{code}")
-
end
-
end
-
-
# Creates new Net::HTTP instance for communication with the
-
# remote service and resources.
-
1
def http(path)
-
4
unless path =~ /\./
-
4
path += ".#{self.format.extension}"
-
end
-
4
RestClient::Resource.new("#{site.scheme}://#{site.host}:#{site.port}#{path}", {:timeout => ApiResource::Base.timeout, :open_timeout => ApiResource::Base.open_timeout})
-
end
-
-
1
def build_request_headers(headers, verb, uri)
-
222
http_format_header(verb).update(headers)
-
end
-
-
1
def http_format_header(verb)
-
222
{}.tap do |ret|
-
222
ret[HTTP_FORMAT_HEADER_NAMES[verb]] = format.mime_type
-
end
-
end
-
end
-
end
-
1
require 'api_resource'
-
-
1
module ApiResource
-
-
1
module Mocks
-
-
1
@@endpoints = {}
-
1
@@path = nil
-
-
# A simple interface class to change the new connection to look like the
-
# old activeresource connection
-
1
class Interface
-
-
1
def initialize(path)
-
218
@path = path
-
end
-
-
1
def get(*args, &block)
-
205
Connection.send(:get, @path, *args, &block)
-
end
-
1
def post(*args, &block)
-
5
Connection.send(:post, @path, *args, &block)
-
end
-
1
def put(*args, &block)
-
2
Connection.send(:put, @path, *args, &block)
-
end
-
1
def delete(*args, &block)
-
4
Connection.send(:delete, @path, *args, &block)
-
end
-
1
def head(*args, &block)
-
1
Connection.send(:head, @path, *args, &block)
-
end
-
end
-
-
# set ApiResource's http
-
1
def self.init
-
2
::ApiResource::Connection.class_eval do
-
2
private
-
2
alias_method :http_without_mock, :http
-
2
def http(path)
-
218
Interface.new(path)
-
end
-
end
-
end
-
-
# set ApiResource's http
-
1
def self.remove
-
1
::ApiResource::Connection.class_eval do
-
1
private
-
1
alias_method :http, :http_without_mock
-
end
-
end
-
-
# clear out the defined mocks
-
1
def self.clear_endpoints
-
1
ret = @@endpoints
-
1
@@endpoints = {}
-
1
ret
-
end
-
# re-set the endpoints
-
1
def self.set_endpoints(new_endpoints)
-
@@endpoints = new_endpoints
-
end
-
# return the defined endpoints
-
1
def self.endpoints
-
@@endpoints
-
end
-
1
def self.define(&block)
-
4
instance_eval(&block) if block_given?
-
end
-
# define an endpoint for the mock
-
1
def self.endpoint(path, &block)
-
23
path, format = path.split(".")
-
23
@@endpoints[path] ||= []
-
23
with_path_and_format(path, format) do
-
23
instance_eval(&block) if block_given?
-
end
-
end
-
# find a matching response
-
1
def self.find_response(request)
-
# these are stored as [[Request, Response], [Request, Response]]
-
211
responses_and_params = self.responses_for_path(request.path)
-
512
ret = (responses_and_params[:responses] || []).select{|pair| pair.first.match?(request)}
-
211
raise Exception.new("More than one response matches #{request}") if ret.length > 1
-
211
return ret.first ? {:response => ret.first[1], :params => responses_and_params[:params]} : nil
-
end
-
-
1
def self.paths_match?(known_path, entered_path)
-
356
PathString.paths_match?(known_path, entered_path)
-
end
-
-
# This method assumes that the two are matching paths
-
# if they aren't the behavior is undefined
-
1
def self.extract_params(known_path, entered_path)
-
13
PathString.extract_params(known_path, entered_path)
-
end
-
-
# returns a hash {:responses => [[Request, Response],[Request,Response]], :params => {...}}
-
# if there is no match returns nil
-
1
def self.responses_for_path(path)
-
214
path = path.split("?").first
-
214
path = path.split(/\./).first
-
# The obvious case
-
214
if @@endpoints[path]
-
195
return {:responses => @@endpoints[path], :params => {}}
-
end
-
# parameter names prefixed with colons should match parts
-
# of the path and push those parameters into the response
-
19
@@endpoints.keys.each do |possible_path|
-
356
if self.paths_match?(possible_path, path)
-
13
return {:responses => @@endpoints[possible_path], :params => self.extract_params(possible_path, path)}
-
end
-
end
-
-
6
return {:responses => nil, :params => nil}
-
end
-
-
-
1
private
-
1
def self.with_path_and_format(path, format, &block)
-
23
@@path, @@format = path, format
-
23
ret = yield
-
23
@@path, @@format = nil, nil
-
23
ret
-
end
-
# define the
-
1
[:post, :put, :get, :delete, :head].each do |verb|
-
5
instance_eval <<-EOE, __FILE__, __LINE__ + 1
-
def #{verb}(response_body, opts = {}, &block)
-
-
raise Exception.new("Must be called from within an endpoint block") unless @@path
-
opts = opts.reverse_merge({:status_code => 200, :response_headers => {}, :params => {}})
-
-
@@endpoints[@@path] << [MockRequest.new(:#{verb}, @@path, :params => opts[:params], :format => @@format), MockResponse.new(response_body, :status_code => opts[:status_code], :headers => opts[:response_headers], :format => @@format, &block)]
-
end
-
EOE
-
end
-
-
1
class MockResponse
-
1
attr_reader :body, :headers, :code, :format, :block
-
1
def initialize(body, opts = {}, &block)
-
44
opts = opts.reverse_merge({:headers => {}, :status_code => 200})
-
44
@body = body
-
44
@headers = opts[:headers]
-
44
@code = opts[:status_code]
-
44
@format = (opts[:format] || :json)
-
44
@block = block if block_given?
-
end
-
1
def []=(key, val)
-
@headers[key] = val
-
end
-
1
def [](key)
-
@headers[key]
-
end
-
-
1
def body
-
220
raise Exception.new("Body must respond to to_#{self.format}") unless @body.respond_to?("to_#{self.format}")
-
220
@body.send("to_#{self.format}")
-
end
-
-
1
def body_as_object
-
return @body
-
end
-
-
1
def generate_response(params)
-
208
@body = @body.instance_exec(params, &self.block) if self.block
-
end
-
end
-
-
1
class MockRequest
-
1
attr_reader :method, :path, :body, :headers, :params, :format, :query
-
-
1
def initialize(method, path, opts = {})
-
248
@method = method.to_sym
-
-
# set the normalized path, format and query string
-
248
@path, @query = path.split("?")
-
248
@path, @format = @path.split(".")
-
-
# if we have params, it is a MockRequest definition
-
248
if opts[:params]
-
34
@params = opts[:params]
-
# otherwise, we need to check either the query string or the body
-
# depending on the http verb
-
else
-
214
case @method
-
when :post, :put
-
5
@params = JSON.parse(opts[:body] || "")
-
when :get, :delete, :head
-
209
@params = typecast_values(
-
Rack::Utils.parse_nested_query(@query || "")
-
)
-
end
-
end
-
248
@body = opts[:body]
-
248
@headers = opts[:headers] || {}
-
248
@headers["Content-Length"] = @body.blank? ? "0" : @body.size.to_s
-
end
-
-
#
-
1
def typecast_values(data)
-
229
if data.is_a?(Hash)
-
211
data.each_pair do |k,v|
-
18
data[k] = typecast_values(v)
-
end
-
elsif data.is_a?(Array)
-
1
data = data.collect{|v|
-
2
typecast_values(v)
-
}
-
else
-
17
data = if data.to_s =~ /^\d+$/
-
data.to_i
-
elsif data =~ /^[\d\.]+$/
-
data.to_f
-
elsif data == "true"
-
11
true
-
elsif data == "false"
-
1
false
-
else
-
5
data
-
end
-
end
-
229
data.nil? ? "" : data
-
end
-
-
# because of the context these come from, we can assume that the path already matches
-
1
def match?(request)
-
301
return false unless self.method == request.method
-
263
return false unless self.format == request.format || request.format.nil? || self.format.nil?
-
263
Comparator.diff(self.params, request.params) == {}
-
end
-
# string representation
-
1
def to_s
-
3
"#{self.method.upcase} #{self.format} #{self.path} #{self.params}"
-
end
-
end
-
1
class Connection
-
-
1
cattr_accessor :requests
-
1
self.requests = []
-
-
# body? methods
-
{ true => %w(post put),
-
1
false => %w(get delete head) }.each do |has_body, methods|
-
2
methods.each do |method|
-
# def post(path, body, headers)
-
# request = ApiResource::Request.new(:post, path, body, headers)
-
# self.class.requests << request
-
# if response = LifebookerClient::Mocks.find_response(request)
-
# response
-
# else
-
# raise InvalidRequestError.new("Could not find a response
-
# recorded for #{request.to_s} - Responses recorded are: -
-
# #{inspect_responses}")
-
# end
-
# end
-
5
instance_eval <<-EOE, __FILE__, __LINE__ + 1
-
def #{method}(path, #{'body, ' if has_body}headers)
-
opts = {:headers => headers}
-
#{"opts[:body] = body" if has_body}
-
request = MockRequest.new(:#{method}, path, opts)
-
self.requests << request
-
if response = Mocks.find_response(request)
-
response[:response].tap{|resp|
-
resp.generate_response(
-
request.params
-
.with_indifferent_access
-
.merge(response[:params].with_indifferent_access)
-
)
-
}
-
else
-
raise ApiResource::ResourceNotFound.new(
-
MockResponse.new({}, {:headers => {"Content-type" => "application/json"}, :status_code => 404}),
-
:message => "\nCould not find a response recorded for \#{request.pretty_inspect}\n" +
-
"Potential Responses Are:\n" +
-
"\#{Array.wrap(Mocks.responses_for_path(request.path)[:responses]).collect(&:first).pretty_inspect}"
-
)
-
end
-
end
-
EOE
-
end
-
end
-
end
-
end
-
end