Merge pull request #564 from google/v0.10

Merge v0.10 branch (now 0.11)
This commit is contained in:
Steve Bazyl 2017-04-03 12:30:21 -07:00 committed by GitHub
commit 8ef5c84c78
34 changed files with 661 additions and 597 deletions

View File

@ -1,27 +1,21 @@
language: ruby language: ruby
sudo: false
rvm: rvm:
- 2.3.0 - 2.3.1
- 2.2 - 2.2.5
- 2.1
- 2.0.0 - 2.0.0
- jruby-9000 - 2.1
- jruby-9.0.5.0
env: env:
global:
- JRUBY_OPTS='-X-C -J-Xmx1024m -J-XX:+UseConcMarkSweepGC'
matrix:
- RAILS_VERSION="~>3.2"
- RAILS_VERSION="~>4.0.0"
- RAILS_VERSION="~>4.1.0"
- RAILS_VERSION="~>4.2.0" - RAILS_VERSION="~>4.2.0"
- RAILS_VERSION="~>5.0.0"
matrix: matrix:
exclude: exclude:
- rvm: 2.0.0 - env: RAILS_VERSION="~>5.0.0"
env: RAILS_VERSION="~>4.2.0" rvm: 2.0.0
script: "bundle exec rake spec:all" - env: RAILS_VERSION="~>5.0.0"
before_install: rvm: 2.1
- sudo apt-get update before_install: gem install bundler
- sudo apt-get install idn
- gem update bundler
notifications: notifications:
email: email:
recipients: recipients:

View File

