require 'aws/core/util'
module AWS
class UnsuccessfulResponse < RuntimeError
attr_reader :code
attr_reader :error_type
attr_reader :error_message
def initialize(code, error_type, error_message)
super "#{error_type} (#{code}): #{error_message}"
@code = code
@error_type = error_type
@error_message = error_message
end
end
class UnknownErrorResponse < RuntimeError
def initialize(body)
super "Unable to parse error code from #{body.inspect}"
end
end
##
# Wrapper object for all responses from AWS. This class gives
# a lot of leeway to how you access the response object.
# You can access the response directly through it's Hash representation,
# which is a direct mapping from the raw XML returned from AWS.
#
# You can also use ruby methods. This object will convert those methods
# in ruby_standard into appropriate keys (camelCase) and look for them
# in the hash. This can be done at any depth.
#
# This class tries not to be too magical to ensure that
# it never gets in the way. All nested objects are queryable like their
# parents are, and all sets and arrays are found and accessible through
# your typical Enumerable interface.
#
# The starting point of the Response querying will vary according to the structure
# returned by the AWS API in question. For some APIs, like EC2, the response is
# a relatively flat:
#
#
# ...
#
# ...
#
#
#
# In this case, your querying will start inside of , ala the first
# method you'll probably call is +data_requested+. For other APIs, the response
# object is a little deeper and looks like this:
#
#
#
#
# ...
#
#
#
# ...
#
#
#
# For these response structures, your query will start inside of ,
# ala your first method call will be +data_requested+. To get access to the request id of
# both of these structures, simply use #request_id on the base response. You'll also
# notice the case differences of the XML tags, this class tries to ensure that case doesn't
# matter when you're querying with methods. If you're using raw hash access then yes the
# case of the keys in question need to match.
#
# This class does ensure that any collection is always an Array, given that
# when AWS returns a single item in a collection, the xml -> hash parser gives a
# single hash back instead of an array. This class will also look for
# array indicators from AWS, like - or and squash them.
#
# If AWS returns an error code, instead of getting a Response back the library
# will instead throw an UnsuccessfulResponse error with the pertinent information.
##
class Response
# Inner proxy class that handles converting ruby methods
# into keys found in the underlying Hash.
class ResponseProxy
include Enumerable
TO_SQUASH = %w(item member)
def initialize(local_root)
first_key = local_root.keys.first
if local_root.keys.length == 1 && TO_SQUASH.include?(first_key)
# Ensure squash key is ignored and it's children are always
# turned into an array.
@local_root = [local_root[first_key]].flatten.map do |entry|
ResponseProxy.new entry
end
else
@local_root = local_root
end
end
def [](key_or_idx)
value_or_proxy @local_root[key_or_idx]
end
##
# Get all keys at the current depth of the Response object.
# This method will raise a NoMethodError if the current
# depth is an array.
##
def keys
@local_root.keys
end
def length
@local_root.length
end
def each(&block)
@local_root.each(&block)
end
def method_missing(name, *args)
if key = key_matching(name)
value_or_proxy @local_root[key]
else
super
end
end
protected
def key_matching(name)
return nil if @local_root.is_a? Array
lower_base_aws_name = AWS::Util.camelcase name.to_s, :lower
upper_base_aws_name = AWS::Util.camelcase name.to_s
keys = @local_root.keys
if keys.include? lower_base_aws_name
lower_base_aws_name
elsif keys.include? upper_base_aws_name
upper_base_aws_name
end
end
def value_or_proxy(value)
case value
when Hash
ResponseProxy.new value
when Array
value.map {|v| ResponseProxy.new v }
else
value
end
end
end
##
# The raw parsed response body in Hash format
##
attr_reader :body
##
# HTTP Status code of the response
##
attr_reader :code
##
# Hash of headers found in the response
##
attr_reader :headers
def initialize(http_response)
if !http_response.success?
parse_and_throw_error_from http_response
end
@code = http_response.code
@body = http_response.parsed_response
@headers = http_response.headers
if @body.is_a?(Hash)
inner = @body[@body.keys.first]
response_root =
if result_key = inner.keys.find {|k| k =~ /Result$/}
inner[result_key]
else
inner
end
if response_root
@request_root = ResponseProxy.new response_root
end
end
end
##
# Direct access to the request body's hash.
# This works on the first level down in the AWS response, bypassing
# the root element of the returned XML so you can work directly in the
# attributes that matter
##
def [](key)
@request_root[key]
end
##
# Delegate first-level method calls to the root Proxy object
##
def method_missing(name, *args)
if @request_root
@request_root.send(name, *args)
else
super
end
end
##
# Get the request ID from this response. Works on all known AWS response formats.
# Some AWS APIs don't give a request id, such as CloudFront. For responses that
# do not have a request id, this method returns nil.
##
def request_id
if metadata = @body[@body.keys.first]["ResponseMetadata"]
metadata["RequestId"]
elsif id = @body[@body.keys.first]["requestId"]
id
else
nil
end
end
protected
def parse_and_throw_error_from(http_response)
if http_response.parsed_response
error = parse_error_from http_response.parsed_response
else
error = { "Message" => http_response.response }
end
raise UnsuccessfulResponse.new(
http_response.code,
error["Code"],
error["Message"]
)
end
def parse_error_from(body)
if body.has_key? "ErrorResponse"
body["ErrorResponse"]["Error"]
elsif body.has_key? "Error"
if body["Error"]["StringToSign"]
body["Error"]["Message"] += " String to Sign: #{body["Error"]["StringToSign"].inspect}"
end
body["Error"]
elsif body.has_key? "Response"
body["Response"]["Errors"]["Error"]
else
raise UnknownErrorResponse.new body
end
end
end
end