204 lines
7.2 KiB
Ruby
204 lines
7.2 KiB
Ruby
# Copyright 2015 Google Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
require 'addressable/uri'
|
|
require 'addressable/template'
|
|
require 'google/apis/core/http_command'
|
|
require 'google/apis/errors'
|
|
require 'json'
|
|
require 'retriable'
|
|
|
|
module Google
|
|
module Apis
|
|
module Core
|
|
# Command for executing most basic API request with JSON requests/responses
|
|
class ApiCommand < HttpCommand
|
|
JSON_CONTENT_TYPE = 'application/json'
|
|
FIELDS_PARAM = 'fields'
|
|
ERROR_REASON_MAPPING = {
|
|
'rateLimitExceeded' => Google::Apis::RateLimitError,
|
|
'userRateLimitExceeded' => Google::Apis::RateLimitError,
|
|
'projectNotLinked' => Google::Apis::ProjectNotLinkedError
|
|
}
|
|
|
|
# JSON serializer for request objects
|
|
# @return [Google::Apis::Core::JsonRepresentation]
|
|
attr_accessor :request_representation
|
|
|
|
# Request body to serialize
|
|
# @return [Object]
|
|
attr_accessor :request_object
|
|
|
|
# JSON serializer for response objects
|
|
# @return [Google::Apis::Core::JsonRepresentation]
|
|
attr_accessor :response_representation
|
|
|
|
# Class to instantiate when de-serializing responses
|
|
# @return [Object]
|
|
attr_accessor :response_class
|
|
|
|
# Serialize the request body
|
|
#
|
|
# @return [void]
|
|
def prepare!
|
|
set_xgac
|
|
if options&.authorization.respond_to? :quota_project_id
|
|
quota_project_id = options.authorization.quota_project_id
|
|
header['X-Goog-User-Project'] = quota_project_id if quota_project_id
|
|
end
|
|
if options && options.api_format_version
|
|
header['X-Goog-Api-Format-Version'] = options.api_format_version.to_s
|
|
end
|
|
query[FIELDS_PARAM] = normalize_fields_param(query[FIELDS_PARAM]) if query.key?(FIELDS_PARAM)
|
|
if request_representation && request_object
|
|
header['Content-Type'] ||= JSON_CONTENT_TYPE
|
|
if options && options.skip_serialization
|
|
self.body = request_object
|
|
else
|
|
self.body = request_representation.new(request_object).to_json(user_options: { skip_undefined: true })
|
|
end
|
|
end
|
|
super
|
|
end
|
|
|
|
# Deserialize the response body if present
|
|
#
|
|
# @param [String] content_type
|
|
# Content type of body
|
|
# @param [String, #read] body
|
|
# Response body
|
|
# @return [Object]
|
|
# Response object
|
|
# noinspection RubyUnusedLocalVariable
|
|
def decode_response_body(content_type, body)
|
|
return super unless response_representation
|
|
return super if options && options.skip_deserialization
|
|
return super if content_type.nil?
|
|
return nil unless content_type.start_with?(JSON_CONTENT_TYPE)
|
|
instance = response_class.new
|
|
response_representation.new(instance).from_json(body, unwrap: response_class)
|
|
instance
|
|
end
|
|
|
|
# Check the response and raise error if needed
|
|
#
|
|
# @param [Fixnum] status
|
|
# HTTP status code of response
|
|
# @param [Hash] header
|
|
# HTTP response headers
|
|
# @param [String] body
|
|
# HTTP response body
|
|
# @param [String] message
|
|
# Error message text
|
|
# @return [void]
|
|
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
|
|
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
|
|
# @raise [Google::Apis::AuthorizationError] Authorization is required
|
|
def check_status(status, header = nil, body = nil, message = nil)
|
|
case status
|
|
when 400, 402...500
|
|
reason, message = parse_error(body)
|
|
if reason
|
|
message = sprintf('%s: %s', reason, message)
|
|
raise ERROR_REASON_MAPPING[reason].new(
|
|
message,
|
|
status_code: status,
|
|
header: header,
|
|
body: body
|
|
) if ERROR_REASON_MAPPING.key?(reason)
|
|
end
|
|
super(status, header, body, message)
|
|
else
|
|
super(status, header, body, message)
|
|
end
|
|
end
|
|
|
|
def allow_form_encoding?
|
|
request_representation.nil? && super
|
|
end
|
|
|
|
private
|
|
|
|
def set_xgac
|
|
old_xgac = header
|
|
.find_all { |k, v| k.downcase == 'x-goog-api-client' }
|
|
.map { |(a, b)| b }
|
|
.join(' ')
|
|
.split
|
|
.find_all { |s| s !~ %r{^gl-ruby/|^gdcl/} }
|
|
.join(' ')
|
|
xgac = "gl-ruby/#{RUBY_VERSION} gdcl/#{Google::Apis::VERSION}"
|
|
xgac = old_xgac.empty? ? xgac : "#{old_xgac} #{xgac}"
|
|
header.delete_if { |k, v| k.downcase == 'x-goog-api-client' }
|
|
header['X-Goog-Api-Client'] = xgac
|
|
end
|
|
|
|
# Attempt to parse a JSON error message
|
|
# @param [String] body
|
|
# HTTP response body
|
|
# @return [Array<(String, String)>]
|
|
# Error reason and message
|
|
def parse_error(body)
|
|
obj = JSON.load(body)
|
|
error = obj['error']
|
|
if error['details']
|
|
return extract_v2_error_details(error)
|
|
elsif error['errors']
|
|
return extract_v1_error_details(error)
|
|
else
|
|
fail 'Can not parse error message. No "details" or "errors" detected'
|
|
end
|
|
rescue
|
|
return [nil, nil]
|
|
end
|
|
|
|
# Extracts details from a v1 error message
|
|
# @param [Hash] error
|
|
# Parsed JSON
|
|
# @return [Array<(String, String)>]
|
|
# Error reason and message
|
|
def extract_v1_error_details(error)
|
|
reason = error['errors'].first['reason']
|
|
message = error['message']
|
|
return [reason, message]
|
|
end
|
|
|
|
# Extracts details from a v2error message
|
|
# @param [Hash] error
|
|
# Parsed JSON
|
|
# @return [Array<(String, String)>]
|
|
# Error reason and message
|
|
def extract_v2_error_details(error)
|
|
reason = error['status']
|
|
message = error['message']
|
|
return [reason, message]
|
|
end
|
|
|
|
# Convert field names from ruby conventions to original names in JSON
|
|
#
|
|
# @param [String] fields
|
|
# Value of 'fields' param
|
|
# @return [String]
|
|
# Updated header value
|
|
def normalize_fields_param(fields)
|
|
# TODO: Generate map of parameter names during code gen. Small possibility that camelization fails
|
|
fields.gsub(/:/, '').gsub(/\w+/) do |str|
|
|
str.gsub(/(?:^|_)([a-z])/){ Regexp.last_match.begin(0) == 0 ? $1 : $1.upcase }
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|