google-api-ruby-client/lib/google/apis/core/batch.rb

237 lines
8.0 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.
# 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 'google/apis/core/multipart'
require 'google/apis/core/http_command'
require 'google/apis/core/upload'
require 'google/apis/core/download'
require 'google/apis/core/composite_io'
require 'addressable/uri'
require 'securerandom'
module Google
module Apis
module Core
# Wrapper request for batching multiple calls in a single server request
class BatchCommand < HttpCommand
MULTIPART_MIXED = 'multipart/mixed'
# @param [symbol] method
# HTTP method
# @param [String,Addressable::URI, Addressable::Template] url
# HTTP URL or template
def initialize(method, url)
super(method, url)
@calls = []
@base_id = SecureRandom.uuid
end
##
# Add a new call to the batch request.
#
# @param [Google::Apis::Core::HttpCommand] call API Request to add
# @yield [result, err] Result & error when response available
# @return [Google::Apis::Core::BatchCommand] self
def add(call, &block)
ensure_valid_command(call)
@calls << [call, block]
self
end
protected
##
# Deconstruct the batch response and process the individual results
#
# @param [String] content_type
# Content type of body
# @param [String, #read] body
# Response body
# @return [Object]
# Response object
def decode_response_body(content_type, body)
m = /.*boundary=(.+)/.match(content_type)
if m
parts = split_parts(body, m[1])
deserializer = CallDeserializer.new
parts.each_index do |index|
response = deserializer.to_http_response(parts[index])
outer_header = response.shift
call_id = header_to_id(outer_header['Content-ID'].first) || index
call, callback = @calls[call_id]
begin
result = call.process_response(*response) unless call.nil?
success(result, &callback)
rescue => e
error(e, &callback)
end
end
end
nil
end
def split_parts(body, boundary)
parts = body.split(/\r?\n?--#{Regexp.escape(boundary)}/)
parts[1...-1]
end
# Encode the batch request
# @return [void]
# @raise [Google::Apis::BatchError] if batch is empty
def prepare!
fail BatchError, 'Cannot make an empty batch request' if @calls.empty?
serializer = CallSerializer.new
multipart = Multipart.new(content_type: MULTIPART_MIXED)
@calls.each_index do |index|
call, _ = @calls[index]
content_id = id_to_header(index)
io = serializer.to_part(call)
multipart.add_upload(io, content_type: 'application/http', content_id: content_id)
end
self.body = multipart.assemble
header['Content-Type'] = multipart.content_type
super
end
def ensure_valid_command(command)
if command.is_a?(Google::Apis::Core::BaseUploadCommand) || command.is_a?(Google::Apis::Core::DownloadCommand)
fail Google::Apis::ClientError, 'Can not include media requests in batch'
end
fail Google::Apis::ClientError, 'Invalid command object' unless command.is_a?(HttpCommand)
end
def id_to_header(call_id)
return sprintf('<%s+%i>', @base_id, call_id)
end
def header_to_id(content_id)
match = /<response-.*\+(\d+)>/.match(content_id)
return match[1].to_i if match
return nil
end
end
# Wrapper request for batching multiple uploads in a single server request
class BatchUploadCommand < BatchCommand
def ensure_valid_command(command)
fail Google::Apis::ClientError, 'Can only include upload commands in batch' \
unless command.is_a?(Google::Apis::Core::BaseUploadCommand)
end
def prepare!
header['X-Goog-Upload-Protocol'] = 'batch'
super
end
end
# Serializes a command for embedding in a multipart batch request
# @private
class CallSerializer
##
# Serialize a single batched call for assembling the multipart message
#
# @param [Google::Apis::Core::HttpCommand] call
# the call to serialize.
# @return [IO]
# the serialized request
def to_part(call)
call.prepare!
# This will add the Authorization header if needed.
call.apply_request_options(call.header)
parts = []
parts << build_head(call)
parts << build_body(call) unless call.body.nil?
length = parts.inject(0) { |a, e| a + e.length }
Google::Apis::Core::CompositeIO.new(*parts)
end
protected
def build_head(call)
request_head = "#{call.method.to_s.upcase} #{Addressable::URI.parse(call.url).request_uri} HTTP/1.1"
call.header.each do |key, value|
request_head << sprintf("\r\n%s: %s", key, value)
end
request_head << sprintf("\r\nHost: %s", call.url.host)
request_head << "\r\n\r\n"
StringIO.new(request_head)
end
def build_body(call)
return nil if call.body.nil?
return call.body if call.body.respond_to?(:read)
StringIO.new(call.body)
end
end
# Deconstructs a raw HTTP response part
# @private
class CallDeserializer
# Parse a batched response.
#
# @param [String] call_response
# the response to parse.
# @return [Array<(Fixnum, Hash, String)>]
# Status, header, and response body.
def to_http_response(call_response)
outer_header, outer_body = split_header_and_body(call_response)
status_line, payload = outer_body.split(/\n/, 2)
_, status = status_line.split(' ', 3)
header, body = split_header_and_body(payload)
[outer_header, status.to_i, header, body]
end
protected
# Auxiliary method to split the header from the body in an HTTP response.
#
# @param [String] response
# the response to parse.
# @return [Array<(HTTP::Message::Headers, String)>]
# the header and the body, separately.
def split_header_and_body(response)
header = HTTP::Message::Headers.new
payload = response.lstrip
while payload
line, payload = payload.split(/\n/, 2)
line.sub!(/\s+\z/, '')
break if line.empty?
match = /\A([^:]+):\s*/.match(line)
fail BatchError, sprintf('Invalid header line in response: %s', line) if match.nil?
header[match[1]] = match.post_match
end
[header, payload]
end
end
end
end
end