@ -1,7 +1,24 @@
# 0.11.0
* *Breaking change* - Fix handling of large numbers during code generation. Previously the
uint64/int64 formats were passed through as strings. They are now coerced to/from Fixnum/Bignum types
* *Breaking change* - No longer normalize unicode strings in URI templates. Mostly affects
Cloud Storage, but other APIs with unicode strings in paths may be affected. Old behavior
can be restored using the `normalize_unicode` request option.
* *Breaking change* -- Moved timeout options from `RequestOptions` to `ClientOptions`
* Remove Hurley as dependency. May cause minor breaking changes if directly accessing the underlying
client connection.
* Drop compatibility with Rails 3.x since that is no longer supported by the Rails community.
* Upgrade mime-types to 3.0
* Move Thor & ActiveSupport to development dependencies. Using the code gengerator
now requires using the Bundle file or install the gem with dev dependencies.
* Treat 429 status codes as rate limit errors
* Fix a potential download corruption if download interrupted and retried against a URL
that does not return partial content.
# 0.10.3 # 0.10.3
* Regenerate APIs * Regenerate APIs
* Enable additional API: * Enable additional API:
* `acceleratedmobilepageurl:v1`` * `acceleratedmobilepageurl:v1`
* `appengine:v1` * `appengine:v1`
* `clouderrorreporting:v1beta1` * `clouderrorreporting:v1beta1`
* `cloudfunctions:v1` * `cloudfunctions:v1`
@ -103,7 +120,6 @@
* Reduce memory footprint used by mimetypes library * Reduce memory footprint used by mimetypes library
* Fix bug with pagination when items collection is nil * Fix bug with pagination when items collection is nil
# 0.9.9 # 0.9.9
* Add monitoring v3, regenerate APIs * Add monitoring v3, regenerate APIs
* Add samples for sheets, bigquery * Add samples for sheets, bigquery

12
Gemfile
View File

@ -6,22 +6,20 @@ gemspec
group :development do group :development do
gem 'bundler', '~> 1.7' gem 'bundler', '~> 1.7'
gem 'rake', '~> 10.0' gem 'rake', '~> 11.2'
gem 'rspec', '~> 3.1' gem 'rspec', '~> 3.1'
gem 'json_spec', '~> 1.1' gem 'json_spec', '~> 1.1'
gem 'webmock', '~> 1.21' gem 'webmock', '~> 2.1'
gem 'simplecov', '~> 0.9' gem 'simplecov', '~> 0.12'
gem 'coveralls', '~> 0.7.11' gem 'coveralls', '~> 0.8'
gem 'rubocop', '~> 0.29' gem 'rubocop', '~> 0.42.0'
gem 'launchy', '~> 2.4' gem 'launchy', '~> 2.4'
gem 'dotenv', '~> 2.0' gem 'dotenv', '~> 2.0'
gem 'fakefs', '~> 0.6', require: "fakefs/safe" gem 'fakefs', '~> 0.6', require: "fakefs/safe"
gem 'google-id-token', '~> 1.3' gem 'google-id-token', '~> 1.3'
gem 'os', '~> 0.9' gem 'os', '~> 0.9'
gem 'rmail', '~> 1.1' gem 'rmail', '~> 1.1'
gem 'sinatra', '~> 1.4'
gem 'redis', '~> 3.2' gem 'redis', '~> 3.2'
gem 'activesupport', '>= 3.2', '< 5.0'
end end
platforms :jruby do platforms :jruby do

View File

@ -1,3 +1,36 @@
# Migrating from version`0.10` to `0.11`
## Unicode normalization
The client no longer normalizes unicode strings in path parameters. This may affect
some applications using multibyte strings that were previously normalized.:
To restore the previous behavior, set the following option prior to creating a service.
```ruby
ClientOptions.default.normalize_unicode = true
```
## Type change for large numbers
Previously, types declared as 64 bit numbers were mapped to strings. These are now mapped to
`Fixednum`/`Bignum`.
## Timeouts
Timeout options have been moved from `RequestOptions` to `ClientOptions`.
Old | New
----------------------------------|-----------------
`RequestOptions.open_timeout_sec` | `ClentOptions.open_timeout_sec`
`RequestOptions.timeout_sec` | `ClentOptions.read_timeout_sec`
`RequestOptions.timeout_sec` | `ClentOptions.send_timeout_sec`
## Batch requests across services no longer supported
It is no longer possible to combine multiple services (e.g. Gail & Drive)
in a batch request. If batching requests that span services, group
requests for each service in their own batch request.
# Migrating from version `0.9.x` to `0.10` # Migrating from version `0.9.x` to `0.10`
Only one minor breaking change was introduced in the `to_json` method due to a version bump for the `representable` gem from `2.3` to `3.0`. If you used the `skip_undefined` in `to_json`, you should replace that with `user_options: { skip_undefined: true }`. Only one minor breaking change was introduced in the `to_json` method due to a version bump for the `representable` gem from `2.3` to `3.0`. If you used the `skip_undefined` in `to_json`, you should replace that with `user_options: { skip_undefined: true }`.

View File

@ -191,6 +191,14 @@ file = drive.create_file(file) # Raises ArgumentError: unknown keywords: id, tit
file = drive.create_file(file, {}) # Returns a Drive::File instance file = drive.create_file(file, {}) # Returns a Drive::File instance
``` ```
### Using raw JSON
To handle JSON serialization or deserialization in the application, set `skip_serialization` or
or `skip_deserializaton` options respectively. When setting `skip_serialization` in a request,
the body object must be a string representing the serialized JSON.
When setting `skip_deserialization` to true, the response from the API will likewise
be a string containing the raw JSON from the server.
## Authorization ## Authorization
@ -283,7 +291,7 @@ The second is to set the environment variable `GOOGLE_API_USE_RAILS_LOGGER` to a
## Samples ## Samples
See the [samples](samples) for examples on how to use the client library for various See the [samples](https://github.com/google/google-api-ruby-client/tree/master/samples) for examples on how to use the client library for various
services. services.
Contributions for additional samples are welcome. See [CONTRIBUTING](CONTRIBUTING.md). Contributions for additional samples are welcome. See [CONTRIBUTING](CONTRIBUTING.md).

View File

@ -1,2 +1,3 @@
require "bundler/gem_tasks" require "bundler/gem_tasks"
task default: :spec

View File

@ -1,6 +1,12 @@
#!/usr/bin/env ruby #!/usr/bin/env ruby
require 'thor' begin
require 'thor'
rescue LoadError => e
puts "Thor is required. Please install the gem with development dependencies."
exit 1
end
require 'open-uri' require 'open-uri'
require 'google/apis/discovery_v1' require 'google/apis/discovery_v1'
require 'logger' require 'logger'

0
dl.rb Normal file
View File

View File

@ -22,12 +22,10 @@ Gem::Specification.new do |spec|
spec.add_runtime_dependency 'representable', '~> 3.0' spec.add_runtime_dependency 'representable', '~> 3.0'
spec.add_runtime_dependency 'retriable', '>= 2.0', '< 4.0' spec.add_runtime_dependency 'retriable', '>= 2.0', '< 4.0'
spec.add_runtime_dependency 'addressable', '~> 2.3' spec.add_runtime_dependency 'addressable', '>= 2.5.1'
spec.add_runtime_dependency 'mime-types', '>= 1.6' spec.add_runtime_dependency 'mime-types', '>= 3.0'
spec.add_runtime_dependency 'hurley', '~> 0.1'
spec.add_runtime_dependency 'googleauth', '~> 0.5' spec.add_runtime_dependency 'googleauth', '~> 0.5'
spec.add_runtime_dependency 'httpclient', '~> 2.7' spec.add_runtime_dependency 'httpclient', '>= 2.8.1', '< 3.0'
spec.add_runtime_dependency 'memoist', '~> 0.11'
spec.add_development_dependency 'thor', '~> 0.19' spec.add_development_dependency 'thor', '~> 0.19'
spec.add_development_dependency 'activesupport', '>= 4.2', '< 5.1'
end end

View File

@ -54,9 +54,13 @@ module Google
def prepare! def prepare!
query[FIELDS_PARAM] = normalize_fields_param(query[FIELDS_PARAM]) if query.key?(FIELDS_PARAM) query[FIELDS_PARAM] = normalize_fields_param(query[FIELDS_PARAM]) if query.key?(FIELDS_PARAM)
if request_representation && request_object if request_representation && request_object
header[:content_type] ||= JSON_CONTENT_TYPE 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 }) self.body = request_representation.new(request_object).to_json(user_options: { skip_undefined: true })
end end
end
super super
end end
@ -71,6 +75,7 @@ module Google
# noinspection RubyUnusedLocalVariable # noinspection RubyUnusedLocalVariable
def decode_response_body(content_type, body) def decode_response_body(content_type, body)
return super unless response_representation return super unless response_representation
return super if options && options.skip_deserialization
return super if content_type.nil? return super if content_type.nil?
return nil unless content_type.start_with?(JSON_CONTENT_TYPE) return nil unless content_type.start_with?(JSON_CONTENT_TYPE)
instance = response_class.new instance = response_class.new
@ -82,7 +87,7 @@ module Google
# #
# @param [Fixnum] status # @param [Fixnum] status
# HTTP status code of response # HTTP status code of response
# @param [Hurley::Header] header # @param [Hash] header
# HTTP response headers # HTTP response headers
# @param [String] body # @param [String] body
# HTTP response body # HTTP response body

View File

@ -19,11 +19,9 @@ require 'google/apis/core/api_command'
require 'google/apis/core/batch' require 'google/apis/core/batch'
require 'google/apis/core/upload' require 'google/apis/core/upload'
require 'google/apis/core/download' require 'google/apis/core/download'
require 'google/apis/core/http_client_adapter'
require 'google/apis/options' require 'google/apis/options'
require 'googleauth' require 'googleauth'
require 'hurley' require 'httpclient'
require 'hurley/addressable'
module Google module Google
module Apis module Apis
@ -86,6 +84,8 @@ module Google
# Base service for all APIs. Not to be used directly. # Base service for all APIs. Not to be used directly.
# #
class BaseService class BaseService
include Logging
# Root URL (host/port) for the API # Root URL (host/port) for the API
# @return [Addressable::URI] # @return [Addressable::URI]
attr_accessor :root_url attr_accessor :root_url
@ -103,7 +103,7 @@ module Google
attr_accessor :batch_path attr_accessor :batch_path
# HTTP client # HTTP client
# @return [Hurley::Client] # @return [HTTPClient]
attr_accessor :client attr_accessor :client
# General settings # General settings
@ -205,7 +205,7 @@ module Google
end end
# Get the current HTTP client # Get the current HTTP client
# @return [Hurley::Client] # @return [HTTPClient]
def client def client
@client ||= new_client @client ||= new_client
end end
@ -375,19 +375,33 @@ module Google
end end
# Create a new HTTP client # Create a new HTTP client
# @return [Hurley::Client] # @return [HTTPClient]
def new_client def new_client
client = Hurley::Client.new client = ::HTTPClient.new
client.connection = Google::Apis::Core::HttpClientAdapter.new unless client_options.use_net_http
client.request_options.timeout = request_options.timeout_sec client.transparent_gzip_decompression = true
client.request_options.open_timeout = request_options.open_timeout_sec client.proxy = client_options.proxy_url if client_options.proxy_url
client.request_options.proxy = client_options.proxy_url
client.request_options.query_class = Hurley::Query::Flat if client_options.open_timeout_sec
client.ssl_options.ca_file = File.join(Google::Apis::ROOT, 'lib', 'cacerts.pem') client.connect_timeout = client_options.open_timeout_sec
client.header[:user_agent] = user_agent end
if client_options.read_timeout_sec
client.receive_timeout = client_options.read_timeout_sec
end
if client_options.send_timeout_sec
client.send_timeout = client_options.send_timeout_sec
end
client.follow_redirect_count = 5
client.default_header = { 'User-Agent' => user_agent }
client.debug_dev = logger if client_options.log_http_requests
client client
end end
# Build the user agent header # Build the user agent header
# @return [String] # @return [String]
def user_agent def user_agent

View File

@ -25,19 +25,19 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
require 'hurley'
require 'google/apis/core/multipart' require 'google/apis/core/multipart'
require 'google/apis/core/http_command' require 'google/apis/core/http_command'
require 'google/apis/core/upload' require 'google/apis/core/upload'
require 'google/apis/core/download' require 'google/apis/core/download'
require 'google/apis/core/composite_io'
require 'addressable/uri' require 'addressable/uri'
require 'securerandom' require 'securerandom'
module Google module Google
module Apis module Apis
module Core module Core
# Wrapper request for batching multiple calls in a single server request # Wrapper request for batching multiple calls in a single server request
class BatchCommand < HttpCommand class BatchCommand < HttpCommand
BATCH_BOUNDARY = 'RubyApiBatchRequest'.freeze
MULTIPART_MIXED = 'multipart/mixed' MULTIPART_MIXED = 'multipart/mixed'
# @param [symbol] method # @param [symbol] method
@ -81,7 +81,7 @@ module Google
parts.each_index do |index| parts.each_index do |index|
response = deserializer.to_http_response(parts[index]) response = deserializer.to_http_response(parts[index])
outer_header = response.shift outer_header = response.shift
call_id = header_to_id(outer_header[:content_id]) || index call_id = header_to_id(outer_header['Content-ID'].first) || index
call, callback = @calls[call_id] call, callback = @calls[call_id]
begin begin
result = call.process_response(*response) unless call.nil? result = call.process_response(*response) unless call.nil?
@ -106,17 +106,16 @@ module Google
fail BatchError, 'Cannot make an empty batch request' if @calls.empty? fail BatchError, 'Cannot make an empty batch request' if @calls.empty?
serializer = CallSerializer.new serializer = CallSerializer.new
multipart = Multipart.new(boundary: BATCH_BOUNDARY, content_type: MULTIPART_MIXED) multipart = Multipart.new(content_type: MULTIPART_MIXED)
@calls.each_index do |index| @calls.each_index do |index|
call, _ = @calls[index] call, _ = @calls[index]
content_id = id_to_header(index) content_id = id_to_header(index)
io = serializer.to_upload_io(call) io = serializer.to_part(call)
multipart.add_upload(io, content_id: content_id) multipart.add_upload(io, content_type: 'application/http', content_id: content_id)
end end
self.body = multipart.assemble self.body = multipart.assemble
header[:content_type] = multipart.content_type header['Content-Type'] = multipart.content_type
header[:content_length] = "#{body.length}"
super super
end end
@ -155,24 +154,20 @@ module Google
# Serializes a command for embedding in a multipart batch request # Serializes a command for embedding in a multipart batch request
# @private # @private
class CallSerializer class CallSerializer
HTTP_CONTENT_TYPE = 'application/http'
## ##
# Serialize a single batched call for assembling the multipart message # Serialize a single batched call for assembling the multipart message
# #
# @param [Google::Apis::Core::HttpCommand] call # @param [Google::Apis::Core::HttpCommand] call
# the call to serialize. # the call to serialize.
# @return [Hurley::UploadIO] # @return [IO]
# the serialized request # the serialized request
def to_upload_io(call) def to_part(call)
call.prepare! call.prepare!
parts = [] parts = []
parts << build_head(call) parts << build_head(call)
parts << build_body(call) unless call.body.nil? parts << build_body(call) unless call.body.nil?
length = parts.inject(0) { |a, e| a + e.length } length = parts.inject(0) { |a, e| a + e.length }
Hurley::UploadIO.new(Hurley::CompositeReadIO.new(length, *parts), Google::Apis::Core::CompositeIO.new(*parts)
HTTP_CONTENT_TYPE,
'ruby-api-request')
end end
protected protected
@ -201,7 +196,7 @@ module Google
# #
# @param [String] call_response # @param [String] call_response
# the response to parse. # the response to parse.
# @return [Array<(Fixnum, Hurley::Header, String)>] # @return [Array<(Fixnum, Hash, String)>]
# Status, header, and response body. # Status, header, and response body.
def to_http_response(call_response) def to_http_response(call_response)
outer_header, outer_body = split_header_and_body(call_response) outer_header, outer_body = split_header_and_body(call_response)
@ -218,10 +213,10 @@ module Google
# #
# @param [String] response # @param [String] response
# the response to parse. # the response to parse.
# @return [Array<(Hurley::Header, String)>] # @return [Array<(HTTP::Message::Headers, String)>]
# the header and the body, separately. # the header and the body, separately.
def split_header_and_body(response) def split_header_and_body(response)
header = Hurley::Header.new header = HTTP::Message::Headers.new
payload = response.lstrip payload = response.lstrip
while payload while payload
line, payload = payload.split(/\n/, 2) line, payload = payload.split(/\n/, 2)

View File

@ -0,0 +1,97 @@
# 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/http_command'
require 'google/apis/core/upload'
require 'google/apis/core/download'
require 'addressable/uri'
require 'securerandom'
module Google
module Apis
module Core
class CompositeIO
def initialize(*ios)
@ios = ios.flatten
@pos = 0
@index = 0
@sizes = @ios.map(&:size)
end
def read(length = nil, buf = nil)
buf = buf ? buf.replace('') : ''
begin
io = @ios[@index]
break if io.nil?
result = io.read(length)
if result
buf << result
if length
length -= result.length
break if length == 0
end
end
@index += 1
end while @index < @ios.length
buf.length > 0 ? buf : nil
end
def size
@sizes.reduce(:+)
end
alias_method :length, :size
def pos
@pos
end
def pos=(pos)
fail ArgumentError, "Position can not be negative" if pos < 0
@pos = pos
new_index = nil
@ios.each_with_index do |io,idx|
size = io.size
if pos <= size
new_index ||= idx
io.pos = pos
pos = 0
else
io.pos = size
pos -= size
end
end
@index = new_index unless new_index.nil?
end
def rewind
self.pos = 0
end
end
end
end
end

View File

@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
require 'google/apis/core/multipart'
require 'google/apis/core/api_command' require 'google/apis/core/api_command'
require 'google/apis/errors' require 'google/apis/errors'
require 'addressable/uri' require 'addressable/uri'
@ -22,7 +21,7 @@ module Google
module Core module Core
# Streaming/resumable media download support # Streaming/resumable media download support
class DownloadCommand < ApiCommand class DownloadCommand < ApiCommand
RANGE_HEADER = 'range' RANGE_HEADER = 'Range'
OK_STATUS = [200, 201, 206] OK_STATUS = [200, 201, 206]
# File or IO to write content to # File or IO to write content to
@ -58,7 +57,7 @@ module Google
# of file content. # of file content.
# #
# @private # @private
# @param [Hurley::Client] client # @param [HTTPClient] client
# HTTP client # HTTP client
# @yield [result, err] Result or error if block supplied # @yield [result, err] Result or error if block supplied
# @return [Object] # @return [Object]
@ -66,40 +65,45 @@ module Google
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required # @raise [Google::Apis::AuthorizationError] Authorization is required
def execute_once(client, &block) def execute_once(client, &block)
response = client.get(@download_url || url) do |req| request_header = header.dup
apply_request_options(req) apply_request_options(request_header)
check_if_rewind_needed = false check_if_rewind_needed = false
if @offset > 0 if @offset > 0
logger.debug { sprintf('Resuming download from offset %d', @offset) } logger.debug { sprintf('Resuming download from offset %d', @offset) }
req.header[RANGE_HEADER] = sprintf('bytes=%d-', @offset) request_header[RANGE_HEADER] = sprintf('bytes=%d-', @offset)
check_if_rewind_needed = true check_if_rewind_needed = true
end end
req.on_body(*OK_STATUS) do |res, chunk|
check_status(res.status_code, chunk) unless res.status_code.nil? http_res = client.get(url.to_s,
if check_if_rewind_needed && res.status_code != 206 query: query,
header: request_header,
follow_redirect: true) do |res, chunk|
status = res.http_header.status_code.to_i
if OK_STATUS.include?(status)
if check_if_rewind_needed && status != 206
# Oh no! Requested a chunk, but received the entire content # Oh no! Requested a chunk, but received the entire content
# Attempt to rewind the stream # Attempt to rewind the stream
@download_io.rewind @download_io.rewind
check_if_rewind_needed = false check_if_rewind_needed = false
end end
# logger.debug { sprintf('Writing chunk (%d bytes, %d total)', chunk.length, bytes_read) }
logger.debug { sprintf('Writing chunk (%d bytes)', chunk.length) }
@offset += chunk.length
@download_io.write(chunk) @download_io.write(chunk)
@download_io.flush @offset += chunk.length
end end
end end
# Since the on_body block only runs on success, check status again just in case it failed @download_io.flush
check_status(response.status_code, response.body) unless OK_STATUS.include?(response.status_code.to_i)
if @close_io_on_finish if @close_io_on_finish
result = nil result = nil
else else
result = @download_io result = @download_io
end end
check_status(http_res.status.to_i, http_res.header, http_res.body)
success(result, &block) success(result, &block)
rescue => e rescue => e
@download_io.flush
error(e, rethrow: true, &block) error(e, rethrow: true, &block)
end end
end end

View File

@ -1,82 +0,0 @@
require 'httpclient'
require 'hurley'
require 'hurley/client'
module Google
module Apis
module Core
# HTTPClient adapter for Hurley.
class HttpClientAdapter
def call(request)
client = ::HTTPClient.new
configure_client(client, request)
begin
::Hurley::Response.new(request) do |res|
http_res = client.request(request.verb.to_s.upcase, request.url.to_s, nil, request.body_io, request.header.to_hash, false) do |http_res, chunk|
copy_response(http_res, res)
res.receive_body(chunk)
end
copy_response(http_res, res)
end
rescue ::HTTPClient::TimeoutError, Errno::ETIMEDOUT
raise ::Hurley::Timeout, $!
rescue ::HTTPClient::BadResponseError => err
if err.message.include?('status 407')
raise ::Hurley::ConnectionFailed, %{407 "Proxy Authentication Required "}
else
raise Hurley::ClientError, $!
end
rescue Errno::ECONNREFUSED, EOFError
raise ::Hurley::ConnectionFailed, $!
rescue => err
if defined?(OpenSSL) && OpenSSL::SSL::SSLError === err
raise Hurley::SSLError, err
else
raise
end
end
end
def copy_response(http_res, res)
unless res.status_code
res.status_code = http_res.status.to_i
http_res.header.all.each do |(k,v)|
res.header[k] = v
end
end
end
def configure_client(client, request)
client.transparent_gzip_decompression = true
if request.options.proxy
proxy = request.options.proxy
client.proxy = sprintf('%s:%d', proxy.host, proxy.port)
if proxy.user && proxy.password
client.set_proxy_auth proxy.user, proxy.password
end
end
if request.options.timeout
client.connect_timeout = request.options.timeout
client.receive_timeout = request.options.timeout
client.send_timeout = request.options.timeout
end
if request.options.open_timeout
client.connect_timeout = request.options.open_timeout
client.send_timeout = request.options.open_timeout
end
ssl_config = client.ssl_config
ssl_opts = request.ssl_options
ssl_config.verify_mode = ssl_opts.openssl_verify_mode
ssl_config.cert_store = ssl_opts.openssl_cert_store
ssl_config.add_trust_ca ssl_opts.ca_file if ssl_opts.ca_file
ssl_config.add_trust_ca ssl_opts.ca_path if ssl_opts.ca_path
ssl_config.client_cert = ssl_opts.openssl_client_cert if ssl_opts.openssl_client_cert
ssl_config.client_key = ssl_opts.openssl_client_key if ssl_opts.openssl_client_key
ssl_config.verify_depth = ssl_opts.verify_depth if ssl_opts.verify_depth
end
end
end
end
end

View File

@ -17,9 +17,6 @@ require 'addressable/template'
require 'google/apis/options' require 'google/apis/options'
require 'google/apis/errors' require 'google/apis/errors'
require 'retriable' require 'retriable'
require 'hurley'
require 'hurley/addressable'
require 'hurley_patches'
require 'google/apis/core/logging' require 'google/apis/core/logging'
require 'pp' require 'pp'
@ -41,7 +38,7 @@ module Google
attr_accessor :url attr_accessor :url
# HTTP headers # HTTP headers
# @return [Hurley::Header] # @return [Hash]
attr_accessor :header attr_accessor :header
# Request body # Request body
@ -53,7 +50,7 @@ module Google
attr_accessor :method attr_accessor :method
# HTTP Client # HTTP Client
# @return [Hurley::Client] # @return [HTTPClient]
attr_accessor :connection attr_accessor :connection
# Query params # Query params
@ -75,7 +72,7 @@ module Google
self.url = url self.url = url
self.url = Addressable::Template.new(url) if url.is_a?(String) self.url = Addressable::Template.new(url) if url.is_a?(String)
self.method = method self.method = method
self.header = Hurley::Header.new self.header = Hash.new
self.body = body self.body = body
self.query = {} self.query = {}
self.params = {} self.params = {}
@ -83,7 +80,7 @@ module Google
# Execute the command, retrying as necessary # Execute the command, retrying as necessary
# #
# @param [Hurley::Client] client # @param [HTTPClient] client
# HTTP client # HTTP client
# @yield [result, err] Result or error if block supplied # @yield [result, err] Result or error if block supplied
# @return [Object] # @return [Object]
@ -142,8 +139,12 @@ module Google
# @private # @private
# @return [void] # @return [void]
def prepare! def prepare!
header.update(options.header) if options && options.header normalize_unicode = true
self.url = url.expand(params) if url.is_a?(Addressable::Template) if options
header.update(options.header) if options.header
normalize_unicode = options.normalize_unicode
end
self.url = url.expand(params, nil, normalize_unicode) if url.is_a?(Addressable::Template)
url.query_values = query.merge(url.query_values || {}) url.query_values = query.merge(url.query_values || {})
if allow_form_encoding? if allow_form_encoding?
@ -154,6 +155,9 @@ module Google
else else
@form_encoded = false @form_encoded = false
end end
self.body = '' unless self.body
end end
# Release any resources used by this command # Release any resources used by this command
@ -166,7 +170,7 @@ module Google
# #
# @param [Fixnum] status # @param [Fixnum] status
# HTTP status code of response # HTTP status code of response
# @param [Hurley::Header] header # @param [Hash] header
# Response headers # Response headers
# @param [String, #read] body # @param [String, #read] body
# Response body # Response body
@ -177,7 +181,7 @@ module Google
# @raise [Google::Apis::AuthorizationError] Authorization is required # @raise [Google::Apis::AuthorizationError] Authorization is required
def process_response(status, header, body) def process_response(status, header, body)
check_status(status, header, body) check_status(status, header, body)
decode_response_body(header[:content_type], body) decode_response_body(header['Content-Type'].first, body)
end end
# Check the response and raise error if needed # Check the response and raise error if needed
@ -185,7 +189,7 @@ module Google
# @param [Fixnum] status # @param [Fixnum] status
# HTTP status code of response # HTTP status code of response
# @param # @param
# @param [Hurley::Header] header # @param [Hash] header
# HTTP response headers # HTTP response headers
# @param [String] body # @param [String] body
# HTTP response body # HTTP response body
@ -201,11 +205,14 @@ module Google
when 200...300 when 200...300
nil nil
when 301, 302, 303, 307 when 301, 302, 303, 307
message ||= sprintf('Redirect to %s', header[:location]) message ||= sprintf('Redirect to %s', header['Location'])
raise Google::Apis::RedirectError.new(message, status_code: status, header: header, body: body) raise Google::Apis::RedirectError.new(message, status_code: status, header: header, body: body)
when 401 when 401
message ||= 'Unauthorized' message ||= 'Unauthorized'
raise Google::Apis::AuthorizationError.new(message, status_code: status, header: header, body: body) raise Google::Apis::AuthorizationError.new(message, status_code: status, header: header, body: body)
when 429
message ||= 'Rate limit exceeded'
raise Google::Apis::RateLimitError.new(message, status_code: status, header: header, body: body)
when 304, 400, 402...500 when 304, 400, 402...500
message ||= 'Invalid request' message ||= 'Invalid request'
raise Google::Apis::ClientError.new(message, status_code: status, header: header, body: body) raise Google::Apis::ClientError.new(message, status_code: status, header: header, body: body)
@ -251,7 +258,16 @@ module Google
# @raise [StandardError] if no block # @raise [StandardError] if no block
def error(err, rethrow: false, &block) def error(err, rethrow: false, &block)
logger.debug { sprintf('Error - %s', PP.pp(err, '')) } logger.debug { sprintf('Error - %s', PP.pp(err, '')) }
err = Google::Apis::TransmissionError.new(err) if err.is_a?(Hurley::ClientError) || err.is_a?(SocketError) if err.is_a?(HTTPClient::BadResponseError)
begin
res = err.res
check_status(res.status.to_i, res.header, res.body)
rescue Google::Apis::Error => e
err = e
end
elsif err.is_a?(HTTPClient::TimeoutError) || err.is_a?(SocketError)
err = Google::Apis::TransmissionError.new(err)
end
block.call(nil, err) if block_given? block.call(nil, err) if block_given?
fail err if rethrow || block.nil? fail err if rethrow || block.nil?
end end
@ -259,7 +275,7 @@ module Google
# Execute the command once. # Execute the command once.
# #
# @private # @private
# @param [Hurley::Client] client # @param [HTTPClient] client
# HTTP client # HTTP client
# @return [Object] # @return [Object]
# @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried # @raise [Google::Apis::ServerError] An error occurred on the server and the request can be retried
@ -269,21 +285,18 @@ module Google
body.rewind if body.respond_to?(:rewind) body.rewind if body.respond_to?(:rewind)
begin begin
logger.debug { sprintf('Sending HTTP %s %s', method, url) } logger.debug { sprintf('Sending HTTP %s %s', method, url) }
response = client.send(method, url, body) do |req| request_header = header.dup
# Temporary workaround for Hurley bug where the connection preference apply_request_options(request_header)
# is ignored and it uses nested anyway
unless form_encoded? http_res = client.request(method.to_s.upcase,
req.url.query_class = Hurley::Query::Flat url.to_s,
query.each do | k, v| query: nil,
req.url.query[k] = normalize_query_value(v) body: body,
end header: request_header,
end follow_redirect: true)
# End workaround logger.debug { http_res.status }
apply_request_options(req) logger.debug { http_res.inspect }
end response = process_response(http_res.status.to_i, http_res.header, http_res.body)
logger.debug { response.status_code }
logger.debug { response.inspect }
response = process_response(response.status_code, response.header, response.body)
success(response) success(response)
rescue => e rescue => e
logger.debug { sprintf('Caught error %s', e) } logger.debug { sprintf('Caught error %s', e) }
@ -292,18 +305,16 @@ module Google
end end
# Update the request with any specified options. # Update the request with any specified options.
# @param [Hurley::Request] req # @param [Hash] header
# HTTP request # HTTP headers
# @return [void] # @return [void]
def apply_request_options(req) def apply_request_options(req_header)
if options.authorization.respond_to?(:apply!) if options.authorization.respond_to?(:apply!)
options.authorization.apply!(req.header) options.authorization.apply!(req_header)
elsif options.authorization.is_a?(String) elsif options.authorization.is_a?(String)
req.header[:authorization] = sprintf('Bearer %s', options.authorization) req_header['Authorization'] = sprintf('Bearer %s', options.authorization)
end end
req.header.update(header) req_header.update(header)
req.options.timeout = options.timeout_sec
req.options.open_timeout = options.open_timeout_sec
end end
def allow_form_encoding? def allow_form_encoding?

View File

@ -55,7 +55,7 @@ module Google
def if_fn(name) def if_fn(name)
ivar_name = "@#{name}".to_sym ivar_name = "@#{name}".to_sym
lambda do |opts| lambda do |opts|
if opts[:options][:user_options] and opts[:options][:user_options][:skip_undefined] if opts[:user_options] && opts[:user_options][:skip_undefined]
if respond_to?(:key?) if respond_to?(:key?)
self.key?(name) || instance_variable_defined?(ivar_name) self.key?(name) || instance_variable_defined?(ivar_name)
else else
@ -72,6 +72,10 @@ module Google
options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : Base64.urlsafe_encode64(value) } options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : Base64.urlsafe_encode64(value) }
options[:parse_filter] = ->(fragment, _doc, *_args) { Base64.urlsafe_decode64(fragment) } options[:parse_filter] = ->(fragment, _doc, *_args) { Base64.urlsafe_decode64(fragment) }
end end
if options[:numeric_string]
options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : value.to_s}
options[:parse_filter] = ->(fragment, _doc, *_args) { fragment.to_i }
end
if options[:type] == DateTime if options[:type] == DateTime
options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : value.is_a?(DateTime) ? value.rfc3339(3) : value.to_s } options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : value.is_a?(DateTime) ? value.rfc3339(3) : value.to_s }
options[:parse_filter] = ->(fragment, _doc, *_args) { DateTime.parse(fragment) } options[:parse_filter] = ->(fragment, _doc, *_args) { DateTime.parse(fragment) }

View File

@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
require 'hurley'
module Google module Google
module Apis module Apis
@ -21,108 +20,60 @@ module Google
# #
# @private # @private
class JsonPart class JsonPart
include Hurley::Multipart::Part
# @return [Fixnum]
# Length of part
attr_reader :length
# @param [String] boundary
# Multipart boundary
# @param [String] value # @param [String] value
# JSON content # JSON content
def initialize(boundary, value, header = {}) # @param [Hash] header
@part = build_part(boundary, value) # Additional headers
@length = @part.bytesize def initialize(value, header = {})
@io = StringIO.new(@part) @value = value
@header = header
end end
private def to_io(boundary)
# Format the part
#
# @param [String] boundary
# Multipart boundary
# @param [String] value
# JSON content
# @return [String]
def build_part(boundary, value)
part = '' part = ''
part << "--#{boundary}\r\n" part << "--#{boundary}\r\n"
part << "Content-Type: application/json\r\n" part << "Content-Type: application/json\r\n"
part << "\r\n" @header.each do |(k, v)|
part << "#{value}\r\n" part << "#{k}: #{v}\r\n"
end end
part << "\r\n"
part << "#{@value}\r\n"
StringIO.new(part)
end end
# Part of a multipart request for holding arbitrary content. Modified end
# from Hurley::Multipart::FilePart to remove Content-Disposition
# Part of a multipart request for holding arbitrary content.
# #
# @private # @private
class FilePart class FilePart
include Hurley::Multipart::Part # @param [IO] io
# @return [Fixnum]
# Length of part
attr_reader :length
# @param [String] boundary
# Multipart boundary
# @param [Google::Apis::Core::UploadIO] io
# IO stream # IO stream
# @param [Hash] header # @param [Hash] header
# Additional headers # Additional headers
def initialize(boundary, io, header = {}) def initialize(io, header = {})
file_length = io.respond_to?(:length) ? io.length : File.size(io.local_path) @io = io
@header = header
@head = build_head(boundary, io.content_type, file_length, @length = io.respond_to?(:size) ? io.size : nil
io.respond_to?(:opts) ? io.opts.merge(header) : header)
@length = @head.bytesize + file_length + FOOT.length
@io = Hurley::CompositeReadIO.new(@length, StringIO.new(@head), io, StringIO.new(FOOT))
end end
private def to_io(boundary)
head = ''
# Construct the header for the part head << "--#{boundary}\r\n"
# @header.each do |(k, v)|
# @param [String] boundary head << "#{k}: #{v}\r\n"
# Multipart boundary
# @param [String] type
# Content type for the part
# @param [Fixnum] content_len
# Length of the part
# @param [Hash] header
# Headers for the part
def build_head(boundary, type, content_len, header)
content_id = ''
if header[:content_id]
content_id = sprintf(CID_FORMAT, header[:content_id])
end end
sprintf(HEAD_FORMAT, head << "Content-Length: #{@length}\r\n" unless @length.nil?
boundary, head << "Content-Transfer-Encoding: binary\r\n"
content_len.to_i, head << "\r\n"
content_id, Google::Apis::Core::CompositeIO.new(StringIO.new(head), @io, StringIO.new("\r\n"))
header[:content_type] || type,
header[:content_transfer_encoding] || DEFAULT_TR_ENCODING)
end end
DEFAULT_TR_ENCODING = 'binary'.freeze
FOOT = "\r\n".freeze
CID_FORMAT = "Content-ID: %s\r\n"
HEAD_FORMAT = <<-END
--%s\r
Content-Length: %d\r
%sContent-Type: %s\r
Content-Transfer-Encoding: %s\r
\r
END
end end
# Helper for building multipart requests # Helper for building multipart requests
class Multipart class Multipart
MULTIPART_RELATED = 'multipart/related' MULTIPART_RELATED = 'multipart/related'
DEFAULT_BOUNDARY = 'RubyApiClientMultiPart'
# @return [String] # @return [String]
# Content type header # Content type header
@ -135,8 +86,8 @@ Content-Transfer-Encoding: %s\r
def initialize(content_type: MULTIPART_RELATED, boundary: nil) def initialize(content_type: MULTIPART_RELATED, boundary: nil)
@parts = [] @parts = []
@boundary = boundary || DEFAULT_BOUNDARY @boundary = boundary || Digest::SHA1.hexdigest(SecureRandom.random_bytes(8))
@content_type = "#{content_type}; boundary=#{boundary}" @content_type = "#{content_type}; boundary=#{@boundary}"
end end
# Append JSON data part # Append JSON data part
@ -147,23 +98,26 @@ Content-Transfer-Encoding: %s\r
# Optional unique ID of this part # Optional unique ID of this part
# @return [self] # @return [self]
def add_json(body, content_id: nil) def add_json(body, content_id: nil)
header = { :content_id => content_id } header = {}
@parts << Google::Apis::Core::JsonPart.new(@boundary, body, header) header['Content-ID'] = content_id unless content_id.nil?
@parts << Google::Apis::Core::JsonPart.new(body, header).to_io(@boundary)
self self
end end
# Append arbitrary data as a part # Append arbitrary data as a part
# #
# @param [Google::Apis::Core::UploadIO] upload_io # @param [IO] upload_io
# IO stream # IO stream
# @param [String] content_id # @param [String] content_id
# Optional unique ID of this part # Optional unique ID of this part
# @return [self] # @return [self]
def add_upload(upload_io, content_id: nil) def add_upload(upload_io, content_type: nil, content_id: nil)
header = { :content_id => content_id } header = {
@parts << Google::Apis::Core::FilePart.new(@boundary, 'Content-Type' => content_type || 'application/octet-stream'
upload_io, }
header) header['Content-Id'] = content_id unless content_id.nil?
@parts << Google::Apis::Core::FilePart.new(upload_io,
header).to_io(@boundary)
self self
end end
@ -172,14 +126,8 @@ Content-Transfer-Encoding: %s\r
# @return [IO] # @return [IO]
# IO stream # IO stream
def assemble def assemble
@parts << Hurley::Multipart::EpiloguePart.new(@boundary) @parts << StringIO.new("--#{@boundary}--\r\n\r\n")
ios = [] Google::Apis::Core::CompositeIO.new(*@parts)
len = 0
@parts.each do |part|
len += part.length
ios << part.to_io
end
Hurley::CompositeReadIO.new(len, *ios)
end end
end end
end end

View File

@ -18,58 +18,18 @@ require 'google/apis/core/api_command'
require 'google/apis/errors' require 'google/apis/errors'
require 'addressable/uri' require 'addressable/uri'
require 'tempfile' require 'tempfile'
begin require 'mime-types'
require 'mime/types/columnar'
rescue LoadError
# Temporary until next major bump when we can tighten
# dependency to mime-types >-=3.0
require 'mime-types'
end
module Google module Google
module Apis module Apis
module Core module Core
# Extension of Hurley's UploadIO to add length accessor
class UploadIO < Hurley::UploadIO
OCTET_STREAM_CONTENT_TYPE = 'application/octet-stream'
# Get the length of the stream
# @return [Fixnum]
def length
io.respond_to?(:length) ? io.length : File.size(local_path)
end
# Create a new instance given a file path
# @param [String, File] file_name
# Path to file
# @param [String] content_type
# Optional content type. If nil, will attempt to auto-detect
# @return [Google::Apis::Core::UploadIO]
def self.from_file(file_name, content_type: nil)
if content_type.nil?
type = MIME::Types.of(file_name)
content_type = type.first.content_type unless type.nil? || type.empty?
end
new(file_name, content_type || OCTET_STREAM_CONTENT_TYPE)
end
# Wraps an IO stream in UploadIO
# @param [#read] io
# IO to wrap
# @param [String] content_type
# Optional content type.
# @return [Google::Apis::Core::UploadIO]
def self.from_io(io, content_type: OCTET_STREAM_CONTENT_TYPE)
new(io, content_type)
end
end
# Base upload command. Not intended to be used directly # Base upload command. Not intended to be used directly
# @private # @private
class BaseUploadCommand < ApiCommand class BaseUploadCommand < ApiCommand
UPLOAD_PROTOCOL_HEADER = 'X-Goog-Upload-Protocol' UPLOAD_PROTOCOL_HEADER = 'X-Goog-Upload-Protocol'
UPLOAD_CONTENT_TYPE_HEADER = 'X-Goog-Upload-Header-Content-Type' UPLOAD_CONTENT_TYPE_HEADER = 'X-Goog-Upload-Header-Content-Type'
UPLOAD_CONTENT_LENGTH = 'X-Goog-Upload-Header-Content-Length' UPLOAD_CONTENT_LENGTH = 'X-Goog-Upload-Header-Content-Length'
CONTENT_TYPE_HEADER = 'Content-Type'
# File name or IO containing the content to upload # File name or IO containing the content to upload
# @return [String, File, #read] # @return [String, File, #read]
@ -83,21 +43,29 @@ module Google
# @return [Google::Apis::Core::UploadIO] # @return [Google::Apis::Core::UploadIO]
attr_accessor :upload_io attr_accessor :upload_io
# Ensure the content is readable and wrapped in an {{Google::Apis::Core::UploadIO}} instance. # Ensure the content is readable and wrapped in an IO instance.
# #
# @return [void] # @return [void]
# @raise [Google::Apis::ClientError] if upload source is invalid # @raise [Google::Apis::ClientError] if upload source is invalid
def prepare! def prepare!
super super
if streamable?(upload_source) if streamable?(upload_source)
self.upload_io = UploadIO.from_io(upload_source, content_type: upload_content_type) self.upload_io = upload_source
@close_io_on_finish = false @close_io_on_finish = false
elsif upload_source.is_a?(String) elsif self.upload_source.is_a?(String)
self.upload_io = UploadIO.from_file(upload_source, content_type: upload_content_type) self.upload_io = File.new(upload_source, 'r')
if self.upload_content_type.nil?
type = MIME::Types.of(upload_source)
self.upload_content_type = type.first.content_type unless type.nil? || type.empty?
end
@close_io_on_finish = true @close_io_on_finish = true
else else
fail Google::Apis::ClientError, 'Invalid upload source' fail Google::Apis::ClientError, 'Invalid upload source'
end end
if self.upload_content_type.nil? || self.upload_content_type.empty?
self.upload_content_type = 'application/octet-stream'
end
puts self.upload_content_type.inspect
end end
# Close IO stream when command done. Only closes the stream if it was opened by the command. # Close IO stream when command done. Only closes the stream if it was opened by the command.
@ -124,13 +92,12 @@ module Google
super super
self.body = upload_io self.body = upload_io
header[UPLOAD_PROTOCOL_HEADER] = RAW_PROTOCOL header[UPLOAD_PROTOCOL_HEADER] = RAW_PROTOCOL
header[UPLOAD_CONTENT_TYPE_HEADER] = upload_io.content_type header[UPLOAD_CONTENT_TYPE_HEADER] = upload_content_type
end end
end end
# Implementation of the multipart upload protocol # Implementation of the multipart upload protocol
class MultipartUploadCommand < BaseUploadCommand class MultipartUploadCommand < BaseUploadCommand
UPLOAD_BOUNDARY = 'RubyApiClientUpload'
MULTIPART_PROTOCOL = 'multipart' MULTIPART_PROTOCOL = 'multipart'
MULTIPART_RELATED = 'multipart/related' MULTIPART_RELATED = 'multipart/related'
@ -140,11 +107,11 @@ module Google
# @raise [Google::Apis::ClientError] if upload source is invalid # @raise [Google::Apis::ClientError] if upload source is invalid
def prepare! def prepare!
super super
@multipart = Multipart.new(boundary: UPLOAD_BOUNDARY, content_type: MULTIPART_RELATED) multipart = Multipart.new
@multipart.add_json(body) multipart.add_json(body)
@multipart.add_upload(upload_io) multipart.add_upload(upload_io, content_type: upload_content_type)
self.body = @multipart.assemble self.body = multipart.assemble
header[:content_type] = @multipart.content_type header['Content-Type'] = multipart.content_type
header[UPLOAD_PROTOCOL_HEADER] = MULTIPART_PROTOCOL header[UPLOAD_PROTOCOL_HEADER] = MULTIPART_PROTOCOL
end end
end end
@ -179,7 +146,7 @@ module Google
# #
# @param [Fixnum] status # @param [Fixnum] status
# HTTP status code of response # HTTP status code of response
# @param [Hurley::Header] header # @param [HTTP::Message::Headers] header
# Response headers # Response headers
# @param [String, #read] body # @param [String, #read] body
# Response body # Response body
@ -189,9 +156,9 @@ module Google
# @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification # @raise [Google::Apis::ClientError] The request is invalid and should not be retried without modification
# @raise [Google::Apis::AuthorizationError] Authorization is required # @raise [Google::Apis::AuthorizationError] Authorization is required
def process_response(status, header, body) def process_response(status, header, body)
@offset = Integer(header[BYTES_RECEIVED_HEADER]) if header.key?(BYTES_RECEIVED_HEADER) @offset = Integer(header[BYTES_RECEIVED_HEADER].first) unless header[BYTES_RECEIVED_HEADER].empty?
@upload_url = header[UPLOAD_URL_HEADER] if header.key?(UPLOAD_URL_HEADER) @upload_url = header[UPLOAD_URL_HEADER].first unless header[UPLOAD_URL_HEADER].empty?
upload_status = header[UPLOAD_STATUS_HEADER] upload_status = header[UPLOAD_STATUS_HEADER].first
logger.debug { sprintf('Upload status %s', upload_status) } logger.debug { sprintf('Upload status %s', upload_status) }
if upload_status == STATUS_ACTIVE if upload_status == STATUS_ACTIVE
@state = :active @state = :active
@ -204,61 +171,69 @@ module Google
super(status, header, body) super(status, header, body)
end end
# Send the start command to initiate the upload
#
# @param [Hurley::Client] client
# HTTP client
# @return [Hurley::Response]
# @raise [Google::Apis::ServerError] Unable to send the request
def send_start_command(client) def send_start_command(client)
logger.debug { sprintf('Sending upload start command to %s', url) } logger.debug { sprintf('Sending upload start command to %s', url) }
client.send(method, url, body) do |req|
apply_request_options(req) request_header = header.dup
req.header[UPLOAD_PROTOCOL_HEADER] = RESUMABLE apply_request_options(request_header)
req.header[UPLOAD_COMMAND_HEADER] = START_COMMAND request_header[UPLOAD_PROTOCOL_HEADER] = RESUMABLE
req.header[UPLOAD_CONTENT_LENGTH] = upload_io.length.to_s request_header[UPLOAD_COMMAND_HEADER] = START_COMMAND
req.header[UPLOAD_CONTENT_TYPE_HEADER] = upload_io.content_type request_header[UPLOAD_CONTENT_LENGTH] = upload_io.size.to_s
end request_header[UPLOAD_CONTENT_TYPE_HEADER] = upload_content_type
client.request(method.to_s.upcase,
url.to_s, query: nil,
body: body,
header: request_header,
follow_redirect: true)
rescue => e rescue => e
raise Google::Apis::ServerError, e.message raise Google::Apis::ServerError, e.message
end end
# Query for the status of an incomplete upload # Query for the status of an incomplete upload
# #
# @param [Hurley::Client] client # @param [HTTPClient] client
# HTTP client # HTTP client
# @return [Hurley::Response] # @return [HTTP::Message]
# @raise [Google::Apis::ServerError] Unable to send the request # @raise [Google::Apis::ServerError] Unable to send the request
def send_query_command(client) def send_query_command(client)
logger.debug { sprintf('Sending upload query command to %s', @upload_url) } logger.debug { sprintf('Sending upload query command to %s', @upload_url) }
client.post(@upload_url, nil) do |req|
apply_request_options(req) request_header = header.dup
req.header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND apply_request_options(request_header)
end request_header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND
client.post(@upload_url, body: '', header: request_header, follow_redirect: true)
end end
# Send the actual content # Send the actual content
# #
# @param [Hurley::Client] client # @param [HTTPClient] client
# HTTP client # HTTP client
# @return [Hurley::Response] # @return [HTTP::Message]
# @raise [Google::Apis::ServerError] Unable to send the request # @raise [Google::Apis::ServerError] Unable to send the request
def send_upload_command(client) def send_upload_command(client)
logger.debug { sprintf('Sending upload command to %s', @upload_url) } logger.debug { sprintf('Sending upload command to %s', @upload_url) }
content = upload_io content = upload_io
content.pos = @offset content.pos = @offset
client.post(@upload_url, content) do |req|
apply_request_options(req) request_header = header.dup
req.header[UPLOAD_COMMAND_HEADER] = UPLOAD_COMMAND apply_request_options(request_header)
req.header[UPLOAD_OFFSET_HEADER] = @offset.to_s request_header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND
end request_header[UPLOAD_COMMAND_HEADER] = UPLOAD_COMMAND
request_header[UPLOAD_OFFSET_HEADER] = @offset.to_s
request_header[CONTENT_TYPE_HEADER] = upload_content_type
client.post(@upload_url, body: content, header: request_header, follow_redirect: true)
end end
# Execute the upload request once. This will typically perform two HTTP requests -- one to initiate or query # Execute the upload request once. This will typically perform two HTTP requests -- one to initiate or query
# for the status of the upload, the second to send the (remaining) content. # for the status of the upload, the second to send the (remaining) content.
# #
# @private # @private
# @param [Hurley::Client] client # @param [HTTPClient] client
# HTTP client # HTTP client
# @yield [result, err] Result or error if block supplied # @yield [result, err] Result or error if block supplied
# @return [Object] # @return [Object]

View File

@ -51,6 +51,8 @@ module Google
when 'string', 'boolean', 'number', 'integer', 'any' when 'string', 'boolean', 'number', 'integer', 'any'
return 'DateTime' if format == 'date-time' return 'DateTime' if format == 'date-time'
return 'Date' if format == 'date' return 'Date' if format == 'date'
return 'Fixnum' if format == 'int64'
return 'Fixnum' if format == 'uint64'
return TYPE_MAP[type] return TYPE_MAP[type]
when 'array' when 'array'
return sprintf('Array<%s>', items.generated_type) return sprintf('Array<%s>', items.generated_type)

View File

@ -33,7 +33,7 @@ class <%= cls.generated_class_name %>
<% elsif property.type == 'array' -%> <% elsif property.type == 'array' -%>
collection :<%= property.generated_name %>, as: '<%= property.name %>'<%= include('representation_type', :lead => ', ', :type => property.items, :api => api) %> collection :<%= property.generated_name %>, as: '<%= property.name %>'<%= include('representation_type', :lead => ', ', :type => property.items, :api => api) %>
<% else -%> <% else -%>
property :<%= property.generated_name %>,<% if property.format == 'byte' %> :base64 => true,<%end%> as: '<%= property.name %>'<%= include('representation_type', :lead => ', ', :type => property, :api => api) %> property :<%= property.generated_name %>,<% if ['uint64', 'int64'].include?(property.format) %> :numeric_string => true,<%end%><% if property.format == 'byte' %> :base64 => true,<%end%> as: '<%= property.name %>'<%= include('representation_type', :lead => ', ', :type => property, :api => api) %>
<% end -%> <% end -%>
<% end -%> <% end -%>
<% end -%> <% end -%>

View File

@ -19,14 +19,18 @@ module Google
:application_name, :application_name,
:application_version, :application_version,
:proxy_url, :proxy_url,
:use_net_http) :open_timeout_sec,
:read_timeout_sec,
:send_timeout_sec,
:log_http_requests)
RequestOptions = Struct.new( RequestOptions = Struct.new(
:authorization, :authorization,
:retries, :retries,
:header, :header,
:timeout_sec, :normalize_unicode,
:open_timeout_sec) :skip_serialization,
:skip_deserialization)
# General client options # General client options
class ClientOptions class ClientOptions
@ -36,7 +40,12 @@ module Google
# @return [String] Version of the application, for identification in the User-Agent header # @return [String] Version of the application, for identification in the User-Agent header
# @!attribute [rw] proxy_url # @!attribute [rw] proxy_url
# @return [String] URL of a proxy server # @return [String] URL of a proxy server
# @!attribute [rw] log_http_requests
# @return [Boolean] True if raw HTTP requests should be logged
# @!attribute [rw] open_timeout_sec
# @return [Fixnum] How long, in seconds, before failed connections time out
# @!attribute [rw] read_timeout_sec
# @return [Fixnum] How long, in seconds, before requests time out
# Get the default options # Get the default options
# @return [Google::Apis::ClientOptions] # @return [Google::Apis::ClientOptions]
def self.default def self.default
@ -50,12 +59,14 @@ module Google
# @return [Signet::OAuth2::Client, #apply(Hash)] OAuth2 credentials # @return [Signet::OAuth2::Client, #apply(Hash)] OAuth2 credentials
# @!attribute [rw] retries # @!attribute [rw] retries
# @return [Fixnum] Number of times to retry requests on server error # @return [Fixnum] Number of times to retry requests on server error
# @!attribute [rw] timeout_sec
# @return [Fixnum] How long, in seconds, before requests time out
# @!attribute [rw] open_timeout_sec
# @return [Fixnum] How long, in seconds, before failed connections time out
# @!attribute [rw] header # @!attribute [rw] header
# @return [Hash<String,String] Additional HTTP headers to include in requests # @return [Hash<String,String] Additional HTTP headers to include in requests
# @!attribute [rw] normalize_unicode
# @return [Boolean] True if unicode strings should be normalized in path parameters
# @!attribute [rw] skip_serialization
# @return [Boolean] True if body object should be treated as raw text instead of an object.
# @!attribute [rw] skip_deserialization
# @return [Boolean] True if response should be returned in raw form instead of deserialized.
# Get the default options # Get the default options
# @return [Google::Apis::RequestOptions] # @return [Google::Apis::RequestOptions]
@ -75,11 +86,12 @@ module Google
end end
end end
ClientOptions.default.use_net_http = false ClientOptions.default.log_http_requests = false
ClientOptions.default.application_name = 'unknown' ClientOptions.default.application_name = 'unknown'
ClientOptions.default.application_version = '0.0.0' ClientOptions.default.application_version = '0.0.0'
RequestOptions.default.retries = 0 RequestOptions.default.retries = 0
RequestOptions.default.open_timeout_sec = 20 RequestOptions.default.normalize_unicode = false
RequestOptions.default.skip_serialization = false
RequestOptions.default.skip_deserialization = false
end end
end end

View File

@ -15,7 +15,7 @@
module Google module Google
module Apis module Apis
# Client library version # Client library version
VERSION = '0.10.3' VERSION = '0.11.0'
# Current operating system # Current operating system
# @private # @private

View File

@ -2,6 +2,5 @@ source 'https://rubygems.org'
gem 'google-api-client', '~> 0.9' gem 'google-api-client', '~> 0.9'
gem 'google-id-token', '~> 1.3' gem 'google-id-token', '~> 1.3'
gem 'sinatra', '~> 1.4'
gem 'redis', '~> 3.2' gem 'redis', '~> 3.2'
gem 'dotenv' gem 'dotenv'

View File

@ -15,7 +15,6 @@
require 'spec_helper' require 'spec_helper'
require 'google/apis/core/api_command' require 'google/apis/core/api_command'
require 'google/apis/core/json_representation' require 'google/apis/core/json_representation'
require 'hurley/test'
RSpec.describe Google::Apis::Core::ApiCommand do RSpec.describe Google::Apis::Core::ApiCommand do
include TestHelpers include TestHelpers
@ -64,6 +63,29 @@ RSpec.describe Google::Apis::Core::ApiCommand do
end end
end end
context('with a raw request body') do
let(:command) do
request = model_class.new
command = Google::Apis::Core::ApiCommand.new(:post, 'https://www.googleapis.com/zoo/animals')
command.request_representation = representer_class
command.request_object = %({"value": "hello"})
command.options.skip_serialization = true
command
end
before(:example) do
stub_request(:post, 'https://www.googleapis.com/zoo/animals')
.to_return(headers: { 'Content-Type' => 'application/json' }, body: %({}))
end
it 'should allow raw JSON if skip_serialization = true' do
command.execute(client)
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals').with do |req|
be_json_eql(%({"value":"hello"})).matches?(req.body)
end).to have_been_made
end
end
context('with a JSON response') do context('with a JSON response') do
let(:command) do let(:command) do
command = Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals') command = Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
@ -86,6 +108,12 @@ RSpec.describe Google::Apis::Core::ApiCommand do
result = command.execute(client) result = command.execute(client)
expect(result.value).to eql 'hello' expect(result.value).to eql 'hello'
end end
it 'should return a raw JSON if skip_deserialization true' do
command.options.skip_deserialization = true
result = command.execute(client)
expect(result).to eql %({"value" : "hello"})
end
end end
context('with an invalid content-type response') do context('with an invalid content-type response') do

View File

@ -15,7 +15,6 @@
require 'spec_helper' require 'spec_helper'
require 'google/apis/core/batch' require 'google/apis/core/batch'
require 'google/apis/core/json_representation' require 'google/apis/core/json_representation'
require 'hurley/test'
RSpec.describe Google::Apis::Core::BatchCommand do RSpec.describe Google::Apis::Core::BatchCommand do
include TestHelpers include TestHelpers
@ -30,19 +29,20 @@ RSpec.describe Google::Apis::Core::BatchCommand do
let(:post_with_string_command) do let(:post_with_string_command) do
command = Google::Apis::Core::HttpCommand.new(:post, 'https://www.googleapis.com/zoo/animals/2') command = Google::Apis::Core::HttpCommand.new(:post, 'https://www.googleapis.com/zoo/animals/2')
command.body = 'Hello world' command.body = 'Hello world'
command.header[:content_type] = 'text/plain' command.header['Content-Type'] = 'text/plain'
command command
end end
let(:post_with_io_command) do let(:post_with_io_command) do
command = Google::Apis::Core::HttpCommand.new(:post, 'https://www.googleapis.com/zoo/animals/3') command = Google::Apis::Core::HttpCommand.new(:post, 'https://www.googleapis.com/zoo/animals/3')
command.body = StringIO.new('Goodbye!') command.body = StringIO.new('Goodbye!')
command.header[:content_type] = 'text/plain' command.header['Content-Type'] = 'text/plain'
command command
end end
before(:example) do before(:example) do
allow(SecureRandom).to receive(:uuid).and_return('ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f') allow(SecureRandom).to receive(:uuid).and_return('ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f')
allow(Digest::SHA1).to receive(:hexdigest).and_return('123abc')
response = <<EOF response = <<EOF
--batch123 --batch123
@ -83,20 +83,20 @@ EOF
command.execute(client) command.execute(client)
expected_body = <<EOF.gsub(/\n/, "\r\n") expected_body = <<EOF.gsub(/\n/, "\r\n")
--RubyApiBatchRequest --123abc
Content-Length: 58
Content-ID: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+0>
Content-Type: application/http Content-Type: application/http
Content-Id: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+0>
Content-Length: 58
Content-Transfer-Encoding: binary Content-Transfer-Encoding: binary
GET /zoo/animals/1? HTTP/1.1 GET /zoo/animals/1? HTTP/1.1
Host: www.googleapis.com Host: www.googleapis.com
--RubyApiBatchRequest --123abc
Content-Length: 96
Content-ID: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+1>
Content-Type: application/http Content-Type: application/http
Content-Id: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+1>
Content-Length: 96
Content-Transfer-Encoding: binary Content-Transfer-Encoding: binary
POST /zoo/animals/2? HTTP/1.1 POST /zoo/animals/2? HTTP/1.1
@ -104,10 +104,10 @@ Content-Type: text/plain
Host: www.googleapis.com Host: www.googleapis.com
Hello world Hello world
--RubyApiBatchRequest --123abc
Content-Length: 93
Content-ID: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+2>
Content-Type: application/http Content-Type: application/http
Content-Id: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+2>
Content-Length: 93
Content-Transfer-Encoding: binary Content-Transfer-Encoding: binary
POST /zoo/animals/3? HTTP/1.1 POST /zoo/animals/3? HTTP/1.1
@ -115,7 +115,7 @@ Content-Type: text/plain
Host: www.googleapis.com Host: www.googleapis.com
Goodbye! Goodbye!
--RubyApiBatchRequest-- --123abc--
EOF EOF
expect(a_request(:post, 'https://www.googleapis.com/batch').with(body: expected_body)).to have_been_made expect(a_request(:post, 'https://www.googleapis.com/batch').with(body: expected_body)).to have_been_made

View File

@ -0,0 +1,76 @@
# 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 'spec_helper'
require 'google/apis/core/composite_io'
RSpec.describe Google::Apis::Core::CompositeIO do
shared_examples 'should act like IO' do
it 'should read from all IOs' do
expect(io.read).to eq 'Hello Cruel World'
end
it 'should respond to size' do
expect(io.size).to eq 17
end
it 'should respond to pos=' do
io.pos = 6
expect(io.read).to eq('Cruel World')
end
it 'should reject negative positions' do
expect { io.pos = -1 }.to raise_error(ArgumentError)
end
it 'should return nil if position beyond size' do
io.pos = 20
expect(io.read).to be_nil
end
it 'should be readable after rewinding' do
expect(io.read).to eq 'Hello Cruel World'
expect(io.read).to be_nil
io.rewind
expect(io.read).to eq 'Hello Cruel World'
end
end
context 'with StringIOs' do
let(:io) do
Google::Apis::Core::CompositeIO.new(
StringIO.new("Hello "),
StringIO.new("Cruel "),
StringIO.new("World"))
end
include_examples 'should act like IO'
end
context 'with Files' do
let(:io) do
files = []
dir = Dir.mktmpdir
['Hello ', 'Cruel ', 'World'].each_with_index do |text, index|
name = File.join(dir, "f#{index}")
File.open(name, 'w') { |f| f.write(text) }
files << name
end
Google::Apis::Core::CompositeIO.new(files.map { |name| File.open(name, 'r') })
end
include_examples 'should act like IO'
end
end

View File

@ -15,7 +15,6 @@
require 'spec_helper' require 'spec_helper'
require 'google/apis/core/download' require 'google/apis/core/download'
require 'google/apis/core/json_representation' require 'google/apis/core/json_representation'
require 'hurley/test'
require 'tempfile' require 'tempfile'
require 'tmpdir' require 'tmpdir'

View File

@ -14,7 +14,6 @@
require 'spec_helper' require 'spec_helper'
require 'google/apis/core/http_command' require 'google/apis/core/http_command'
require 'hurley/test'
RSpec.describe Google::Apis::Core::HttpCommand do RSpec.describe Google::Apis::Core::HttpCommand do
include TestHelpers include TestHelpers
@ -300,4 +299,31 @@ RSpec.describe Google::Apis::Core::HttpCommand do
command.options.retries = 0 command.options.retries = 0
expect { command.execute(client) }.to raise_error(Google::Apis::TransmissionError) expect { command.execute(client) }.to raise_error(Google::Apis::TransmissionError)
end end
it 'should raise rate limit error for 429 status codes' do
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [429, ''])
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
command.options.retries = 0
expect { command.execute(client) }.to raise_error(Google::Apis::RateLimitError)
end
it 'should not normalize unicode values by default' do
stub_request(:get, 'https://www.googleapis.com/Cafe%CC%81').to_return(status: [200, ''])
template = Addressable::Template.new('https://www.googleapis.com/{path}')
command = Google::Apis::Core::HttpCommand.new(:get, template)
command.params[:path] = "Cafe\u0301"
command.options.retries = 0
command.execute(client)
end
it 'should normalize unicode values when requested' do
stub_request(:get, 'https://www.googleapis.com/Caf%C3%A9').to_return(status: [200, ''])
template = Addressable::Template.new('https://www.googleapis.com/{path}')
command = Google::Apis::Core::HttpCommand.new(:get, template)
command.params[:path] = "Cafe\u0301"
command.options.retries = 0
command.options.normalize_unicode = true
command.execute(client)
end
end end

View File

@ -33,6 +33,7 @@ RSpec.describe Google::Apis::Core::JsonRepresentation do
attr_accessor :date_value attr_accessor :date_value
attr_accessor :nil_date_value attr_accessor :nil_date_value
attr_accessor :bytes_value attr_accessor :bytes_value
attr_accessor :big_value
attr_accessor :items attr_accessor :items
attr_accessor :child attr_accessor :child
attr_accessor :children attr_accessor :children
@ -51,6 +52,7 @@ RSpec.describe Google::Apis::Core::JsonRepresentation do
property :date_value, as: 'dateValue', type: DateTime property :date_value, as: 'dateValue', type: DateTime
property :nil_date_value, as: 'nullDateValue', type: DateTime property :nil_date_value, as: 'nullDateValue', type: DateTime
property :bytes_value, as: 'bytesValue', base64: true property :bytes_value, as: 'bytesValue', base64: true
property :big_value, as: 'bigValue', numeric_string: true
property :items property :items
property :child, class: klass do property :child, class: klass do
property :value property :value
@ -106,6 +108,10 @@ RSpec.describe Google::Apis::Core::JsonRepresentation do
it 'serializes object collections' do it 'serializes object collections' do
expect(json).to be_json_eql(%([{"value" : "child"}])).at_path('children') expect(json).to be_json_eql(%([{"value" : "child"}])).at_path('children')
end end
it 'serializes numeric strings' do
expect(json).to be_json_eql(%("1208925819614629174706176")).at_path('bigValue')
end
end end
context 'with model object' do context 'with model object' do
@ -124,6 +130,7 @@ RSpec.describe Google::Apis::Core::JsonRepresentation do
model.child.value = 'child' model.child.value = 'child'
model.children = [model.child] model.children = [model.child]
model.nil_date_value = nil model.nil_date_value = nil
model.big_value = 1208925819614629174706176
model model
end end
@ -143,6 +150,7 @@ RSpec.describe Google::Apis::Core::JsonRepresentation do
boolean_value_true: true, boolean_value_true: true,
boolean_value_false: false, boolean_value_false: false,
bytes_value: 'Hello world', bytes_value: 'Hello world',
big_value: 1208925819614629174706176,
items: [1, 2, 3], items: [1, 2, 3],
child: { child: {
value: 'child' value: 'child'
@ -165,6 +173,7 @@ RSpec.describe Google::Apis::Core::JsonRepresentation do
"numericValue": 123, "numericValue": 123,
"dateValue": "2015-05-01T12:00:00+00:00", "dateValue": "2015-05-01T12:00:00+00:00",
"bytesValue": "SGVsbG8gd29ybGQ=", "bytesValue": "SGVsbG8gd29ybGQ=",
"bigValue": "1208925819614629174706176",
"items": [1,2,3], "items": [1,2,3],
"child": {"value" : "hello"}, "child": {"value" : "hello"},
"children": [{"value" : "hello"}] "children": [{"value" : "hello"}]
@ -204,5 +213,10 @@ EOF
it 'serializes object collections' do it 'serializes object collections' do
expect(model.children[0].value).to eql 'hello' expect(model.children[0].value).to eql 'hello'
end end
it 'deserializes numeric strings' do
expect(model.big_value).to eql 1208925819614629174706176
end
end end
end end

View File

@ -16,8 +16,6 @@ require 'spec_helper'
require 'google/apis/options' require 'google/apis/options'
require 'google/apis/core/base_service' require 'google/apis/core/base_service'
require 'google/apis/core/json_representation' require 'google/apis/core/json_representation'
require 'hurley/test'
require 'ostruct'
RSpec.describe Google::Apis::Core::BaseService do RSpec.describe Google::Apis::Core::BaseService do
include TestHelpers include TestHelpers
@ -95,6 +93,18 @@ RSpec.describe Google::Apis::Core::BaseService do
end end
end end
context 'with proxy' do
after(:example) do
Google::Apis::ClientOptions.default.proxy_url = nil
end
it 'should allow proxy URLs as strings' do
Google::Apis::ClientOptions.default.proxy_url = 'http://gateway.example.com:1234'
service.client
end
end
context 'when making simple commands' do context 'when making simple commands' do
let(:command) { service.send(:make_simple_command, :get, 'zoo/animals', authorization: 'foo') } let(:command) { service.send(:make_simple_command, :get, 'zoo/animals', authorization: 'foo') }
@ -190,6 +200,8 @@ EOF
context 'with batch uploads' do context 'with batch uploads' do
before(:example) do before(:example) do
allow(SecureRandom).to receive(:uuid).and_return('b1981e17-f622-49af-b2eb-203308b1b17d')
allow(Digest::SHA1).to receive(:hexdigest).and_return('outer', 'inner')
response = <<EOF.gsub(/\n/, "\r\n") response = <<EOF.gsub(/\n/, "\r\n")
--batch123 --batch123
Content-Type: application/http Content-Type: application/http
@ -227,6 +239,44 @@ EOF
end.to yield_with_args('Hello', nil) end.to yield_with_args('Hello', nil)
end end
it 'should send nested multipart' do
service.batch_upload do |service|
command = service.send(:make_upload_command, :post, 'zoo/animals', {})
command.upload_source = StringIO.new('test')
command.upload_content_type = 'text/plain'
service.send(:execute_or_queue_command, command)
end
expected_body = <<EOF.gsub(/\n/, "\r\n")
--outer
Content-Type: application/http
Content-Id: <b1981e17-f622-49af-b2eb-203308b1b17d+0>
Content-Length: 303
Content-Transfer-Encoding: binary
POST /upload/zoo/animals? HTTP/1.1
Content-Type: multipart/related; boundary=inner
X-Goog-Upload-Protocol: multipart
Host: www.googleapis.com
--inner
Content-Type: application/json
--inner
Content-Type: text/plain
Content-Length: 4
Content-Transfer-Encoding: binary
test
--inner--
--outer--
EOF
expect(a_request(:put, 'https://www.googleapis.com/upload/').with(body: expected_body)).to have_been_made
end
it 'should disallow downloads in batch' do it 'should disallow downloads in batch' do
expect do |b| expect do |b|
service.batch_upload do |service| service.batch_upload do |service|

View File

@ -15,70 +15,6 @@
require 'spec_helper' require 'spec_helper'
require 'google/apis/core/upload' require 'google/apis/core/upload'
require 'google/apis/core/json_representation' require 'google/apis/core/json_representation'
require 'hurley/test'
# TODO: JSON Response decoding
# TODO: Upload from IO
# TODO: Upload from file
RSpec.describe Google::Apis::Core::UploadIO do
context 'from_file' do
let(:upload_io) { Google::Apis::Core::UploadIO.from_file(file) }
context 'with text file' do
let(:file) { File.join(FIXTURES_DIR, 'files', 'test.txt') }
it 'should infer content type from file' do
expect(upload_io.content_type).to eql('text/plain')
end
it 'should allow overriding the mime type' do
io = Google::Apis::Core::UploadIO.from_file(file, content_type: 'application/json')
expect(io.content_type).to eql('application/json')
end
end
context 'with unknown type' do
let(:file) { File.join(FIXTURES_DIR, 'files', 'test.blah') }
it 'should use the default mime type' do
expect(upload_io.content_type).to eql('application/octet-stream')
end
it 'should allow overriding the mime type' do
io = Google::Apis::Core::UploadIO.from_file(file, content_type: 'application/json')
expect(io.content_type).to eql('application/json')
end
it 'should setup length of the stream' do
upload_io = Google::Apis::Core::UploadIO.from_file(file)
expect(upload_io.length).to eq File.size(file)
end
end
end
context 'from_io' do
context 'with i/o stream' do
let(:io) { StringIO.new 'Hello google' }
it 'should setup default content-type' do
upload_io = Google::Apis::Core::UploadIO.from_io(io)
expect(upload_io.content_type).to eql Google::Apis::Core::UploadIO::OCTET_STREAM_CONTENT_TYPE
end
it 'should allow overring the mime type' do
upload_io = Google::Apis::Core::UploadIO.from_io(io, content_type: 'application/x-gzip')
expect(upload_io.content_type).to eq('application/x-gzip')
end
it 'should setup length of the stream' do
upload_io = Google::Apis::Core::UploadIO.from_io(io)
expect(upload_io.length).to eq 'Hello google'.length
end
end
end
end
RSpec.describe Google::Apis::Core::RawUploadCommand do RSpec.describe Google::Apis::Core::RawUploadCommand do
include TestHelpers include TestHelpers
@ -170,21 +106,22 @@ RSpec.describe Google::Apis::Core::MultipartUploadCommand do
before(:example) do before(:example) do
stub_request(:post, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world)) stub_request(:post, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world))
allow(Digest::SHA1).to receive(:hexdigest).and_return('123abc')
end end
it 'should send content' do it 'should send content' do
expected_body = <<EOF.gsub(/\n/, "\r\n") expected_body = <<EOF.gsub(/\n/, "\r\n")
--RubyApiClientUpload --123abc
Content-Type: application/json Content-Type: application/json
metadata metadata
--RubyApiClientUpload --123abc
Content-Length: 11
Content-Type: text/plain Content-Type: text/plain
Content-Length: 11
Content-Transfer-Encoding: binary Content-Transfer-Encoding: binary
Hello world Hello world
--RubyApiClientUpload-- --123abc--
EOF EOF
command.execute(client) command.execute(client)

View File

@ -80,9 +80,8 @@ RSpec.describe Google::Apis::Error do
context '@cause is falsy' do context '@cause is falsy' do
before do before do
subject.class.superclass.any_instance.stub(:backtrace) do expect_any_instance_of(subject.class.superclass).to receive(:backtrace).and_return(
"super class's #backtrace called" "super class's #backtrace called")
end
end end
it "calls super class's #backtrace" do it "calls super class's #backtrace" do

View File

@ -1,103 +0,0 @@
#
# Copyright (c) 2015 Rick Olson
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
require 'hurley'
require 'hurley/client'
require 'hurley/connection'
require 'net/https'
# Temporary monkey patch for streaming downloads. These are fixed in HEAD,
# but pending a 0.3 release.
if Hurley::VERSION == '0.2'
module Hurley
class Response
def location
@location ||= begin
return unless loc = @header[:location]
verb = STATUS_FORCE_GET.include?(status_code) ? :get : request.verb
statuses, receiver = request.send(:body_receiver)
new_request = Request.new(verb, request.url.join(Url.parse(loc)), request.header, request.body, request.options, request.ssl_options)
new_request.on_body(*statuses, &receiver) unless receiver.is_a?(Hurley::BodyReceiver)
new_request
end
end
end
class Connection
def call(request)
net_http_connection(request) do |http|
begin
Response.new(request) do |res|
perform_request(http, request, res)
# net/http only raises exception on 407 with ssl...?
if res.status_code == 407
raise ConnectionFailed, %(407 "Proxy Authentication Required")
end
end
rescue *NET_HTTP_EXCEPTIONS => err
if defined?(OpenSSL) && OpenSSL::SSL::SSLError === err
raise SSLError, err
else
raise ConnectionFailed, err
end
end
end
rescue ::Timeout::Error => err
raise Timeout, err
end
private
def net_http_request(request)
http_req = Net::HTTPGenericRequest.new(
request.verb.to_s.upcase, # request method
!!request.body, # is there a request body
:head != request.verb, # is there a response body
request.url.request_uri, # request uri path
request.header, # request headers
)
if body = request.body_io
http_req.body_stream = body
end
http_req
end
def perform_request(http, request, res)
http.request(net_http_request(request)) do |http_res|
res.status_code = http_res.code.to_i
http_res.each_header do |key, value|
res.header[key] = value
end
if :get == request.verb
http_res.read_body { |chunk| res.receive_body(chunk) }
else
res.receive_body(http_res.body)
end
end
end
end
end
end