chore: Split google-apis-core into a separate gem (#2134)
This commit is contained in:
1 parent
1bc1737dc1
commit
a6a5b6071e
66 files changed
+763
-151
No files matched your search
@@ -3,6 +3,7 @@ source 'https://rubygems.org'
|
||||
# Specify your gem's dependencies in google-apis.gemspec
|
||||
gemspec
|
||||
|
||||
gem 'google-apis-core', path: '../google-apis-core'
|
||||
|
||||
group :development do
|
||||
gem 'bundler', '>= 1.7'
|
||||
@@ -42,14 +43,6 @@ platforms :ruby do
|
||||
end
|
||||
end
|
||||
|
||||
platforms :mri_21, :mri_22, :mri_23 do
|
||||
group :development do
|
||||
gem 'derailed_benchmarks'
|
||||
gem 'stackprof'
|
||||
gem 'rack-test'
|
||||
end
|
||||
end
|
||||
|
||||
if ENV['RAILS_VERSION']
|
||||
gem 'rails', ENV['RAILS_VERSION']
|
||||
end
|
||||
@@ -1,3 +1,62 @@
|
||||
require "bundler/gem_tasks"
|
||||
|
||||
namespace :metrics do
|
||||
task :lines do
|
||||
lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
|
||||
for file_name in FileList['lib/**/*.rb', 'bin/generate-api']
|
||||
f = File.open(file_name)
|
||||
while line = f.gets
|
||||
lines += 1
|
||||
next if line =~ /^\s*$/
|
||||
next if line =~ /^\s*#/
|
||||
codelines += 1
|
||||
end
|
||||
puts "L: #{sprintf('%4d', lines)}, " +
|
||||
"LOC #{sprintf('%4d', codelines)} | #{file_name}"
|
||||
total_lines += lines
|
||||
total_codelines += codelines
|
||||
|
||||
lines, codelines = 0, 0
|
||||
end
|
||||
|
||||
puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
|
||||
end
|
||||
end
|
||||
|
||||
require 'rubocop/rake_task'
|
||||
|
||||
desc 'Run RuboCop on the lib directory'
|
||||
RuboCop::RakeTask.new(:rubocop) do |task|
|
||||
task.patterns = ['lib/**/*.rb']
|
||||
# only show the files with failures
|
||||
task.formatters = ['progress']
|
||||
# don't abort rake on failure
|
||||
task.fail_on_error = false
|
||||
end
|
||||
|
||||
require 'rake/clean'
|
||||
|
||||
CLOBBER.include('coverage')
|
||||
|
||||
require 'rspec/core/rake_task'
|
||||
|
||||
namespace :spec do
|
||||
RSpec::Core::RakeTask.new(:all)
|
||||
end
|
||||
|
||||
desc 'Alias to spec:all'
|
||||
task 'spec' => 'spec:all'
|
||||
|
||||
begin
|
||||
require 'yard'
|
||||
require 'yard/rake/yardoc_task'
|
||||
|
||||
YARD::Rake::YardocTask.new do |t|
|
||||
t.files = ['lib/**/*.rb', 'generated/**/*.rb']
|
||||
t.options = ['--verbose', '--markup', 'markdown']
|
||||
end
|
||||
rescue LoadError
|
||||
puts "YARD not available"
|
||||
end
|
||||
|
||||
task :default => :spec
|
||||
@@ -13,24 +13,19 @@ Gem::Specification.new do |gem|
|
||||
gem.metadata = {
|
||||
"documentation_uri" => "https://googleapis.dev/ruby/google-api-client/v#{Google::Apis::VERSION}"
|
||||
}
|
||||
|
||||
gem.files = `git ls-files -z`
|
||||
.split("\x0")
|
||||
.reject { |f| f.match(%r{^(spec|script)/|^api_names_out|synth\.metadata}) }
|
||||
gem.files = Dir.glob("lib/**/*") +
|
||||
Dir.glob("generated/**/*.rb") +
|
||||
Dir.glob("docs/**/*") +
|
||||
Dir.glob("bin/**/*") +
|
||||
Dir.glob("*.md") +
|
||||
[".yardopts", "LICENSE"]
|
||||
|
||||
gem.executables = gem.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
||||
gem.require_paths = %w[lib generated]
|
||||
|
||||
gem.required_ruby_version = '>= 2.4'
|
||||
|
||||
gem.add_runtime_dependency 'representable', '~> 3.0'
|
||||
gem.add_runtime_dependency 'retriable', '>= 2.0', '< 4.0'
|
||||
gem.add_runtime_dependency 'addressable', '~> 2.5', '>= 2.5.1'
|
||||
gem.add_runtime_dependency 'mini_mime', '~> 1.0'
|
||||
gem.add_runtime_dependency 'signet', '~> 0.12'
|
||||
gem.add_runtime_dependency 'googleauth', '~> 0.9'
|
||||
gem.add_runtime_dependency 'httpclient', '>= 2.8.1', '< 3.0'
|
||||
gem.add_runtime_dependency 'rexml'
|
||||
gem.add_runtime_dependency 'google-apis-core', '~> 0.0'
|
||||
gem.add_development_dependency 'thor', '~> 0.19'
|
||||
gem.add_development_dependency 'activesupport', '>= 4.2', '< 5.1'
|
||||
end
|
||||
@@ -1,143 +0,0 @@
|
||||
# Copyright 2010 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 'webrick'
|
||||
|
||||
launchy_available =
|
||||
begin
|
||||
require 'launchy'
|
||||
true
|
||||
rescue LoadError
|
||||
warn "Attempted to require google/api_client/auth/installed_app.rb when" \
|
||||
" launchy is not available. The InstalledAppFlow class is disabled."
|
||||
false
|
||||
end
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
|
||||
# Small helper for the sample apps for performing OAuth 2.0 flows from the command
|
||||
# line or in any other installed app environment.
|
||||
#
|
||||
# This class is used in some sample apps and tests but is not really part
|
||||
# of the client libraries, and probably does not belong here. As such, it
|
||||
# is deprecated. If you do choose to use it, note that you must include the
|
||||
# `launchy` gem in your bundle, as it is required by this class but not
|
||||
# listed in the google-api-client gem's requirements.
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# flow = Google::APIClient::InstalledAppFlow.new(
|
||||
# :client_id => '691380668085.apps.googleusercontent.com',
|
||||
# :client_secret => '...',
|
||||
# :scope => 'https://www.googleapis.com/auth/drive'
|
||||
# )
|
||||
# authorization = flow.authorize
|
||||
# Drive = Google::Apis::DriveV2
|
||||
# drive = Drive::DriveService.new
|
||||
# drive.authorization = authorization
|
||||
#
|
||||
# @deprecated Use google-auth-library-ruby instead
|
||||
class InstalledAppFlow
|
||||
|
||||
RESPONSE_BODY = <<-HTML
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
function closeWindow() {
|
||||
window.open('', '_self', '');
|
||||
window.close();
|
||||
}
|
||||
setTimeout(closeWindow, 10);
|
||||
</script>
|
||||
</head>
|
||||
<body>You may close this window.</body>
|
||||
</html>
|
||||
HTML
|
||||
|
||||
##
|
||||
# Configure the flow
|
||||
#
|
||||
# @param [Hash] options The configuration parameters for the client.
|
||||
# @option options [Fixnum] :port
|
||||
# Port to run the embedded server on. Defaults to 9292
|
||||
# @option options [String] :client_id
|
||||
# A unique identifier issued to the client to identify itself to the
|
||||
# authorization server.
|
||||
# @option options [String] :client_secret
|
||||
# A shared symmetric secret issued by the authorization server,
|
||||
# which is used to authenticate the client.
|
||||
# @option options [String] :scope
|
||||
# The scope of the access request, expressed either as an Array
|
||||
# or as a space-delimited String.
|
||||
#
|
||||
# @see Signet::OAuth2::Client
|
||||
def initialize(options)
|
||||
@port = options[:port] || 9292
|
||||
@authorization = Signet::OAuth2::Client.new({
|
||||
:authorization_uri => 'https://accounts.google.com/o/oauth2/auth',
|
||||
:token_credential_uri => 'https://accounts.google.com/o/oauth2/token',
|
||||
:redirect_uri => "http://localhost:#{@port}/"}.update(options)
|
||||
)
|
||||
end
|
||||
|
||||
##
|
||||
# Request authorization. Opens a browser and waits for response.
|
||||
#
|
||||
# @param [Google::APIClient::Storage] storage
|
||||
# Optional object that responds to :write_credentials, used to serialize
|
||||
# the OAuth 2 credentials after completing the flow.
|
||||
#
|
||||
# @return [Signet::OAuth2::Client]
|
||||
# Authorization instance, nil if user cancelled.
|
||||
def authorize(storage=nil, options={})
|
||||
auth = @authorization
|
||||
|
||||
server = WEBrick::HTTPServer.new(
|
||||
:Port => @port,
|
||||
:BindAddress =>"localhost",
|
||||
:Logger => WEBrick::Log.new(STDOUT, 0),
|
||||
:AccessLog => []
|
||||
)
|
||||
begin
|
||||
trap("INT") { server.shutdown }
|
||||
|
||||
server.mount_proc '/' do |req, res|
|
||||
auth.code = req.query['code']
|
||||
if auth.code
|
||||
auth.fetch_access_token!
|
||||
end
|
||||
res.status = WEBrick::HTTPStatus::RC_ACCEPTED
|
||||
res.body = RESPONSE_BODY
|
||||
server.stop
|
||||
end
|
||||
|
||||
Launchy.open(auth.authorization_uri(options).to_s)
|
||||
server.start
|
||||
ensure
|
||||
server.shutdown
|
||||
end
|
||||
if @authorization.access_token
|
||||
if storage.respond_to?(:write_credentials)
|
||||
storage.write_credentials(@authorization)
|
||||
end
|
||||
return @authorization
|
||||
else
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end if launchy_available
|
||||
@@ -1,94 +0,0 @@
|
||||
# Copyright 2010 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.
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
##
|
||||
# Helper for loading keys from the PKCS12 files downloaded when
|
||||
# setting up service accounts at the APIs Console.
|
||||
#
|
||||
# @deprecated Use google-auth-library-ruby instead
|
||||
module KeyUtils
|
||||
##
|
||||
# Loads a key from PKCS12 file, assuming a single private key
|
||||
# is present.
|
||||
#
|
||||
# @param [String] keyfile
|
||||
# Path of the PKCS12 file to load. If not a path to an actual file,
|
||||
# assumes the string is the content of the file itself.
|
||||
# @param [String] passphrase
|
||||
# Passphrase for unlocking the private key
|
||||
#
|
||||
# @return [OpenSSL::PKey] The private key for signing assertions.
|
||||
def self.load_from_pkcs12(keyfile, passphrase)
|
||||
load_key(keyfile, passphrase) do |content, pass_phrase|
|
||||
OpenSSL::PKCS12.new(content, pass_phrase).key
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
##
|
||||
# Loads a key from a PEM file.
|
||||
#
|
||||
# @param [String] keyfile
|
||||
# Path of the PEM file to load. If not a path to an actual file,
|
||||
# assumes the string is the content of the file itself.
|
||||
# @param [String] passphrase
|
||||
# Passphrase for unlocking the private key
|
||||
#
|
||||
# @return [OpenSSL::PKey] The private key for signing assertions.
|
||||
#
|
||||
def self.load_from_pem(keyfile, passphrase)
|
||||
load_key(keyfile, passphrase) do | content, pass_phrase|
|
||||
OpenSSL::PKey::RSA.new(content, pass_phrase)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
##
|
||||
# Helper for loading keys from file or memory. Accepts a block
|
||||
# to handle the specific file format.
|
||||
#
|
||||
# @param [String] keyfile
|
||||
# Path of thefile to load. If not a path to an actual file,
|
||||
# assumes the string is the content of the file itself.
|
||||
# @param [String] passphrase
|
||||
# Passphrase for unlocking the private key
|
||||
#
|
||||
# @yield [String, String]
|
||||
# Key file & passphrase to extract key from
|
||||
# @yieldparam [String] keyfile
|
||||
# Contents of the file
|
||||
# @yieldparam [String] passphrase
|
||||
# Passphrase to unlock key
|
||||
# @yieldreturn [OpenSSL::PKey]
|
||||
# Private key
|
||||
#
|
||||
# @return [OpenSSL::PKey] The private key for signing assertions.
|
||||
def self.load_key(keyfile, passphrase, &block)
|
||||
begin
|
||||
begin
|
||||
content = File.open(keyfile, 'rb') { |io| io.read }
|
||||
rescue
|
||||
content = keyfile
|
||||
end
|
||||
block.call(content, passphrase)
|
||||
rescue OpenSSL::OpenSSLError
|
||||
raise ArgumentError.new("Invalid keyfile or passphrase")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,104 +0,0 @@
|
||||
# Copyright 2013 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 'signet/oauth_2/client'
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
##
|
||||
# Represents cached OAuth 2 tokens stored on local disk in a
|
||||
# JSON serialized file. Meant to resemble the serialized format
|
||||
# http://google-api-python-client.googlecode.com/hg/docs/epy/oauth2client.file.Storage-class.html
|
||||
#
|
||||
# @deprecated Use google-auth-library-ruby instead
|
||||
class Storage
|
||||
|
||||
AUTHORIZATION_URI = 'https://accounts.google.com/o/oauth2/auth'
|
||||
TOKEN_CREDENTIAL_URI = 'https://accounts.google.com/o/oauth2/token'
|
||||
|
||||
# @return [Object] Storage object.
|
||||
attr_accessor :store
|
||||
|
||||
# @return [Signet::OAuth2::Client]
|
||||
attr_reader :authorization
|
||||
|
||||
##
|
||||
# Initializes the Storage object.
|
||||
#
|
||||
# @param [Object] store
|
||||
# Storage object
|
||||
def initialize(store)
|
||||
@store= store
|
||||
@authorization = nil
|
||||
end
|
||||
|
||||
##
|
||||
# Write the credentials to the specified store.
|
||||
#
|
||||
# @param [Signet::OAuth2::Client] authorization
|
||||
# Optional authorization instance. If not provided, the authorization
|
||||
# already associated with this instance will be written.
|
||||
def write_credentials(authorization=nil)
|
||||
@authorization = authorization if authorization
|
||||
if @authorization.respond_to?(:refresh_token) && @authorization.refresh_token
|
||||
store.write_credentials(credentials_hash)
|
||||
end
|
||||
end
|
||||
|
||||
##
|
||||
# Loads credentials and authorizes an client.
|
||||
# @return [Object] Signet::OAuth2::Client or NIL
|
||||
def authorize
|
||||
@authorization = nil
|
||||
cached_credentials = load_credentials
|
||||
if cached_credentials && cached_credentials.size > 0
|
||||
@authorization = Signet::OAuth2::Client.new(cached_credentials)
|
||||
@authorization.issued_at = Time.at(cached_credentials['issued_at'].to_i)
|
||||
self.refresh_authorization if @authorization.expired?
|
||||
end
|
||||
return @authorization
|
||||
end
|
||||
|
||||
##
|
||||
# refresh credentials and save them to store
|
||||
def refresh_authorization
|
||||
authorization.refresh!
|
||||
self.write_credentials
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
##
|
||||
# Attempt to read in credentials from the specified store.
|
||||
def load_credentials
|
||||
store.load_credentials
|
||||
end
|
||||
|
||||
##
|
||||
# @return [Hash] with credentials
|
||||
def credentials_hash
|
||||
{
|
||||
:access_token => authorization.access_token,
|
||||
:authorization_uri => AUTHORIZATION_URI,
|
||||
:client_id => authorization.client_id,
|
||||
:client_secret => authorization.client_secret,
|
||||
:expires_in => authorization.expires_in,
|
||||
:refresh_token => authorization.refresh_token,
|
||||
:token_credential_uri => TOKEN_CREDENTIAL_URI,
|
||||
:issued_at => authorization.issued_at.to_i
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,57 +0,0 @@
|
||||
# Copyright 2013 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 'json'
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
##
|
||||
# Represents cached OAuth 2 tokens stored on local disk in a
|
||||
# JSON serialized file. Meant to resemble the serialized format
|
||||
# http://google-api-python-client.googlecode.com/hg/docs/epy/oauth2client.file.Storage-class.html
|
||||
#
|
||||
# @deprecated Use google-auth-library-ruby instead
|
||||
class FileStore
|
||||
|
||||
attr_accessor :path
|
||||
|
||||
##
|
||||
# Initializes the FileStorage object.
|
||||
#
|
||||
# @param [String] path
|
||||
# Path to the credentials file.
|
||||
def initialize(path)
|
||||
@path= path
|
||||
end
|
||||
|
||||
##
|
||||
# Attempt to read in credentials from the specified file.
|
||||
def load_credentials
|
||||
open(path, 'r') { |f| JSON.parse(f.read) }
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
##
|
||||
# Write the credentials to the specified file.
|
||||
#
|
||||
# @param [Hash] credentials_hash
|
||||
def write_credentials(credentials_hash)
|
||||
open(self.path, 'w+') do |f|
|
||||
f.write(credentials_hash.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,59 +0,0 @@
|
||||
# Copyright 2013 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 'json'
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
# @deprecated Use google-auth-library-ruby instead
|
||||
class RedisStore
|
||||
|
||||
DEFAULT_REDIS_CREDENTIALS_KEY = "google_api_credentials"
|
||||
|
||||
attr_accessor :redis
|
||||
|
||||
##
|
||||
# Initializes the RedisStore object.
|
||||
#
|
||||
# @param [Object] redis
|
||||
# Redis instance
|
||||
# @param [Object] key
|
||||
# Optional key to store credentials under. Defaults to 'google_api_credentials'
|
||||
def initialize(redis, key = nil)
|
||||
@redis= redis
|
||||
@redis_credentials_key = key
|
||||
end
|
||||
|
||||
##
|
||||
# Attempt to read in credentials from redis.
|
||||
# @return [Hash]
|
||||
def load_credentials
|
||||
credentials = redis.get redis_credentials_key
|
||||
JSON.parse(credentials) if credentials
|
||||
end
|
||||
|
||||
def redis_credentials_key
|
||||
@redis_credentials_key || DEFAULT_REDIS_CREDENTIALS_KEY
|
||||
end
|
||||
|
||||
##
|
||||
# Write the credentials to redis.
|
||||
#
|
||||
# @param [Hash] credentials_hash
|
||||
def write_credentials(credentials_hash)
|
||||
redis.set(redis_credentials_key, credentials_hash.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,176 +0,0 @@
|
||||
# Copyright 2010 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 'json'
|
||||
require 'googleauth'
|
||||
|
||||
|
||||
module Google
|
||||
class APIClient
|
||||
##
|
||||
# Manages the persistence of client configuration data and secrets. Format
|
||||
# inspired by the Google API Python client.
|
||||
#
|
||||
# @see https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
|
||||
# @deprecated Use google-auth-library-ruby instead
|
||||
# @example
|
||||
# {
|
||||
# "web": {
|
||||
# "client_id": "asdfjasdljfasdkjf",
|
||||
# "client_secret": "1912308409123890",
|
||||
# "redirect_uris": ["https://www.example.com/oauth2callback"],
|
||||
# "auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
# "token_uri": "https://accounts.google.com/o/oauth2/token"
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# @example
|
||||
# {
|
||||
# "installed": {
|
||||
# "client_id": "837647042410-75ifg...usercontent.com",
|
||||
# "client_secret":"asdlkfjaskd",
|
||||
# "redirect_uris": ["http://localhost", "urn:ietf:oauth:2.0:oob"],
|
||||
# "auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||
# "token_uri": "https://accounts.google.com/o/oauth2/token"
|
||||
# }
|
||||
# }
|
||||
class ClientSecrets
|
||||
##
|
||||
# Reads client configuration from a file
|
||||
#
|
||||
# @param [String] filename
|
||||
# Path to file to load
|
||||
#
|
||||
# @return [Google::APIClient::ClientSecrets]
|
||||
# OAuth client settings
|
||||
def self.load(filename=nil)
|
||||
if filename && File.directory?(filename)
|
||||
search_path = File.expand_path(filename)
|
||||
filename = nil
|
||||
end
|
||||
while filename == nil
|
||||
search_path ||= File.expand_path('.')
|
||||
if File.exists?(File.join(search_path, 'client_secrets.json'))
|
||||
filename = File.join(search_path, 'client_secrets.json')
|
||||
elsif search_path == File.expand_path('..', search_path)
|
||||
raise ArgumentError,
|
||||
'No client_secrets.json filename supplied ' +
|
||||
'and/or could not be found in search path.'
|
||||
else
|
||||
search_path = File.expand_path(File.join(search_path, '..'))
|
||||
end
|
||||
end
|
||||
data = File.open(filename, 'r') { |file| JSON.load(file.read) }
|
||||
return self.new(data)
|
||||
end
|
||||
|
||||
##
|
||||
# Initialize OAuth client settings.
|
||||
#
|
||||
# @param [Hash] options
|
||||
# Parsed client secrets files
|
||||
def initialize(options={})
|
||||
# Client auth configuration
|
||||
@flow = options[:flow] || options.keys.first.to_s || 'web'
|
||||
fdata = options[@flow.to_sym] || options[@flow]
|
||||
@client_id = fdata[:client_id] || fdata["client_id"]
|
||||
@client_secret = fdata[:client_secret] || fdata["client_secret"]
|
||||
@redirect_uris = fdata[:redirect_uris] || fdata["redirect_uris"]
|
||||
@redirect_uris ||= [fdata[:redirect_uri] || fdata["redirect_uri"]].compact
|
||||
@javascript_origins = (
|
||||
fdata[:javascript_origins] ||
|
||||
fdata["javascript_origins"]
|
||||
)
|
||||
@javascript_origins ||= [fdata[:javascript_origin] || fdata["javascript_origin"]].compact
|
||||
@authorization_uri = fdata[:auth_uri] || fdata["auth_uri"]
|
||||
@authorization_uri ||= fdata[:authorization_uri]
|
||||
@token_credential_uri = fdata[:token_uri] || fdata["token_uri"]
|
||||
@token_credential_uri ||= fdata[:token_credential_uri]
|
||||
|
||||
# Associated token info
|
||||
@access_token = fdata[:access_token] || fdata["access_token"]
|
||||
@refresh_token = fdata[:refresh_token] || fdata["refresh_token"]
|
||||
@id_token = fdata[:id_token] || fdata["id_token"]
|
||||
@expires_in = fdata[:expires_in] || fdata["expires_in"]
|
||||
@expires_at = fdata[:expires_at] || fdata["expires_at"]
|
||||
@issued_at = fdata[:issued_at] || fdata["issued_at"]
|
||||
end
|
||||
|
||||
attr_reader(
|
||||
:flow, :client_id, :client_secret, :redirect_uris, :javascript_origins,
|
||||
:authorization_uri, :token_credential_uri, :access_token,
|
||||
:refresh_token, :id_token, :expires_in, :expires_at, :issued_at
|
||||
)
|
||||
|
||||
##
|
||||
# Serialize back to the original JSON form
|
||||
#
|
||||
# @return [String]
|
||||
# JSON
|
||||
def to_json
|
||||
return Json.dump(to_hash)
|
||||
end
|
||||
|
||||
def to_hash
|
||||
{
|
||||
self.flow => ({
|
||||
'client_id' => self.client_id,
|
||||
'client_secret' => self.client_secret,
|
||||
'redirect_uris' => self.redirect_uris,
|
||||
'javascript_origins' => self.javascript_origins,
|
||||
'auth_uri' => self.authorization_uri,
|
||||
'token_uri' => self.token_credential_uri,
|
||||
'access_token' => self.access_token,
|
||||
'refresh_token' => self.refresh_token,
|
||||
'id_token' => self.id_token,
|
||||
'expires_in' => self.expires_in,
|
||||
'expires_at' => self.expires_at,
|
||||
'issued_at' => self.issued_at
|
||||
}).inject({}) do |accu, (k, v)|
|
||||
# Prunes empty values from JSON output.
|
||||
unless v == nil || (v.respond_to?(:empty?) && v.empty?)
|
||||
accu[k] = v
|
||||
end
|
||||
accu
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
def to_authorization
|
||||
# NOTE: Do not rely on this default value, as it may change
|
||||
new_authorization = Signet::OAuth2::Client.new
|
||||
new_authorization.client_id = self.client_id
|
||||
new_authorization.client_secret = self.client_secret
|
||||
new_authorization.authorization_uri = (
|
||||
self.authorization_uri ||
|
||||
'https://accounts.google.com/o/oauth2/auth'
|
||||
)
|
||||
new_authorization.token_credential_uri = (
|
||||
self.token_credential_uri ||
|
||||
'https://accounts.google.com/o/oauth2/token'
|
||||
)
|
||||
new_authorization.redirect_uri = self.redirect_uris.first
|
||||
|
||||
# These are supported, but unlikely.
|
||||
new_authorization.access_token = self.access_token
|
||||
new_authorization.refresh_token = self.refresh_token
|
||||
new_authorization.id_token = self.id_token
|
||||
new_authorization.expires_in = self.expires_in
|
||||
new_authorization.issued_at = self.issued_at if self.issued_at
|
||||
new_authorization.expires_at = self.expires_at if self.expires_at
|
||||
return new_authorization
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,58 +0,0 @@
|
||||
# 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/version'
|
||||
require 'logger'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
ROOT = File.expand_path('..', File.dirname(__dir__))
|
||||
|
||||
# @!attribute [rw] logger
|
||||
# @return [Logger] The logger.
|
||||
def self.logger
|
||||
@logger ||= rails_logger || default_logger
|
||||
end
|
||||
|
||||
class << self
|
||||
attr_writer :logger
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Create and configure a logger
|
||||
# @return [Logger]
|
||||
def self.default_logger
|
||||
logger = Logger.new($stdout)
|
||||
logger.level = Logger::WARN
|
||||
logger
|
||||
end
|
||||
|
||||
# Check to see if client is being used in a Rails environment and get the logger if present.
|
||||
# Setting the ENV variable 'GOOGLE_API_USE_RAILS_LOGGER' to false will force the client
|
||||
# to use its own logger.
|
||||
#
|
||||
# @return [Logger]
|
||||
def self.rails_logger
|
||||
if 'true' == ENV.fetch('GOOGLE_API_USE_RAILS_LOGGER', 'true') &&
|
||||
defined?(::Rails) &&
|
||||
::Rails.respond_to?(:logger) &&
|
||||
!::Rails.logger.nil?
|
||||
::Rails.logger
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,209 +0,0 @@
|
||||
# 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_api_client_header
|
||||
set_user_project_header
|
||||
if 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)
|
||||
body = "{}" if body.empty?
|
||||
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_api_client_header
|
||||
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
|
||||
|
||||
def set_user_project_header
|
||||
quota_project_id = options.quota_project
|
||||
if !quota_project_id && options&.authorization.respond_to?(:quota_project_id)
|
||||
quota_project_id = options.authorization.quota_project_id
|
||||
end
|
||||
header['X-Goog-User-Project'] = quota_project_id if quota_project_id
|
||||
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
|
||||
@@ -1,447 +0,0 @@
|
||||
# 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/version'
|
||||
require 'google/apis/core/api_command'
|
||||
require 'google/apis/core/batch'
|
||||
require 'google/apis/core/upload'
|
||||
require 'google/apis/core/download'
|
||||
require 'google/apis/options'
|
||||
require 'googleauth'
|
||||
require 'httpclient'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
module Core
|
||||
# Helper class for enumerating over a result set requiring multiple fetches
|
||||
class PagedResults
|
||||
include Enumerable
|
||||
|
||||
attr_reader :last_result
|
||||
|
||||
# @param [BaseService] service
|
||||
# Current service instance
|
||||
# @param [Fixnum] max
|
||||
# Maximum number of items to iterate over. Nil if no limit
|
||||
# @param [Boolean] cache
|
||||
# True (default) if results should be cached so multiple iterations can be used.
|
||||
# @param [Symbol] items
|
||||
# Name of the field in the result containing the items. Defaults to :items
|
||||
def initialize(service, max: nil, items: :items, cache: true, response_page_token: :next_page_token, &block)
|
||||
@service = service
|
||||
@block = block
|
||||
@max = max
|
||||
@items_field = items
|
||||
@response_page_token_field = response_page_token
|
||||
if cache
|
||||
@result_cache = Hash.new do |h, k|
|
||||
h[k] = @block.call(k, @service)
|
||||
end
|
||||
@fetch_proc = Proc.new { |token| @result_cache[token] }
|
||||
else
|
||||
@fetch_proc = Proc.new { |token| @block.call(token, @service) }
|
||||
end
|
||||
end
|
||||
|
||||
# Iterates over result set, fetching additional pages as needed
|
||||
def each
|
||||
page_token = nil
|
||||
item_count = 0
|
||||
loop do
|
||||
@last_result = @fetch_proc.call(page_token)
|
||||
items = @last_result.send(@items_field)
|
||||
if items.kind_of?(Array)
|
||||
for item in items
|
||||
item_count = item_count + 1
|
||||
break if @max && item_count > @max
|
||||
yield item
|
||||
end
|
||||
elsif items.kind_of?(Hash)
|
||||
items.each do |key, val|
|
||||
item_count = item_count + 1
|
||||
break if @max && item_count > @max
|
||||
yield key, val
|
||||
end
|
||||
elsif items
|
||||
# yield singular non-nil items (for genomics API)
|
||||
yield items
|
||||
end
|
||||
break if @max && item_count >= @max
|
||||
next_page_token = @last_result.send(@response_page_token_field)
|
||||
break if next_page_token.nil? || next_page_token == page_token
|
||||
page_token = next_page_token
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Base service for all APIs. Not to be used directly.
|
||||
#
|
||||
class BaseService
|
||||
include Logging
|
||||
|
||||
# Root URL (host/port) for the API
|
||||
# @return [Addressable::URI]
|
||||
attr_accessor :root_url
|
||||
|
||||
# Additional path prefix for all API methods
|
||||
# @return [Addressable::URI]
|
||||
attr_accessor :base_path
|
||||
|
||||
# Alternate path prefix for media uploads
|
||||
# @return [Addressable::URI]
|
||||
attr_accessor :upload_path
|
||||
|
||||
# Alternate path prefix for all batch methods
|
||||
# @return [Addressable::URI]
|
||||
attr_accessor :batch_path
|
||||
|
||||
# HTTP client
|
||||
# @return [HTTPClient]
|
||||
attr_accessor :client
|
||||
|
||||
# General settings
|
||||
# @return [Google::Apis::ClientOptions]
|
||||
attr_accessor :client_options
|
||||
|
||||
# Default options for all requests
|
||||
# @return [Google::Apis::RequestOptions]
|
||||
attr_accessor :request_options
|
||||
|
||||
# @param [String,Addressable::URI] root_url
|
||||
# Root URL for the API
|
||||
# @param [String,Addressable::URI] base_path
|
||||
# Additional path prefix for all API methods
|
||||
# @api private
|
||||
def initialize(root_url, base_path)
|
||||
self.root_url = root_url
|
||||
self.base_path = base_path
|
||||
self.upload_path = "upload/#{base_path}"
|
||||
self.batch_path = 'batch'
|
||||
self.client_options = Google::Apis::ClientOptions.default.dup
|
||||
self.request_options = Google::Apis::RequestOptions.default.dup
|
||||
end
|
||||
|
||||
# @!attribute [rw] authorization
|
||||
# @return [Signet::OAuth2::Client]
|
||||
# OAuth2 credentials
|
||||
def authorization=(authorization)
|
||||
request_options.authorization = authorization
|
||||
end
|
||||
|
||||
def authorization
|
||||
request_options.authorization
|
||||
end
|
||||
|
||||
# TODO: with(options) method
|
||||
|
||||
# Perform a batch request. Calls made within the block are sent in a single network
|
||||
# request to the server.
|
||||
#
|
||||
# @example
|
||||
# service.batch do |s|
|
||||
# s.get_item(id1) do |res, err|
|
||||
# # process response for 1st call
|
||||
# end
|
||||
# # ...
|
||||
# s.get_item(idN) do |res, err|
|
||||
# # process response for Nth call
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# @param [Hash, Google::Apis::RequestOptions] options
|
||||
# Request-specific options
|
||||
# @yield [self]
|
||||
# @return [void]
|
||||
def batch(options = nil)
|
||||
batch_command = BatchCommand.new(:post, Addressable::URI.parse(root_url + batch_path))
|
||||
batch_command.options = request_options.merge(options)
|
||||
apply_command_defaults(batch_command)
|
||||
begin
|
||||
start_batch(batch_command)
|
||||
yield self
|
||||
ensure
|
||||
end_batch
|
||||
end
|
||||
batch_command.execute(client)
|
||||
end
|
||||
|
||||
# Perform a batch upload request. Calls made within the block are sent in a single network
|
||||
# request to the server. Batch uploads are useful for uploading multiple small files. For larger
|
||||
# files, use single requests which use a resumable upload protocol.
|
||||
#
|
||||
# @example
|
||||
# service.batch do |s|
|
||||
# s.insert_item(upload_source: 'file1.txt') do |res, err|
|
||||
# # process response for 1st call
|
||||
# end
|
||||
# # ...
|
||||
# s.insert_item(upload_source: 'fileN.txt') do |res, err|
|
||||
# # process response for Nth call
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# @param [Hash, Google::Apis::RequestOptions] options
|
||||
# Request-specific options
|
||||
# @yield [self]
|
||||
# @return [void]
|
||||
def batch_upload(options = nil)
|
||||
batch_command = BatchUploadCommand.new(:put, Addressable::URI.parse(root_url + upload_path))
|
||||
batch_command.options = request_options.merge(options)
|
||||
apply_command_defaults(batch_command)
|
||||
begin
|
||||
start_batch(batch_command)
|
||||
yield self
|
||||
ensure
|
||||
end_batch
|
||||
end
|
||||
batch_command.execute(client)
|
||||
end
|
||||
|
||||
# Get the current HTTP client
|
||||
# @return [HTTPClient]
|
||||
def client
|
||||
@client ||= new_client
|
||||
end
|
||||
|
||||
|
||||
# Simple escape hatch for making API requests directly to a given
|
||||
# URL. This is not intended to be used as a generic HTTP client
|
||||
# and should be used only in cases where no service method exists
|
||||
# (e.g. fetching an export link for a Google Drive file.)
|
||||
#
|
||||
# @param [Symbol] method
|
||||
# HTTP method as symbol (e.g. :get, :post, :put, ...)
|
||||
# @param [String] url
|
||||
# URL to call
|
||||
# @param [Hash<String,String>] params
|
||||
# Optional hash of query parameters
|
||||
# @param [#read] body
|
||||
# Optional body for POST/PUT
|
||||
# @param [IO, String] download_dest
|
||||
# IO stream or filename to receive content download
|
||||
# @param [Google::Apis::RequestOptions] options
|
||||
# Request-specific options
|
||||
#
|
||||
# @yield [result, err] Result & error if block supplied
|
||||
# @yieldparam result [String] HTTP response body
|
||||
# @yieldparam err [StandardError] error object if request failed
|
||||
#
|
||||
# @return [String] HTTP response body
|
||||
def http(method, url, params: nil, body: nil, download_dest: nil, options: nil, &block)
|
||||
if download_dest
|
||||
command = DownloadCommand.new(method, url, body: body)
|
||||
else
|
||||
command = HttpCommand.new(method, url, body: body)
|
||||
end
|
||||
command.options = request_options.merge(options)
|
||||
apply_command_defaults(command)
|
||||
command.query.merge(Hash(params))
|
||||
execute_or_queue_command(command, &block)
|
||||
end
|
||||
|
||||
# Executes a given query with paging, automatically retrieving
|
||||
# additional pages as necessary. Requires a block that returns the
|
||||
# result set of a page. The current page token is supplied as an argument
|
||||
# to the block.
|
||||
#
|
||||
# Note: The returned enumerable also contains a `last_result` field
|
||||
# containing the full result of the last query executed.
|
||||
#
|
||||
# @param [Fixnum] max
|
||||
# Maximum number of items to iterate over. Defaults to nil -- no upper bound.
|
||||
# @param [Symbol] items
|
||||
# Name of the field in the result containing the items. Defaults to :items
|
||||
# @param [Boolean] cache
|
||||
# True (default) if results should be cached so multiple iterations can be used.
|
||||
# @return [Enumerble]
|
||||
# @yield [token, service]
|
||||
# Current page token & service instance
|
||||
# @yieldparam [String] token
|
||||
# Current page token to be used in the query
|
||||
# @yieldparam [service]
|
||||
# Current service instance
|
||||
# @since 0.9.4
|
||||
#
|
||||
# @example Retrieve all files,
|
||||
# file_list = service.fetch_all { |token, s| s.list_files(page_token: token) }
|
||||
# file_list.each { |f| ... }
|
||||
def fetch_all(max: nil, items: :items, cache: true, response_page_token: :next_page_token, &block)
|
||||
fail "fetch_all may not be used inside a batch" if batch?
|
||||
return PagedResults.new(self, max: max, items: items, cache: cache, response_page_token: response_page_token, &block)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Create a new upload command.
|
||||
#
|
||||
# @param [symbol] method
|
||||
# HTTP method for uploading (typically :put or :post)
|
||||
# @param [String] path
|
||||
# Additional path to upload endpoint, appended to API base path
|
||||
# @param [Hash, Google::Apis::RequestOptions] options
|
||||
# Request-specific options
|
||||
# @return [Google::Apis::Core::UploadCommand]
|
||||
def make_upload_command(method, path, options)
|
||||
template = Addressable::Template.new(root_url + upload_path + path)
|
||||
if batch?
|
||||
command = MultipartUploadCommand.new(method, template)
|
||||
else
|
||||
command = ResumableUploadCommand.new(method, template)
|
||||
end
|
||||
command.options = request_options.merge(options)
|
||||
apply_command_defaults(command)
|
||||
command
|
||||
end
|
||||
|
||||
# Create a new download command.
|
||||
#
|
||||
# @param [symbol] method
|
||||
# HTTP method for uploading (typically :get)
|
||||
# @param [String] path
|
||||
# Additional path to download endpoint, appended to API base path
|
||||
# @param [Hash, Google::Apis::RequestOptions] options
|
||||
# Request-specific options
|
||||
# @return [Google::Apis::Core::DownloadCommand]
|
||||
def make_download_command(method, path, options)
|
||||
template = Addressable::Template.new(root_url + base_path + path)
|
||||
command = DownloadCommand.new(method, template)
|
||||
command.options = request_options.merge(options)
|
||||
command.query['alt'] = 'media'
|
||||
apply_command_defaults(command)
|
||||
command
|
||||
end
|
||||
|
||||
# Create a new command.
|
||||
#
|
||||
# @param [symbol] method
|
||||
# HTTP method (:get, :post, :delete, etc...)
|
||||
# @param [String] path
|
||||
# Additional path, appended to API base path
|
||||
# @param [Hash, Google::Apis::RequestOptions] options
|
||||
# Request-specific options
|
||||
# @return [Google::Apis::Core::DownloadCommand]
|
||||
def make_simple_command(method, path, options)
|
||||
full_path =
|
||||
if path.start_with? "/"
|
||||
path[1..-1]
|
||||
else
|
||||
base_path + path
|
||||
end
|
||||
template = Addressable::Template.new(root_url + full_path)
|
||||
command = ApiCommand.new(method, template)
|
||||
command.options = request_options.merge(options)
|
||||
apply_command_defaults(command)
|
||||
command
|
||||
end
|
||||
|
||||
# Execute the request. If a batch is in progress, the request is added to the batch instead.
|
||||
#
|
||||
# @param [Google::Apis::Core::HttpCommand] command
|
||||
# Command to execute
|
||||
# @return [Object] response object if command executed and no callback supplied
|
||||
# @yield [result, err] Result & error if block supplied
|
||||
# @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 execute_or_queue_command(command, &callback)
|
||||
batch_command = current_batch
|
||||
if batch_command
|
||||
fail "Can not combine services in a batch" if Thread.current[:google_api_batch_service] != self
|
||||
batch_command.add(command, &callback)
|
||||
nil
|
||||
else
|
||||
command.execute(client, &callback)
|
||||
end
|
||||
end
|
||||
|
||||
# Update commands with service-specific options. To be implemented by subclasses
|
||||
# @param [Google::Apis::Core::HttpCommand] _command
|
||||
def apply_command_defaults(_command)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Get the current batch context
|
||||
#
|
||||
# @return [Google:Apis::Core::BatchRequest]
|
||||
def current_batch
|
||||
Thread.current[:google_api_batch]
|
||||
end
|
||||
|
||||
# Check if a batch is in progress
|
||||
# @return [Boolean]
|
||||
def batch?
|
||||
!current_batch.nil?
|
||||
end
|
||||
|
||||
# Start a new thread-local batch context
|
||||
# @param [Google::Apis::Core::BatchCommand] cmd
|
||||
def start_batch(cmd)
|
||||
fail "Batch already in progress" if batch?
|
||||
Thread.current[:google_api_batch] = cmd
|
||||
Thread.current[:google_api_batch_service] = self
|
||||
end
|
||||
|
||||
# Clear thread-local batch context
|
||||
def end_batch
|
||||
Thread.current[:google_api_batch] = nil
|
||||
Thread.current[:google_api_batch_service] = nil
|
||||
end
|
||||
|
||||
# Create a new HTTP client
|
||||
# @return [HTTPClient]
|
||||
def new_client
|
||||
client = ::HTTPClient.new
|
||||
|
||||
if client_options.transparent_gzip_decompression
|
||||
client.transparent_gzip_decompression = client_options.transparent_gzip_decompression
|
||||
end
|
||||
|
||||
client.proxy = client_options.proxy_url if client_options.proxy_url
|
||||
|
||||
if client_options.open_timeout_sec
|
||||
client.connect_timeout = client_options.open_timeout_sec
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
# Build the user agent header
|
||||
# @return [String]
|
||||
def user_agent
|
||||
sprintf('%s/%s google-api-ruby-client/%s %s (gzip)',
|
||||
client_options.application_name,
|
||||
client_options.application_version,
|
||||
Google::Apis::VERSION,
|
||||
Google::Apis::OS_VERSION)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,236 +0,0 @@
|
||||
# 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
|
||||
@@ -1,97 +0,0 @@
|
||||
# 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
|
||||
@@ -1,118 +0,0 @@
|
||||
# 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/api_command'
|
||||
require 'google/apis/errors'
|
||||
require 'addressable/uri'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
module Core
|
||||
# Streaming/resumable media download support
|
||||
class DownloadCommand < ApiCommand
|
||||
RANGE_HEADER = 'Range'
|
||||
OK_STATUS = [200, 201, 206]
|
||||
|
||||
# File or IO to write content to
|
||||
# @return [String, File, #write]
|
||||
attr_accessor :download_dest
|
||||
|
||||
# Ensure the download destination is a writable stream.
|
||||
#
|
||||
# @return [void]
|
||||
def prepare!
|
||||
@state = :start
|
||||
@download_url = nil
|
||||
@offset = 0
|
||||
if download_dest.respond_to?(:write)
|
||||
@download_io = download_dest
|
||||
@close_io_on_finish = false
|
||||
elsif download_dest.is_a?(String)
|
||||
@download_io = File.open(download_dest, 'wb')
|
||||
@close_io_on_finish = true
|
||||
else
|
||||
@download_io = StringIO.new('', 'wb')
|
||||
@close_io_on_finish = false
|
||||
end
|
||||
super
|
||||
end
|
||||
|
||||
# Close IO stream when command done. Only closes the stream if it was opened by the command.
|
||||
def release!
|
||||
@download_io.close if @close_io_on_finish
|
||||
end
|
||||
|
||||
# Execute the upload request once. Overrides the default implementation to handle streaming/chunking
|
||||
# of file content.
|
||||
#
|
||||
# @private
|
||||
# @param [HTTPClient] client
|
||||
# HTTP client
|
||||
# @yield [result, err] Result or error if block supplied
|
||||
# @return [Object]
|
||||
# @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 execute_once(client, &block)
|
||||
request_header = header.dup
|
||||
apply_request_options(request_header)
|
||||
download_offset = nil
|
||||
|
||||
if @offset > 0
|
||||
logger.debug { sprintf('Resuming download from offset %d', @offset) }
|
||||
request_header[RANGE_HEADER] = sprintf('bytes=%d-', @offset)
|
||||
end
|
||||
|
||||
http_res = client.get(url.to_s,
|
||||
query: query,
|
||||
header: request_header,
|
||||
follow_redirect: true) do |res, chunk|
|
||||
status = res.http_header.status_code.to_i
|
||||
next unless OK_STATUS.include?(status)
|
||||
|
||||
download_offset ||= (status == 206 ? @offset : 0)
|
||||
download_offset += chunk.bytesize
|
||||
|
||||
if download_offset - chunk.bytesize == @offset
|
||||
next_chunk = chunk
|
||||
else
|
||||
# Oh no! Requested a chunk, but received the entire content
|
||||
chunk_index = @offset - (download_offset - chunk.bytesize)
|
||||
next_chunk = chunk.byteslice(chunk_index..-1)
|
||||
next if next_chunk.nil?
|
||||
end
|
||||
|
||||
# logger.debug { sprintf('Writing chunk (%d bytes, %d total)', chunk.length, bytes_read) }
|
||||
@download_io.write(next_chunk)
|
||||
|
||||
@offset += next_chunk.bytesize
|
||||
end
|
||||
|
||||
@download_io.flush if @download_io.respond_to?(:flush)
|
||||
|
||||
if @close_io_on_finish
|
||||
result = nil
|
||||
else
|
||||
result = @download_io
|
||||
end
|
||||
check_status(http_res.status.to_i, http_res.header, http_res.body)
|
||||
success(result, &block)
|
||||
rescue => e
|
||||
@download_io.flush if @download_io.respond_to?(:flush)
|
||||
error(e, rethrow: true, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,44 +0,0 @@
|
||||
# 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.
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
module Core
|
||||
# Adds to_hash to objects
|
||||
module Hashable
|
||||
# Convert object to hash representation
|
||||
#
|
||||
# @return [Hash]
|
||||
def to_h
|
||||
Hash[instance_variables.map { |k| [k[1..-1].to_sym, Hashable.process_value(instance_variable_get(k))] }]
|
||||
end
|
||||
|
||||
# Recursively serialize an object
|
||||
#
|
||||
# @param [Object] val
|
||||
# @return [Hash]
|
||||
def self.process_value(val)
|
||||
case val
|
||||
when Hash
|
||||
Hash[val.map {|k, v| [k.to_sym, Hashable.process_value(v)] }]
|
||||
when Array
|
||||
val.map{ |v| Hashable.process_value(v) }
|
||||
else
|
||||
val.respond_to?(:to_h) ? val.to_h : val
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,447 +0,0 @@
|
||||
# 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/options'
|
||||
require 'google/apis/errors'
|
||||
require 'retriable'
|
||||
require 'google/apis/core/logging'
|
||||
require 'pp'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
module Core
|
||||
# Command for HTTP request/response.
|
||||
class HttpCommand
|
||||
include Logging
|
||||
|
||||
RETRIABLE_ERRORS = [Google::Apis::ServerError, Google::Apis::RateLimitError, Google::Apis::TransmissionError]
|
||||
|
||||
begin
|
||||
require 'opencensus'
|
||||
OPENCENSUS_AVAILABLE = true
|
||||
rescue LoadError
|
||||
OPENCENSUS_AVAILABLE = false
|
||||
end
|
||||
|
||||
# Request options
|
||||
# @return [Google::Apis::RequestOptions]
|
||||
attr_accessor :options
|
||||
|
||||
# HTTP request URL
|
||||
# @return [String, Addressable::URI]
|
||||
attr_accessor :url
|
||||
|
||||
# HTTP headers
|
||||
# @return [Hash]
|
||||
attr_accessor :header
|
||||
|
||||
# Request body
|
||||
# @return [#read]
|
||||
attr_accessor :body
|
||||
|
||||
# HTTP method
|
||||
# @return [symbol]
|
||||
attr_accessor :method
|
||||
|
||||
# HTTP Client
|
||||
# @return [HTTPClient]
|
||||
attr_accessor :connection
|
||||
|
||||
# Query params
|
||||
# @return [Hash]
|
||||
attr_accessor :query
|
||||
|
||||
# Path params for URL Template
|
||||
# @return [Hash]
|
||||
attr_accessor :params
|
||||
|
||||
# @param [symbol] method
|
||||
# HTTP method
|
||||
# @param [String,Addressable::URI, Addressable::Template] url
|
||||
# HTTP URL or template
|
||||
# @param [String, #read] body
|
||||
# Request body
|
||||
def initialize(method, url, body: nil)
|
||||
self.options = Google::Apis::RequestOptions.default.dup
|
||||
self.url = url
|
||||
self.url = Addressable::Template.new(url) if url.is_a?(String)
|
||||
self.method = method
|
||||
self.header = Hash.new
|
||||
self.body = body
|
||||
self.query = {}
|
||||
self.params = {}
|
||||
@opencensus_span = nil
|
||||
end
|
||||
|
||||
# Execute the command, retrying as necessary
|
||||
#
|
||||
# @param [HTTPClient] client
|
||||
# HTTP client
|
||||
# @yield [result, err] Result or error if block supplied
|
||||
# @return [Object]
|
||||
# @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 execute(client)
|
||||
prepare!
|
||||
opencensus_begin_span
|
||||
begin
|
||||
Retriable.retriable tries: options.retries + 1,
|
||||
base_interval: 1,
|
||||
multiplier: 2,
|
||||
on: RETRIABLE_ERRORS do |try|
|
||||
# This 2nd level retriable only catches auth errors, and supports 1 retry, which allows
|
||||
# auth to be re-attempted without having to retry all sorts of other failures like
|
||||
# NotFound, etc
|
||||
auth_tries = (try == 1 && authorization_refreshable? ? 2 : 1)
|
||||
Retriable.retriable tries: auth_tries,
|
||||
on: [Google::Apis::AuthorizationError, Signet::AuthorizationError, Signet::RemoteServerError, Signet::UnexpectedStatusError],
|
||||
on_retry: proc { |*| refresh_authorization } do
|
||||
execute_once(client).tap do |result|
|
||||
if block_given?
|
||||
yield result, nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
rescue => e
|
||||
if block_given?
|
||||
yield nil, e
|
||||
else
|
||||
raise e
|
||||
end
|
||||
end
|
||||
ensure
|
||||
opencensus_end_span
|
||||
@http_res = nil
|
||||
release!
|
||||
end
|
||||
|
||||
# Refresh the authorization authorization after a 401 error
|
||||
#
|
||||
# @private
|
||||
# @return [void]
|
||||
def refresh_authorization
|
||||
# Handled implicitly by auth lib, here in case need to override
|
||||
logger.debug('Retrying after authentication failure')
|
||||
end
|
||||
|
||||
# Check if attached credentials can be automatically refreshed
|
||||
# @return [Boolean]
|
||||
def authorization_refreshable?
|
||||
options.authorization.respond_to?(:apply!)
|
||||
end
|
||||
|
||||
# Prepare the request (e.g. calculate headers, add query params, serialize data, etc) before sending
|
||||
#
|
||||
# @private
|
||||
# @return [void]
|
||||
def prepare!
|
||||
normalize_unicode = true
|
||||
if options
|
||||
header.update(options.header) if options.header
|
||||
query.update(options.query) if options.query
|
||||
normalize_unicode = options.normalize_unicode
|
||||
end
|
||||
self.url = url.expand(params, nil, normalize_unicode) if url.is_a?(Addressable::Template)
|
||||
url.query_values = normalize_query_values(query).merge(url.query_values || {})
|
||||
|
||||
if allow_form_encoding?
|
||||
@form_encoded = true
|
||||
self.body = Addressable::URI.form_encode(url.query_values(Array))
|
||||
self.header['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
self.url.query_values = {}
|
||||
else
|
||||
@form_encoded = false
|
||||
end
|
||||
|
||||
self.body = '' unless self.body
|
||||
end
|
||||
|
||||
# Release any resources used by this command
|
||||
# @private
|
||||
# @return [void]
|
||||
def release!
|
||||
end
|
||||
|
||||
# Check the response and either decode body or raise error
|
||||
#
|
||||
# @param [Fixnum] status
|
||||
# HTTP status code of response
|
||||
# @param [Hash] header
|
||||
# Response headers
|
||||
# @param [String, #read] body
|
||||
# Response body
|
||||
# @return [Object]
|
||||
# Response object
|
||||
# @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 process_response(status, header, body)
|
||||
check_status(status, header, body)
|
||||
decode_response_body(header['Content-Type'].first, body)
|
||||
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)
|
||||
# TODO: 304 Not Modified depends on context...
|
||||
case status
|
||||
when 200...300
|
||||
nil
|
||||
when 301, 302, 303, 307
|
||||
message ||= sprintf('Redirect to %s', header['Location'])
|
||||
raise Google::Apis::RedirectError.new(message, status_code: status, header: header, body: body)
|
||||
when 401
|
||||
message ||= 'Unauthorized'
|
||||
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
|
||||
message ||= 'Invalid request'
|
||||
raise Google::Apis::ClientError.new(message, status_code: status, header: header, body: body)
|
||||
when 500...600
|
||||
message ||= 'Server error'
|
||||
raise Google::Apis::ServerError.new(message, status_code: status, header: header, body: body)
|
||||
else
|
||||
logger.warn(sprintf('Encountered unexpected status code %s', status))
|
||||
message ||= 'Unknown error'
|
||||
raise Google::Apis::TransmissionError.new(message, status_code: status, header: header, body: body)
|
||||
end
|
||||
end
|
||||
|
||||
# Process the actual response body. Intended to be overridden by subclasses
|
||||
#
|
||||
# @param [String] _content_type
|
||||
# Content type of body
|
||||
# @param [String, #read] body
|
||||
# Response body
|
||||
# @return [Object]
|
||||
def decode_response_body(_content_type, body)
|
||||
body
|
||||
end
|
||||
|
||||
# Process a success response
|
||||
# @param [Object] result
|
||||
# Result object
|
||||
# @return [Object] result if no block given
|
||||
# @yield [result, nil] if block given
|
||||
def success(result, &block)
|
||||
logger.debug { sprintf('Success - %s', safe_object_representation(result)) }
|
||||
block.call(result, nil) if block_given?
|
||||
result
|
||||
end
|
||||
|
||||
# Process an error response
|
||||
# @param [StandardError] err
|
||||
# Error object
|
||||
# @param [Boolean] rethrow
|
||||
# True if error should be raised again after handling
|
||||
# @return [void]
|
||||
# @yield [nil, err] if block given
|
||||
# @raise [StandardError] if no block
|
||||
def error(err, rethrow: false, &block)
|
||||
logger.debug { sprintf('Error - %s', PP.pp(err, '')) }
|
||||
if err.is_a?(HTTPClient::BadResponseError)
|
||||
begin
|
||||
res = err.res
|
||||
raise Google::Apis::TransmissionError.new(err) if res.nil?
|
||||
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?
|
||||
fail err if rethrow || block.nil?
|
||||
end
|
||||
|
||||
# Execute the command once.
|
||||
#
|
||||
# @private
|
||||
# @param [HTTPClient] client
|
||||
# HTTP client
|
||||
# @return [Object]
|
||||
# @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 execute_once(client)
|
||||
body.rewind if body.respond_to?(:rewind)
|
||||
begin
|
||||
logger.debug { sprintf('Sending HTTP %s %s', method, url) }
|
||||
request_header = header.dup
|
||||
apply_request_options(request_header)
|
||||
|
||||
@http_res = client.request(method.to_s.upcase,
|
||||
url.to_s,
|
||||
query: nil,
|
||||
body: body,
|
||||
header: request_header,
|
||||
follow_redirect: true)
|
||||
logger.debug { @http_res.status }
|
||||
logger.debug { safe_response_representation @http_res }
|
||||
response = process_response(@http_res.status.to_i, @http_res.header, @http_res.body)
|
||||
success(response)
|
||||
rescue => e
|
||||
logger.debug { sprintf('Caught error %s', e) }
|
||||
error(e, rethrow: true)
|
||||
end
|
||||
end
|
||||
|
||||
# Update the request with any specified options.
|
||||
# @param [Hash] req_header
|
||||
# HTTP headers
|
||||
# @return [void]
|
||||
def apply_request_options(req_header)
|
||||
if options.authorization.respond_to?(:apply!)
|
||||
options.authorization.apply!(req_header)
|
||||
elsif options.authorization.is_a?(String)
|
||||
req_header['Authorization'] = sprintf('Bearer %s', options.authorization)
|
||||
end
|
||||
req_header.update(header)
|
||||
end
|
||||
|
||||
def allow_form_encoding?
|
||||
[:post, :put].include?(method) && body.nil?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
UNSAFE_CLASS_NAMES = [
|
||||
"Google::Apis::CloudkmsV1::DecryptResponse"
|
||||
]
|
||||
|
||||
def safe_object_representation obj
|
||||
name = obj.class.name
|
||||
if UNSAFE_CLASS_NAMES.include? name
|
||||
"#<#{name} (fields redacted)>"
|
||||
else
|
||||
PP.pp(obj, "")
|
||||
end
|
||||
end
|
||||
|
||||
def safe_response_representation http_res
|
||||
if respond_to?(:response_class) && response_class.is_a?(Class) &&
|
||||
UNSAFE_CLASS_NAMES.include?(response_class.name)
|
||||
return "#<#{http_res.class.name} (fields redacted)>"
|
||||
end
|
||||
http_res.inspect
|
||||
end
|
||||
|
||||
def opencensus_begin_span
|
||||
return unless OPENCENSUS_AVAILABLE && options.use_opencensus
|
||||
return if @opencensus_span
|
||||
return unless OpenCensus::Trace.span_context
|
||||
|
||||
@opencensus_span = OpenCensus::Trace.start_span url.path.to_s
|
||||
@opencensus_span.kind = OpenCensus::Trace::SpanBuilder::CLIENT
|
||||
@opencensus_span.put_attribute "http.host", url.host.to_s
|
||||
@opencensus_span.put_attribute "http.method", method.to_s.upcase
|
||||
@opencensus_span.put_attribute "http.path", url.path.to_s
|
||||
if body.respond_to? :bytesize
|
||||
@opencensus_span.put_message_event \
|
||||
OpenCensus::Trace::SpanBuilder::SENT, 1, body.bytesize
|
||||
end
|
||||
|
||||
formatter = OpenCensus::Trace.config.http_formatter
|
||||
if formatter.respond_to? :header_name
|
||||
header[formatter.header_name] = formatter.serialize @opencensus_span.context.trace_context
|
||||
end
|
||||
rescue StandardError => e
|
||||
# Log exceptions and continue, so opencensus failures don't cause
|
||||
# the entire request to fail.
|
||||
logger.debug { sprintf('Error opening OpenCensus span: %s', e) }
|
||||
end
|
||||
|
||||
def opencensus_end_span
|
||||
return unless OPENCENSUS_AVAILABLE
|
||||
return unless @opencensus_span
|
||||
return unless OpenCensus::Trace.span_context
|
||||
|
||||
if @http_res
|
||||
if @http_res.body.respond_to? :bytesize
|
||||
@opencensus_span.put_message_event \
|
||||
OpenCensus::Trace::SpanBuilder::RECEIVED, 1, @http_res.body.bytesize
|
||||
end
|
||||
status = @http_res.status.to_i
|
||||
if status > 0
|
||||
@opencensus_span.set_status map_http_status status
|
||||
@opencensus_span.put_attribute "http.status_code", status
|
||||
end
|
||||
end
|
||||
|
||||
OpenCensus::Trace.end_span @opencensus_span
|
||||
@opencensus_span = nil
|
||||
rescue StandardError => e
|
||||
# Log exceptions and continue, so failures don't cause leaks by
|
||||
# aborting cleanup.
|
||||
logger.debug { sprintf('Error finishing OpenCensus span: %s', e) }
|
||||
end
|
||||
|
||||
def form_encoded?
|
||||
@form_encoded
|
||||
end
|
||||
|
||||
def map_http_status http_status
|
||||
case http_status
|
||||
when 200..399 then 0 # OK
|
||||
when 400 then 3 # INVALID_ARGUMENT
|
||||
when 401 then 16 # UNAUTHENTICATED
|
||||
when 403 then 7 # PERMISSION_DENIED
|
||||
when 404 then 5 # NOT_FOUND
|
||||
when 429 then 8 # RESOURCE_EXHAUSTED
|
||||
when 501 then 12 # UNIMPLEMENTED
|
||||
when 503 then 14 # UNAVAILABLE
|
||||
when 504 then 4 # DEADLINE_EXCEEDED
|
||||
else 2 # UNKNOWN
|
||||
end
|
||||
end
|
||||
|
||||
def normalize_query_values(input)
|
||||
input.inject({}) do |h, (k, v)|
|
||||
h[k] = normalize_query_value(v)
|
||||
h
|
||||
end
|
||||
end
|
||||
|
||||
def normalize_query_value(v)
|
||||
case v
|
||||
when Array
|
||||
v.map { |v2| normalize_query_value(v2) }
|
||||
when nil
|
||||
nil
|
||||
else
|
||||
v.to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,153 +0,0 @@
|
||||
# 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 'representable/json'
|
||||
require 'representable/json/hash'
|
||||
require 'base64'
|
||||
require 'date'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
module Core
|
||||
# Support for serializing hashes + property value/nil/unset tracking
|
||||
# To be included in representers as a feature.
|
||||
# @private
|
||||
module JsonRepresentationSupport
|
||||
def self.included(base)
|
||||
base.extend(JsonSupport)
|
||||
end
|
||||
|
||||
# @private
|
||||
module JsonSupport
|
||||
# Returns a customized getter function for Representable. Allows
|
||||
# indifferent hash/attribute access.
|
||||
#
|
||||
# @param [String] name Property name
|
||||
# @return [Proc]
|
||||
def getter_fn(name)
|
||||
ivar_name = "@#{name}".to_sym
|
||||
lambda do |_|
|
||||
if respond_to?(:fetch)
|
||||
fetch(name, instance_variable_get(ivar_name))
|
||||
else
|
||||
instance_variable_get(ivar_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a customized function for Representable that checks whether or not
|
||||
# an attribute should be serialized. Allows proper patch semantics by distinguishing
|
||||
# between nil & unset values
|
||||
#
|
||||
# @param [String] name Property name
|
||||
# @return [Proc]
|
||||
def if_fn(name)
|
||||
ivar_name = "@#{name}".to_sym
|
||||
lambda do |opts|
|
||||
if opts[:user_options] && opts[:user_options][:skip_undefined]
|
||||
if respond_to?(:key?)
|
||||
self.key?(name) || instance_variable_defined?(ivar_name)
|
||||
else
|
||||
instance_variable_defined?(ivar_name)
|
||||
end
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def set_default_options(name, options)
|
||||
if options[:base64]
|
||||
options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : Base64.urlsafe_encode64(value) }
|
||||
options[:parse_filter] = ->(fragment, _doc, *_args) { Base64.urlsafe_decode64(fragment) }
|
||||
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
|
||||
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) }
|
||||
end
|
||||
if options[:type] == Date
|
||||
options[:render_filter] = ->(value, _doc, *_args) { value.nil? ? nil : value.to_s}
|
||||
options[:parse_filter] = ->(fragment, _doc, *_args) { Date.parse(fragment) }
|
||||
end
|
||||
|
||||
options[:render_nil] = true
|
||||
options[:getter] = getter_fn(name)
|
||||
options[:if] = if_fn(name)
|
||||
end
|
||||
|
||||
# Define a single value property
|
||||
#
|
||||
# @param [String] name
|
||||
# Property name
|
||||
# @param [Hash] options
|
||||
def property(name, options = {})
|
||||
set_default_options(name, options)
|
||||
super(name, options)
|
||||
end
|
||||
|
||||
# Define a collection property
|
||||
#
|
||||
# @param [String] name
|
||||
# Property name
|
||||
# @param [Hash] options
|
||||
def collection(name, options = {})
|
||||
set_default_options(name, options)
|
||||
super(name, options)
|
||||
end
|
||||
|
||||
# Define a hash property
|
||||
#
|
||||
# @param [String] name
|
||||
# Property name
|
||||
# @param [Hash] options
|
||||
def hash(name = nil, options = nil)
|
||||
return super() unless name # Allow Object.hash
|
||||
set_default_options(name, options)
|
||||
super(name, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Base decorator for JSON representers
|
||||
#
|
||||
# @see https://github.com/apotonick/representable
|
||||
class JsonRepresentation < Representable::Decorator
|
||||
include Representable::JSON
|
||||
feature JsonRepresentationSupport
|
||||
end
|
||||
|
||||
module JsonObjectSupport
|
||||
def self.included(base)
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def from_json(json)
|
||||
representation = self.const_get(:Representation)
|
||||
representation.new(self.new).from_json(json, unwrap: self)
|
||||
end
|
||||
end
|
||||
|
||||
def to_json(*a)
|
||||
representation = self.class.const_get(:Representation)
|
||||
representation.new(self).to_hash(user_options: { skip_undefined: true }).to_json(*a)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,30 +0,0 @@
|
||||
# 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'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
module Core
|
||||
# Logging support
|
||||
module Logging
|
||||
# Get the logger instance
|
||||
# @return [Logger]
|
||||
def logger
|
||||
Google::Apis.logger
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,135 +0,0 @@
|
||||
# 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.
|
||||
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
module Core
|
||||
# Part of a multipart request for holding JSON data
|
||||
#
|
||||
# @private
|
||||
class JsonPart
|
||||
|
||||
# @param [String] value
|
||||
# JSON content
|
||||
# @param [Hash] header
|
||||
# Additional headers
|
||||
def initialize(value, header = {})
|
||||
@value = value
|
||||
@header = header
|
||||
end
|
||||
|
||||
def to_io(boundary)
|
||||
part = ''
|
||||
part << "--#{boundary}\r\n"
|
||||
part << "Content-Type: application/json\r\n"
|
||||
@header.each do |(k, v)|
|
||||
part << "#{k}: #{v}\r\n"
|
||||
end
|
||||
part << "\r\n"
|
||||
part << "#{@value}\r\n"
|
||||
StringIO.new(part)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# Part of a multipart request for holding arbitrary content.
|
||||
#
|
||||
# @private
|
||||
class FilePart
|
||||
# @param [IO] io
|
||||
# IO stream
|
||||
# @param [Hash] header
|
||||
# Additional headers
|
||||
def initialize(io, header = {})
|
||||
@io = io
|
||||
@header = header
|
||||
@length = io.respond_to?(:size) ? io.size : nil
|
||||
end
|
||||
|
||||
def to_io(boundary)
|
||||
head = ''
|
||||
head << "--#{boundary}\r\n"
|
||||
@header.each do |(k, v)|
|
||||
head << "#{k}: #{v}\r\n"
|
||||
end
|
||||
head << "Content-Length: #{@length}\r\n" unless @length.nil?
|
||||
head << "Content-Transfer-Encoding: binary\r\n"
|
||||
head << "\r\n"
|
||||
Google::Apis::Core::CompositeIO.new(StringIO.new(head), @io, StringIO.new("\r\n"))
|
||||
end
|
||||
end
|
||||
|
||||
# Helper for building multipart requests
|
||||
class Multipart
|
||||
MULTIPART_RELATED = 'multipart/related'
|
||||
|
||||
# @return [String]
|
||||
# Content type header
|
||||
attr_reader :content_type
|
||||
|
||||
# @param [String] content_type
|
||||
# Content type for the multipart request
|
||||
# @param [String] boundary
|
||||
# Part delimiter
|
||||
|
||||
def initialize(content_type: MULTIPART_RELATED, boundary: nil)
|
||||
@parts = []
|
||||
@boundary = boundary || Digest::SHA1.hexdigest(SecureRandom.random_bytes(8))
|
||||
@content_type = "#{content_type}; boundary=#{@boundary}"
|
||||
end
|
||||
|
||||
# Append JSON data part
|
||||
#
|
||||
# @param [String] body
|
||||
# JSON text
|
||||
# @param [String] content_id
|
||||
# Optional unique ID of this part
|
||||
# @return [self]
|
||||
def add_json(body, content_id: nil)
|
||||
header = {}
|
||||
header['Content-ID'] = content_id unless content_id.nil?
|
||||
@parts << Google::Apis::Core::JsonPart.new(body, header).to_io(@boundary)
|
||||
self
|
||||
end
|
||||
|
||||
# Append arbitrary data as a part
|
||||
#
|
||||
# @param [IO] upload_io
|
||||
# IO stream
|
||||
# @param [String] content_id
|
||||
# Optional unique ID of this part
|
||||
# @return [self]
|
||||
def add_upload(upload_io, content_type: nil, content_id: nil)
|
||||
header = {
|
||||
'Content-Type' => content_type || 'application/octet-stream'
|
||||
}
|
||||
header['Content-Id'] = content_id unless content_id.nil?
|
||||
@parts << Google::Apis::Core::FilePart.new(upload_io,
|
||||
header).to_io(@boundary)
|
||||
self
|
||||
end
|
||||
|
||||
# Assemble the multipart requests
|
||||
#
|
||||
# @return [IO]
|
||||
# IO stream
|
||||
def assemble
|
||||
@parts << StringIO.new("--#{@boundary}--\r\n\r\n")
|
||||
Google::Apis::Core::CompositeIO.new(*@parts)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,273 +0,0 @@
|
||||
# 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/api_command'
|
||||
require 'google/apis/errors'
|
||||
require 'addressable/uri'
|
||||
require 'tempfile'
|
||||
require 'mini_mime'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
module Core
|
||||
# Base upload command. Not intended to be used directly
|
||||
# @private
|
||||
class BaseUploadCommand < ApiCommand
|
||||
UPLOAD_PROTOCOL_HEADER = 'X-Goog-Upload-Protocol'
|
||||
UPLOAD_CONTENT_TYPE_HEADER = 'X-Goog-Upload-Header-Content-Type'
|
||||
UPLOAD_CONTENT_LENGTH = 'X-Goog-Upload-Header-Content-Length'
|
||||
CONTENT_TYPE_HEADER = 'Content-Type'
|
||||
|
||||
# File name or IO containing the content to upload
|
||||
# @return [String, File, #read]
|
||||
attr_accessor :upload_source
|
||||
|
||||
# Content type of the upload material
|
||||
# @return [String]
|
||||
attr_accessor :upload_content_type
|
||||
|
||||
# Content, as UploadIO
|
||||
# @return [Google::Apis::Core::UploadIO]
|
||||
attr_accessor :upload_io
|
||||
|
||||
# Ensure the content is readable and wrapped in an IO instance.
|
||||
#
|
||||
# @return [void]
|
||||
# @raise [Google::Apis::ClientError] if upload source is invalid
|
||||
def prepare!
|
||||
super
|
||||
if streamable?(upload_source)
|
||||
self.upload_io = upload_source
|
||||
@close_io_on_finish = false
|
||||
elsif self.upload_source.is_a?(String)
|
||||
self.upload_io = File.new(upload_source, 'r')
|
||||
if self.upload_content_type.nil?
|
||||
type = MiniMime.lookup_by_filename(upload_source)
|
||||
self.upload_content_type = type && type.content_type
|
||||
end
|
||||
@close_io_on_finish = true
|
||||
else
|
||||
fail Google::Apis::ClientError, 'Invalid upload source'
|
||||
end
|
||||
if self.upload_content_type.nil? || self.upload_content_type.empty?
|
||||
self.upload_content_type = 'application/octet-stream'
|
||||
end
|
||||
end
|
||||
|
||||
# Close IO stream when command done. Only closes the stream if it was opened by the command.
|
||||
def release!
|
||||
upload_io.close if @close_io_on_finish
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def streamable?(upload_source)
|
||||
upload_source.is_a?(IO) || upload_source.is_a?(StringIO) || upload_source.is_a?(Tempfile)
|
||||
end
|
||||
end
|
||||
|
||||
# Implementation of the raw upload protocol
|
||||
class RawUploadCommand < BaseUploadCommand
|
||||
RAW_PROTOCOL = 'raw'
|
||||
|
||||
# Ensure the content is readable and wrapped in an {{Google::Apis::Core::UploadIO}} instance.
|
||||
#
|
||||
# @return [void]
|
||||
# @raise [Google::Apis::ClientError] if upload source is invalid
|
||||
def prepare!
|
||||
super
|
||||
self.body = upload_io
|
||||
header[UPLOAD_PROTOCOL_HEADER] = RAW_PROTOCOL
|
||||
header[UPLOAD_CONTENT_TYPE_HEADER] = upload_content_type
|
||||
end
|
||||
end
|
||||
|
||||
# Implementation of the multipart upload protocol
|
||||
class MultipartUploadCommand < BaseUploadCommand
|
||||
MULTIPART_PROTOCOL = 'multipart'
|
||||
MULTIPART_RELATED = 'multipart/related'
|
||||
|
||||
# Encode the multipart request
|
||||
#
|
||||
# @return [void]
|
||||
# @raise [Google::Apis::ClientError] if upload source is invalid
|
||||
def prepare!
|
||||
super
|
||||
multipart = Multipart.new
|
||||
multipart.add_json(body)
|
||||
multipart.add_upload(upload_io, content_type: upload_content_type)
|
||||
self.body = multipart.assemble
|
||||
header['Content-Type'] = multipart.content_type
|
||||
header[UPLOAD_PROTOCOL_HEADER] = MULTIPART_PROTOCOL
|
||||
end
|
||||
end
|
||||
|
||||
# Implementation of the resumable upload protocol
|
||||
class ResumableUploadCommand < BaseUploadCommand
|
||||
UPLOAD_COMMAND_HEADER = 'X-Goog-Upload-Command'
|
||||
UPLOAD_OFFSET_HEADER = 'X-Goog-Upload-Offset'
|
||||
BYTES_RECEIVED_HEADER = 'X-Goog-Upload-Size-Received'
|
||||
UPLOAD_URL_HEADER = 'X-Goog-Upload-URL'
|
||||
UPLOAD_STATUS_HEADER = 'X-Goog-Upload-Status'
|
||||
STATUS_ACTIVE = 'active'
|
||||
STATUS_FINAL = 'final'
|
||||
STATUS_CANCELLED = 'cancelled'
|
||||
RESUMABLE = 'resumable'
|
||||
START_COMMAND = 'start'
|
||||
QUERY_COMMAND = 'query'
|
||||
UPLOAD_COMMAND = 'upload, finalize'
|
||||
|
||||
# Reset upload to initial state.
|
||||
#
|
||||
# @return [void]
|
||||
# @raise [Google::Apis::ClientError] if upload source is invalid
|
||||
def prepare!
|
||||
@state = :start
|
||||
@upload_url = nil
|
||||
@offset = 0
|
||||
# Prevent the command from populating the body with form encoding, by
|
||||
# asserting that it already has a body. Form encoding is never used
|
||||
# by upload requests.
|
||||
self.body = '' unless self.body
|
||||
super
|
||||
end
|
||||
|
||||
# Check the to see if the upload is complete or needs to be resumed.
|
||||
#
|
||||
# @param [Fixnum] status
|
||||
# HTTP status code of response
|
||||
# @param [HTTP::Message::Headers] header
|
||||
# Response headers
|
||||
# @param [String, #read] body
|
||||
# Response body
|
||||
# @return [Object]
|
||||
# Response object
|
||||
# @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 process_response(status, header, body)
|
||||
@offset = Integer(header[BYTES_RECEIVED_HEADER].first) unless header[BYTES_RECEIVED_HEADER].empty?
|
||||
@upload_url = header[UPLOAD_URL_HEADER].first unless header[UPLOAD_URL_HEADER].empty?
|
||||
upload_status = header[UPLOAD_STATUS_HEADER].first
|
||||
logger.debug { sprintf('Upload status %s', upload_status) }
|
||||
if upload_status == STATUS_ACTIVE
|
||||
@state = :active
|
||||
elsif upload_status == STATUS_FINAL
|
||||
@state = :final
|
||||
elsif upload_status == STATUS_CANCELLED
|
||||
@state = :cancelled
|
||||
fail Google::Apis::ClientError, body
|
||||
end
|
||||
super(status, header, body)
|
||||
end
|
||||
|
||||
def send_start_command(client)
|
||||
logger.debug { sprintf('Sending upload start command to %s', url) }
|
||||
|
||||
request_header = header.dup
|
||||
apply_request_options(request_header)
|
||||
request_header[UPLOAD_PROTOCOL_HEADER] = RESUMABLE
|
||||
request_header[UPLOAD_COMMAND_HEADER] = START_COMMAND
|
||||
request_header[UPLOAD_CONTENT_LENGTH] = upload_io.size.to_s
|
||||
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
|
||||
raise Google::Apis::ServerError, e.message
|
||||
end
|
||||
|
||||
# Query for the status of an incomplete upload
|
||||
#
|
||||
# @param [HTTPClient] client
|
||||
# HTTP client
|
||||
# @return [HTTP::Message]
|
||||
# @raise [Google::Apis::ServerError] Unable to send the request
|
||||
def send_query_command(client)
|
||||
logger.debug { sprintf('Sending upload query command to %s', @upload_url) }
|
||||
|
||||
request_header = header.dup
|
||||
apply_request_options(request_header)
|
||||
request_header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND
|
||||
|
||||
client.post(@upload_url, body: '', header: request_header, follow_redirect: true)
|
||||
end
|
||||
|
||||
|
||||
# Send the actual content
|
||||
#
|
||||
# @param [HTTPClient] client
|
||||
# HTTP client
|
||||
# @return [HTTP::Message]
|
||||
# @raise [Google::Apis::ServerError] Unable to send the request
|
||||
def send_upload_command(client)
|
||||
logger.debug { sprintf('Sending upload command to %s', @upload_url) }
|
||||
|
||||
content = upload_io
|
||||
content.pos = @offset
|
||||
|
||||
request_header = header.dup
|
||||
apply_request_options(request_header)
|
||||
request_header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND
|
||||
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
|
||||
|
||||
# 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.
|
||||
#
|
||||
# @private
|
||||
# @param [HTTPClient] client
|
||||
# HTTP client
|
||||
# @yield [result, err] Result or error if block supplied
|
||||
# @return [Object]
|
||||
# @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 execute_once(client, &block)
|
||||
case @state
|
||||
when :start
|
||||
response = send_start_command(client)
|
||||
result = process_response(response.status_code, response.header, response.body)
|
||||
when :active
|
||||
response = send_query_command(client)
|
||||
result = process_response(response.status_code, response.header, response.body)
|
||||
when :cancelled, :final
|
||||
error(@last_error, rethrow: true, &block)
|
||||
end
|
||||
if @state == :active
|
||||
response = send_upload_command(client)
|
||||
result = process_response(response.status_code, response.header, response.body)
|
||||
end
|
||||
|
||||
success(result, &block) if @state == :final
|
||||
rescue => e
|
||||
# Some APIs like Youtube generate non-retriable 401 errors and mark
|
||||
# the upload as finalized. Save the error just in case we get
|
||||
# retried.
|
||||
@last_error = e
|
||||
error(e, rethrow: true, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,89 +0,0 @@
|
||||
# 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.
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
# Base error, capable of wrapping another
|
||||
class Error < StandardError
|
||||
attr_reader :status_code
|
||||
attr_reader :header
|
||||
attr_reader :body
|
||||
|
||||
def initialize(err, status_code: nil, header: nil, body: nil)
|
||||
@cause = nil
|
||||
|
||||
if err.respond_to?(:backtrace)
|
||||
super(err.message)
|
||||
@cause = err
|
||||
else
|
||||
super(err.to_s)
|
||||
end
|
||||
@status_code = status_code
|
||||
@header = header.dup unless header.nil?
|
||||
@body = body
|
||||
end
|
||||
|
||||
def backtrace
|
||||
if @cause
|
||||
@cause.backtrace
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def inspect
|
||||
extra = ""
|
||||
extra << " status_code: #{status_code.inspect}" unless status_code.nil?
|
||||
extra << " header: #{header.inspect}" unless header.nil?
|
||||
extra << " body: #{body.inspect}" unless body.nil?
|
||||
|
||||
"#<#{self.class.name}: #{message}#{extra}>"
|
||||
end
|
||||
end
|
||||
|
||||
# An error which is raised when there is an unexpected response or other
|
||||
# transport error that prevents an operation from succeeding.
|
||||
class TransmissionError < Error
|
||||
end
|
||||
|
||||
# An exception that is raised if a redirect is required
|
||||
#
|
||||
class RedirectError < Error
|
||||
end
|
||||
|
||||
# A 4xx class HTTP error occurred.
|
||||
class ClientError < Error
|
||||
end
|
||||
|
||||
# A 4xx class HTTP error occurred.
|
||||
class RateLimitError < Error
|
||||
end
|
||||
|
||||
# A 403 HTTP error occurred.
|
||||
class ProjectNotLinkedError < Error
|
||||
end
|
||||
|
||||
# A 401 HTTP error occurred.
|
||||
class AuthorizationError < Error
|
||||
end
|
||||
|
||||
# A 5xx class HTTP error occurred.
|
||||
class ServerError < Error
|
||||
end
|
||||
|
||||
# Error class for problems in batch requests.
|
||||
class BatchError < Error
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,116 +0,0 @@
|
||||
# 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.
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
# General options for API requests
|
||||
ClientOptions = Struct.new(
|
||||
:application_name,
|
||||
:application_version,
|
||||
:proxy_url,
|
||||
:open_timeout_sec,
|
||||
:read_timeout_sec,
|
||||
:send_timeout_sec,
|
||||
:log_http_requests,
|
||||
:transparent_gzip_decompression)
|
||||
|
||||
RequestOptions = Struct.new(
|
||||
:authorization,
|
||||
:retries,
|
||||
:header,
|
||||
:normalize_unicode,
|
||||
:skip_serialization,
|
||||
:skip_deserialization,
|
||||
:api_format_version,
|
||||
:use_opencensus,
|
||||
:quota_project,
|
||||
:query)
|
||||
|
||||
# General client options
|
||||
class ClientOptions
|
||||
# @!attribute [rw] application_name
|
||||
# @return [String] Name of the application, for identification in the User-Agent header
|
||||
# @!attribute [rw] application_version
|
||||
# @return [String] Version of the application, for identification in the User-Agent header
|
||||
# @!attribute [rw] proxy_url
|
||||
# @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
|
||||
# @!attribute [rw] transparent_gzip_decompression
|
||||
# @return [Boolean] True if gzip compression needs to be enabled
|
||||
# Get the default options
|
||||
# @return [Google::Apis::ClientOptions]
|
||||
def self.default
|
||||
@options ||= ClientOptions.new
|
||||
end
|
||||
end
|
||||
|
||||
# Request options
|
||||
class RequestOptions
|
||||
# @!attribute [rw] authorization
|
||||
# @return [Signet::OAuth2::Client, #apply(Hash)] OAuth2 credentials.
|
||||
# @!attribute [rw] retries
|
||||
# @return [Fixnum] Number of times to retry requests on server error.
|
||||
# @!attribute [rw] header
|
||||
# @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.
|
||||
# @!attribute [rw] api_format_version
|
||||
# @return [Fixnum] Version of the error format to request/expect.
|
||||
# @!attribute [rw] use_opencensus
|
||||
# @return [Boolean] Whether OpenCensus spans should be generated for requests. Default is true.
|
||||
# @!attribute [rw] quota_project
|
||||
# @return [String] Project ID to charge quota, or `nil` to default to the credentials-specified project.
|
||||
# @!attribute [rw] query
|
||||
# @return [Hash<String,String>] Additional HTTP URL query parameters to include in requests.
|
||||
|
||||
# Get the default options
|
||||
# @return [Google::Apis::RequestOptions]
|
||||
def self.default
|
||||
@options ||= RequestOptions.new
|
||||
end
|
||||
|
||||
def merge(options)
|
||||
return self if options.nil?
|
||||
|
||||
new_options = dup
|
||||
members.each do |opt|
|
||||
opt = opt.to_sym
|
||||
new_options[opt] = options[opt] unless options[opt].nil?
|
||||
end
|
||||
new_options
|
||||
end
|
||||
end
|
||||
|
||||
ClientOptions.default.log_http_requests = false
|
||||
ClientOptions.default.application_name = 'unknown'
|
||||
ClientOptions.default.application_version = '0.0.0'
|
||||
ClientOptions.default.transparent_gzip_decompression = true
|
||||
RequestOptions.default.retries = 0
|
||||
RequestOptions.default.normalize_unicode = false
|
||||
RequestOptions.default.skip_serialization = false
|
||||
RequestOptions.default.skip_deserialization = false
|
||||
RequestOptions.default.api_format_version = nil
|
||||
RequestOptions.default.use_opencensus = true
|
||||
RequestOptions.default.quota_project = nil
|
||||
end
|
||||
end
|
||||
@@ -18,27 +18,5 @@ module Google
|
||||
module Apis
|
||||
# Client library version
|
||||
VERSION = "0.52.0".freeze
|
||||
|
||||
# Current operating system
|
||||
# @private
|
||||
OS_VERSION = begin
|
||||
if RUBY_PLATFORM =~ /mswin|win32|mingw|bccwin|cygwin/
|
||||
output, _ = Open3.capture2('ver')
|
||||
output.sub(/\s*\[Version\s*/, '/').sub(']', '')
|
||||
elsif RUBY_PLATFORM =~ /darwin/i
|
||||
output, _ = Open3.capture2('sw_vers', '-productVersion')
|
||||
"Mac OS X/#{output}"
|
||||
elsif RUBY_PLATFORM == 'java'
|
||||
require 'java'
|
||||
name = java.lang.System.getProperty('os.name')
|
||||
version = java.lang.System.getProperty('os.version')
|
||||
"#{name}/#{version}"
|
||||
else
|
||||
output, _ = Open3.capture2('uname', '-sr')
|
||||
output.sub(' ', '/')
|
||||
end.strip
|
||||
rescue
|
||||
RUBY_PLATFORM
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,22 +0,0 @@
|
||||
namespace :metrics do
|
||||
task :lines do
|
||||
lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
|
||||
for file_name in FileList['lib/**/*.rb', 'bin/generate-api']
|
||||
f = File.open(file_name)
|
||||
while line = f.gets
|
||||
lines += 1
|
||||
next if line =~ /^\s*$/
|
||||
next if line =~ /^\s*#/
|
||||
codelines += 1
|
||||
end
|
||||
puts "L: #{sprintf('%4d', lines)}, " +
|
||||
"LOC #{sprintf('%4d', codelines)} | #{file_name}"
|
||||
total_lines += lines
|
||||
total_codelines += codelines
|
||||
|
||||
lines, codelines = 0, 0
|
||||
end
|
||||
|
||||
puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
|
||||
end
|
||||
end
|
||||
@@ -1,10 +0,0 @@
|
||||
require 'rubocop/rake_task'
|
||||
|
||||
desc 'Run RuboCop on the lib directory'
|
||||
RuboCop::RakeTask.new(:rubocop) do |task|
|
||||
task.patterns = ['lib/**/*.rb']
|
||||
# only show the files with failures
|
||||
task.formatters = ['progress']
|
||||
# don't abort rake on failure
|
||||
task.fail_on_error = false
|
||||
end
|
||||
@@ -1,11 +0,0 @@
|
||||
require 'rake/clean'
|
||||
require 'rspec/core/rake_task'
|
||||
|
||||
CLOBBER.include('coverage')
|
||||
|
||||
namespace :spec do
|
||||
RSpec::Core::RakeTask.new(:all)
|
||||
end
|
||||
|
||||
desc 'Alias to spec:all'
|
||||
task 'spec' => 'spec:all'
|
||||
@@ -1,11 +0,0 @@
|
||||
begin
|
||||
require 'yard'
|
||||
require 'yard/rake/yardoc_task'
|
||||
|
||||
YARD::Rake::YardocTask.new do |t|
|
||||
t.files = ['lib/**/*.rb', 'generated/**/*.rb']
|
||||
t.options = ['--verbose', '--markup', 'markdown']
|
||||
end
|
||||
rescue LoadError
|
||||
puts "YARD not available"
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
{ "access_token":"access_token_123456789",
|
||||
"authorization_uri":"https://accounts.google.com/o/oauth2/auth",
|
||||
"client_id":"123456789p.apps.googleusercontent.com",
|
||||
"client_secret":"very_secret",
|
||||
"expires_in":3600,
|
||||
"refresh_token":"refresh_token_12345679",
|
||||
"token_credential_uri":"https://accounts.google.com/o/oauth2/token",
|
||||
"issued_at":1386053761}
|
||||
Whitespace-only changes.
@@ -1 +0,0 @@
|
||||
{"installed":{"auth_uri":"https://accounts.google.com/o/oauth2/auth","client_secret":"i8YaXdGgiQ4_KrTVNGsB7QP1","token_uri":"https://accounts.google.com/o/oauth2/token","client_email":"","client_x509_cert_url":"","client_id":"898243283568.apps.googleusercontent.com","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}}
|
||||
@@ -1 +0,0 @@
|
||||
{"invalid"}
|
||||
@@ -1 +0,0 @@
|
||||
Hello world
|
||||
@@ -1,41 +0,0 @@
|
||||
require 'spec_helper'
|
||||
|
||||
require 'google/api_client/auth/installed_app'
|
||||
|
||||
describe Google::APIClient::InstalledAppFlow do
|
||||
let(:secrets) do
|
||||
file = File.join(FIXTURES_DIR, 'files', 'client_secrets.json')
|
||||
Google::APIClient::ClientSecrets.load(file)
|
||||
end
|
||||
|
||||
subject(:flow) { Google::APIClient::InstalledAppFlow.new(
|
||||
:client_id => secrets.client_id,
|
||||
:client_secret => secrets.client_secret,
|
||||
:scope => 'https://www.googleapis.com/auth/drive.metadata.readonly',
|
||||
:port => 5000)}
|
||||
|
||||
context 'initialization' do
|
||||
|
||||
it 'should be valid instance' do
|
||||
expect(flow).to be_a Google::APIClient::InstalledAppFlow
|
||||
end
|
||||
|
||||
it 'should have valid port' do
|
||||
expect(flow.instance_variable_get("@port")).to eq 5000
|
||||
end
|
||||
|
||||
it 'should have default port' do
|
||||
another_flow = Google::APIClient::InstalledAppFlow.new(
|
||||
:client_id => secrets.client_id,
|
||||
:client_secret => secrets.client_secret,
|
||||
:scope => 'https://www.googleapis.com/auth/drive.metadata.readonly')
|
||||
expect(another_flow.instance_variable_get("@port")).to eq 9292
|
||||
end
|
||||
|
||||
it 'should have valid client_id' do
|
||||
auth = flow.instance_variable_get "@authorization"
|
||||
expect(auth.client_id).to eq "898243283568.apps.googleusercontent.com"
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,128 +0,0 @@
|
||||
require 'spec_helper'
|
||||
|
||||
require 'google/api_client/auth/storage'
|
||||
|
||||
describe Google::APIClient::Storage do
|
||||
let(:root_path) { ROOT_DIR }
|
||||
let(:json_file) { File.expand_path(File.join(FIXTURES_DIR, 'files', 'auth_stored_credentials.json')) }
|
||||
|
||||
let(:store) { double }
|
||||
let(:client_stub) { double }
|
||||
subject { Google::APIClient::Storage.new(store) }
|
||||
|
||||
describe 'constants' do
|
||||
it "should have valid authorization and token_credential_uri" do
|
||||
expect(Google::APIClient::Storage::AUTHORIZATION_URI).to eq "https://accounts.google.com/o/oauth2/auth"
|
||||
expect(Google::APIClient::Storage::TOKEN_CREDENTIAL_URI).to eq "https://accounts.google.com/o/oauth2/token"
|
||||
end
|
||||
end
|
||||
describe 'authorize' do
|
||||
it 'should authorize' do
|
||||
expect(subject).to respond_to(:authorization)
|
||||
expect(subject.authorization).to be_nil
|
||||
expect(subject.store).to be == store
|
||||
end
|
||||
end
|
||||
|
||||
describe 'authorize' do
|
||||
describe 'with credentials' do
|
||||
|
||||
it 'should initialize a new OAuth Client' do
|
||||
expect(subject).to receive(:load_credentials).and_return({:first => 'a dummy'})
|
||||
expect(client_stub).to receive(:issued_at=)
|
||||
expect(client_stub).to receive(:expired?).and_return(false)
|
||||
expect(Signet::OAuth2::Client).to receive(:new).and_return(client_stub)
|
||||
expect(subject).not_to receive(:refresh_authorization)
|
||||
subject.authorize
|
||||
end
|
||||
|
||||
it 'should refresh authorization' do
|
||||
expect(subject).to receive(:load_credentials).and_return({:first => 'a dummy'})
|
||||
expect(client_stub).to receive(:issued_at=)
|
||||
expect(client_stub).to receive(:expired?).and_return(true)
|
||||
expect(Signet::OAuth2::Client).to receive(:new).and_return(client_stub)
|
||||
expect(subject).to receive(:refresh_authorization)
|
||||
auth = subject.authorize
|
||||
expect(auth).to be == subject.authorization
|
||||
expect(auth).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'without credentials' do
|
||||
|
||||
it 'should return nil' do
|
||||
expect(subject.authorization).to be_nil
|
||||
expect(subject).to receive(:load_credentials).and_return({})
|
||||
expect(subject.authorize).to be_nil
|
||||
expect(subject.authorization).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'write_credentials' do
|
||||
it 'should call store to write credentials' do
|
||||
authorization_stub = double
|
||||
expect(authorization_stub).to receive(:refresh_token).and_return(true)
|
||||
expect(subject).to receive(:credentials_hash)
|
||||
expect(subject.store).to receive(:write_credentials)
|
||||
subject.write_credentials(authorization_stub)
|
||||
expect(subject.authorization).to be == authorization_stub
|
||||
end
|
||||
|
||||
it 'should not call store to write credentials' do
|
||||
expect(subject).not_to receive(:credentials_hash)
|
||||
expect(subject.store).not_to receive(:write_credentials)
|
||||
expect {
|
||||
subject.write_credentials()
|
||||
}.not_to raise_error
|
||||
end
|
||||
|
||||
it 'should not call store to write credentials' do
|
||||
expect(subject).not_to receive(:credentials_hash)
|
||||
expect(subject.store).not_to receive(:write_credentials)
|
||||
expect {
|
||||
subject.write_credentials('something')
|
||||
}.not_to raise_error
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'refresh_authorization' do
|
||||
it 'should call refresh and write credentials' do
|
||||
expect(subject).to receive(:write_credentials)
|
||||
authorization_stub = double
|
||||
expect(subject).to receive(:authorization).and_return(authorization_stub)
|
||||
expect(authorization_stub).to receive(:refresh!).and_return(true)
|
||||
subject.refresh_authorization
|
||||
end
|
||||
end
|
||||
|
||||
describe 'load_credentials' do
|
||||
it 'should call store to load credentials' do
|
||||
expect(subject.store).to receive(:load_credentials)
|
||||
subject.send(:load_credentials)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'credentials_hash' do
|
||||
it 'should return an hash' do
|
||||
authorization_stub = double
|
||||
expect(authorization_stub).to receive(:access_token)
|
||||
expect(authorization_stub).to receive(:client_id)
|
||||
expect(authorization_stub).to receive(:client_secret)
|
||||
expect(authorization_stub).to receive(:expires_in)
|
||||
expect(authorization_stub).to receive(:refresh_token)
|
||||
expect(authorization_stub).to receive(:issued_at).and_return('100')
|
||||
allow(subject).to receive(:authorization).and_return(authorization_stub)
|
||||
credentials = subject.send(:credentials_hash)
|
||||
expect(credentials).to include(:access_token)
|
||||
expect(credentials).to include(:authorization_uri)
|
||||
expect(credentials).to include(:client_id)
|
||||
expect(credentials).to include(:client_secret)
|
||||
expect(credentials).to include(:expires_in)
|
||||
expect(credentials).to include(:refresh_token)
|
||||
expect(credentials).to include(:token_credential_uri)
|
||||
expect(credentials).to include(:issued_at)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,48 +0,0 @@
|
||||
require 'spec_helper'
|
||||
|
||||
require 'google/api_client/auth/storages/file_store'
|
||||
|
||||
describe Google::APIClient::FileStore do
|
||||
let(:json_file) { File.join(FIXTURES_DIR, 'files', 'auth_stored_credentials.json') }
|
||||
|
||||
let(:credentials_hash) {{
|
||||
"access_token"=>"my_access_token",
|
||||
"authorization_uri"=>"https://accounts.google.com/o/oauth2/auth",
|
||||
"client_id"=>"123456_test_client_id@.apps.googleusercontent.com",
|
||||
"client_secret"=>"123456_client_secret",
|
||||
"expires_in"=>3600,
|
||||
"refresh_token"=>"my_refresh_token",
|
||||
"token_credential_uri"=>"https://accounts.google.com/o/oauth2/token",
|
||||
"issued_at"=>1384440275
|
||||
}}
|
||||
|
||||
subject{Google::APIClient::FileStore.new('a file path')}
|
||||
|
||||
it 'should have a path' do
|
||||
expect(subject.path).to be == 'a file path'
|
||||
subject.path = 'an other file path'
|
||||
expect(subject.path).to be == 'an other file path'
|
||||
end
|
||||
|
||||
it 'should load credentials' do
|
||||
subject.path = json_file
|
||||
credentials = subject.load_credentials
|
||||
expect(credentials).to include(
|
||||
'access_token', 'authorization_uri', 'refresh_token', 'client_id',
|
||||
'client_secret', 'expires_in', 'token_credential_uri', 'issued_at')
|
||||
end
|
||||
|
||||
it 'should write credentials' do
|
||||
io_stub = StringIO.new
|
||||
expect(subject).to receive(:open).and_return(io_stub)
|
||||
subject.write_credentials(credentials_hash)
|
||||
end
|
||||
|
||||
it 'should not load credentials' do
|
||||
file = StringIO.new
|
||||
file.write "{invalid_hash}"
|
||||
invalid_subject = Google::APIClient::FileStore.new(file)
|
||||
expect(invalid_subject.load_credentials).to be_nil
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,68 +0,0 @@
|
||||
require 'spec_helper'
|
||||
|
||||
require 'google/api_client/auth/storages/redis_store'
|
||||
|
||||
describe Google::APIClient::RedisStore do
|
||||
let(:root_path) { File.expand_path(File.join(__FILE__, '..', '..', '..', '..', '..')) }
|
||||
let(:json_file) { File.expand_path(File.join(root_path, 'fixtures', 'files', 'auth_stored_credentials.json')) }
|
||||
let(:redis) {double}
|
||||
|
||||
let(:credentials_hash) { {
|
||||
"access_token" => "my_access_token",
|
||||
"authorization_uri" => "https://accounts.google.com/o/oauth2/auth",
|
||||
"client_id" => "123456_test_client_id@.apps.googleusercontent.com",
|
||||
"client_secret" => "123456_client_secret",
|
||||
"expires_in" => 3600,
|
||||
"refresh_token" => "my_refresh_token",
|
||||
"token_credential_uri" => "https://accounts.google.com/o/oauth2/token",
|
||||
"issued_at" => 1384440275
|
||||
} }
|
||||
|
||||
subject { Google::APIClient::RedisStore.new('a redis instance') }
|
||||
|
||||
it 'should have a redis instance' do
|
||||
expect(subject.redis).to be == 'a redis instance'
|
||||
subject.redis = 'an other redis instance'
|
||||
expect(subject.redis).to be == 'an other redis instance'
|
||||
end
|
||||
|
||||
describe 'load_credentials' do
|
||||
|
||||
it 'should load credentials' do
|
||||
subject.redis= redis
|
||||
expect(redis).to receive(:get).and_return(credentials_hash.to_json)
|
||||
expect(subject.load_credentials).to be == credentials_hash
|
||||
end
|
||||
|
||||
it 'should return nil' do
|
||||
subject.redis= redis
|
||||
expect(redis).to receive(:get).and_return(nil)
|
||||
expect(subject.load_credentials).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'redis_credentials_key' do
|
||||
context 'without given key' do
|
||||
it 'should return default key' do
|
||||
expect(subject.redis_credentials_key).to be == "google_api_credentials"
|
||||
end
|
||||
end
|
||||
context 'with given key' do
|
||||
let(:redis_store) { Google::APIClient::RedisStore.new('a redis instance', 'another_google_api_credentials') }
|
||||
it 'should use given key' do
|
||||
expect(redis_store.redis_credentials_key).to be == "another_google_api_credentials"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe 'write credentials' do
|
||||
|
||||
it 'should write credentials' do
|
||||
subject.redis= redis
|
||||
expect(redis).to receive(:set).and_return('ok')
|
||||
expect(subject.write_credentials(credentials_hash)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,387 +0,0 @@
|
||||
# Copyright 2013 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/api_client/client_secrets'
|
||||
|
||||
FIXTURES_PATH = File.expand_path('../../../fixtures', __FILE__)
|
||||
|
||||
RSpec.describe Google::APIClient::ClientSecrets do
|
||||
describe '::new' do
|
||||
let(:filename) { File.join(FIXTURES_PATH, 'files', 'client_secrets.json') }
|
||||
let(:data) { File.open(filename, 'r') { |file| JSON.load(file.read) } }
|
||||
|
||||
context 'without options' do
|
||||
subject { Google::APIClient::ClientSecrets.new(data) }
|
||||
|
||||
describe 'existence of instance variables' do
|
||||
it '@access_token' do
|
||||
expect(subject).to respond_to(:access_token)
|
||||
end
|
||||
it '@authorization_uri' do
|
||||
expect(subject).to respond_to(:authorization_uri)
|
||||
end
|
||||
it '@client_id' do
|
||||
expect(subject).to respond_to(:client_id)
|
||||
end
|
||||
it '@client_secret' do
|
||||
expect(subject).to respond_to(:client_secret)
|
||||
end
|
||||
it '@expires_at' do
|
||||
expect(subject).to respond_to(:expires_at)
|
||||
end
|
||||
it '@expires_in' do
|
||||
expect(subject).to respond_to(:expires_in)
|
||||
end
|
||||
it '@id_token' do
|
||||
expect(subject).to respond_to(:id_token)
|
||||
end
|
||||
it '@issued_at' do
|
||||
expect(subject).to respond_to(:issued_at)
|
||||
end
|
||||
it '@javascript_origins' do
|
||||
expect(subject).to respond_to(:javascript_origins)
|
||||
end
|
||||
it '@redirect_uris' do
|
||||
expect(subject).to respond_to(:redirect_uris)
|
||||
end
|
||||
it '@refresh_token' do
|
||||
expect(subject).to respond_to(:refresh_token)
|
||||
end
|
||||
it '@token_credential_uri' do
|
||||
expect(subject).to respond_to(:token_credential_uri)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'value of instance variables' do
|
||||
it 'should set @access_token' do
|
||||
expect(subject.access_token).to be_nil
|
||||
end
|
||||
it 'should set @authorization_uri' do
|
||||
expect(subject.authorization_uri).to eq 'https://accounts.google.com/o/oauth2/auth'
|
||||
end
|
||||
it 'should set @client_id' do
|
||||
expect(subject.client_id).to eq '898243283568.apps.googleusercontent.com'
|
||||
end
|
||||
it 'should set @client_secret' do
|
||||
expect(subject.client_secret).to eq 'i8YaXdGgiQ4_KrTVNGsB7QP1'
|
||||
end
|
||||
it 'should set @expires_at' do
|
||||
expect(subject.expires_at).to be_nil
|
||||
end
|
||||
it 'should set @expires_in' do
|
||||
expect(subject.expires_in).to be_nil
|
||||
end
|
||||
it 'should set @flow' do
|
||||
expect(subject.flow).to eq 'installed'
|
||||
end
|
||||
it 'should set @id_token' do
|
||||
expect(subject.id_token).to be_nil
|
||||
end
|
||||
it 'should set @issued_at' do
|
||||
expect(subject.issued_at).to be_nil
|
||||
end
|
||||
it 'should set @javascript_origins' do
|
||||
expect(subject.javascript_origins).to eq []
|
||||
end
|
||||
it 'should set @redirect_uris' do
|
||||
expect(subject.redirect_uris).to eq []
|
||||
end
|
||||
it 'should set @refresh_token' do
|
||||
expect(subject.refresh_token).to be_nil
|
||||
end
|
||||
it 'should set @token_credential_uri' do
|
||||
expect(subject.token_credential_uri).to eq 'https://accounts.google.com/o/oauth2/token'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with options' do
|
||||
context 'option keys are string' do
|
||||
let(:string_options) do
|
||||
{ 'samples' =>
|
||||
{
|
||||
'access_token' => 'sample_access_token',
|
||||
'auth_uri' => 'sample_auth_uri',
|
||||
'authorization_uri' => 'sample_authorization_uri',
|
||||
'client_id' => 'sample_client_id',
|
||||
'client_secret' => 'sample_client_secret',
|
||||
'expires_at' => 'sample_expires_at',
|
||||
'expires_in' => 'sample_expires_in',
|
||||
'id_token' => 'sample_id_token',
|
||||
'issued_at' => 'sample_issued_at',
|
||||
'javascript_origin' => 'sample_javascript_origin',
|
||||
'javascript_origins' => 'sample_javascript_origins',
|
||||
'redirect_uris' => 'sample_redirect_uris',
|
||||
'refresh_token' => 'sample_refresh_token',
|
||||
'token_credential_uri' => 'sample_token_credential_uri',
|
||||
'token_uri' => 'sample_token_uri' } }
|
||||
end
|
||||
subject { Google::APIClient::ClientSecrets.new(string_options) }
|
||||
|
||||
describe 'existence of instance variables' do
|
||||
it '@access_token' do
|
||||
expect(subject).to respond_to(:access_token)
|
||||
end
|
||||
it '@authorization_uri' do
|
||||
expect(subject).to respond_to(:authorization_uri)
|
||||
end
|
||||
it '@client_id' do
|
||||
expect(subject).to respond_to(:client_id)
|
||||
end
|
||||
it '@client_secret' do
|
||||
expect(subject).to respond_to(:client_secret)
|
||||
end
|
||||
it '@expires_at' do
|
||||
expect(subject).to respond_to(:expires_at)
|
||||
end
|
||||
it '@expires_in' do
|
||||
expect(subject).to respond_to(:expires_in)
|
||||
end
|
||||
it '@id_token' do
|
||||
expect(subject).to respond_to(:id_token)
|
||||
end
|
||||
it '@issued_at' do
|
||||
expect(subject).to respond_to(:issued_at)
|
||||
end
|
||||
it '@javascript_origins' do
|
||||
expect(subject).to respond_to(:javascript_origins)
|
||||
end
|
||||
it '@redirect_uris' do
|
||||
expect(subject).to respond_to(:redirect_uris)
|
||||
end
|
||||
it '@refresh_token' do
|
||||
expect(subject).to respond_to(:refresh_token)
|
||||
end
|
||||
it '@token_credential_uri' do
|
||||
expect(subject).to respond_to(:token_credential_uri)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'value of instance variables' do
|
||||
it 'should set @access_token' do
|
||||
expect(subject.access_token).to eq 'sample_access_token'
|
||||
end
|
||||
it 'should set @authorization_uri' do
|
||||
expect(subject.authorization_uri).to eq 'sample_auth_uri'
|
||||
end
|
||||
it 'should set @client_id' do
|
||||
expect(subject.client_id).to eq 'sample_client_id'
|
||||
end
|
||||
it 'should set @client_secret' do
|
||||
expect(subject.client_secret).to eq 'sample_client_secret'
|
||||
end
|
||||
it 'should set @expires_at' do
|
||||
expect(subject.expires_at).to eq 'sample_expires_at'
|
||||
end
|
||||
it 'should set @expires_in' do
|
||||
expect(subject.expires_in).to eq 'sample_expires_in'
|
||||
end
|
||||
it 'should set @flow' do
|
||||
expect(subject.flow).to eq 'samples'
|
||||
end
|
||||
it 'should set @id_token' do
|
||||
expect(subject.id_token).to eq 'sample_id_token'
|
||||
end
|
||||
it 'should set @issued_at' do
|
||||
expect(subject.issued_at).to eq 'sample_issued_at'
|
||||
end
|
||||
it 'should set @javascript_origins' do
|
||||
expect(subject.javascript_origins).to eq 'sample_javascript_origins'
|
||||
end
|
||||
it 'should set @redirect_uris' do
|
||||
expect(subject.redirect_uris).to eq 'sample_redirect_uris'
|
||||
end
|
||||
it 'should set @refresh_token' do
|
||||
expect(subject.refresh_token).to eq 'sample_refresh_token'
|
||||
end
|
||||
it 'should set @token_credential_uri' do
|
||||
expect(subject.token_credential_uri).to eq 'sample_token_uri'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'option keys are symbol' do
|
||||
let(:symbol_options) do
|
||||
{ samples:
|
||||
{
|
||||
access_token: 'sample_access_token',
|
||||
auth_uri: 'sample_auth_uri',
|
||||
authorization_uri: 'sample_authorization_uri',
|
||||
client_id: 'sample_client_id',
|
||||
client_secret: 'sample_client_secret',
|
||||
expires_at: 'sample_expires_at',
|
||||
expires_in: 'sample_expires_in',
|
||||
id_token: 'sample_id_token',
|
||||
issued_at: 'sample_issued_at',
|
||||
javascript_origin: 'sample_javascript_origin',
|
||||
javascript_origins: 'sample_javascript_origins',
|
||||
redirect_uris: 'sample_redirect_uris',
|
||||
refresh_token: 'sample_refresh_token',
|
||||
token_credential_uri: 'sample_token_credential_uri',
|
||||
token_uri: 'sample_token_uri' } }
|
||||
end
|
||||
subject { Google::APIClient::ClientSecrets.new(symbol_options) }
|
||||
|
||||
describe 'existence of instance variables' do
|
||||
it '@access_token' do
|
||||
expect(subject).to respond_to(:access_token)
|
||||
end
|
||||
it '@authorization_uri' do
|
||||
expect(subject).to respond_to(:authorization_uri)
|
||||
end
|
||||
it '@client_id' do
|
||||
expect(subject).to respond_to(:client_id)
|
||||
end
|
||||
it '@client_secret' do
|
||||
expect(subject).to respond_to(:client_secret)
|
||||
end
|
||||
it '@expires_at' do
|
||||
expect(subject).to respond_to(:expires_at)
|
||||
end
|
||||
it '@expires_in' do
|
||||
expect(subject).to respond_to(:expires_in)
|
||||
end
|
||||
it '@id_token' do
|
||||
expect(subject).to respond_to(:id_token)
|
||||
end
|
||||
it '@issued_at' do
|
||||
expect(subject).to respond_to(:issued_at)
|
||||
end
|
||||
it '@javascript_origins' do
|
||||
expect(subject).to respond_to(:javascript_origins)
|
||||
end
|
||||
it '@redirect_uris' do
|
||||
expect(subject).to respond_to(:redirect_uris)
|
||||
end
|
||||
it '@refresh_token' do
|
||||
expect(subject).to respond_to(:refresh_token)
|
||||
end
|
||||
it '@token_credential_uri' do
|
||||
expect(subject).to respond_to(:token_credential_uri)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'value of instance variables' do
|
||||
it 'should set @access_token' do
|
||||
expect(subject.access_token).to eq 'sample_access_token'
|
||||
end
|
||||
it 'should set @authorization_uri' do
|
||||
expect(subject.authorization_uri).to eq 'sample_auth_uri'
|
||||
end
|
||||
it 'should set @client_id' do
|
||||
expect(subject.client_id).to eq 'sample_client_id'
|
||||
end
|
||||
it 'should set @client_secret' do
|
||||
expect(subject.client_secret).to eq 'sample_client_secret'
|
||||
end
|
||||
it 'should set @expires_at' do
|
||||
expect(subject.expires_at).to eq 'sample_expires_at'
|
||||
end
|
||||
it 'should set @expires_in' do
|
||||
expect(subject.expires_in).to eq 'sample_expires_in'
|
||||
end
|
||||
it 'should set @flow' do
|
||||
expect(subject.flow).to eq 'samples'
|
||||
end
|
||||
it 'should set @id_token' do
|
||||
expect(subject.id_token).to eq 'sample_id_token'
|
||||
end
|
||||
it 'should set @issued_at' do
|
||||
expect(subject.issued_at).to eq 'sample_issued_at'
|
||||
end
|
||||
it 'should set @javascript_origins' do
|
||||
expect(subject.javascript_origins).to eq 'sample_javascript_origins'
|
||||
end
|
||||
it 'should set @redirect_uris' do
|
||||
expect(subject.redirect_uris).to eq 'sample_redirect_uris'
|
||||
end
|
||||
it 'should set @refresh_token' do
|
||||
expect(subject.refresh_token).to eq 'sample_refresh_token'
|
||||
end
|
||||
it 'should set @token_credential_uri' do
|
||||
expect(subject.token_credential_uri).to eq 'sample_token_uri'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with JSON file' do
|
||||
let(:file) { File.join(FIXTURES_PATH, 'files', 'client_secrets.json') }
|
||||
subject(:secrets) { Google::APIClient::ClientSecrets.load(file)}
|
||||
|
||||
it 'should load the correct client ID' do
|
||||
expect(secrets.client_id).to be == '898243283568.apps.googleusercontent.com'
|
||||
end
|
||||
|
||||
it 'should load the correct client secret' do
|
||||
expect(secrets.client_secret).to be == 'i8YaXdGgiQ4_KrTVNGsB7QP1'
|
||||
end
|
||||
|
||||
context 'serialzed to hash' do
|
||||
subject(:hash) { secrets.to_hash }
|
||||
it 'should contain the flow as the first key' do
|
||||
expect(hash).to have_key "installed"
|
||||
end
|
||||
|
||||
it 'should contain the client ID' do
|
||||
expect(hash["installed"]["client_id"]).to be == '898243283568.apps.googleusercontent.com'
|
||||
end
|
||||
|
||||
it 'should contain the client secret' do
|
||||
expect(hash["installed"]["client_secret"]).to be == 'i8YaXdGgiQ4_KrTVNGsB7QP1'
|
||||
end
|
||||
|
||||
it 'should remove empty value' do
|
||||
expect(hash["installed"]["redirect_uris"]).to be nil
|
||||
end
|
||||
|
||||
it 'should remove nil values' do
|
||||
expect(hash["installed"]["issued_at"]).to be nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no existing JSON file' do
|
||||
it 'should raise exception' do
|
||||
file = File.join(FIXTURES_PATH, 'files', 'no_file.json')
|
||||
expect { Google::APIClient::ClientSecrets.load(file) }.to raise_exception(Errno::ENOENT)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid JSON file' do
|
||||
it 'should raise exception' do
|
||||
file = File.join(FIXTURES_PATH, 'files', 'invalid.json')
|
||||
expect { Google::APIClient::ClientSecrets.load(file) }.to raise_exception(JSON::ParserError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with folder name, which have json file in parents' do
|
||||
it 'should load the correct client id' do
|
||||
folder = File.join(FIXTURES_PATH, 'files', 'child')
|
||||
secrets = Google::APIClient::ClientSecrets.load(folder)
|
||||
expect(secrets.client_id).to be == '898243283568.apps.googleusercontent.com'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with folder wihout client_secrets.json' do
|
||||
it "should raise exception", fakefs: true do
|
||||
FileUtils.mkdir("/tmp")
|
||||
expect { Google::APIClient::ClientSecrets.load('/tmp') }.to raise_exception(ArgumentError)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,384 +0,0 @@
|
||||
# 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/api_command'
|
||||
require 'google/apis/core/json_representation'
|
||||
|
||||
RSpec.describe Google::Apis::Core::ApiCommand do
|
||||
include TestHelpers
|
||||
include_context 'HTTP client'
|
||||
|
||||
let(:model_class) do
|
||||
Class.new do
|
||||
attr_accessor :value
|
||||
end
|
||||
end
|
||||
|
||||
let(:representer_class) do
|
||||
Class.new(Google::Apis::Core::JsonRepresentation) do
|
||||
property :value
|
||||
end
|
||||
end
|
||||
|
||||
let(:x_goog_api_client_value) { "gl-ruby/#{RUBY_VERSION} gdcl/#{Google::Apis::VERSION}" }
|
||||
|
||||
context('with preparation') do
|
||||
let(:command) do
|
||||
Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
end
|
||||
|
||||
it 'should set X-Goog-Api-Client header if none is set' do
|
||||
command.prepare!
|
||||
expect(command.header['X-Goog-Api-Client']).to eql x_goog_api_client_value
|
||||
end
|
||||
|
||||
it 'should append to x-goog-api-client header with case difference' do
|
||||
command.header['x-goog-api-client'] = "foo/1.2.3"
|
||||
command.prepare!
|
||||
expect(command.header['X-Goog-Api-Client']).to eql "foo/1.2.3 #{x_goog_api_client_value}"
|
||||
end
|
||||
|
||||
it 'should append to multiple x-goog-api-client headers' do
|
||||
command.header['x-goog-api-client'] = "foo/1.2.3"
|
||||
command.header['X-Goog-Api-Client'] = "bar/4.5.6"
|
||||
command.prepare!
|
||||
expect(command.header['X-Goog-Api-Client']).to eql "foo/1.2.3 bar/4.5.6 #{x_goog_api_client_value}"
|
||||
expect(command.header['x-goog-api-client']).to be nil
|
||||
end
|
||||
|
||||
it 'should override existing gl-ruby/ and gdcl/ clauses' do
|
||||
command.header['x-goog-api-client'] = "gl-ruby/0.0 foo/1.2.3"
|
||||
command.header['X-Goog-Api-Client'] = "bar/4.5.6 gdcl/0.0"
|
||||
command.prepare!
|
||||
expect(command.header['X-Goog-Api-Client']).to eql "foo/1.2.3 bar/4.5.6 #{x_goog_api_client_value}"
|
||||
expect(command.header['x-goog-api-client']).to be nil
|
||||
end
|
||||
|
||||
it 'should not set the X-Goog-User-Project header if there is no quota_project' do
|
||||
command.prepare!
|
||||
expect(command.header['X-Goog-User-Project']).to be_nil
|
||||
end
|
||||
|
||||
it 'should set the X-Goog-User-Project to the credentials quota_project' do
|
||||
command.options.authorization = OpenStruct.new quota_project_id: "b_project_id"
|
||||
command.prepare!
|
||||
expect(command.header['X-Goog-User-Project']).to eql "b_project_id"
|
||||
end
|
||||
|
||||
it 'should set the X-Goog-User-Project to a custom quota_project in preference to credentials' do
|
||||
command.options.authorization = OpenStruct.new quota_project_id: "b_project_id"
|
||||
command.options.quota_project = "c_project_id"
|
||||
command.prepare!
|
||||
expect(command.header['X-Goog-User-Project']).to eql "c_project_id"
|
||||
end
|
||||
end
|
||||
|
||||
context('with a request body') do
|
||||
let(:command) do
|
||||
request = model_class.new
|
||||
request.value = 'hello'
|
||||
command = Google::Apis::Core::ApiCommand.new(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
command.request_representation = representer_class
|
||||
command.request_object = request
|
||||
command.query['a'] = 'b'
|
||||
command
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
stub_request(:post, 'https://www.googleapis.com/zoo/animals?a=b')
|
||||
.to_return(headers: { 'Content-Type' => 'application/json' }, body: %({}))
|
||||
end
|
||||
|
||||
it 'should serialize the request object' do
|
||||
command.execute(client)
|
||||
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals?a=b').with do |req|
|
||||
be_json_eql(%({"value":"hello"})).matches?(req.body)
|
||||
end).to have_been_made
|
||||
end
|
||||
|
||||
it 'should not form encode query parameters when body expected but nil' do
|
||||
command.query['a'] = 'b'
|
||||
command.request_object = nil
|
||||
command.execute(client)
|
||||
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals?a=b').with(body: nil)).to have_been_made
|
||||
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
|
||||
let(:command) do
|
||||
command = Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
command.response_representation = representer_class
|
||||
command.response_class = model_class
|
||||
command
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(headers: { 'Content-Type' => 'application/json' }, body: %({"value" : "hello"}))
|
||||
end
|
||||
|
||||
it 'should return a model instance' do
|
||||
result = command.execute(client)
|
||||
expect(result).to be_kind_of(model_class)
|
||||
end
|
||||
|
||||
it 'should return a populated object' do
|
||||
result = command.execute(client)
|
||||
expect(result.value).to eql 'hello'
|
||||
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
|
||||
|
||||
context('with an invalid content-type response') do
|
||||
let(:command) do
|
||||
command = Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
command.response_representation = representer_class
|
||||
command.response_class = model_class
|
||||
command
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(headers: { 'Content-Type' => 'text/plain' }, body: %(Ignore me))
|
||||
end
|
||||
|
||||
it 'should return nil' do
|
||||
result = command.execute(client)
|
||||
expect(result).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
context('with a field parameter') do
|
||||
let(:command) do
|
||||
command = Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
command.query['fields'] = ':items(:id, :long_name, :a_really_long_name), shouldBeLeftAlone '
|
||||
command
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
stub_request(:get, /.*/)
|
||||
.to_return(headers: { 'Content-Type' => 'application/json' }, body: %({}))
|
||||
end
|
||||
|
||||
it 'should normalize fields params' do
|
||||
command.execute(client)
|
||||
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.with(query: { 'fields' => 'items(id, longName, aReallyLongName), shouldBeLeftAlone ' })) .to have_been_made
|
||||
end
|
||||
end
|
||||
|
||||
context('with a rate limit response') do
|
||||
let(:command) do
|
||||
Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
json = <<EOF
|
||||
{
|
||||
"error": {
|
||||
"errors": [
|
||||
{
|
||||
"domain": "global",
|
||||
"reason": "rateLimitExceeded",
|
||||
"message": "Rate limit exceeded"
|
||||
}
|
||||
],
|
||||
"code": 403,
|
||||
"message": "Rate limit exceeded"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(status: [403, 'Rate Limit Exceeded'], headers: { 'Content-Type' => 'application/json' }, body: json)
|
||||
.to_return(headers: { 'Content-Type' => 'application/json' }, body: %({}))
|
||||
end
|
||||
|
||||
it 'should retry' do
|
||||
command.execute(client)
|
||||
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')).to have_been_made.times(2)
|
||||
end
|
||||
end
|
||||
|
||||
context('with a project not linked response') do
|
||||
let(:command) do
|
||||
Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
json = <<EOF
|
||||
{
|
||||
"error": {
|
||||
"errors": [
|
||||
{
|
||||
"domain": "global",
|
||||
"reason": "projectNotLinked",
|
||||
"message": "The project id used to call the Google Play Developer API has not been linked in the Google Play Developer Console."
|
||||
}
|
||||
],
|
||||
"code": 403,
|
||||
"message": "The project id used to call the Google Play Developer API has not been linked in the Google Play Developer Console."
|
||||
}
|
||||
}
|
||||
EOF
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(status: [403, 'The project id used to call the Google Play Developer API has not been linked in the Google Play Developer Console.'], headers: {
|
||||
'Content-Type' => 'application/json'
|
||||
}, body: json)
|
||||
.to_return(headers: { 'Content-Type' => 'application/json' }, body: %({}))
|
||||
end
|
||||
|
||||
it 'should raise project not linked error' do
|
||||
expect { command.execute(client) }.to raise_error(Google::Apis::ProjectNotLinkedError)
|
||||
end
|
||||
|
||||
it 'should raise an error with the reason and message' do
|
||||
expect { command.execute(client) }.to raise_error(
|
||||
/projectNotLinked: The project id used to call the Google Play Developer API has not been linked in the Google Play Developer Console./)
|
||||
end
|
||||
end
|
||||
|
||||
context('with a client error response') do
|
||||
let(:command) do
|
||||
Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
json = <<EOF
|
||||
{
|
||||
"error": {
|
||||
"errors": [
|
||||
{
|
||||
"domain": "global",
|
||||
"reason": "timeRangeEmpty",
|
||||
"message": "The specified time range is empty."
|
||||
}
|
||||
],
|
||||
"code": 400,
|
||||
"message": "The specified time range is empty."
|
||||
}
|
||||
}
|
||||
EOF
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(status: [400, 'Bad Request'], headers: { 'Content-Type' => 'application/json' }, body: json)
|
||||
end
|
||||
|
||||
it 'should raise client error' do
|
||||
expect { command.execute(client) }.to raise_error(Google::Apis::ClientError)
|
||||
end
|
||||
|
||||
it 'should raise an error with the reason and message' do
|
||||
expect { command.execute(client) }.to raise_error(
|
||||
/timeRangeEmpty: The specified time range is empty/)
|
||||
end
|
||||
end
|
||||
|
||||
context('with an empty error body') do
|
||||
let(:command) do
|
||||
Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
json = %({})
|
||||
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(status: [403, 'Rate Limit Exceeded'], headers: { 'Content-Type' => 'application/json' }, body: json)
|
||||
end
|
||||
|
||||
it 'should raise client error' do
|
||||
expect { command.execute(client) }.to raise_error(Google::Apis::ClientError)
|
||||
end
|
||||
|
||||
it 'should use the default error message' do
|
||||
expect { command.execute(client) }.to raise_error(/Invalid request/)
|
||||
end
|
||||
end
|
||||
|
||||
context('with v2 error messages') do
|
||||
let(:command) do
|
||||
cmd = Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
cmd.options.api_format_version = 2
|
||||
cmd
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
json = <<EOF
|
||||
{
|
||||
"error": {
|
||||
"code": 400,
|
||||
"message": "Illegal character ':' in log name",
|
||||
"status": "INVALID_ARGUMENT",
|
||||
"details": [
|
||||
{
|
||||
"@type": "type.googleapis.com/google.logging.v2.WriteLogEntriesPartialErrors",
|
||||
"logEntryErrors": {
|
||||
"0": {
|
||||
"code": 3,
|
||||
"message": "Illegal character ':' in log name"
|
||||
},
|
||||
"1": {
|
||||
"code": 7,
|
||||
"message": "User not authorized."
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.with(headers: {'X-Goog-Api-Format-Version' => '2'})
|
||||
.to_return(status: [400, 'Invalid Argument'], headers: { 'Content-Type' => 'application/json' }, body: json)
|
||||
end
|
||||
|
||||
it 'should raise client error' do
|
||||
expect { command.execute(client) }.to raise_error(Google::Apis::ClientError)
|
||||
end
|
||||
|
||||
it 'should raise an error with the reason and message' do
|
||||
expect { command.execute(client) }.to raise_error(
|
||||
/INVALID_ARGUMENT: Illegal character ':' in log name/)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,195 +0,0 @@
|
||||
# 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/batch'
|
||||
require 'google/apis/core/json_representation'
|
||||
|
||||
RSpec.describe Google::Apis::Core::BatchCommand do
|
||||
include TestHelpers
|
||||
include_context 'HTTP client'
|
||||
|
||||
let(:command) do
|
||||
command = Google::Apis::Core::BatchCommand.new(:post, 'https://www.googleapis.com/batch')
|
||||
end
|
||||
|
||||
let(:get_command) { Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals/1') }
|
||||
|
||||
let(:post_with_string_command) do
|
||||
command = Google::Apis::Core::HttpCommand.new(:post, 'https://www.googleapis.com/zoo/animals/2')
|
||||
command.body = 'Hello world'
|
||||
command.header['Content-Type'] = 'text/plain'
|
||||
command
|
||||
end
|
||||
|
||||
let(:post_with_io_command) do
|
||||
command = Google::Apis::Core::HttpCommand.new(:post, 'https://www.googleapis.com/zoo/animals/3')
|
||||
command.body = StringIO.new('Goodbye!')
|
||||
command.header['Content-Type'] = 'text/plain'
|
||||
command
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
allow(SecureRandom).to receive(:uuid).and_return('ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f')
|
||||
allow(Digest::SHA1).to receive(:hexdigest).and_return('123abc')
|
||||
|
||||
response = <<EOF
|
||||
--batch123
|
||||
Content-Type: application/http
|
||||
Content-ID: <response-ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+0>
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
Hello
|
||||
--batch123
|
||||
Content-Type: application/http
|
||||
Content-ID: <response-ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+2>
|
||||
|
||||
HTTP/1.1 500 Server Error
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
Error!
|
||||
--batch123
|
||||
Content-Type: application/http
|
||||
Content-ID: <response-ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+1>
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
world
|
||||
--batch123--
|
||||
EOF
|
||||
stub_request(:post, 'https://www.googleapis.com/batch')
|
||||
.to_return(headers: { 'Content-Type' => 'multipart/mixed; boundary=batch123' }, body: response)
|
||||
end
|
||||
|
||||
it 'should send content' do
|
||||
b = ->(_res, _err) {}
|
||||
command.add(get_command, &b)
|
||||
command.add(post_with_string_command, &b)
|
||||
command.add(post_with_io_command, &b)
|
||||
command.execute(client)
|
||||
|
||||
expected_body = <<EOF.gsub(/\n/, "\r\n")
|
||||
--123abc
|
||||
Content-Type: application/http
|
||||
Content-Id: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+0>
|
||||
Content-Length: 58
|
||||
Content-Transfer-Encoding: binary
|
||||
|
||||
GET /zoo/animals/1? HTTP/1.1
|
||||
Host: www.googleapis.com
|
||||
|
||||
|
||||
--123abc
|
||||
Content-Type: application/http
|
||||
Content-Id: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+1>
|
||||
Content-Length: 96
|
||||
Content-Transfer-Encoding: binary
|
||||
|
||||
POST /zoo/animals/2? HTTP/1.1
|
||||
Content-Type: text/plain
|
||||
Host: www.googleapis.com
|
||||
|
||||
Hello world
|
||||
--123abc
|
||||
Content-Type: application/http
|
||||
Content-Id: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+2>
|
||||
Content-Length: 93
|
||||
Content-Transfer-Encoding: binary
|
||||
|
||||
POST /zoo/animals/3? HTTP/1.1
|
||||
Content-Type: text/plain
|
||||
Host: www.googleapis.com
|
||||
|
||||
Goodbye!
|
||||
--123abc--
|
||||
|
||||
EOF
|
||||
expect(a_request(:post, 'https://www.googleapis.com/batch').with(body: expected_body)).to have_been_made
|
||||
end
|
||||
|
||||
it 'should send content with authorization header when set' do
|
||||
b = ->(_res, _err) {}
|
||||
signet_auth = object_double(Signet::OAuth2::Client.new).tap do |a|
|
||||
allow(a).to receive(:apply!) do |header|
|
||||
header['Authorization'] = 'Bearer a_token_value'
|
||||
end
|
||||
end
|
||||
command.add(get_command.tap { |c| c.options.authorization = 'oauthtoken' }, &b)
|
||||
command.add(post_with_string_command.tap { |c| c.options.authorization = signet_auth }, &b)
|
||||
command.add(post_with_io_command, &b)
|
||||
command.execute(client)
|
||||
|
||||
expected_body = <<EOF.gsub(/\n/, "\r\n")
|
||||
--123abc
|
||||
Content-Type: application/http
|
||||
Content-Id: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+0>
|
||||
Content-Length: 92
|
||||
Content-Transfer-Encoding: binary
|
||||
|
||||
GET /zoo/animals/1? HTTP/1.1
|
||||
Authorization: Bearer oauthtoken
|
||||
Host: www.googleapis.com
|
||||
|
||||
|
||||
--123abc
|
||||
Content-Type: application/http
|
||||
Content-Id: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+1>
|
||||
Content-Length: 133
|
||||
Content-Transfer-Encoding: binary
|
||||
|
||||
POST /zoo/animals/2? HTTP/1.1
|
||||
Content-Type: text/plain
|
||||
Authorization: Bearer a_token_value
|
||||
Host: www.googleapis.com
|
||||
|
||||
Hello world
|
||||
--123abc
|
||||
Content-Type: application/http
|
||||
Content-Id: <ffe23d1b-e8f7-47f5-8c01-2a30cf8ecb8f+2>
|
||||
Content-Length: 93
|
||||
Content-Transfer-Encoding: binary
|
||||
|
||||
POST /zoo/animals/3? HTTP/1.1
|
||||
Content-Type: text/plain
|
||||
Host: www.googleapis.com
|
||||
|
||||
Goodbye!
|
||||
--123abc--
|
||||
|
||||
EOF
|
||||
expect(a_request(:post, 'https://www.googleapis.com/batch').with(body: expected_body)).to have_been_made
|
||||
end
|
||||
|
||||
it 'should send decode responses' do
|
||||
expect do |b|
|
||||
command.add(get_command) do |res, err|
|
||||
b.to_proc.call(1, res, err)
|
||||
end
|
||||
command.add(post_with_string_command) do |res, err|
|
||||
b.to_proc.call(2, res, err)
|
||||
end
|
||||
command.add(post_with_io_command) do |res, err|
|
||||
b.to_proc.call(3, res, err)
|
||||
end
|
||||
command.execute(client)
|
||||
end.to yield_successive_args([1, 'Hello', nil], [3, nil, an_instance_of(Google::Apis::ServerError)], [2, 'world', nil],)
|
||||
end
|
||||
|
||||
it 'should raise error if batch is empty' do
|
||||
expect { command.execute(client) }.to raise_error(Google::Apis::BatchError)
|
||||
end
|
||||
end
|
||||
@@ -1,76 +0,0 @@
|
||||
# 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
|
||||
@@ -1,138 +0,0 @@
|
||||
# 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/download'
|
||||
require 'google/apis/core/json_representation'
|
||||
require 'tempfile'
|
||||
require 'tmpdir'
|
||||
|
||||
RSpec.describe Google::Apis::Core::DownloadCommand do
|
||||
include TestHelpers
|
||||
include_context 'HTTP client'
|
||||
|
||||
let(:command) do
|
||||
command = Google::Apis::Core::DownloadCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
command.download_dest = dest
|
||||
command
|
||||
end
|
||||
|
||||
shared_examples 'should download' do
|
||||
context 'with successful response' do
|
||||
before(:example) do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(headers: { 'Content-Type' => 'application/json' }, body: %(Hello world))
|
||||
end
|
||||
|
||||
it 'should not include a range header' do
|
||||
command.execute(client)
|
||||
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.with { |req| !req.headers.key?('Range') }
|
||||
).to have_been_made
|
||||
end
|
||||
|
||||
it 'should receive content' do
|
||||
expect(received).to eql 'Hello world'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with error' do
|
||||
before(:example) do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(status: [404, 'Not Found'], body: '')
|
||||
end
|
||||
|
||||
it 'should raise error' do
|
||||
expect {received}.to raise_error(Google::Apis::ClientError)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with disconnects' do
|
||||
before(:example) do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(body: ['Hello ', Timeout::Error])
|
||||
.to_return(status: [206, 'Partial content'], body: 'world')
|
||||
end
|
||||
|
||||
it 'should receive entire content' do
|
||||
expect(received).to eql('Hello world')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with disconnects and no partial response' do
|
||||
before(:example) do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(body: ['Hello ', Timeout::Error])
|
||||
.to_return(body: 'Hello world')
|
||||
end
|
||||
|
||||
it 'should receive entire content' do
|
||||
expect(received).to eql('Hello world')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with redirect' do
|
||||
before(:example) do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(status: [302, 'Content moved'], headers: { 'Location' => 'https://content.googleapis.com/files/12345' }, body: %(Content moved))
|
||||
stub_request(:get, 'https://content.googleapis.com/files/12345')
|
||||
.to_return(headers: { 'Content-Type' => 'application/json' }, body: %(Hello world))
|
||||
end
|
||||
|
||||
it 'should receive content' do
|
||||
expect(received).to eql 'Hello world'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with filename destination' do
|
||||
let(:dest) { File.join(Dir.mktmpdir, 'test.txt') }
|
||||
let(:received) do
|
||||
command.execute(client)
|
||||
File.read(dest)
|
||||
end
|
||||
|
||||
include_examples 'should download'
|
||||
end
|
||||
|
||||
context 'with default destination' do
|
||||
let(:dest) { nil }
|
||||
let(:received) { command.execute(client).string }
|
||||
include_examples 'should download'
|
||||
end
|
||||
|
||||
context 'with IO destination' do
|
||||
let(:dest) { Tempfile.new('test') }
|
||||
let(:received) do
|
||||
command.execute(client)
|
||||
dest.rewind
|
||||
dest.read
|
||||
end
|
||||
|
||||
include_examples 'should download'
|
||||
end
|
||||
|
||||
context 'with #write destination' do
|
||||
let(:dest) { WritableIO.new(StringIO.new) }
|
||||
let(:received) { command.execute(client).io.string }
|
||||
|
||||
include_examples 'should download'
|
||||
|
||||
WritableIO = Struct.new(:io) do
|
||||
def write(data)
|
||||
io.write(data)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,60 +0,0 @@
|
||||
# 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/hashable'
|
||||
|
||||
RSpec.describe Google::Apis::Core::Hashable do
|
||||
let(:child_class) do
|
||||
Class.new do
|
||||
include Google::Apis::Core::Hashable
|
||||
attr_accessor :value
|
||||
end
|
||||
end
|
||||
|
||||
let(:model_class) do
|
||||
Class.new do
|
||||
include Google::Apis::Core::Hashable
|
||||
attr_accessor :value
|
||||
attr_accessor :value2
|
||||
attr_accessor :children
|
||||
end
|
||||
end
|
||||
|
||||
let(:model) do
|
||||
obj = model_class.new
|
||||
obj.value = 'hello'
|
||||
obj.value2 = {
|
||||
a: 'a'
|
||||
}
|
||||
child = child_class.new
|
||||
child.value = 'goodbye'
|
||||
obj.children = [child]
|
||||
obj
|
||||
end
|
||||
|
||||
let(:hash) { model.to_h }
|
||||
|
||||
it 'should serialize attributes' do
|
||||
expect(hash).to include(value: 'hello')
|
||||
end
|
||||
|
||||
it 'should serialize collections' do
|
||||
expect(hash).to include(children: [{ value: 'goodbye' }])
|
||||
end
|
||||
|
||||
it 'should serialize hashes' do
|
||||
expect(hash[:value2]).to include(a: 'a')
|
||||
end
|
||||
end
|
||||
@@ -1,508 +0,0 @@
|
||||
# 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/http_command'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
module CloudkmsV1
|
||||
class DecryptResponse
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe Google::Apis::Core::HttpCommand do
|
||||
include TestHelpers
|
||||
include_context 'HTTP client'
|
||||
|
||||
context('with credentials') do
|
||||
let(:command) do
|
||||
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
command.options.authorization = Signet::OAuth2::Client.new({
|
||||
:token_credential_uri => 'https://accounts.google.com/o/oauth2/token'
|
||||
})
|
||||
command
|
||||
end
|
||||
|
||||
context('with one token error') do
|
||||
before(:example) do
|
||||
stub_request(:post, 'https://accounts.google.com/o/oauth2/token').to_return(
|
||||
{ status: [500, 'Server error'] },
|
||||
{ status: [200, ''], headers: { 'Content-Type' => 'application/json' }, body: '{"access_token": "foo"}' })
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world))
|
||||
end
|
||||
|
||||
it 'should send credentials' do
|
||||
result = command.execute(client)
|
||||
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.with { |req| req.headers['Authorization'] == 'Bearer foo' }).to have_been_made
|
||||
expect(result).to eql 'Hello world'
|
||||
end
|
||||
end
|
||||
|
||||
context('with two token errors') do
|
||||
before(:example) do
|
||||
stub_request(:post, 'https://accounts.google.com/o/oauth2/token').to_return(
|
||||
{ status: [500, 'Server error'] },
|
||||
{ status: [401, 'Unauthorized'] })
|
||||
end
|
||||
|
||||
it 'should raise error' do
|
||||
expect { command.execute(client) }.to raise_error(Signet::AuthorizationError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context('with credentials') do
|
||||
let(:command) do
|
||||
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
command.options.authorization = authorization
|
||||
command
|
||||
end
|
||||
|
||||
context('that are refreshable') do
|
||||
let(:authorization) do
|
||||
calls = 0
|
||||
auth = object_double(Signet::OAuth2::Client.new)
|
||||
allow(auth).to receive(:apply!) do |header|
|
||||
header['Authorization'] = sprintf('Bearer a_token_value_%d', calls)
|
||||
calls += 1
|
||||
end
|
||||
auth
|
||||
end
|
||||
|
||||
it 'should send credentials' do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world))
|
||||
result = command.execute(client)
|
||||
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.with { |req| req.headers['Authorization'] == 'Bearer a_token_value_0' }).to have_been_made
|
||||
end
|
||||
|
||||
context('with authorizaton error') do
|
||||
before(:example) do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(status: [401, 'Unauthorized'])
|
||||
.to_return(body: %(Hello world))
|
||||
end
|
||||
|
||||
it 'should refresh if auth error received' do
|
||||
result = command.execute(client)
|
||||
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.with { |req| req.headers['Authorization'] == 'Bearer a_token_value_1' }).to have_been_made
|
||||
end
|
||||
|
||||
it 'should ignore retry count' do
|
||||
command.options.retries = 0
|
||||
result = command.execute(client)
|
||||
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.with { |req| req.headers['Authorization'] == 'Bearer a_token_value_1' }).to have_been_made
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context('that are bare tokens`') do
|
||||
let(:authorization) { 'a_token_value' }
|
||||
|
||||
it 'should send credentials' do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world))
|
||||
result = command.execute(client)
|
||||
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.with { |req| expect(req.headers['Authorization']).to eql 'Bearer a_token_value' }).to have_been_made
|
||||
end
|
||||
|
||||
it 'should send not refresh' do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [401, 'Unauthorized'])
|
||||
expect { command.execute(client) }.to raise_error(Google::Apis::AuthorizationError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context('with a successful response') do
|
||||
let(:command) do
|
||||
Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world))
|
||||
end
|
||||
|
||||
it 'should return the response body if block not present' do
|
||||
result = command.execute(client)
|
||||
expect(result).to eql 'Hello world'
|
||||
end
|
||||
|
||||
it 'should call block if present' do
|
||||
expect { |b| command.execute(client, &b) }.to yield_with_args('Hello world', nil)
|
||||
end
|
||||
end
|
||||
|
||||
context('with server errors') do
|
||||
let(:command) do
|
||||
Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(status: [500, 'Server error']).times(2)
|
||||
.to_return(body: %(Hello world))
|
||||
end
|
||||
|
||||
it 'should return the response body' do
|
||||
result = command.execute(client)
|
||||
expect(result).to eql 'Hello world'
|
||||
end
|
||||
|
||||
it 'should raise error if retries exceeded' do
|
||||
command.options.retries = 1
|
||||
expect { command.execute(client) }.to raise_error(Google::Apis::ServerError)
|
||||
end
|
||||
|
||||
|
||||
context('with retries exceeded') do
|
||||
before(:example) do
|
||||
command.options.retries = 1
|
||||
end
|
||||
|
||||
let(:err) do
|
||||
begin
|
||||
command.execute(client)
|
||||
rescue Google::Apis::Error => e
|
||||
e
|
||||
end
|
||||
end
|
||||
|
||||
it 'should raise error with HTTP status code' do
|
||||
expect(err.status_code).to eq 500
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
context('with callbacks') do
|
||||
it 'should return the response body after retries' do
|
||||
expect { |b| command.execute(client, &b) }.to yield_with_args('Hello world', nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context('with unknown errors') do
|
||||
let(:command) do
|
||||
Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_raise(HTTPClient::BadResponseError) # empty error, no res value
|
||||
end
|
||||
|
||||
it 'should raise transmission error' do
|
||||
command.options.retries = 1
|
||||
|
||||
err = nil
|
||||
begin
|
||||
command.execute(client)
|
||||
rescue Google::Apis::Error => e
|
||||
err = e
|
||||
end
|
||||
|
||||
expect(err).to be_a(Google::Apis::TransmissionError)
|
||||
expect(err.cause).to be_a(HTTPClient::BadResponseError)
|
||||
expect(err.cause.res).to be_nil # no res value
|
||||
end
|
||||
end
|
||||
|
||||
context('with options') do
|
||||
let(:command) do
|
||||
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
command.options.header = { 'X-Foo' => 'bar' }
|
||||
command
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(body: %(Hello world))
|
||||
end
|
||||
|
||||
it 'should send user headers' do
|
||||
result = command.execute(client)
|
||||
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.with { |req| req.headers['X-Foo'] == 'bar' }).to have_been_made
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
context('with redirects') do
|
||||
let(:command) do
|
||||
Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(status: [302, 'Redirect'], headers: { 'Location' => 'https://zoo.googleapis.com/animals' })
|
||||
stub_request(:get, 'https://zoo.googleapis.com/animals')
|
||||
.to_return(body: %(Hello world))
|
||||
end
|
||||
|
||||
it 'should return the response body' do
|
||||
result = command.execute(client)
|
||||
expect(result).to eql 'Hello world'
|
||||
end
|
||||
end
|
||||
|
||||
context('with too many redirects') do
|
||||
let(:command) do
|
||||
Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(status: [302, 'Redirect'], headers: { 'Location' => 'https://www.googleapis.com/zoo/animals' }).times(6)
|
||||
end
|
||||
|
||||
it 'should raise error if retries exceeded' do
|
||||
expect { command.execute(client) }.to raise_error(Google::Apis::RedirectError)
|
||||
end
|
||||
end
|
||||
|
||||
context('with no server response') do
|
||||
let(:command) do
|
||||
Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_timeout
|
||||
end
|
||||
|
||||
it 'should raise transmission error' do
|
||||
expect { command.execute(client) }.to raise_error(Google::Apis::TransmissionError)
|
||||
end
|
||||
end
|
||||
|
||||
context('with invalid status code') do
|
||||
let(:command) do
|
||||
Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(status: [0, 'Wat!?'])
|
||||
end
|
||||
|
||||
it 'should raise transmission error' do
|
||||
expect { command.execute(client) }.to raise_error(Google::Apis::TransmissionError)
|
||||
end
|
||||
end
|
||||
|
||||
context('with client errors') do
|
||||
let(:command) do
|
||||
Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(status: [400, 'Invalid request'])
|
||||
end
|
||||
|
||||
it 'should raise error without retry' do
|
||||
command.options.retries = 1
|
||||
expect { command.execute(client) }.to raise_error(Google::Apis::ClientError)
|
||||
end
|
||||
|
||||
it 'should call block if present' do
|
||||
expect { |b| command.execute(client, &b) }.to yield_with_args(nil, an_instance_of(Google::Apis::ClientError))
|
||||
end
|
||||
|
||||
it 'should not swallow errors raised in block' do
|
||||
expect { command.execute(client) { raise "Potatoes detected in tailpipe" } }.to raise_error("Potatoes detected in tailpipe")
|
||||
end
|
||||
end
|
||||
|
||||
context('with opencensus integration') do
|
||||
it 'should create an opencensus span for a successful call' do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [200, ''], body: "Hello world")
|
||||
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
OpenCensus::Trace.start_request_trace do |span_context|
|
||||
command.execute(client)
|
||||
spans = span_context.build_contained_spans
|
||||
expect(spans.size).to eql 1
|
||||
span = spans.first
|
||||
expect(span.name.value).to eql '/zoo/animals'
|
||||
expect(span.status.code).to eql 0
|
||||
attrs = span.attributes
|
||||
expect(attrs['http.host'].value).to eql 'www.googleapis.com'
|
||||
expect(attrs['http.method'].value).to eql 'GET'
|
||||
expect(attrs['http.path'].value).to eql '/zoo/animals'
|
||||
expect(attrs['http.status_code']).to eql 200
|
||||
events = span.time_events
|
||||
expect(events.size).to eql 2
|
||||
expect(events[0].type).to eql :SENT
|
||||
expect(events[0].uncompressed_size).to eql 0
|
||||
expect(events[1].type).to eql :RECEIVED
|
||||
expect(events[1].uncompressed_size).to eql 11
|
||||
end
|
||||
end
|
||||
|
||||
it 'should create an opencensus span for a call failure' do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [403, ''])
|
||||
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
OpenCensus::Trace.start_request_trace do |span_context|
|
||||
expect { command.execute(client) }.to raise_error(Google::Apis::ClientError)
|
||||
spans = span_context.build_contained_spans
|
||||
expect(spans.size).to eql 1
|
||||
span = spans.first
|
||||
expect(span.name.value).to eql '/zoo/animals'
|
||||
expect(span.status.code).to eql 7
|
||||
attrs = span.attributes
|
||||
expect(attrs['http.host'].value).to eql 'www.googleapis.com'
|
||||
expect(attrs['http.method'].value).to eql 'GET'
|
||||
expect(attrs['http.path'].value).to eql '/zoo/animals'
|
||||
expect(attrs['http.status_code']).to eql 403
|
||||
end
|
||||
end
|
||||
|
||||
it 'should propagate trace context header' do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world))
|
||||
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
OpenCensus::Trace.start_request_trace do |span_context|
|
||||
result = command.execute(client)
|
||||
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
.with { |req| !req.headers['Traceparent'].empty? }).to have_been_made
|
||||
end
|
||||
end
|
||||
|
||||
it 'should not create an opencensus span if disabled' do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(status: [200, ''])
|
||||
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
command.options.use_opencensus = false
|
||||
OpenCensus::Trace.start_request_trace do |span_context|
|
||||
command.execute(client)
|
||||
spans = span_context.build_contained_spans
|
||||
expect(spans.size).to eql 0
|
||||
end
|
||||
end
|
||||
end if Google::Apis::Core::HttpCommand::OPENCENSUS_AVAILABLE
|
||||
|
||||
it 'should send repeated query parameters' do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals?a=1&a=2&a=3')
|
||||
.to_return(status: [200, ''])
|
||||
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
command.query['a'] = [1,2,3]
|
||||
command.execute(client)
|
||||
end
|
||||
|
||||
it 'should not remove initial query parameters' do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals?a=1&a=2&a=3&foo=bar')
|
||||
.to_return(status: [200, ''])
|
||||
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals?foo=bar')
|
||||
command.query['a'] = [1,2,3]
|
||||
command.execute(client)
|
||||
end
|
||||
|
||||
it 'should send falsey query parameters' do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals?a=0&b=false')
|
||||
.to_return(status: [200, ''])
|
||||
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
command.query['a'] = 0
|
||||
command.query['b'] = false
|
||||
command.execute(client)
|
||||
end
|
||||
|
||||
it 'should send DateTime query parameters' do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals?a=2019-06-22T13:51:37-07:00&b=2019-06-22T13:51:37-07:00&b=2019-07-23T14:54:12-07:00')
|
||||
.to_return(status: [200, ''])
|
||||
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
date1 = DateTime.new(2019, 6, 22, 13, 51, 37, "-0700")
|
||||
date2 = DateTime.new(2019, 7, 23, 14, 54, 12, "-0700")
|
||||
command.query['a'] = date1
|
||||
command.query['b'] = [date1, date2]
|
||||
command.execute(client)
|
||||
end
|
||||
|
||||
it 'should form encode parameters when method is POST and no body present' do
|
||||
stub_request(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
.with(body: 'a=1&a=2&a=3&b=hello&c=&d=0')
|
||||
.to_return(status: [200, ''])
|
||||
command = Google::Apis::Core::HttpCommand.new(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
command.query['a'] = [1,2,3]
|
||||
command.query['b'] = 'hello'
|
||||
command.query['c'] = nil
|
||||
command.query['d'] = 0
|
||||
command.execute(client)
|
||||
end
|
||||
|
||||
it 'should prepend user query parameters from options and not remove initial query parameters', :focus do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals?a=1&a=2&a=3&b=false&foo=bar')
|
||||
.to_return(status: [200, ''])
|
||||
command = Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals?foo=bar')
|
||||
command.options.query = {
|
||||
'a' => [1,2,3],
|
||||
'b' => false
|
||||
}
|
||||
command.execute(client)
|
||||
end
|
||||
|
||||
it 'should raise transmission error instead of socket error' do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_raise(SocketError)
|
||||
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::TransmissionError)
|
||||
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
|
||||
|
||||
describe "#safe_object_representation" do
|
||||
let(:command) do
|
||||
Google::Apis::Core::HttpCommand.new(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
end
|
||||
|
||||
it "should show fields in a normal object" do
|
||||
obj = Object.new
|
||||
obj.instance_variable_set(:@foobar, "hi")
|
||||
str = command.send(:safe_object_representation, obj)
|
||||
expect(str).to match /@foobar/
|
||||
end
|
||||
|
||||
it "should not show fields in a restricted object" do
|
||||
obj = Google::Apis::CloudkmsV1::DecryptResponse.new
|
||||
obj.instance_variable_set(:@foobar, "hi")
|
||||
str = command.send(:safe_object_representation, obj)
|
||||
expect(str).not_to match /@foobar/
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,265 +0,0 @@
|
||||
# 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/json_representation'
|
||||
|
||||
RSpec.describe Google::Apis::Core::JsonRepresentation do
|
||||
let(:child_class) do
|
||||
Class.new do
|
||||
attr_accessor :value
|
||||
end
|
||||
end
|
||||
|
||||
let(:model_class) do
|
||||
rep_class = representer_class
|
||||
Class.new do
|
||||
attr_accessor :unset_value
|
||||
attr_accessor :nil_value
|
||||
attr_accessor :numeric_value
|
||||
attr_accessor :string_value
|
||||
attr_accessor :boolean_value_true
|
||||
attr_accessor :boolean_value_false
|
||||
attr_accessor :datetime_value
|
||||
attr_accessor :nil_datetime_value
|
||||
attr_accessor :date_value
|
||||
attr_accessor :nil_date_value
|
||||
attr_accessor :bytes_value
|
||||
attr_accessor :big_value
|
||||
attr_accessor :items
|
||||
attr_accessor :child
|
||||
attr_accessor :children
|
||||
const_set(:Representation, rep_class)
|
||||
include Google::Apis::Core::JsonObjectSupport
|
||||
end
|
||||
end
|
||||
|
||||
let(:representer_class) do
|
||||
klass = child_class
|
||||
Class.new(Google::Apis::Core::JsonRepresentation) do
|
||||
property :unset_value, as: 'unsetValue'
|
||||
property :nil_value, as: 'nilValue'
|
||||
property :numeric_value, as: 'numericValue'
|
||||
property :string_value, as: 'stringValue'
|
||||
property :boolean_value_true, as: 'booleanValueTrue'
|
||||
property :boolean_value_false, as: 'booleanValueFalse'
|
||||
property :datetime_value, as: 'dateTimeValue', type: DateTime
|
||||
property :nil_datetime_value, as: 'nullDateTimeValue', type: DateTime
|
||||
property :date_value, as: 'dateValue', type: Date
|
||||
property :nil_date_value, as: 'nullDateValue', type: Date
|
||||
property :bytes_value, as: 'bytesValue', base64: true
|
||||
property :big_value, as: 'bigValue', numeric_string: true
|
||||
property :items
|
||||
property :child, class: klass do
|
||||
property :value
|
||||
end
|
||||
collection :children, class: klass do
|
||||
property :value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'it serializes' do
|
||||
it 'does not serialize unset values' do
|
||||
expect(json).not_to have_json_path('unsetValue')
|
||||
end
|
||||
|
||||
it 'serializes explicit nil values' do
|
||||
expect(json).to be_json_eql(%(null)).at_path('nilValue')
|
||||
end
|
||||
|
||||
it 'serializes numeric values' do
|
||||
expect(json).to be_json_eql(%(123)).at_path('numericValue')
|
||||
end
|
||||
|
||||
it 'serializes string values' do
|
||||
expect(json).to be_json_eql(%("test")).at_path('stringValue')
|
||||
end
|
||||
|
||||
it 'serializes boolean values' do
|
||||
expect(json).to be_json_eql(%(true)).at_path('booleanValueTrue')
|
||||
expect(json).to be_json_eql(%(false)).at_path('booleanValueFalse')
|
||||
end
|
||||
|
||||
it 'serializes datetime values' do
|
||||
expect(json).to be_json_eql(%("2015-05-01T12:00:00.000+00:00")).at_path('dateTimeValue')
|
||||
end
|
||||
|
||||
it 'allows nil datetime values' do
|
||||
expect(json).to be_json_eql(%(null)).at_path('nullDateTimeValue')
|
||||
end
|
||||
|
||||
it 'serializes date values' do
|
||||
expect(json).to be_json_eql(%("2015-05-01")).at_path('dateValue')
|
||||
end
|
||||
|
||||
it 'allows nil date values' do
|
||||
expect(json).to be_json_eql(%(null)).at_path('nullDateValue')
|
||||
end
|
||||
|
||||
it 'serializes byte values to base64' do
|
||||
expect(json).to be_json_eql(%("SGVsbG8gd29ybGQ=")).at_path('bytesValue')
|
||||
end
|
||||
|
||||
it 'serializes basic collections' do
|
||||
expect(json).to be_json_eql(%([1,2,3])).at_path('items')
|
||||
end
|
||||
|
||||
it 'serializes nested objects' do
|
||||
expect(json).to be_json_eql(%({"value" : "child"})).at_path('child')
|
||||
end
|
||||
|
||||
it 'serializes object collections' do
|
||||
expect(json).to be_json_eql(%([{"value" : "child"}])).at_path('children')
|
||||
end
|
||||
|
||||
it 'serializes numeric strings' do
|
||||
expect(json).to be_json_eql(%("1208925819614629174706176")).at_path('bigValue')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with model object' do
|
||||
let(:json) { representer_class.new(model).to_json(user_options: { skip_undefined: true }) }
|
||||
let(:model) do
|
||||
model = model_class.new
|
||||
model.nil_value = nil
|
||||
model.numeric_value = 123
|
||||
model.string_value = 'test'
|
||||
model.datetime_value = DateTime.new(2015, 5, 1, 12)
|
||||
model.nil_datetime_value = nil
|
||||
model.date_value = Date.new(2015, 5, 1)
|
||||
model.nil_date_value = nil
|
||||
model.boolean_value_true = true
|
||||
model.boolean_value_false = false
|
||||
model.bytes_value = 'Hello world'
|
||||
model.items = [1, 2, 3]
|
||||
model.child = child_class.new
|
||||
model.child.value = 'child'
|
||||
model.children = [model.child]
|
||||
model.big_value = 1208925819614629174706176
|
||||
model
|
||||
end
|
||||
|
||||
include_examples 'it serializes'
|
||||
|
||||
it 'supports standard serialization from JSON gem' do
|
||||
str = JSON.generate(model)
|
||||
expect(str).to start_with("{\"")
|
||||
end
|
||||
|
||||
it 'supports pretty serialization from JSON gem' do
|
||||
str = JSON.pretty_generate(model)
|
||||
expect(str).to start_with("{\n ")
|
||||
end
|
||||
end
|
||||
|
||||
context 'with hash' do
|
||||
let(:json) { representer_class.new(model).to_json(user_options: { skip_undefined: true }) }
|
||||
let(:model) do
|
||||
{
|
||||
nil_value: nil,
|
||||
string_value: 'test',
|
||||
numeric_value: 123,
|
||||
datetime_value: DateTime.new(2015, 5, 1, 12),
|
||||
nil_datetime_value: nil,
|
||||
date_value: Date.new(2015, 5, 1),
|
||||
nil_date_value: nil,
|
||||
boolean_value_true: true,
|
||||
boolean_value_false: false,
|
||||
bytes_value: 'Hello world',
|
||||
big_value: 1208925819614629174706176,
|
||||
items: [1, 2, 3],
|
||||
child: {
|
||||
value: 'child'
|
||||
},
|
||||
children: [{ value: 'child' }]
|
||||
}
|
||||
end
|
||||
|
||||
include_examples 'it serializes'
|
||||
end
|
||||
|
||||
context 'when de-serializing' do
|
||||
let(:model) { representer_class.new(model_class.new).from_json(json) }
|
||||
let(:json) do
|
||||
json = <<EOF
|
||||
{ "stringValue": "test",
|
||||
"nilValue": null,
|
||||
"booleanValueTrue": true,
|
||||
"booleanValueFalse": false,
|
||||
"numericValue": 123,
|
||||
"dateTimeValue": "2015-05-01T12:00:00+00:00",
|
||||
"nullDateTimeValue": null,
|
||||
"dateValue": "2015-05-01",
|
||||
"nullDateValue": null,
|
||||
"bytesValue": "SGVsbG8gd29ybGQ=",
|
||||
"bigValue": "1208925819614629174706176",
|
||||
"items": [1,2,3],
|
||||
"child": {"value" : "hello"},
|
||||
"children": [{"value" : "hello"}]
|
||||
}
|
||||
EOF
|
||||
end
|
||||
|
||||
it 'deserializes string values' do
|
||||
expect(model.string_value).to eql 'test'
|
||||
end
|
||||
|
||||
it 'deserializes null values' do
|
||||
expect(model.nil_value).to be_nil
|
||||
end
|
||||
|
||||
it 'deserializes numeric values' do
|
||||
expect(model.numeric_value).to eql 123
|
||||
end
|
||||
|
||||
it 'deserializes boolean values' do
|
||||
expect(model.boolean_value_true).to be_truthy
|
||||
expect(model.boolean_value_false).to be_falsey
|
||||
end
|
||||
|
||||
it 'deserializes datetime values' do
|
||||
expect(model.datetime_value).to eql DateTime.new(2015, 5, 1, 12)
|
||||
end
|
||||
|
||||
it 'deserializes null datetime values' do
|
||||
expect(model.nil_datetime_value).to be_nil
|
||||
end
|
||||
|
||||
it 'deserializes date values' do
|
||||
expect(model.date_value).to eql Date.new(2015, 5, 1)
|
||||
end
|
||||
|
||||
it 'deserializes null date values' do
|
||||
expect(model.nil_date_value).to be_nil
|
||||
end
|
||||
|
||||
it 'deserializes basic collections' do
|
||||
expect(model.items).to contain_exactly(1, 2, 3)
|
||||
end
|
||||
|
||||
it 'deserializes nested objects' do
|
||||
expect(model.child.value).to eql 'hello'
|
||||
end
|
||||
|
||||
it 'serializes object collections' do
|
||||
expect(model.children[0].value).to eql 'hello'
|
||||
end
|
||||
|
||||
it 'deserializes numeric strings' do
|
||||
expect(model.big_value).to eql 1208925819614629174706176
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
@@ -1,428 +0,0 @@
|
||||
# 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/options'
|
||||
require 'google/apis/core/base_service'
|
||||
require 'google/apis/core/json_representation'
|
||||
|
||||
RSpec.describe Google::Apis::Core::BaseService do
|
||||
include TestHelpers
|
||||
|
||||
let(:service) { Google::Apis::Core::BaseService.new('https://www.googleapis.com/', '') }
|
||||
let(:service_with_base_path) { Google::Apis::Core::BaseService.new('https://www.googleapis.com/', 'my_service/v1/') }
|
||||
let(:x_goog_api_client_value) { "gl-ruby/#{RUBY_VERSION} gdcl/#{Google::Apis::VERSION}" }
|
||||
|
||||
before do
|
||||
Google::Apis::ClientOptions.default.application_name = 'test'
|
||||
Google::Apis::ClientOptions.default.application_version = '1.0'
|
||||
end
|
||||
|
||||
it 'should inherit default options' do
|
||||
expect(service.client_options.application_name).to eql 'test'
|
||||
end
|
||||
|
||||
it 'should include application in user agent' do
|
||||
agent = service.send(:user_agent)
|
||||
expect(agent).to match /^test\/1.0/
|
||||
end
|
||||
|
||||
it 'should include os version in user agent' do
|
||||
agent = service.send(:user_agent)
|
||||
expect(agent).to match Regexp.new(Regexp.escape(Google::Apis::OS_VERSION))
|
||||
end
|
||||
|
||||
it 'should inherit authorization' do
|
||||
Google::Apis::RequestOptions.default.authorization = 'a token'
|
||||
expect(service.authorization).to eql 'a token'
|
||||
end
|
||||
|
||||
it 'should allow overiding authorization' do
|
||||
Google::Apis::RequestOptions.default.authorization = 'a token'
|
||||
service.authorization = 'another token'
|
||||
expect(service.authorization).to eql 'another token'
|
||||
end
|
||||
|
||||
shared_examples 'with options' do
|
||||
it 'should inherit service options' do
|
||||
service.request_options.retries = 2
|
||||
expect(command.options.retries).to eql 2
|
||||
end
|
||||
|
||||
it 'should include per-request options' do
|
||||
expect(command.options.authorization).to eql 'foo'
|
||||
end
|
||||
|
||||
it 'should allow per-request options to override service options' do
|
||||
service.request_options.authorization = 'bar'
|
||||
expect(command.options.authorization).to eql 'foo'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when making raw http requests' do
|
||||
context 'with :get methods' do
|
||||
before(:example) do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: 'hello world')
|
||||
end
|
||||
|
||||
it 'should return body for get' do
|
||||
response = service.http(:get, 'https://www.googleapis.com/zoo/animals')
|
||||
expect(response).to eq 'hello world'
|
||||
end
|
||||
|
||||
it 'should return allow downloads' do
|
||||
response = service.http(:get, 'https://www.googleapis.com/zoo/animals', download_dest: StringIO.new)
|
||||
expect(response.string).to eq 'hello world'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with :post methods' do
|
||||
before(:example) do
|
||||
stub_request(:post, 'https://www.googleapis.com/zoo/animals').to_return(body: '')
|
||||
end
|
||||
|
||||
it 'should post body' do
|
||||
service.http(:post, 'https://www.googleapis.com/zoo/animals', body: 'hello')
|
||||
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
.with(body: 'hello')).to have_been_made
|
||||
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
|
||||
let(:command) { service.send(:make_simple_command, :get, 'zoo/animals', authorization: 'foo') }
|
||||
|
||||
it 'should return the correct command type' do
|
||||
expect(command).to be_an_instance_of(Google::Apis::Core::ApiCommand)
|
||||
end
|
||||
|
||||
it 'should build a correct URL' do
|
||||
url = command.url.expand({}).to_s
|
||||
expect(url).to eql 'https://www.googleapis.com/zoo/animals'
|
||||
end
|
||||
|
||||
it 'should send the command with x-goog-api-client header' do
|
||||
stub_request(:get, 'https://www.googleapis.com/zoo/animals').to_return(body: '')
|
||||
service.send(:execute_or_queue_command, command)
|
||||
expected_headers = {'X-Goog-Api-Client': x_goog_api_client_value}
|
||||
expect(a_request(:get, 'https://www.googleapis.com/zoo/animals').with(headers: expected_headers)).to have_been_made
|
||||
end
|
||||
|
||||
include_examples 'with options'
|
||||
end
|
||||
|
||||
context 'when making simple commands with a base path' do
|
||||
it 'should return the correct URL for a relative path' do
|
||||
command = service_with_base_path.send(:make_simple_command, :get, 'zoo/animals', authorization: 'foo')
|
||||
url = command.url.expand({}).to_s
|
||||
expect(url).to eql 'https://www.googleapis.com/my_service/v1/zoo/animals'
|
||||
end
|
||||
|
||||
it 'should return the correct URL for an absolute path' do
|
||||
command = service_with_base_path.send(:make_simple_command, :get, '/zoo/animals', authorization: 'foo')
|
||||
url = command.url.expand({}).to_s
|
||||
expect(url).to eql 'https://www.googleapis.com/zoo/animals'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when making download commands' do
|
||||
let(:command) { service.send(:make_download_command, :get, 'zoo/animals', authorization: 'foo') }
|
||||
|
||||
it 'should return the correct command type' do
|
||||
expect(command).to be_an_instance_of(Google::Apis::Core::DownloadCommand)
|
||||
end
|
||||
|
||||
it 'should build a correct URL' do
|
||||
url = command.url.expand({}).to_s
|
||||
expect(url).to eql 'https://www.googleapis.com/zoo/animals'
|
||||
end
|
||||
|
||||
it 'should include alt=media in params' do
|
||||
expect(command.query).to include('alt' => 'media')
|
||||
end
|
||||
|
||||
include_examples 'with options'
|
||||
end
|
||||
|
||||
context 'when making upload commands' do
|
||||
let(:command) { service.send(:make_upload_command, :post, 'zoo/animals', authorization: 'foo') }
|
||||
|
||||
it 'should return the correct command type' do
|
||||
expect(command).to be_an_instance_of(Google::Apis::Core::ResumableUploadCommand)
|
||||
end
|
||||
|
||||
it 'should build a correct URL' do
|
||||
url = command.url.expand({}).to_s
|
||||
expect(url).to eql 'https://www.googleapis.com/upload/zoo/animals'
|
||||
end
|
||||
|
||||
include_examples 'with options'
|
||||
end
|
||||
|
||||
context 'with batch' do
|
||||
before(:example) do
|
||||
response = <<EOF.gsub(/\n/, "\r\n")
|
||||
--batch123
|
||||
Content-Type: application/http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
Hello
|
||||
--batch123--
|
||||
EOF
|
||||
stub_request(:post, 'https://www.googleapis.com/batch')
|
||||
.to_return(headers: { 'Content-Type' => 'multipart/mixed; boundary=batch123' }, body: response)
|
||||
end
|
||||
|
||||
it 'should add commands to a batch' do
|
||||
expect do |b|
|
||||
service.batch do |service|
|
||||
command = service.send(:make_simple_command, :get, 'zoo/animals', {})
|
||||
service.send(:execute_or_queue_command, command, &b)
|
||||
end
|
||||
end.to yield_with_args('Hello', nil)
|
||||
end
|
||||
|
||||
it 'should disallow uploads in batch' do
|
||||
expect do |b|
|
||||
service.batch do |service|
|
||||
command = service.send(:make_upload_command, :post, 'zoo/animals', {})
|
||||
service.send(:execute_or_queue_command, command, &b)
|
||||
end
|
||||
end.to raise_error(Google::Apis::ClientError)
|
||||
end
|
||||
|
||||
it 'should disallow downloads in batch' do
|
||||
expect do |b|
|
||||
service.batch do |service|
|
||||
command = service.send(:make_download_command, :get, 'zoo/animals', {})
|
||||
service.send(:execute_or_queue_command, command, &b)
|
||||
end
|
||||
end.to raise_error(Google::Apis::ClientError)
|
||||
end
|
||||
|
||||
it 'should prevent mixing services in batch' do
|
||||
expect do |b|
|
||||
service.batch do |service|
|
||||
command = service.send(:make_simple_command, :get, 'zoo/animals', {})
|
||||
service.send(:execute_or_queue_command, command, &b)
|
||||
|
||||
service2 = service.dup
|
||||
command2 = service.send(:make_simple_command, :get, 'zoo/animals', {})
|
||||
service2.send(:execute_or_queue_command, command2, &b)
|
||||
end
|
||||
end.to raise_error
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
context 'with batch uploads' 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")
|
||||
--batch123
|
||||
Content-Type: application/http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
|
||||
Hello
|
||||
--batch123--
|
||||
EOF
|
||||
stub_request(:put, 'https://www.googleapis.com/upload/')
|
||||
.to_return(headers: { 'Content-Type' => 'multipart/mixed; boundary=batch123' }, body: response)
|
||||
end
|
||||
|
||||
it 'should add upload to a batch' do
|
||||
expect do |b|
|
||||
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, &b)
|
||||
end
|
||||
end.to yield_with_args('Hello', nil)
|
||||
end
|
||||
|
||||
it 'should use multipart upload' do
|
||||
expect do |b|
|
||||
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'
|
||||
expect(command).to be_an_instance_of(Google::Apis::Core::MultipartUploadCommand)
|
||||
service.send(:execute_or_queue_command, command, &b)
|
||||
end
|
||||
end.to yield_with_args('Hello', nil)
|
||||
end
|
||||
|
||||
it 'should send nested multipart' do
|
||||
Google::Apis::RequestOptions.default.authorization = 'a token'
|
||||
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: \\d+
|
||||
Content-Transfer-Encoding: binary
|
||||
|
||||
POST /upload/zoo/animals\\? HTTP/1\\.1
|
||||
X-Goog-Api-Client: #{Regexp.escape(x_goog_api_client_value)}
|
||||
Content-Type: multipart/related; boundary=inner
|
||||
X-Goog-Upload-Protocol: multipart
|
||||
Authorization: Bearer a token
|
||||
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: Regexp.new(expected_body))).to have_been_made
|
||||
end
|
||||
|
||||
it 'should disallow downloads in batch' do
|
||||
expect do |b|
|
||||
service.batch_upload do |service|
|
||||
command = service.send(:make_download_command, :get, 'zoo/animals', {})
|
||||
service.send(:execute_or_queue_command, command, &b)
|
||||
end
|
||||
end.to raise_error(Google::Apis::ClientError)
|
||||
end
|
||||
|
||||
it 'should disallow simple commands in batch' do
|
||||
expect do |b|
|
||||
service.batch_upload do |service|
|
||||
command = service.send(:make_simple_command, :get, 'zoo/animals', {})
|
||||
service.send(:execute_or_queue_command, command, &b)
|
||||
end
|
||||
end.to raise_error(Google::Apis::ClientError)
|
||||
end
|
||||
|
||||
context 'with fetch_all' do
|
||||
let(:responses) do
|
||||
data = {}
|
||||
data[nil] = OpenStruct.new(
|
||||
next_page_token: 'p1', alt_page_token: 'p2', items: ['a', 'b', 'c'], alt_items: [1, 2, 3], singular: 'foo', hash_: { 'foo' => 1, 'bar' => 2 })
|
||||
data['p1'] = OpenStruct.new(
|
||||
next_page_token: 'p2', items: ['d', 'e', 'f'], alt_items: [4, 5, 6], singular: 'bar', hash_: nil)
|
||||
data['p2'] = OpenStruct.new(
|
||||
next_page_token: nil, alt_page_token: nil, items: ['g', 'h', 'i'], alt_items: [7, 8, 9], singular: 'baz', hash_: { 'baz' => 3 })
|
||||
data
|
||||
end
|
||||
|
||||
let(:items) { service.fetch_all { |token| responses[token] } }
|
||||
|
||||
it 'should fetch pages until next page token is nil' do
|
||||
expect(items).to contain_exactly('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i')
|
||||
end
|
||||
|
||||
it 'should stop on repeated page token' do
|
||||
responses['p2'].next_page_token = 'p2'
|
||||
expect(items).to contain_exactly('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i')
|
||||
end
|
||||
|
||||
it 'should allow selecting another field for response page token' do
|
||||
expect(service.fetch_all(response_page_token: :alt_page_token) { |token| responses[token] }
|
||||
).to contain_exactly('a', 'b', 'c', 'g', 'h', 'i')
|
||||
end
|
||||
|
||||
it 'should ignore nil collections' do
|
||||
responses['p1'].items = nil
|
||||
expect(items).to contain_exactly('a', 'b', 'c', 'g', 'h', 'i')
|
||||
end
|
||||
|
||||
it 'should allow selecting another field for items' do
|
||||
expect(service.fetch_all(items: :alt_items) { |token| responses[token] } ).to contain_exactly(1, 2, 3, 4, 5, 6, 7, 8, 9)
|
||||
end
|
||||
|
||||
it 'should allow iterating over singular items' do
|
||||
expect(service.fetch_all(items: :singular) { |token| responses[token] } ).to contain_exactly('foo', 'bar', 'baz')
|
||||
end
|
||||
|
||||
it 'should collate hash entries' do
|
||||
expect(service.fetch_all(items: :hash_) { |token| responses[token] } ).to contain_exactly(['foo', 1], ['bar', 2], ['baz', 3])
|
||||
end
|
||||
|
||||
it 'should allow limiting the number of items to fetch' do
|
||||
expect(service.fetch_all(max: 5) { |token| responses[token] } ).to contain_exactly('a', 'b', 'c', 'd', 'e')
|
||||
end
|
||||
|
||||
it 'should yield the next token' do
|
||||
expect do |b|
|
||||
service.fetch_all do |token|
|
||||
b.to_proc.call(token)
|
||||
responses[token]
|
||||
end.count
|
||||
end.to yield_successive_args(nil, 'p1', 'p2')
|
||||
end
|
||||
|
||||
it 'should cache results' do
|
||||
count = 0
|
||||
items = service.fetch_all do |token|
|
||||
count = count + 1
|
||||
responses[token]
|
||||
end
|
||||
|
||||
items.each{ |i| puts i }
|
||||
items.each{ |i| puts i }
|
||||
|
||||
expect(count).to eq 3
|
||||
end
|
||||
|
||||
it 'should allow disabling caching' do
|
||||
count = 0
|
||||
items = service.fetch_all(cache: false) do |token|
|
||||
count = count + 1
|
||||
responses[token]
|
||||
end
|
||||
|
||||
items.each{ |i| puts i }
|
||||
items.each{ |i| puts i }
|
||||
|
||||
expect(count).to eq 6
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,247 +0,0 @@
|
||||
# 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/upload'
|
||||
require 'google/apis/core/json_representation'
|
||||
|
||||
RSpec.describe Google::Apis::Core::RawUploadCommand do
|
||||
include TestHelpers
|
||||
include_context 'HTTP client'
|
||||
|
||||
let(:command) do
|
||||
command = Google::Apis::Core::RawUploadCommand.new(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
command.upload_source = file
|
||||
command.upload_content_type = 'text/plain'
|
||||
command
|
||||
end
|
||||
|
||||
shared_examples 'should upload' do
|
||||
before(:example) do
|
||||
stub_request(:post, 'https://www.googleapis.com/zoo/animals').to_return(body: '')
|
||||
end
|
||||
|
||||
it 'should send content' do
|
||||
command.execute(client)
|
||||
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
.with(body: "Hello world")).to have_been_made
|
||||
end
|
||||
|
||||
it 'should send upload protocol' do
|
||||
command.execute(client)
|
||||
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
.with { |req| req.headers['X-Goog-Upload-Protocol'] == 'raw' }).to have_been_made
|
||||
end
|
||||
|
||||
it 'should send content-type header' do
|
||||
command.execute(client)
|
||||
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
.with { |req| req.headers['X-Goog-Upload-Header-Content-Type'] == 'text/plain' }).to have_been_made
|
||||
end
|
||||
end
|
||||
|
||||
context('with StringIO input') do
|
||||
let(:file) { StringIO.new("Hello world") }
|
||||
include_examples 'should upload'
|
||||
end
|
||||
|
||||
context('with IO input') do
|
||||
let(:file) { File.open(File.join(FIXTURES_DIR, 'files', 'test.txt'), 'r') }
|
||||
include_examples 'should upload'
|
||||
|
||||
it 'should not close stream' do
|
||||
expect(file.closed?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context('with Tempfile input') do
|
||||
let(:file) do
|
||||
temp_file = Tempfile.new("tempfile")
|
||||
temp_file.write("Hello world")
|
||||
temp_file.rewind
|
||||
temp_file
|
||||
end
|
||||
include_examples 'should upload'
|
||||
|
||||
it 'should not close stream' do
|
||||
expect(file.closed?).to be false
|
||||
end
|
||||
end
|
||||
|
||||
context('with file path input') do
|
||||
let(:file) { File.join(FIXTURES_DIR, 'files', 'test.txt') }
|
||||
include_examples 'should upload'
|
||||
end
|
||||
|
||||
context('with invalid input') do
|
||||
let(:file) { -> {} }
|
||||
it 'should raise client error' do
|
||||
expect { command.execute(client) }.to raise_error(Google::Apis::ClientError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe Google::Apis::Core::MultipartUploadCommand do
|
||||
include TestHelpers
|
||||
include_context 'HTTP client'
|
||||
|
||||
let(:command) do
|
||||
command = Google::Apis::Core::MultipartUploadCommand.new(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
command.upload_source = StringIO.new('Hello world')
|
||||
command.upload_content_type = 'text/plain'
|
||||
command.body = 'metadata'
|
||||
command
|
||||
end
|
||||
|
||||
before(:example) do
|
||||
stub_request(:post, 'https://www.googleapis.com/zoo/animals').to_return(body: %(Hello world))
|
||||
allow(Digest::SHA1).to receive(:hexdigest).and_return('123abc')
|
||||
end
|
||||
|
||||
it 'should send content' do
|
||||
expected_body = <<EOF.gsub(/\n/, "\r\n")
|
||||
--123abc
|
||||
Content-Type: application/json
|
||||
|
||||
metadata
|
||||
--123abc
|
||||
Content-Type: text/plain
|
||||
Content-Length: 11
|
||||
Content-Transfer-Encoding: binary
|
||||
|
||||
Hello world
|
||||
--123abc--
|
||||
|
||||
EOF
|
||||
command.execute(client)
|
||||
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
.with(body: expected_body)).to have_been_made
|
||||
end
|
||||
|
||||
it 'should send upload protocol' do
|
||||
command.execute(client)
|
||||
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
.with { |req| req.headers['X-Goog-Upload-Protocol'] == 'multipart' }).to have_been_made
|
||||
end
|
||||
end
|
||||
|
||||
RSpec.describe Google::Apis::Core::ResumableUploadCommand do
|
||||
include TestHelpers
|
||||
include_context 'HTTP client'
|
||||
|
||||
let(:command) do
|
||||
command = Google::Apis::Core::ResumableUploadCommand.new(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
command.upload_source = StringIO.new('Hello world')
|
||||
command.upload_content_type = 'text/plain'
|
||||
command
|
||||
end
|
||||
|
||||
context 'with uninterrupted upload' do
|
||||
before(:example) do
|
||||
stub_request(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(headers: { 'X-Goog-Upload-Status' => 'active', 'X-Goog-Upload-URL' => 'https://www.googleapis.com/zoo/animals' })
|
||||
.to_return(headers: { 'X-Goog-Upload-Status' => 'final' }, body: %(OK))
|
||||
end
|
||||
|
||||
it 'should send upload protocol' do
|
||||
command.execute(client)
|
||||
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
.with { |req| req.headers['X-Goog-Upload-Protocol'] == 'resumable' }).to have_been_made
|
||||
end
|
||||
|
||||
it 'should send start command' do
|
||||
command.execute(client)
|
||||
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
.with { |req| req.headers['X-Goog-Upload-Command'] == 'start' }).to have_been_made
|
||||
end
|
||||
|
||||
it 'should send upload command' do
|
||||
command.execute(client)
|
||||
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
.with { |req| req.headers['X-Goog-Upload-Command'].include?('upload') }).to have_been_made
|
||||
end
|
||||
|
||||
it 'should send upload content' do
|
||||
command.execute(client)
|
||||
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
.with(body: 'Hello world')).to have_been_made
|
||||
end
|
||||
|
||||
it 'should generate a proper opencensus span' do
|
||||
OpenCensus::Trace.start_request_trace do |span_context|
|
||||
command.execute(client)
|
||||
spans = span_context.build_contained_spans
|
||||
expect(spans.size).to eql 1
|
||||
span = spans.first
|
||||
expect(span.name.value).to eql '/zoo/animals'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with retriable error on start' do
|
||||
before(:example) do
|
||||
stub_request(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_timeout
|
||||
.to_return(headers: { 'X-Goog-Upload-Status' => 'active', 'X-Goog-Upload-URL' => 'https://www.googleapis.com/zoo/animals' })
|
||||
.to_return(headers: { 'X-Goog-Upload-Status' => 'active', 'X-Goog-Upload-Size-Received' => '6' })
|
||||
.to_return(headers: { 'X-Goog-Upload-Status' => 'final' }, body: %(OK))
|
||||
end
|
||||
|
||||
it 'should retry start command and continue' do
|
||||
command.execute(client)
|
||||
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
.with(body: 'Hello world')).to have_been_made
|
||||
end
|
||||
end
|
||||
|
||||
context 'with non-retriable authorization error on start' do
|
||||
before(:example) do
|
||||
stub_request(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(status: [401, 'unauthorized'], headers: { 'X-Goog-Upload-Status' => 'final' }, body: %(unauthorized))
|
||||
end
|
||||
|
||||
it 'should propagate the original error' do
|
||||
expect { command.execute(client) }.to raise_error Google::Apis::AuthorizationError
|
||||
end
|
||||
end
|
||||
|
||||
context 'with interruption' do
|
||||
before(:example) do
|
||||
stub_request(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(headers: { 'X-Goog-Upload-Status' => 'active', 'X-Goog-Upload-URL' => 'https://www.googleapis.com/zoo/animals' })
|
||||
.to_return(status: [500, 'Server error'])
|
||||
.to_return(headers: { 'X-Goog-Upload-Status' => 'active', 'X-Goog-Upload-Size-Received' => '6' })
|
||||
.to_return(headers: { 'X-Goog-Upload-Status' => 'final' }, body: %(OK))
|
||||
end
|
||||
|
||||
it 'should send remaining upload content after failure' do
|
||||
command.execute(client)
|
||||
expect(a_request(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
.with(body: 'world')).to have_been_made
|
||||
end
|
||||
end
|
||||
|
||||
context 'with cancelled upload' do
|
||||
before(:example) do
|
||||
stub_request(:post, 'https://www.googleapis.com/zoo/animals')
|
||||
.to_return(headers: { 'X-Goog-Upload-Status' => 'active', 'X-Goog-Upload-URL' => 'https://www.googleapis.com/zoo/animals' })
|
||||
.to_return(status: [500, 'Server error'])
|
||||
.to_return(headers: { 'X-Goog-Upload-Status' => 'cancelled' })
|
||||
end
|
||||
|
||||
it 'should raise error' do
|
||||
expect { command.execute(client) }.to raise_error Google::Apis::ClientError
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,93 +0,0 @@
|
||||
# Copyright 2016 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'
|
||||
|
||||
RSpec.describe Google::Apis::Error do
|
||||
describe 'constructor' do
|
||||
subject { described_class.new(error) }
|
||||
|
||||
describe 'message' do
|
||||
context 'first arg is a string which does not respond to :backtrace' do
|
||||
let(:error) { 'an error occurred' }
|
||||
|
||||
it 'creates a instance of Error with message' do
|
||||
expect(subject.message).to eq 'an error occurred'
|
||||
end
|
||||
end
|
||||
|
||||
context 'first arg responds to :backtrace' do
|
||||
let(:error) { double }
|
||||
|
||||
it 'creates a instance of Error with message' do
|
||||
allow(error).to receive(:respond_to?).with(:backtrace) { true }
|
||||
allow(error).to receive(:message) { 'error message' }
|
||||
expect(subject.message).to eq 'error message'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'instance variables @body, @header, and @status_code' do
|
||||
let(:error) { 'an error occurred' }
|
||||
|
||||
context 'params :body, :header, and :status_code are not given' do
|
||||
it 'instance variables @body, @header, and @status_code will be nil' do
|
||||
expect(subject.body).to be_nil
|
||||
expect(subject.header).to be_nil
|
||||
expect(subject.status_code).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'params :body, :header, and :status_code are given' do
|
||||
subject do
|
||||
described_class.new(
|
||||
error,
|
||||
body: 'this is body',
|
||||
header: 'this is header',
|
||||
status_code: 200)
|
||||
end
|
||||
|
||||
it 'instance variables @body, @header, and @status_code will not be nil' do
|
||||
expect(subject.body).to eq 'this is body'
|
||||
expect(subject.header).to eq 'this is header'
|
||||
expect(subject.status_code).to eq 200
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#backtrace' do
|
||||
subject { described_class.new('error') }
|
||||
let(:cause) { instance_double('Error', backtrace: 'this is backtrace') }
|
||||
|
||||
context '@cause is truthy' do
|
||||
it 'calls @cause.backtrace' do
|
||||
subject.instance_variable_set(:@cause, cause)
|
||||
expect(subject.backtrace).to eq 'this is backtrace'
|
||||
end
|
||||
end
|
||||
|
||||
context '@cause is falsy' do
|
||||
before do
|
||||
expect_any_instance_of(subject.class.superclass).to receive(:backtrace).and_return(
|
||||
"super class's #backtrace called")
|
||||
end
|
||||
|
||||
it "calls super class's #backtrace" do
|
||||
subject.instance_variable_set(:@cause, false)
|
||||
expect(subject.backtrace).to eq "super class's #backtrace called"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,100 +0,0 @@
|
||||
# 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'
|
||||
require 'google/apis/core/logging'
|
||||
|
||||
|
||||
RSpec.describe Google::Apis do
|
||||
it 'should have a default logger' do
|
||||
expect(Google::Apis.logger).to be_an_instance_of(Logger)
|
||||
end
|
||||
|
||||
context 'with service' do
|
||||
let(:service) do
|
||||
Class.new do
|
||||
include Google::Apis::Core::Logging
|
||||
end.new
|
||||
end
|
||||
|
||||
it 'should have a logger' do
|
||||
expect(service.logger).to be_an_instance_of(Logger)
|
||||
end
|
||||
|
||||
it 'should use the default logger' do
|
||||
expect(service.logger).to be Google::Apis.logger
|
||||
end
|
||||
|
||||
it 'should allow custom loggers' do
|
||||
Google::Apis.logger = Logger.new(STDERR)
|
||||
expect(service.logger).to be Google::Apis.logger
|
||||
end
|
||||
end
|
||||
|
||||
context 'with Rails' do
|
||||
|
||||
before(:example) do
|
||||
Google::Apis.logger = nil
|
||||
Kernel.const_set('Rails', Module.new) unless defined?(::Rails)
|
||||
end
|
||||
|
||||
let(:logger) { Logger.new(STDERR) }
|
||||
|
||||
let(:service) do
|
||||
Class.new do
|
||||
include Google::Apis::Core::Logging
|
||||
end.new
|
||||
end
|
||||
|
||||
context 'with logger present' do
|
||||
before(:example) do
|
||||
allow(::Rails).to receive(:logger).and_return(logger)
|
||||
end
|
||||
|
||||
it 'should use the Rails logger' do
|
||||
expect(service.logger).to be Rails.logger
|
||||
end
|
||||
end
|
||||
|
||||
context 'with ENV bypass' do
|
||||
before(:example) do
|
||||
allow(::Rails).to receive(:logger).and_return(logger)
|
||||
allow(::ENV).to receive(:fetch).and_return('false')
|
||||
end
|
||||
|
||||
it 'should use own logger' do
|
||||
expect(service.logger).not_to be Rails.logger
|
||||
end
|
||||
|
||||
it 'should have a logger' do
|
||||
expect(service.logger).to be_an_instance_of(Logger)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with logger not present' do
|
||||
before(:example) do
|
||||
allow(::Rails).to receive(:logger).and_return(nil)
|
||||
end
|
||||
|
||||
it 'should use own logger' do
|
||||
expect(service.logger).not_to be Rails.logger
|
||||
end
|
||||
|
||||
it 'should have a logger' do
|
||||
expect(service.logger).to be_an_instance_of(Logger)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,40 +0,0 @@
|
||||
# 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/options'
|
||||
|
||||
RSpec.describe Google::Apis::RequestOptions do
|
||||
let(:options) { Google::Apis::RequestOptions.new }
|
||||
|
||||
it 'should not merge nil values' do
|
||||
options.retries = 1
|
||||
expect(options.merge(authorization: 'foo').retries).to eql 1
|
||||
end
|
||||
|
||||
it 'should merge non-nil values' do
|
||||
options.retries = 1
|
||||
expect(options.merge(authorization: 'foo').authorization).to eql 'foo'
|
||||
end
|
||||
|
||||
it 'should merge from options' do
|
||||
opts = Google::Apis::RequestOptions.new
|
||||
opts.authorization = 'foo'
|
||||
expect(options.merge(opts).authorization).to eql 'foo'
|
||||
end
|
||||
|
||||
it 'should allow nil in merge' do
|
||||
expect(options.merge(nil)).to be_an_instance_of(Google::Apis::RequestOptions)
|
||||
end
|
||||
end
|
||||
@@ -17,9 +17,4 @@ require 'google/apis/version'
|
||||
|
||||
RSpec.describe Google::Apis::VERSION do
|
||||
it { is_expected.to be_a(String) }
|
||||
|
||||
describe Google::Apis::OS_VERSION do
|
||||
it { is_expected.to be_a(String) }
|
||||
it { is_expected.not_to match /\n/ }
|
||||
end
|
||||
end
|
||||
@@ -16,7 +16,6 @@ SPEC_DIR = File.expand_path(File.dirname(__FILE__))
|
||||
ROOT_DIR = File.expand_path(File.join(SPEC_DIR, '..'))
|
||||
LIB_DIR = File.expand_path(File.join(ROOT_DIR, 'lib'))
|
||||
GENERATED_DIR = File.expand_path(File.join(ROOT_DIR, 'generated'))
|
||||
THIRD_PARTY_DIR = File.expand_path(File.join(ROOT_DIR, 'third_party'))
|
||||
FIXTURES_DIR = File.expand_path(File.join(SPEC_DIR, 'fixtures'))
|
||||
|
||||
$LOAD_PATH.uniq!
|
||||
|
||||
Reference in new issue
Block a user