Import refactored library. See MIGRATING.MD for details
This commit is contained in:
1 parent
9b7809174d
commit
f584a2cec8
373 files changed
+255715
-474
No files matched your search
@@ -0,0 +1,126 @@
|
||||
# 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'
|
||||
require 'launchy'
|
||||
|
||||
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.
|
||||
#
|
||||
# @example
|
||||
#
|
||||
# client = Google::APIClient.new
|
||||
# flow = Google::APIClient::InstalledAppFlow.new(
|
||||
# :client_id => '691380668085.apps.googleusercontent.com',
|
||||
# :client_secret => '...',
|
||||
# :scope => 'https://www.googleapis.com/auth/drive'
|
||||
# )
|
||||
# client.authorization = flow.authorize
|
||||
#
|
||||
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)
|
||||
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.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
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
# 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
|
||||
#
|
||||
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.
|
||||
#
|
||||
# @params [Object] Storage object
|
||||
def initialize(store)
|
||||
@store= store
|
||||
@authorization = nil
|
||||
end
|
||||
|
||||
##
|
||||
# Write the credentials to the specified store.
|
||||
#
|
||||
# @params [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
|
||||
@@ -0,0 +1,58 @@
|
||||
# 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
|
||||
#
|
||||
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 [Signet::OAuth2::Client] authorization
|
||||
# Optional authorization instance. If not provided, the authorization
|
||||
# already associated with this instance will be written.
|
||||
def write_credentials(credentials_hash)
|
||||
open(self.path, 'w+') do |f|
|
||||
f.write(credentials_hash.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,54 @@
|
||||
# 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
|
||||
class RedisStore
|
||||
|
||||
DEFAULT_REDIS_CREDENTIALS_KEY = "google_api_credentials"
|
||||
|
||||
attr_accessor :redis
|
||||
|
||||
##
|
||||
# Initializes the RedisStore object.
|
||||
#
|
||||
# @params [Object] Redis instance
|
||||
def initialize(redis, key = nil)
|
||||
@redis= redis
|
||||
@redis_credentials_key = key
|
||||
end
|
||||
|
||||
##
|
||||
# Attempt to read in credentials from redis.
|
||||
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.
|
||||
#
|
||||
# @params [Hash] credentials
|
||||
def write_credentials(credentials_hash)
|
||||
redis.set(redis_credentials_key, credentials_hash.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,179 @@
|
||||
# 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 'compat/multi_json'
|
||||
|
||||
|
||||
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
|
||||
#
|
||||
# @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 == '/' || search_path =~ /[a-zA-Z]:[\/\\]/
|
||||
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| MultiJson.load(file.read) }
|
||||
return self.new(data)
|
||||
end
|
||||
|
||||
##
|
||||
# Intialize 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]
|
||||
@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 MultiJson.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
|
||||
gem 'signet', '>= 0.4.0'
|
||||
require 'signet/oauth_2/client'
|
||||
# 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
|
||||
@@ -0,0 +1,48 @@
|
||||
# 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 ge the logger if present
|
||||
# @return [Logger]
|
||||
def self.rails_logger
|
||||
::Rails.logger if defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,128 @@
|
||||
# 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 'active_support/inflector'
|
||||
require 'addressable/uri'
|
||||
require 'addressable/template'
|
||||
require 'google/apis/core/http_command'
|
||||
require 'google/apis/errors'
|
||||
require 'multi_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'
|
||||
RATE_LIMIT_ERRORS = %w(rateLimitExceeded userRateLimitExceeded)
|
||||
|
||||
# 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!
|
||||
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
|
||||
self.body = request_representation.new(request_object).to_json(skip_undefined: true)
|
||||
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 content_type.nil?
|
||||
return nil unless content_type.start_with?(JSON_CONTENT_TYPE)
|
||||
instance = response_class.new
|
||||
response_representation.new(instance).from_json(body, unwrap: response_class)
|
||||
instance
|
||||
end
|
||||
|
||||
# Check the response and raise error if needed
|
||||
#
|
||||
# @param [Fixnum] status
|
||||
# HTTP status code of response
|
||||
# @param [String] body
|
||||
# HTTP response body
|
||||
# @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, body = nil)
|
||||
case status
|
||||
when 400, 402...500
|
||||
error = parse_error(body)
|
||||
if error
|
||||
logger.debug { error }
|
||||
fail Google::Apis::RateLimitError, error if RATE_LIMIT_ERRORS.include?(error['reason'])
|
||||
end
|
||||
super(status, error)
|
||||
else
|
||||
super(status, body)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Attempt to parse a JSON error message, returning the first found error
|
||||
# @param [String] body
|
||||
# HTTP response body
|
||||
# @return [Hash]
|
||||
def parse_error(body)
|
||||
hash = MultiJson.load(body)
|
||||
hash['error']['errors'].first
|
||||
rescue
|
||||
nil
|
||||
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_]+/) { |str| ActiveSupport::Inflector.camelize(str, false) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,314 @@
|
||||
# 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 'hurley'
|
||||
require 'hurley/addressable'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
module Core
|
||||
# Base service for all APIs. Not to be used directly.
|
||||
#
|
||||
class BaseService
|
||||
# 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 [Hurley::Client]
|
||||
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 |service|
|
||||
# service.get_item(id1) do |res, err|
|
||||
# # process response for 1st call
|
||||
# end
|
||||
# # ...
|
||||
# service.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
|
||||
Thread.current[:google_api_batch] = batch_command
|
||||
yield self
|
||||
ensure
|
||||
Thread.current[:google_api_batch] = nil
|
||||
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 |service|
|
||||
# service.insert_item(upload_source: 'file1.txt') do |res, err|
|
||||
# # process response for 1st call
|
||||
# end
|
||||
# # ...
|
||||
# service.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
|
||||
Thread.current[:google_api_batch] = batch_command
|
||||
yield self
|
||||
ensure
|
||||
Thread.current[:google_api_batch] = nil
|
||||
end
|
||||
batch_command.execute(client)
|
||||
end
|
||||
|
||||
# Get the current HTTP client
|
||||
# @return [Hurley::Client]
|
||||
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
|
||||
|
||||
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)
|
||||
template = Addressable::Template.new(root_url + base_path + 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
|
||||
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
|
||||
|
||||
# Create a new HTTP client
|
||||
# @return [Hurley::Client]
|
||||
def new_client
|
||||
client = Hurley::Client.new
|
||||
client.request_options.timeout = request_options.timeout_sec
|
||||
client.request_options.proxy = client_options.proxy_url
|
||||
client.request_options.query_class = Hurley::Query::Flat
|
||||
client.ssl_options.ca_file = File.join(Google::Apis::ROOT, 'lib', 'cacerts.pem')
|
||||
client.header[:user_agent] = user_agent
|
||||
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
|
||||
@@ -0,0 +1,222 @@
|
||||
# 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 'hurley'
|
||||
require 'google/apis/core/multipart'
|
||||
require 'google/apis/core/http_command'
|
||||
require 'google/apis/core/upload'
|
||||
require 'google/apis/core/download'
|
||||
require 'addressable/uri'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
module Core
|
||||
# Wrapper request for batching multiple calls in a single server request
|
||||
class BatchCommand < HttpCommand
|
||||
BATCH_BOUNDARY = 'RubyApiBatchRequest'.freeze
|
||||
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 = []
|
||||
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|
|
||||
call, callback = @calls[index]
|
||||
begin
|
||||
result = call.process_response(*deserializer.to_http_response(parts[index])) 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(boundary: BATCH_BOUNDARY, content_type: MULTIPART_MIXED)
|
||||
@calls.each do |(call, _)|
|
||||
io = serializer.to_upload_io(call)
|
||||
multipart.add_upload(io)
|
||||
end
|
||||
self.body = multipart.assemble
|
||||
|
||||
header[:content_type] = multipart.content_type
|
||||
header[:content_length] = "#{body.length}"
|
||||
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
|
||||
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
|
||||
HTTP_CONTENT_TYPE = 'application/http'
|
||||
|
||||
##
|
||||
# Serialize a single batched call for assembling the multipart message
|
||||
#
|
||||
# @param [Google::Apis::Core::HttpCommand] call
|
||||
# the call to serialize.
|
||||
# @return [Hurley::UploadIO]
|
||||
# the serialized request
|
||||
def to_upload_io(call)
|
||||
call.prepare!
|
||||
parts = []
|
||||
parts << build_head(call)
|
||||
parts << build_body(call) unless call.body.nil?
|
||||
length = parts.inject(0) { |a, e| a + e.length }
|
||||
Hurley::UploadIO.new(Hurley::CompositeReadIO.new(length, *parts),
|
||||
HTTP_CONTENT_TYPE,
|
||||
'ruby-api-request')
|
||||
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
|
||||
# Convert a single batched response into a BatchedCallResponse object.
|
||||
#
|
||||
# @param [String] call_response
|
||||
# the response to parse.
|
||||
# @return [Array<(Fixnum, Hurley::Header, String)>]
|
||||
# Status, header, and response body.
|
||||
def to_http_response(call_response)
|
||||
_, 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)
|
||||
[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<(Hurley::Header, String)>]
|
||||
# the header and the body, separately.
|
||||
def split_header_and_body(response)
|
||||
header = Hurley::Header.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
|
||||
@@ -0,0 +1,94 @@
|
||||
# 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/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'
|
||||
|
||||
# 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 [Hurley::Client] 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)
|
||||
client.get(@download_url || url) do |req|
|
||||
apply_request_options(req)
|
||||
if @offset > 0
|
||||
logger.debug { sprintf('Resuming download from offset %d', @offset) }
|
||||
req.header[RANGE_HEADER] = sprintf('bytes=%d-', @offset)
|
||||
end
|
||||
req.on_body do |res, chunk|
|
||||
check_status(res.status_code, chunk) unless res.status_code.nil?
|
||||
logger.debug { sprintf('Writing chunk (%d bytes)', chunk.length) }
|
||||
@offset += chunk.length
|
||||
@download_io.write(chunk)
|
||||
@download_io.flush
|
||||
end
|
||||
end
|
||||
if @close_io_on_finish
|
||||
result = nil
|
||||
else
|
||||
result = @download_io
|
||||
end
|
||||
success(result, &block)
|
||||
rescue => e
|
||||
error(e, rethrow: true, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
# 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
|
||||
@@ -0,0 +1,275 @@
|
||||
# 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 'hurley'
|
||||
require 'hurley/addressable'
|
||||
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]
|
||||
|
||||
# Request options
|
||||
# @return [Google::Apis::RequestOptions]
|
||||
attr_accessor :options
|
||||
|
||||
# HTTP request URL
|
||||
# @return [String, Addressable::URI]
|
||||
attr_accessor :url
|
||||
|
||||
# HTTP headers
|
||||
# @return [Hurley::Header]
|
||||
attr_accessor :header
|
||||
|
||||
# Request body
|
||||
# @return [#read]
|
||||
attr_accessor :body
|
||||
|
||||
# HTTP method
|
||||
# @return [symbol]
|
||||
attr_accessor :method
|
||||
|
||||
# HTTP Client
|
||||
# @return [Hurley::Client]
|
||||
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 = Hurley::Header.new
|
||||
self.body = body
|
||||
self.query = {}
|
||||
self.params = {}
|
||||
end
|
||||
|
||||
# Execute the command, retrying as necessary
|
||||
#
|
||||
# @param [Hurley::Client] 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!
|
||||
proc = block_given? ? Proc.new : nil
|
||||
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],
|
||||
on_retry: proc { |*| refresh_authorization } do
|
||||
return execute_once(client, &proc)
|
||||
end
|
||||
end
|
||||
rescue => e
|
||||
raise e if proc.nil?
|
||||
end
|
||||
ensure
|
||||
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, serialize data, etc) before sending
|
||||
#
|
||||
# @private
|
||||
# @return [void]
|
||||
def prepare!
|
||||
header.update(options.header) if options && options.header
|
||||
self.url = url.expand(params) if url.is_a?(Addressable::Template)
|
||||
url.query_values = query
|
||||
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 [Hurley::Header] 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, body)
|
||||
decode_response_body(header[:content_type], body)
|
||||
end
|
||||
|
||||
# Check the response and raise error if needed
|
||||
#
|
||||
# @param [Fixnum] status
|
||||
# HTTP status code of response
|
||||
# @param [String] body
|
||||
# HTTP response body
|
||||
# @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, body = nil)
|
||||
# TODO: 304 Not Modified depends on context...
|
||||
case status
|
||||
when 200...300
|
||||
nil
|
||||
when 301, 302, 303, 307
|
||||
fail Google::Apis::RedirectError, header[:location]
|
||||
when 401
|
||||
fail Google::Apis::AuthorizationError, body
|
||||
when 304, 400, 402...500
|
||||
fail Google::Apis::ClientError, body
|
||||
when 500...600
|
||||
fail Google::Apis::ServerError, body
|
||||
else
|
||||
logger.warn(sprintf('Encountered unexpected status code %s', status))
|
||||
fail Google::Apis::TransmissionError, 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', PP.pp(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, '')) }
|
||||
err = Google::Apis::TransmissionError.new(err) if err.is_a?(Hurley::ClientError)
|
||||
block.call(nil, err) if block_given?
|
||||
fail err if rethrow || block.nil?
|
||||
end
|
||||
|
||||
# Execute the command once.
|
||||
#
|
||||
# @private
|
||||
# @param [Hurley::Client] 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)
|
||||
body.rewind if body.respond_to?(:rewind)
|
||||
begin
|
||||
logger.debug { sprintf('Sending HTTP %s %s', method, url) }
|
||||
response = client.send(method, url, body) do |req|
|
||||
apply_request_options(req)
|
||||
end
|
||||
logger.debug { response.status_code }
|
||||
logger.debug { response.inspect }
|
||||
response = process_response(response.status_code, response.header, response.body)
|
||||
success(response, &block)
|
||||
rescue => e
|
||||
logger.debug { sprintf('Caught error %s', e) }
|
||||
error(e, rethrow: true, &block)
|
||||
end
|
||||
end
|
||||
|
||||
# Update the request with any specified options.
|
||||
# @param [Hurley::Request] req
|
||||
# HTTP request
|
||||
# @return [void]
|
||||
def apply_request_options(req)
|
||||
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)
|
||||
req.options.timeout = options.timeout_sec
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,122 @@
|
||||
# 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 'representable/coercion'
|
||||
require 'base64'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
module Core
|
||||
# Support for serializing hashes + propery 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?(:[])
|
||||
self[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[: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) { Base64.urlsafe_encode64(value) }
|
||||
options[:parse_filter] = ->(fragment, _doc, *_args) { Base64.urlsafe_decode64(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, options)
|
||||
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
|
||||
include Representable::Coercion
|
||||
feature JsonRepresentationSupport
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,30 @@
|
||||
# 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
|
||||
@@ -0,0 +1,173 @@
|
||||
# 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 'hurley'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
module Core
|
||||
# Part of a multipart request for holding JSON data
|
||||
#
|
||||
# @private
|
||||
class JsonPart
|
||||
include Hurley::Multipart::Part
|
||||
|
||||
# @return [Fixnum]
|
||||
# Length of part
|
||||
attr_reader :length
|
||||
|
||||
# @param [String] boundary
|
||||
# Multipart boundary
|
||||
# @param [String] value
|
||||
# JSON content
|
||||
def initialize(boundary, value)
|
||||
@part = build_part(boundary, value)
|
||||
@length = @part.bytesize
|
||||
@io = StringIO.new(@part)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Format the part
|
||||
#
|
||||
# @param [String] boundary
|
||||
# Multipart boundary
|
||||
# @param [String] value
|
||||
# JSON content
|
||||
# @return [String]
|
||||
def build_part(boundary, value)
|
||||
part = ''
|
||||
part << "--#{boundary}\r\n"
|
||||
part << "Content-Type: application/json\r\n"
|
||||
part << "\r\n"
|
||||
part << "#{value}\r\n"
|
||||
end
|
||||
end
|
||||
|
||||
# Part of a multipart request for holding arbitrary content. Modified
|
||||
# from Hurley::Multipart::FilePart to remove Content-Disposition
|
||||
#
|
||||
# @private
|
||||
class FilePart
|
||||
include Hurley::Multipart::Part
|
||||
|
||||
# @return [Fixnum]
|
||||
# Length of part
|
||||
attr_reader :length
|
||||
|
||||
# @param [String] boundary
|
||||
# Multipart boundary
|
||||
# @param [Google::Apis::Core::UploadIO] io
|
||||
# IO stream
|
||||
# @param [Hash] header
|
||||
# Additional headers
|
||||
def initialize(boundary, io, header = {})
|
||||
file_length = io.respond_to?(:length) ? io.length : File.size(io.local_path)
|
||||
|
||||
@head = build_head(boundary, io.content_type, file_length,
|
||||
io.respond_to?(:opts) ? io.opts.merge(header) : header)
|
||||
|
||||
@length = @head.bytesize + file_length + FOOT.length
|
||||
@io = Hurley::CompositeReadIO.new(@length, StringIO.new(@head), io, StringIO.new(FOOT))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Construct the header for the part
|
||||
#
|
||||
# @param [String] boundary
|
||||
# Multipart boundary
|
||||
# @param [String] type
|
||||
# Content type for the part
|
||||
# @param [Fixnum] content_len
|
||||
# Length of the part
|
||||
# @param [Hash] header
|
||||
# Headers for the part
|
||||
def build_head(boundary, type, content_len, header)
|
||||
sprintf(HEAD_FORMAT,
|
||||
boundary,
|
||||
content_len.to_i,
|
||||
header[:content_type] || type,
|
||||
header[:content_transfer_encoding] || DEFAULT_TR_ENCODING)
|
||||
end
|
||||
|
||||
DEFAULT_TR_ENCODING = 'binary'.freeze
|
||||
FOOT = "\r\n".freeze
|
||||
HEAD_FORMAT = <<-END
|
||||
--%s\r
|
||||
Content-Length: %d\r
|
||||
Content-Type: %s\r
|
||||
Content-Transfer-Encoding: %s\r
|
||||
\r
|
||||
END
|
||||
end
|
||||
|
||||
# Helper for building multipart requests
|
||||
class Multipart
|
||||
MULTIPART_RELATED = 'multipart/related'
|
||||
DEFAULT_BOUNDARY = 'RubyApiClientMultiPart'
|
||||
|
||||
# @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 || DEFAULT_BOUNDARY
|
||||
@content_type = "#{content_type}; boundary=#{boundary}"
|
||||
end
|
||||
|
||||
# Append JSON data part
|
||||
#
|
||||
# @param [String] body
|
||||
# JSON text
|
||||
# @return [self]
|
||||
def add_json(body)
|
||||
@parts << Google::Apis::Core::JsonPart.new(@boundary, body)
|
||||
self
|
||||
end
|
||||
|
||||
# Append arbitrary data as a part
|
||||
#
|
||||
# @param [Google::Apis::Core::UploadIO] upload_io
|
||||
# IO stream
|
||||
# @return [self]
|
||||
def add_upload(upload_io)
|
||||
@parts << Google::Apis::Core::FilePart.new(@boundary, upload_io)
|
||||
self
|
||||
end
|
||||
|
||||
# Assemble the multipart requests
|
||||
#
|
||||
# @return [IO]
|
||||
# IO stream
|
||||
def assemble
|
||||
@parts << Hurley::Multipart::EpiloguePart.new(@boundary)
|
||||
ios = []
|
||||
len = 0
|
||||
@parts.each do |part|
|
||||
len += part.length
|
||||
ios << part.to_io
|
||||
end
|
||||
Hurley::CompositeReadIO.new(len, *ios)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,275 @@
|
||||
# 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 'mime-types'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
module Core
|
||||
# Extension of Hurley's UploadIO to add length accessor
|
||||
class UploadIO < Hurley::UploadIO
|
||||
OCTET_STREAM_CONTENT_TYPE = 'application/octet-stream'
|
||||
|
||||
# Get the length of the stream
|
||||
# @return [Fixnum]
|
||||
def length
|
||||
io.respond_to?(:length) ? io.length : File.size(local_path)
|
||||
end
|
||||
|
||||
# Create a new instance given a file path
|
||||
# @param [String, File] file_name
|
||||
# Path to file
|
||||
# @param [String] content_type
|
||||
# Optional content type. If nil, will attempt to auto-detect
|
||||
# @return [Google::Apis::Core::UploadIO]
|
||||
def self.from_file(file_name, content_type: nil)
|
||||
if content_type.nil?
|
||||
type = MIME::Types.of(file_name)
|
||||
content_type = type.first.content_type unless type.nil? || type.empty?
|
||||
end
|
||||
new(file_name, content_type || OCTET_STREAM_CONTENT_TYPE)
|
||||
end
|
||||
|
||||
# Wraps an IO stream in UploadIO
|
||||
# @param [#read] io
|
||||
# IO to wrap
|
||||
# @param [String] content_type
|
||||
# Optional content type.
|
||||
# @return [Google::Apis::Core::UploadIO]
|
||||
def self.from_io(io, content_type: OCTET_STREAM_CONTENT_TYPE)
|
||||
new(io, content_type)
|
||||
end
|
||||
end
|
||||
|
||||
# Base upload command. Not intended to be used directly
|
||||
# @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'
|
||||
|
||||
# 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 {{Google::Apis::Core::UploadIO}} instance.
|
||||
#
|
||||
# @return [void]
|
||||
# @raise [Google::Apis::ClientError] if upload source is invalid
|
||||
def prepare!
|
||||
super
|
||||
if upload_source.is_a?(IO) || upload_source.is_a?(StringIO)
|
||||
self.upload_io = UploadIO.from_io(upload_source, content_type: upload_content_type)
|
||||
@close_io_on_finish = false
|
||||
elsif upload_source.is_a?(String)
|
||||
self.upload_io = UploadIO.from_file(upload_source, content_type: upload_content_type)
|
||||
@close_io_on_finish = true
|
||||
else
|
||||
fail Google::Apis::ClientError, 'Invalid upload source'
|
||||
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
|
||||
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_io.content_type
|
||||
end
|
||||
end
|
||||
|
||||
# Implementation of the multipart upload protocol
|
||||
class MultipartUploadCommand < BaseUploadCommand
|
||||
UPLOAD_BOUNDARY = 'RubyApiClientUpload'
|
||||
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(boundary: UPLOAD_BOUNDARY, content_type: MULTIPART_RELATED)
|
||||
@multipart.add_json(body)
|
||||
@multipart.add_upload(upload_io)
|
||||
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
|
||||
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 [Hurley::Header] 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]) if header.key?(BYTES_RECEIVED_HEADER)
|
||||
@upload_url = header[UPLOAD_URL_HEADER] if header.key?(UPLOAD_URL_HEADER)
|
||||
|
||||
upload_status = header[UPLOAD_STATUS_HEADER]
|
||||
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
|
||||
|
||||
# Send the start command to initiate the upload
|
||||
#
|
||||
# @param [Hurley::Client] client
|
||||
# HTTP client
|
||||
# @return [Hurley::Response]
|
||||
# @raise [Google::Apis::ServerError] Unable to send the request
|
||||
def send_start_command(client)
|
||||
logger.debug { sprintf('Sending upload start command to %s', url) }
|
||||
client.send(method, url, body) do |req|
|
||||
apply_request_options(req)
|
||||
req.header[UPLOAD_PROTOCOL_HEADER] = RESUMABLE
|
||||
req.header[UPLOAD_COMMAND_HEADER] = START_COMMAND
|
||||
req.header[UPLOAD_CONTENT_LENGTH] = upload_io.length.to_s
|
||||
req.header[UPLOAD_CONTENT_TYPE_HEADER] = upload_io.content_type
|
||||
end
|
||||
rescue => e
|
||||
raise Google::Apis::ServerError, e.message
|
||||
end
|
||||
|
||||
# Query for the status of an incomplete upload
|
||||
#
|
||||
# @param [Hurley::Client] client
|
||||
# HTTP client
|
||||
# @return [Hurley::Response]
|
||||
# @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) }
|
||||
client.post(@upload_url, nil) do |req|
|
||||
apply_request_options(req)
|
||||
req.header[UPLOAD_COMMAND_HEADER] = QUERY_COMMAND
|
||||
end
|
||||
end
|
||||
|
||||
# Send the actual content
|
||||
#
|
||||
# @param [Hurley::Client] client
|
||||
# HTTP client
|
||||
# @return [Hurley::Response]
|
||||
# @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
|
||||
client.post(@upload_url, content) do |req|
|
||||
apply_request_options(req)
|
||||
req.header[UPLOAD_COMMAND_HEADER] = UPLOAD_COMMAND
|
||||
req.header[UPLOAD_OFFSET_HEADER] = @offset.to_s
|
||||
end
|
||||
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 [Hurley::Client] 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)
|
||||
if @state == :start
|
||||
response = send_start_command(client)
|
||||
else
|
||||
response = send_query_command(client)
|
||||
end
|
||||
result = process_response(response.status_code, response.header, response.body)
|
||||
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
|
||||
error(e, rethrow: true, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,69 @@
|
||||
# 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
|
||||
def initialize(err)
|
||||
@cause = nil
|
||||
|
||||
if err.respond_to?(:backtrace)
|
||||
super(err.message)
|
||||
@cause = err
|
||||
else
|
||||
super(err.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
def backtrace
|
||||
if @cause
|
||||
@cause.backtrace
|
||||
else
|
||||
super
|
||||
end
|
||||
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 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
|
||||
@@ -0,0 +1,70 @@
|
||||
# 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/discovery_v1'
|
||||
require 'google/apis/generator/annotator'
|
||||
require 'google/apis/generator/model'
|
||||
require 'google/apis/generator/template'
|
||||
require 'active_support/inflector'
|
||||
require 'yaml'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
# Generates ruby classes for APIs from discovery documents
|
||||
# @private
|
||||
class Generator
|
||||
Discovery = Google::Apis::DiscoveryV1
|
||||
|
||||
# Load templates
|
||||
def initialize(api_names: nil)
|
||||
@names = Google::Apis::Generator::Names.new(api_names || File.join(Google::Apis::ROOT, 'api_names.yaml'))
|
||||
@module_template = Template.load('module.rb')
|
||||
@service_template = Template.load('service.rb')
|
||||
@classes_template = Template.load('classes.rb')
|
||||
@representations_template = Template.load('representations.rb')
|
||||
end
|
||||
|
||||
# Generates ruby source for an API
|
||||
#
|
||||
# @param [String] json
|
||||
# API Description, as JSON text
|
||||
# @return [Hash<String,String>]
|
||||
# Hash of generated files keyed by path
|
||||
def render(json)
|
||||
api = parse_description(json)
|
||||
Annotator.process(api, @names)
|
||||
base_path = ActiveSupport::Inflector.underscore(api.qualified_name)
|
||||
context = {
|
||||
'api' => api
|
||||
}
|
||||
files = {}
|
||||
files[base_path + '.rb'] = @module_template.render(context)
|
||||
files[File.join(base_path, 'service.rb')] = @service_template.render(context)
|
||||
files[File.join(base_path, 'classes.rb')] = @classes_template.render(context)
|
||||
files[File.join(base_path, 'representations.rb')] = @representations_template.render(context)
|
||||
files
|
||||
end
|
||||
|
||||
# Dump mapping of API names
|
||||
# @return [String] Mapping of paths to ruby names in YAML format
|
||||
def dump_api_names
|
||||
@names.dump
|
||||
end
|
||||
|
||||
def parse_description(json)
|
||||
Discovery::RestDescription::Representation.new(Discovery::RestDescription.new).from_json(json)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,271 @@
|
||||
# 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 'logger'
|
||||
require 'erb'
|
||||
require 'yaml'
|
||||
require 'multi_json'
|
||||
require 'active_support/inflector'
|
||||
require 'google/apis/core/logging'
|
||||
require 'google/apis/generator/template'
|
||||
require 'google/apis/generator/model'
|
||||
require 'google/apis/generator/helpers'
|
||||
require 'addressable/uri'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
# @private
|
||||
class Generator
|
||||
# Helper for picking names for methods, properties, types, etc. Performs various normaliations
|
||||
# as well as allows for overriding individual names from a configuration file for cases
|
||||
# where algorithmic approaches produce poor APIs.
|
||||
class Names
|
||||
include Google::Apis::Core::Logging
|
||||
include NameHelpers
|
||||
|
||||
def initialize(file_path = nil)
|
||||
if file_path
|
||||
logger.info { sprintf('Loading API names from %s', file_path) }
|
||||
@names = YAML.load(File.read(file_path)) || {}
|
||||
else
|
||||
@names = {}
|
||||
end
|
||||
@path = []
|
||||
end
|
||||
|
||||
def with_path(path)
|
||||
@path.push(path)
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
@path.pop
|
||||
end
|
||||
end
|
||||
|
||||
def infer_parameter_name
|
||||
pick_name(normalize_param_name(@path.last))
|
||||
end
|
||||
|
||||
# Determine the ruby method name to generate for a given method in discovery.
|
||||
# @param [Google::Apis::DiscoveryV1::RestMethod] method
|
||||
# Fragment of the discovery doc describing the method
|
||||
def infer_method_name(method)
|
||||
pick_name(infer_method_name_for_rpc(method) || infer_method_name_from_id(method))
|
||||
end
|
||||
|
||||
def infer_property_name
|
||||
pick_name(normalize_property_name(@path.last))
|
||||
end
|
||||
|
||||
def pick_name(alt_name)
|
||||
preferred_name = @names[key]
|
||||
if preferred_name && preferred_name == alt_name
|
||||
logger.warn { sprintf("Unnecessary name override '%s': %s", key, alt_name) }
|
||||
elsif preferred_name.nil?
|
||||
preferred_name = @names[key] = alt_name
|
||||
end
|
||||
preferred_name
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
@names[key] = value
|
||||
end
|
||||
|
||||
def dump
|
||||
YAML.dump(@names)
|
||||
end
|
||||
|
||||
def key
|
||||
@path.reduce('') { |a, e| a + '/' + e }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# For RPC style methods, pick a name based off the request objects.
|
||||
# @param [Google::Apis::DiscoveryV1::RestMethod] method
|
||||
# Fragment of the discovery doc describing the method
|
||||
def infer_method_name_for_rpc(method)
|
||||
return nil if method.request.nil?
|
||||
verb = ActiveSupport::Inflector.underscore(method.id.split('.').last)
|
||||
match = method.request._ref.match(/(.*)(?i:request)/)
|
||||
return nil if match.nil?
|
||||
name = ActiveSupport::Inflector.underscore(match[1])
|
||||
return nil unless name == verb || name.start_with?(verb + '_')
|
||||
name
|
||||
end
|
||||
|
||||
# For REST style methods, build a method name from the verb/resource(s) in the method
|
||||
# id. IDs are in the form <api>.<resource>.<verb>
|
||||
# @param [Google::Apis::DiscoveryV1::RestMethod] method
|
||||
# Fragment of the discovery doc describing the method
|
||||
def infer_method_name_from_id(method)
|
||||
parts = method.id.split('.')
|
||||
parts.shift
|
||||
verb = parts.pop
|
||||
return ActiveSupport::Inflector.underscore(verb) if parts.empty?
|
||||
resource_name = parts.pop
|
||||
method_name = verb + '_'
|
||||
method_name += parts.map { |p| ActiveSupport::Inflector.singularize(p) }.join('_') + '_' unless parts.empty?
|
||||
if pluralize_method?(verb)
|
||||
method_name += ActiveSupport::Inflector.pluralize(resource_name)
|
||||
else
|
||||
method_name += ActiveSupport::Inflector.singularize(resource_name)
|
||||
end
|
||||
ActiveSupport::Inflector.underscore(method_name)
|
||||
end
|
||||
end
|
||||
|
||||
# Modifies an API description to support ruby code generation. Primarily does:
|
||||
# - Ensure all names follow appopriate ruby conventions
|
||||
# - Maps types to ruby types, classes, and resolves $refs
|
||||
# - Attempts to simplify names where possible to make APIs more sensible
|
||||
class Annotator
|
||||
include NameHelpers
|
||||
include Google::Apis::Core::Logging
|
||||
|
||||
# Don't expose these in the API directly.
|
||||
PARAMETER_BLACKLIST = %w(alt access_token bearer_token oauth_token pp prettyPrint
|
||||
$.xgafv callback upload_protocol uploadType)
|
||||
|
||||
# Prepare the API for the templates.
|
||||
# @param [Google::Apis::DiscoveryV1::RestDescription] description
|
||||
# API Description
|
||||
def self.process(description, api_names = nil)
|
||||
Annotator.new(description, api_names).annotate_api
|
||||
end
|
||||
|
||||
# @param [Google::Apis::DiscoveryV1::RestDescription] description
|
||||
# API Description
|
||||
# @param [Google::Api::Generator::Names] api_names
|
||||
# Name helper instanace
|
||||
def initialize(description, api_names = nil)
|
||||
api_names = Names.new if api_names.nil?
|
||||
@names = api_names
|
||||
@rest_description = description
|
||||
@registered_types = []
|
||||
@deferred_types = []
|
||||
@strip_prefixes = []
|
||||
@all_methods = {}
|
||||
@path = []
|
||||
end
|
||||
|
||||
def annotate_api
|
||||
@names.with_path(@rest_description.id) do
|
||||
@strip_prefixes << @rest_description.name
|
||||
if @rest_description.auth
|
||||
@rest_description.auth.oauth2.scopes.each do |key, value|
|
||||
value.constant = constantize_scope(key)
|
||||
end
|
||||
end
|
||||
@rest_description.parameters.reject! { |k, _v| PARAMETER_BLACKLIST.include?(k) }
|
||||
annotate_parameters(@rest_description.parameters)
|
||||
annotate_resource(@rest_description.name, @rest_description)
|
||||
@rest_description.schemas.each do |k, v|
|
||||
annotate_type(k, v, @rest_description)
|
||||
end unless @rest_description.schemas.nil?
|
||||
end
|
||||
resolve_type_references
|
||||
resolve_variants
|
||||
end
|
||||
|
||||
def annotate_type(name, type, parent)
|
||||
@names.with_path(name) do
|
||||
type.name = name
|
||||
type.path = @names.key
|
||||
type.generated_name = @names.infer_property_name
|
||||
if type.type == 'object'
|
||||
type.generated_class_name = ActiveSupport::Inflector.camelize(type.generated_name)
|
||||
@registered_types << type
|
||||
end
|
||||
type.parent = parent
|
||||
@deferred_types << type if type._ref
|
||||
type.properties.each do |k, v|
|
||||
annotate_type(k, v, type)
|
||||
end unless type.properties.nil?
|
||||
if type.additional_properties
|
||||
type.type = 'hash'
|
||||
annotate_type(ActiveSupport::Inflector.singularize(type.generated_name), type.additional_properties,
|
||||
parent)
|
||||
end
|
||||
annotate_type(ActiveSupport::Inflector.singularize(type.generated_name), type.items, parent) if type.items
|
||||
end
|
||||
end
|
||||
|
||||
def annotate_resource(name, resource, parent_resource = nil)
|
||||
@strip_prefixes << name
|
||||
resource.parent = parent_resource unless parent_resource.nil?
|
||||
resource.api_methods.each do |_k, v|
|
||||
annotate_method(v, resource)
|
||||
end unless resource.api_methods.nil?
|
||||
|
||||
resource.resources.each do |k, v|
|
||||
annotate_resource(k, v, resource)
|
||||
end unless resource.resources.nil?
|
||||
end
|
||||
|
||||
def annotate_method(method, parent_resource = nil)
|
||||
@names.with_path(method.id) do
|
||||
method.parent = parent_resource
|
||||
method.generated_name = @names.infer_method_name(method)
|
||||
check_duplicate_method(method)
|
||||
annotate_parameters(method.parameters)
|
||||
end
|
||||
end
|
||||
|
||||
def annotate_parameters(parameters)
|
||||
parameters.each do |key, value|
|
||||
@names.with_path(key) do
|
||||
value.name = key
|
||||
value.generated_name = @names.infer_parameter_name
|
||||
@deferred_types << value if value._ref
|
||||
end
|
||||
end unless parameters.nil?
|
||||
end
|
||||
|
||||
def resolve_type_references
|
||||
@deferred_types.each do |type|
|
||||
if type._ref
|
||||
ref = @rest_description.schemas[type._ref]
|
||||
ivars = ref.instance_variables - [:@name, :@generated_name]
|
||||
(ivars).each do |var|
|
||||
type.instance_variable_set(var, ref.instance_variable_get(var))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def resolve_variants
|
||||
@deferred_types.each do |type|
|
||||
if type.variant
|
||||
type.variant.map.each do |v|
|
||||
ref = @rest_description.schemas[v._ref]
|
||||
ref.base_ref = type
|
||||
ref.discriminant = type.variant.discriminant
|
||||
ref.discriminant_value = v.type_value
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def check_duplicate_method(m)
|
||||
if @all_methods.include?(m.generated_name)
|
||||
logger.error { sprintf('Duplicate method %s generated', m.generated_name) }
|
||||
fail 'Duplicate name generated'
|
||||
end
|
||||
@all_methods[m.generated_name] = m
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,74 @@
|
||||
module Google
|
||||
module Apis
|
||||
# @private
|
||||
class Generator
|
||||
# Methods for validating & normalizing symbols
|
||||
module NameHelpers
|
||||
KEYWORDS = %w(__ENCODING__ def in self __LINE__ defined? module super __FILE__ do next then BEGIN
|
||||
else nil true END elsif not undef alias end or unless and ensure redo until begin
|
||||
false rescue when break for retry while case if return yield class)
|
||||
PLURAL_METHODS = %w(list search)
|
||||
|
||||
# Check to see if the method name should be plauralized
|
||||
# @return [Boolean]
|
||||
def pluralize_method?(method_name)
|
||||
PLURAL_METHODS.include?(method_name)
|
||||
end
|
||||
|
||||
# Check to see if the method is either a keyword or built-in method on object
|
||||
# @return [Boolean]
|
||||
def reserved?(name)
|
||||
keyword?(name) || object_method?(name)
|
||||
end
|
||||
|
||||
# Check to see if the name is a ruby keyword
|
||||
# @return [Boolean]
|
||||
def keyword?(name)
|
||||
KEYWORDS.include?(name)
|
||||
end
|
||||
|
||||
# Check to see if the method already exists on ruby objects
|
||||
# @return [Boolean]
|
||||
def object_method?(name)
|
||||
Object.new.respond_to?(name)
|
||||
end
|
||||
|
||||
# Convert a parameter name to ruby conventions
|
||||
# @param [String] name
|
||||
# @return [String] updated param name
|
||||
def normalize_param_name(name)
|
||||
name = ActiveSupport::Inflector.underscore(name.gsub(/\W/, '_'))
|
||||
if reserved?(name)
|
||||
logger.warn { sprintf('Found reserved keyword \'%1$s\'', name) }
|
||||
name += '_'
|
||||
logger.warn { sprintf('Changed to \'%1$s\'', name) }
|
||||
end
|
||||
name
|
||||
end
|
||||
|
||||
# Convert a property name to ruby conventions
|
||||
# @param [String] name
|
||||
# @return [String]
|
||||
def normalize_property_name(name)
|
||||
name = ActiveSupport::Inflector.underscore(name.gsub(/\W/, '_'))
|
||||
if object_method?(name)
|
||||
logger.warn { sprintf('Found reserved property \'%1$s\'', name) }
|
||||
name += '_prop'
|
||||
logger.warn { sprintf('Changed to \'%1$s\'', name) }
|
||||
end
|
||||
name
|
||||
end
|
||||
|
||||
# Converts a scope string into a ruby constant
|
||||
# @param [String] url
|
||||
# Url to convert
|
||||
# @return [String]
|
||||
def constantize_scope(url)
|
||||
scope = Addressable::URI.parse(url).path[1..-1].upcase.gsub(/\W/, '_')
|
||||
scope = 'AUTH_SCOPE' if scope.nil? || scope.empty?
|
||||
scope
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,130 @@
|
||||
# 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 'active_support/inflector'
|
||||
require 'google/apis/discovery_v1'
|
||||
|
||||
# Extend the discovery API classes with additional data needed to make
|
||||
# code generation produce better results
|
||||
module Google
|
||||
module Apis
|
||||
module DiscoveryV1
|
||||
TYPE_MAP = {
|
||||
'string' => 'String',
|
||||
'boolean' => 'Boolean',
|
||||
'number' => 'Float',
|
||||
'integer' => 'Fixnum',
|
||||
'any' => 'Object'
|
||||
}
|
||||
|
||||
class JsonSchema
|
||||
attr_accessor :name
|
||||
attr_accessor :generated_name
|
||||
attr_accessor :generated_class_name
|
||||
attr_accessor :base_ref
|
||||
attr_accessor :parent
|
||||
attr_accessor :discriminant
|
||||
attr_accessor :discriminant_value
|
||||
attr_accessor :path
|
||||
|
||||
def properties
|
||||
@properties ||= {}
|
||||
end
|
||||
|
||||
def qualified_name
|
||||
parent.qualified_name + '::' + generated_class_name
|
||||
end
|
||||
|
||||
def generated_type
|
||||
case type
|
||||
when 'string', 'boolean', 'number', 'integer', 'any'
|
||||
return 'DateTime' if format == 'date-time'
|
||||
return 'Date' if format == 'date'
|
||||
return TYPE_MAP[type]
|
||||
when 'array'
|
||||
return sprintf('Array<%s>', items.generated_type)
|
||||
when 'hash'
|
||||
return sprintf('Hash<String,%s>', additional_properties.generated_type)
|
||||
when 'object'
|
||||
return qualified_name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class RestMethod
|
||||
attr_accessor :generated_name
|
||||
attr_accessor :parent
|
||||
|
||||
def path_parameters
|
||||
return [] if parameter_order.nil? || parameters.nil?
|
||||
parameter_order.map { |name| parameters[name] }.select { |param| param.location == 'path' }
|
||||
end
|
||||
|
||||
def query_parameters
|
||||
return [] if parameters.nil?
|
||||
parameters.values.select { |param| param.location == 'query' }
|
||||
end
|
||||
end
|
||||
|
||||
class RestResource
|
||||
attr_accessor :parent
|
||||
|
||||
def all_methods
|
||||
m = []
|
||||
m << api_methods.values unless api_methods.nil?
|
||||
m << resources.map { |_k, r| r.all_methods } unless resources.nil?
|
||||
m.flatten
|
||||
end
|
||||
end
|
||||
|
||||
class RestDescription
|
||||
def version
|
||||
ActiveSupport::Inflector.camelize(@version.gsub(/\W/, '-')).gsub(/-/, '_')
|
||||
end
|
||||
|
||||
def name
|
||||
ActiveSupport::Inflector.camelize(@name)
|
||||
end
|
||||
|
||||
def module_name
|
||||
name + version
|
||||
end
|
||||
|
||||
def qualified_name
|
||||
sprintf('Google::Apis::%s', module_name)
|
||||
end
|
||||
|
||||
def service_name
|
||||
class_name = (canonical_name || name).gsub(/\W/, '')
|
||||
ActiveSupport::Inflector.camelize(sprintf('%sService', class_name))
|
||||
end
|
||||
|
||||
def all_methods
|
||||
m = []
|
||||
m << api_methods.values unless api_methods.nil?
|
||||
m << resources.map { |_k, r| r.all_methods } unless resources.nil?
|
||||
m.flatten
|
||||
end
|
||||
|
||||
class Auth
|
||||
class Oauth2
|
||||
class Scope
|
||||
attr_accessor :constant
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,124 @@
|
||||
# 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 'active_support/inflector'
|
||||
require 'erb'
|
||||
require 'ostruct'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
# @private
|
||||
class Generator
|
||||
# Directory containing ERB templates
|
||||
TEMPLATE_DIR = File.expand_path('../templates', __FILE__)
|
||||
|
||||
# Helpers used in ERB templates
|
||||
module TemplateHelpers
|
||||
# Get the include path for a ruby module/class
|
||||
#
|
||||
# @param [String] module_name
|
||||
# Fully qualified module/class name
|
||||
# @return [String]
|
||||
# Path to file
|
||||
def to_path(module_name)
|
||||
ActiveSupport::Inflector.underscore(module_name)
|
||||
end
|
||||
|
||||
# Render a block comment
|
||||
#
|
||||
# @param [String] str
|
||||
# Comment string
|
||||
# @param [Fixnum] spaces_before
|
||||
# Number of spaces to indent the comment hash
|
||||
# @param [Fixnum] spaces_after
|
||||
# Number of spaces to indent after the comment hash for subsequent lines
|
||||
# @return [String] formatted comment
|
||||
def block_comment(str, spaces_before = 0, spaces_after = 0)
|
||||
return '' if str.nil?
|
||||
pre = ' ' * spaces_before
|
||||
post = ' ' * spaces_after
|
||||
lines = str.gsub(/([{}])/, '`').scan(/.{1,78}(?:\W|$)/).map(&:strip)
|
||||
lines.join("\n" + pre + '#' + post)
|
||||
end
|
||||
|
||||
# Indent a block of text
|
||||
#
|
||||
# @param [String] str
|
||||
# Content to indent
|
||||
# @param [Fixnum] spaces
|
||||
# Number of spaces to indent
|
||||
# @return [String] formatted content
|
||||
def indent(str, spaces)
|
||||
pre = ' ' * spaces
|
||||
str = pre + str.split(/\n/).join("\n" + pre) + "\n"
|
||||
return str unless str.strip.empty?
|
||||
nil
|
||||
end
|
||||
|
||||
# Include a partial inside a template.
|
||||
#
|
||||
# @private
|
||||
# @param [String] partial
|
||||
# Name of the template
|
||||
# @param [Hash] context
|
||||
# Context used to render
|
||||
# @return [String] rendered content
|
||||
def include(partial, context)
|
||||
template = Template.new(sprintf('_%s.tmpl', partial))
|
||||
template.render(context)
|
||||
end
|
||||
end
|
||||
|
||||
# Holds local vars/helpers for template rendering
|
||||
class Context < OpenStruct
|
||||
include TemplateHelpers
|
||||
|
||||
# Get the context for ERB evaluation
|
||||
# @return [Binding]
|
||||
def to_binding
|
||||
binding
|
||||
end
|
||||
end
|
||||
|
||||
# ERB template for the code generator
|
||||
class Template
|
||||
# Loads a template from the template dir. Automatically
|
||||
# appends the .tmpl suffix
|
||||
#
|
||||
# @param [String] template_name
|
||||
# Name of the template file
|
||||
def self.load(template_name)
|
||||
Template.new(sprintf('%s.tmpl', template_name))
|
||||
end
|
||||
|
||||
# @param [String] template_name
|
||||
# Name of the template file
|
||||
def initialize(template_name)
|
||||
file = File.join(TEMPLATE_DIR, template_name)
|
||||
@erb = ERB.new(File.read(file), nil, '-')
|
||||
end
|
||||
|
||||
# Render the template
|
||||
#
|
||||
# @param [Hash] context
|
||||
# Variables to set when rendering the template
|
||||
# @return [String] rendered template
|
||||
def render(context)
|
||||
ctx = Context.new(context)
|
||||
@erb.result(ctx.to_binding)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
<% if cls.type == 'object' -%>
|
||||
|
||||
# <%= block_comment(cls.description, 0, 1) %>
|
||||
class <%= cls.generated_class_name %><% if cls.base_ref %> < <%= cls.base_ref.generated_type %><% end %>
|
||||
include Google::Apis::Core::Hashable
|
||||
<% for property in cls.properties.values -%>
|
||||
|
||||
# <%= block_comment(property.description, 2, 1) %>
|
||||
# Corresponds to the JSON property `<%= property.name %>`
|
||||
# @return [<%= property.generated_type %>]
|
||||
attr_accessor :<%= property.generated_name %>
|
||||
<% if property.type == 'boolean' -%>
|
||||
alias_method :<%= property.generated_name %>?, :<%= property.generated_name %>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
|
||||
def initialize(**args)
|
||||
<% if cls.discriminant -%>
|
||||
@<%= cls.properties[cls.discriminant].generated_name %> = '<%= cls.discriminant_value %>'
|
||||
<% end -%>
|
||||
update!(**args)
|
||||
end
|
||||
|
||||
# Update properties of this object
|
||||
def update!(**args)
|
||||
<% for property in cls.properties.values -%>
|
||||
@<%= property.generated_name %> = args[:<%= property.generated_name %>] unless args[:<%= property.generated_name %>].nil?
|
||||
<% end -%>
|
||||
end
|
||||
<% for child_class in cls.properties.values -%>
|
||||
<% if child_class._ref.nil? -%>
|
||||
<%= indent(include('class', :cls => child_class, :api => api), 2) -%>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
end
|
||||
<% elsif cls.items && cls.items._ref.nil? -%>
|
||||
<%= include('class', :cls => cls.items, :api => api) -%>
|
||||
<% elsif cls.additional_properties && cls.additional_properties._ref.nil? -%>
|
||||
<%= include('class', :cls => cls.additional_properties, :api => api) -%>
|
||||
<% end -%>
|
||||
@@ -0,0 +1,90 @@
|
||||
|
||||
# <%= block_comment(api_method.description, 0, 1) %>
|
||||
<% for param in api_method.path_parameters -%>
|
||||
# @param [<% if param.repeated? %>Array<<%= param.generated_type %>>, <% end %><%= param.generated_type %>] <%= param.generated_name %>
|
||||
<% if param.description -%>
|
||||
# <%= block_comment(param.description, 0, 3) %>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<% if api_method.request -%>
|
||||
# @param [<%= api.schemas[api_method.request._ref].generated_type %>] <%= api.schemas[api_method.request._ref].generated_name %>_object
|
||||
<% end -%>
|
||||
<% for param in api_method.query_parameters -%>
|
||||
# @param [<% if param.repeated? %>Array<<%= param.generated_type %>>, <% end %><%= param.generated_type %>] <%= param.generated_name %>
|
||||
<% if param.description -%>
|
||||
# <%= block_comment(param.description, 0, 3) %>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<% for param in api.parameters.values.reject {|p| p.name == 'key'} -%>
|
||||
# @param [<% if param.repeated? %>Array<<%= param.generated_type %>>, <% end %><%= param.generated_type %>] <%= param.generated_name %>
|
||||
<% if param.description -%>
|
||||
# <%= block_comment(param.description, 0, 3) %>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<% if api_method.supports_media_upload? -%>
|
||||
# @param [IO, String] upload_source
|
||||
# IO stream or filename containing content to upload
|
||||
# @param [String] content_type
|
||||
# Content type of the uploaded content.
|
||||
<% elsif api_method.supports_media_download? -%>
|
||||
# @param [IO, String] download_dest
|
||||
# IO stream or filename to receive content download
|
||||
<% end -%>
|
||||
# @param [Google::Apis::RequestOptions] options
|
||||
# Request-specific options
|
||||
#
|
||||
# @yield [result, err] Result & error if block supplied
|
||||
<% if api_method.response -%>
|
||||
# @yieldparam result [<%= api.schemas[api_method.response._ref].generated_type %>] parsed result object
|
||||
# @yieldparam err [StandardError] error object if request failed
|
||||
#
|
||||
# @return [<%= api.schemas[api_method.response._ref].generated_type %>]
|
||||
<% else -%>
|
||||
# @yieldparam result [NilClass] No result returned for this method
|
||||
# @yieldparam err [StandardError] error object if request failed
|
||||
#
|
||||
# @return [void]
|
||||
<% end -%>
|
||||
#
|
||||
# @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 <%= api_method.generated_name %>(<% for param in api_method.path_parameters %><%= param.generated_name %>, <% end %><% if api_method.request %><%= api.schemas[api_method.request._ref].generated_name %>_object = nil, <% end %><% for param in api_method.query_parameters %><%= param.generated_name %>: nil, <% end %><% for param in api.parameters.values.reject {|p| p.name == 'key'} %><%= param.generated_name %>: nil, <% end %><% if api_method.supports_media_upload? %>upload_source: nil, content_type: nil, <% elsif api_method.supports_media_download? %>download_dest: nil, <% end %>options: nil, &block)
|
||||
path = '<%= api_method.path %>'
|
||||
<% if api_method.supports_media_upload? -%>
|
||||
if upload_source.nil?
|
||||
command = make_simple_command(:<%= api_method.http_method.downcase %>, path, options)
|
||||
else
|
||||
command = make_upload_command(:<%= api_method.http_method.downcase %>, path, options)
|
||||
command.upload_source = upload_source
|
||||
command.upload_content_type = content_type
|
||||
end
|
||||
<% elsif api_method.supports_media_download? -%>
|
||||
if download_dest.nil?
|
||||
command = make_simple_command(:<%= api_method.http_method.downcase %>, path, options)
|
||||
else
|
||||
command = make_download_command(:<%= api_method.http_method.downcase %>, path, options)
|
||||
command.download_dest = download_dest
|
||||
end
|
||||
<% else -%>
|
||||
command = make_simple_command(:<%= api_method.http_method.downcase %>, path, options)
|
||||
<% end -%>
|
||||
<% if api_method.request -%>
|
||||
command.request_representation = <%= api.schemas[api_method.request._ref].generated_type %>::Representation
|
||||
command.request_object = <%= api.schemas[api_method.request._ref].generated_name %>_object
|
||||
<% end -%>
|
||||
<% if api_method.response -%>
|
||||
command.response_representation = <%= api.schemas[api_method.response._ref].generated_type %>::Representation
|
||||
command.response_class = <%= api.schemas[api_method.response._ref].generated_type %>
|
||||
<% end -%>
|
||||
<% for param in api_method.path_parameters -%>
|
||||
command.params['<%= param.name %>'] = <%= param.generated_name %> unless <%= param.generated_name %>.nil?
|
||||
<% end -%>
|
||||
<% for param in api_method.query_parameters -%>
|
||||
command.query['<%= param.name %>'] = <%= param.generated_name %> unless <%= param.generated_name %>.nil?
|
||||
<% end -%>
|
||||
<% for param in api.parameters.values.reject {|p| p.name == 'key'} -%>
|
||||
command.query['<%= param.name %>'] = <%= param.generated_name %> unless <%= param.generated_name %>.nil?
|
||||
<% end -%>
|
||||
execute_or_queue_command(command, &block)
|
||||
end
|
||||
@@ -0,0 +1,51 @@
|
||||
<% if cls.type == 'object' -%>
|
||||
|
||||
# @private
|
||||
class <%= cls.generated_class_name %>
|
||||
class Representation < Google::Apis::Core::JsonRepresentation
|
||||
<% if api.features && api.features.include?('dataWrapper') -%>
|
||||
self.representation_wrap = lambda { |args| :data if args[:unwrap] == <%= cls.generated_type %> }
|
||||
<% end -%>
|
||||
<% if cls.variant -%>
|
||||
def from_hash(hash, *args)
|
||||
case hash['<%= cls.variant.discriminant %>']
|
||||
<% for v in cls.variant.map -%>
|
||||
<% ref = api.schemas[v._ref] %>
|
||||
when '<%= v.type_value %>'
|
||||
<%= ref.generated_type %>::Representation.new(<%= ref.generated_type %>.new).from_hash(hash, *args)
|
||||
<% end -%>
|
||||
end
|
||||
end
|
||||
|
||||
def to_hash(*args)
|
||||
case represented
|
||||
<% for v in cls.variant.map -%>
|
||||
<% ref = api.schemas[v._ref] %>
|
||||
when <%= ref.generated_type %>
|
||||
<%= ref.generated_type %>::Representation.new(represented).to_hash(*args)
|
||||
<% end -%>
|
||||
end
|
||||
end
|
||||
<% else -%>
|
||||
<% for property in cls.properties.values -%>
|
||||
<% if property.type == 'hash' -%>
|
||||
hash :<%= property.generated_name %>, as: '<%= property.name %>'<%= include('representation_type', :lead => ', ', :type => property.additional_properties, :api => api) %>
|
||||
<% elsif property.type == 'array' -%>
|
||||
collection :<%= property.generated_name %>, as: '<%= property.name %>'<%= include('representation_type', :lead => ', ', :type => property.items, :api => api) %>
|
||||
<% else -%>
|
||||
property :<%= property.generated_name %>,<% if property.format == 'byte' %> :base64 => true,<%end%> as: '<%= property.name %>'<%= include('representation_type', :lead => ', ', :type => property, :api => api) %>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
end
|
||||
<% for child_class in cls.properties.values -%>
|
||||
<% if child_class._ref.nil? -%>
|
||||
<%= indent(include('representation', :cls => child_class, :api => api), 2) -%>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
end
|
||||
<% elsif cls.items && cls.items._ref.nil? -%>
|
||||
<%= include('representation', :cls => cls.items, :api => api) -%>
|
||||
<% elsif cls.additional_properties && cls.additional_properties._ref.nil? -%>
|
||||
<%= include('representation', :cls => cls.additional_properties, :api => api) -%>
|
||||
<% end -%>
|
||||
@@ -0,0 +1,15 @@
|
||||
<% if cls.type == 'object' -%>
|
||||
|
||||
class <%= cls.generated_class_name %>
|
||||
class Representation < Google::Apis::Core::JsonRepresentation; end
|
||||
<% for child_class in cls.properties.values -%>
|
||||
<% if child_class._ref.nil? -%>
|
||||
<%= indent(include('representation_stub', :cls => child_class), 2) -%>
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
end
|
||||
<% elsif cls.items && cls.items._ref.nil? -%>
|
||||
<%= include('representation_stub', :cls => cls.items, :api => api) -%>
|
||||
<% elsif cls.additional_properties && cls.additional_properties._ref.nil? -%>
|
||||
<%= include('representation_stub', :cls => cls.additional_properties, :api => api) -%>
|
||||
<% end -%>
|
||||
@@ -0,0 +1,10 @@
|
||||
<% if type.type == 'array' -%>
|
||||
<%= lead %>:class => Array do
|
||||
include Representable::JSON::Collection
|
||||
items<%= include('representation_type', :lead => ' ' , :type => type.items, :api => api) %>
|
||||
end
|
||||
<% elsif ['date', 'date-time'].include?(type.format) -%>
|
||||
<%= lead %>type: <%= type.generated_type %>
|
||||
<% elsif type.type == 'object' -%>
|
||||
<%= lead %>class: <%= type.generated_type %>, decorator: <%= type.generated_type %>::Representation
|
||||
<% end -%>
|
||||
@@ -0,0 +1,29 @@
|
||||
# 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 'date'
|
||||
require 'google/apis/core/base_service'
|
||||
require 'google/apis/core/json_representation'
|
||||
require 'google/apis/core/hashable'
|
||||
require 'google/apis/errors'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
module <%= api.module_name %>
|
||||
<% for cls in api.schemas.values.partition(&:variant).flatten -%>
|
||||
<%= indent(include('class', :cls => cls, :api => api), 6) -%>
|
||||
<% end -%>
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
# 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 '<%= to_path(api.qualified_name) %>/service.rb'
|
||||
require '<%= to_path(api.qualified_name) %>/classes.rb'
|
||||
require '<%= to_path(api.qualified_name) %>/representations.rb'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
# <%= api.title %>
|
||||
#
|
||||
# <%= block_comment(api.description, 4, 1) %>
|
||||
#
|
||||
<% if api.documentation_link -%>
|
||||
# @see <%= api.documentation_link %>
|
||||
<% end -%>
|
||||
module <%= api.module_name %>
|
||||
VERSION = '<%= api.version %>'
|
||||
REVISION = '<%= api.revision %>'
|
||||
<% if api.auth && api.auth.oauth2 -%>
|
||||
<% for scope_string, scope in api.auth.oauth2.scopes -%>
|
||||
|
||||
# <%= scope.description %>
|
||||
<%= scope.constant %> = '<%= scope_string %>'
|
||||
<% end -%>
|
||||
<% end -%>
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
# 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 'date'
|
||||
require 'google/apis/core/base_service'
|
||||
require 'google/apis/core/json_representation'
|
||||
require 'google/apis/core/hashable'
|
||||
require 'google/apis/errors'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
module <%= api.module_name %>
|
||||
<% for cls in api.schemas.values.partition(&:variant).flatten -%>
|
||||
<%= indent(include('representation_stub', :cls => cls), 6) -%>
|
||||
<% end -%>
|
||||
<% for cls in api.schemas.values.partition(&:variant).flatten -%>
|
||||
<%= indent(include('representation', :cls => cls, :api => api), 6) -%>
|
||||
<% end -%>
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,60 @@
|
||||
# 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/base_service'
|
||||
require 'google/apis/core/json_representation'
|
||||
require 'google/apis/core/hashable'
|
||||
require 'google/apis/errors'
|
||||
|
||||
module Google
|
||||
module Apis
|
||||
module <%= api.module_name %>
|
||||
# <%= api.title %>
|
||||
#
|
||||
# <%= block_comment(api.description, 6, 2) %>
|
||||
#
|
||||
# @example
|
||||
# require '<%= to_path(api.qualified_name) %>'
|
||||
#
|
||||
# <%= api.name %> = <%= api.qualified_name %> # Alias the module
|
||||
# service = <%= api.name %>::<%= api.service_name %>.new
|
||||
#
|
||||
<% if api.documentation_link -%>
|
||||
# @see <%= api.documentation_link %>
|
||||
<% end -%>
|
||||
class <%= api.service_name %> < Google::Apis::Core::BaseService
|
||||
<% for param in api.parameters.values.reject {|p| p.name == 'fields'} -%>
|
||||
# @return [<%= param.generated_type %>]
|
||||
# <%= block_comment(param.description, 8, 2) %>
|
||||
attr_accessor :<%= param.generated_name %>
|
||||
|
||||
<% end -%>
|
||||
def initialize
|
||||
super('<%= api.root_url %>', '<%= api.service_path %>')
|
||||
end
|
||||
<% for api_method in api.all_methods -%>
|
||||
<%= indent(include('method', :api_method => api_method, :api => api), 8) -%>
|
||||
<% end -%>
|
||||
|
||||
protected
|
||||
|
||||
def apply_command_defaults(command)
|
||||
<% for param in api.parameters.values.reject {|p| p.name == 'fields'} -%>
|
||||
command.query['<%= param.name %>'] = <%= param.generated_name %> unless <%= param.generated_name %>.nil?
|
||||
<% end -%>
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,79 @@
|
||||
# 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)
|
||||
|
||||
RequestOptions = Struct.new(
|
||||
:authorization,
|
||||
:retries,
|
||||
:header,
|
||||
:timeout_sec)
|
||||
|
||||
# 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
|
||||
|
||||
# Get the default options
|
||||
# @return [Google::Apis::ClientOptions]
|
||||
def self.default
|
||||
@options ||= ClientOptions.new
|
||||
end
|
||||
end
|
||||
|
||||
# Request options
|
||||
class RequestOptions
|
||||
# @!attribute [rw] credentials
|
||||
# @return [Signet::OAuth2::Client, #apply(Hash)] OAuth2 credentials
|
||||
# @!attribute [rw] retries
|
||||
# @return [Fixnum] Number of times to retry requests on server error
|
||||
# @!attribute [rw] timeout_sec
|
||||
# @return [Fixnum] How long, in seconds, before requests time out
|
||||
# @!attribute [rw] header
|
||||
# @return [Hash<String,String] Additional HTTP headers 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
|
||||
%w(authorization retries timeout_sec).each do |opt|
|
||||
opt = opt.to_sym
|
||||
new_options[opt] = options[opt] unless options[opt].nil?
|
||||
end
|
||||
new_options
|
||||
end
|
||||
end
|
||||
|
||||
ClientOptions.default.application_name = 'unknown'
|
||||
ClientOptions.default.application_version = '0.0.0'
|
||||
|
||||
RequestOptions.default.retries = 0
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,39 @@
|
||||
# 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
|
||||
# Client library version
|
||||
VERSION = '0.9.pre1'
|
||||
|
||||
# Current operating system
|
||||
# @private
|
||||
OS_VERSION = begin
|
||||
if RUBY_PLATFORM =~ /mswin|win32|mingw|bccwin|cygwin/
|
||||
`ver`.sub(/\s*\[Version\s*/, '/').sub(']', '').strip
|
||||
elsif RUBY_PLATFORM =~ /darwin/i
|
||||
"Mac OS X/#{`sw_vers -productVersion`}"
|
||||
elsif RUBY_PLATFORM == 'java'
|
||||
require 'java'
|
||||
name = java.lang.System.getProperty('os.name')
|
||||
version = java.lang.System.getProperty('os.version')
|
||||
"#{name}/#{version}"
|
||||
else
|
||||
`uname -sr`.sub(' ', '/')
|
||||
end
|
||||
rescue
|
||||
RUBY_PLATFORM
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in new issue
Block a user