Import refactored library. See MIGRATING.MD for details

This commit is contained in:
Steven Bazyl committed 2015-06-23 15:15:14 -07:00
1 parent 9b7809174d
commit f584a2cec8
373 files changed
+255715 -474

No files matched your search

+126
View File
@@ -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
+102
View File
@@ -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
+179
View File
@@ -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
+48
View File
@@ -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
+128
View File
@@ -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
+314
View File
@@ -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
+222
View File
@@ -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
+94
View File
@@ -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
+44
View File
@@ -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
+275
View File
@@ -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
+122
View File
@@ -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
+30
View File
@@ -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
+173
View File
@@ -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
+275
View File
@@ -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
+69
View File
@@ -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
+70
View File
@@ -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
+271
View File
@@ -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
+74
View File
@@ -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
+130
View File
@@ -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
+124
View File
@@ -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
+79
View File
@@ -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
+39
View File
@@ -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