1 # Copyright 2010 Google Inc.
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
7 # http://www.apache.org/licenses/LICENSE-2.0
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
20 require 'google/api_client/errors'
21 require 'google/api_client/environment'
22 require 'google/api_client/discovery'
23 require 'google/api_client/reference'
24 require 'google/api_client/result'
27 # TODO(bobaman): Document all this stuff.
31 # This class manages APIs communication.
34 # Creates a new Google API client.
36 # @param [Hash] options The configuration parameters for the client.
37 # @option options [Symbol, #generate_authenticated_request] :authorization
39 # The authorization mechanism used by the client. The following
40 # mechanisms are supported out-of-the-box:
42 # <li><code>:two_legged_oauth_1</code></li>
43 # <li><code>:oauth_1</code></li>
44 # <li><code>:oauth_2</code></li>
46 # @option options [String] :host ("www.googleapis.com")
47 # The API hostname used by the client. This rarely needs to be changed.
48 # @option options [String] :application_name
49 # The name and version of the application using the client. This should
50 # be given in the form `"{name}/{version}"`.
51 # @option options [String] :user_agent
52 # ("{app_name} google-api-ruby-client/{version} {os_name}/{os_version}")
53 # The user agent used by the client. Most developers will want to
54 # leave this value alone and use the `:application_name` option instead.
55 def initialize(options={})
56 # Normalize key to String to allow indifferent access.
57 options = options.inject({}) do |accu, (key, value)|
58 accu[key.to_s] = value
61 # Almost all API usage will have a host of 'www.googleapis.com'.
62 self.host = options["host"] || 'www.googleapis.com'
63 # Most developers will want to leave this value alone and use the
64 # application_name option.
65 self.user_agent = options["user_agent"] || (
66 (options["application_name"] || '')
67 'google-api-ruby-client/' + VERSION::STRING +
70 # The writer method understands a few Symbols and will generate useful
71 # default authentication mechanisms.
72 self.authorization = options["authorization"] || :oauth_2
73 self.key = options["key"]
74 self.user_ip = options["user_ip"]
75 # The HTTP adapter controls all of the HTTP traffic the client generates.
76 # By default, Net::HTTP is used, but adding support for other clients
78 if options["http_adapter"]
79 self.http_adapter = options["http_adapter"]
81 require 'httpadapter/adapters/net_http'
82 # NOTE: Do not rely on this default value, as it may change
83 self.http_adapter = HTTPAdapter::NetHTTPAdapter.new
86 @discovery_documents = {}
92 # Returns the authorization mechanism used by the client.
94 # @return [#generate_authenticated_request] The authorization mechanism.
95 attr_reader :authorization
98 # Sets the authorization mechanism used by the client.
100 # @param [#generate_authenticated_request] new_authorization
101 # The new authorization mechanism.
102 def authorization=(new_authorization)
103 case new_authorization
104 when :oauth_1, :oauth
105 require 'signet/oauth_1/client'
106 # NOTE: Do not rely on this default value, as it may change
107 new_authorization = Signet::OAuth1::Client.new(
108 :temporary_credential_uri =>
109 'https://www.google.com/accounts/OAuthGetRequestToken',
110 :authorization_uri =>
111 'https://www.google.com/accounts/OAuthAuthorizeToken',
112 :token_credential_uri =>
113 'https://www.google.com/accounts/OAuthGetAccessToken',
114 :client_credential_key => 'anonymous',
115 :client_credential_secret => 'anonymous'
117 when :two_legged_oauth_1, :two_legged_oauth
118 require 'signet/oauth_1/client'
119 # NOTE: Do not rely on this default value, as it may change
120 new_authorization = Signet::OAuth1::Client.new(
121 :client_credential_key => nil,
122 :client_credential_secret => nil,
126 require 'signet/oauth_2/client'
127 # NOTE: Do not rely on this default value, as it may change
128 new_authorization = Signet::OAuth2::Client.new(
129 :authorization_uri =>
130 'https://accounts.google.com/o/oauth2/auth',
131 :token_credential_uri =>
132 'https://accounts.google.com/o/oauth2/token'
135 # No authorization mechanism
137 if !new_authorization.respond_to?(:generate_authenticated_request)
139 'Expected authorization mechanism to respond to ' +
140 '#generate_authenticated_request.'
143 @authorization = new_authorization
144 return @authorization
148 # The application's API key issued by the API console.
150 # @return [String] The API key..
154 # The IP address of the user this request is being performed on behalf of.
156 # @return [String] The user's IP address.
157 attr_accessor :user_ip
160 # Returns the HTTP adapter used by the client.
162 # @return [HTTPAdapter]
163 # The HTTP adapter object. The object must include the
164 # HTTPAdapter module and conform to its interface.
165 attr_reader :http_adapter
168 # Returns the HTTP adapter used by the client.
170 # @return [HTTPAdapter]
171 # The HTTP adapter object. The object must include the
172 # HTTPAdapter module and conform to its interface.
173 def http_adapter=(new_http_adapter)
174 if new_http_adapter.kind_of?(HTTPAdapter)
175 @http_adapter = new_http_adapter
177 raise TypeError, "Expected HTTPAdapter, got #{new_http_adapter.class}."
182 # The API hostname used by the client.
185 # The API hostname. Should almost always be 'www.googleapis.com'.
189 # The user agent used by the client.
192 # The user agent string used in the User-Agent header.
193 attr_accessor :user_agent
196 # Returns the URI for the directory document.
198 # @return [Addressable::URI] The URI of the directory document.
200 template = Addressable::Template.new(
201 "https://{host}/discovery/v1/apis"
203 return template.expand({"host" => self.host})
207 # Manually registers a URI as a discovery document for a specific version
210 # @param [String, Symbol] api The service name.
211 # @param [String] version The desired version of the service.
212 # @param [Addressable::URI] uri The URI of the discovery document.
213 def register_discovery_uri(api, version, uri)
215 version = version || 'v1'
216 @discovery_uris["#{api}:#{version}"] = uri
220 # Returns the URI for the discovery document.
222 # @param [String, Symbol] api The service name.
223 # @param [String] version The desired version of the service.
224 # @return [Addressable::URI] The URI of the discovery document.
225 def discovery_uri(api, version=nil)
227 version = version || 'v1'
228 return @discovery_uris["#{api}:#{version}"] ||= (begin
229 template = Addressable::Template.new(
230 "https://{host}/discovery/v1/apis/" +
231 "{api}/{version}/rest"
242 # Manually registers a pre-loaded discovery document for a specific version
245 # @param [String, Symbol] api The service name.
246 # @param [String] version The desired version of the service.
247 # @param [String, StringIO] discovery_document
248 # The contents of the discovery document.
249 def register_discovery_document(api, version, discovery_document)
251 version = version || 'v1'
252 if discovery_document.kind_of?(StringIO)
253 discovery_document.rewind
254 discovery_document = discovery_document.string
255 elsif discovery_document.respond_to?(:to_str)
256 discovery_document = discovery_document.to_str
259 "Expected String or StringIO, got #{discovery_document.class}."
261 @discovery_documents["#{api}:#{version}"] =
262 ::JSON.parse(discovery_document)
266 # Returns the parsed directory document.
268 # @return [Hash] The parsed JSON from the directory document.
269 def directory_document
270 return @directory_document ||= (begin
271 request = self.generate_request(
272 :http_method => 'GET',
273 :uri => self.directory_uri,
274 :authenticated => false
276 response = self.transmit(request)
277 status, headers, body = response
278 if status >= 200 && status < 300
279 # TODO(bobaman) Better status code handling?
280 merged_body = body.inject(StringIO.new) do |accu, chunk|
284 ::JSON.parse(merged_body.string)
285 elsif status >= 400 && status < 500
286 _, request_uri, _, _ = request
288 "Could not retrieve discovery document at: #{request_uri}"
289 elsif status >= 500 && status < 600
290 _, request_uri, _, _ = request
292 "Could not retrieve discovery document at: #{request_uri}"
294 _, request_uri, _, _ = request
295 raise TransmissionError,
296 "Could not retrieve discovery document at: #{request_uri}"
302 # Returns the parsed discovery document.
304 # @param [String, Symbol] api The service name.
305 # @param [String] version The desired version of the service.
306 # @return [Hash] The parsed JSON from the discovery document.
307 def discovery_document(api, version=nil)
309 version = version || 'v1'
310 return @discovery_documents["#{api}:#{version}"] ||= (begin
311 request = self.generate_request(
312 :http_method => 'GET',
313 :uri => self.discovery_uri(api, version),
314 :authenticated => false
316 response = self.transmit(request)
317 status, headers, body = response
318 if status >= 200 && status < 300
319 # TODO(bobaman) Better status code handling?
320 merged_body = body.inject(StringIO.new) do |accu, chunk|
324 ::JSON.parse(merged_body.string)
325 elsif status >= 400 && status < 500
326 _, request_uri, _, _ = request
328 "Could not retrieve discovery document at: #{request_uri}"
329 elsif status >= 500 && status < 600
330 _, request_uri, _, _ = request
332 "Could not retrieve discovery document at: #{request_uri}"
334 _, request_uri, _, _ = request
335 raise TransmissionError,
336 "Could not retrieve discovery document at: #{request_uri}"
342 # Returns all APIs published in the directory document.
344 # @return [Array] The list of available APIs.
346 @directory_apis ||= (begin
347 document_base = self.directory_uri
348 if self.directory_document && self.directory_document['items']
349 self.directory_document['items'].map do |discovery_document|
350 Google::APIClient::API.new(
362 # Returns the service object for a given service name and service version.
364 # @param [String, Symbol] api The service name.
365 # @param [String] version The desired version of the service.
367 # @return [Google::APIClient::API] The service object.
368 def discovered_api(api, version=nil)
369 if !api.kind_of?(String) && !api.kind_of?(Symbol)
371 "Expected String or Symbol, got #{api.class}."
374 version = version || 'v1'
375 return @discovered_apis["#{api}:#{version}"] ||= begin
376 document_base = self.discovery_uri(api, version)
377 discovery_document = self.discovery_document(api, version)
378 if document_base && discovery_document
379 Google::APIClient::API.new(
390 # Returns the method object for a given RPC name and service version.
392 # @param [String, Symbol] rpc_name The RPC name of the desired method.
393 # @param [String] version The desired version of the service.
395 # @return [Google::APIClient::Method] The method object.
396 def discovered_method(rpc_name, api, version=nil)
397 if !rpc_name.kind_of?(String) && !rpc_name.kind_of?(Symbol)
399 "Expected String or Symbol, got #{rpc_name.class}."
401 rpc_name = rpc_name.to_s
403 version = version || 'v1'
404 service = self.discovered_api(api, version)
405 if service.to_h[rpc_name]
406 return service.to_h[rpc_name]
413 # Returns the service object with the highest version number.
415 # @note <em>Warning</em>: This method should be used with great care.
416 # As APIs are updated, minor differences between versions may cause
417 # incompatibilities. Requesting a specific version will avoid this issue.
419 # @param [String, Symbol] api The name of the service.
421 # @return [Google::APIClient::API] The service object.
422 def preferred_version(api)
423 if !api.kind_of?(String) && !api.kind_of?(Symbol)
425 "Expected String or Symbol, got #{api.class}."
428 # TODO(bobaman): Update to use directory API.
429 return self.discovered_apis.detect do |a|
430 a.name == api && a.preferred == true
435 # Generates a request.
437 # @param [Google::APIClient::Method, String] api_method
438 # The method object or the RPC name of the method being executed.
439 # @param [Hash, Array] parameters
440 # The parameters to send to the method.
441 # @param [String] body The body of the request.
442 # @param [Hash, Array] headers The HTTP headers for the request.
443 # @param [Hash] options
444 # The configuration parameters for the request.
445 # - <code>:version</code> —
446 # The service version. Only used if <code>api_method</code> is a
447 # <code>String</code>. Defaults to <code>'v1'</code>.
448 # - <code>:authorization</code> —
449 # The authorization mechanism for the response. Used only if
450 # <code>:authenticated</code> is <code>true</code>.
451 # - <code>:authenticated</code> —
452 # <code>true</code> if the request must be signed or otherwise
453 # authenticated, <code>false</code>
454 # otherwise. Defaults to <code>true</code> if an authorization
455 # mechanism has been set, <code>false</code> otherwise.
457 # @return [Array] The generated request.
460 # request = client.generate_request(
461 # :api_method => 'chili.activities.list',
463 # {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
465 # method, uri, headers, body = request
466 def generate_request(options={})
467 # Note: The merge method on a Hash object will coerce an API Reference
468 # object into a Hash and merge with the default options.
471 :authorization => self.authorization,
473 :user_ip => self.user_ip
475 # The Reference object is going to need this to do method ID lookups.
476 options[:client] = self
477 # The default value for the :authenticated option depends on whether an
478 # authorization mechanism has been set.
479 if options[:authorization]
480 options = {:authenticated => true}.merge(options)
482 options = {:authenticated => false}.merge(options)
484 reference = Google::APIClient::Reference.new(options)
485 request = reference.to_request
486 if options[:authenticated]
487 request = self.generate_authenticated_request(:request => request)
493 # Signs a request using the current authorization mechanism.
495 # @param [Hash] options The options to pass through.
497 # @return [Array] The signed or otherwise authenticated request.
498 def generate_authenticated_request(options={})
499 return authorization.generate_authenticated_request(options)
503 # Transmits the request using the current HTTP adapter.
505 # @param [Array] request The request to transmit.
506 # @param [#transmit] adapter The HTTP adapter.
508 # @return [Array] The response from the server.
509 def transmit(request, adapter=self.http_adapter)
510 if self.user_agent != nil
511 # If there's no User-Agent header, set one.
512 method, uri, headers, body = request
513 unless headers.kind_of?(Enumerable)
514 # We need to use some Enumerable methods, relying on the presence of
520 if self.user_agent.kind_of?(String)
521 unless headers.any? { |k, v| k.downcase == 'User-Agent'.downcase }
522 headers = headers.to_a.insert(0, ['User-Agent', self.user_agent])
524 elsif self.user_agent != nil
526 "Expected User-Agent to be String, got #{self.user_agent.class}"
529 adapter.transmit([method, uri, headers, body])
533 # Executes a request, wrapping it in a Result object.
535 # @param [Google::APIClient::Method, String] api_method
536 # The method object or the RPC name of the method being executed.
537 # @param [Hash, Array] parameters
538 # The parameters to send to the method.
539 # @param [String] body The body of the request.
540 # @param [Hash, Array] headers The HTTP headers for the request.
541 # @param [Hash] options
542 # The configuration parameters for the request.
543 # - <code>:version</code> —
544 # The service version. Only used if <code>api_method</code> is a
545 # <code>String</code>. Defaults to <code>'v1'</code>.
546 # - <code>:adapter</code> —
548 # - <code>:authorization</code> —
549 # The authorization mechanism for the response. Used only if
550 # <code>:authenticated</code> is <code>true</code>.
551 # - <code>:authenticated</code> —
552 # <code>true</code> if the request must be signed or otherwise
553 # authenticated, <code>false</code>
554 # otherwise. Defaults to <code>true</code>.
556 # @return [Array] The response from the API.
559 # request = client.generate_request(
560 # :api_method => 'chili.activities.list',
562 # {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
565 # This block of code allows us to accept multiple parameter passing
566 # styles, and maintaining some backwards compatibility.
568 # Note: I'm extremely tempted to deprecate this style of execute call.
569 if params.last.respond_to?(:to_hash) && params.size == 1
574 options[:api_method] = params.shift if params.size > 0
575 options[:parameters] = params.shift if params.size > 0
576 options[:merged_body] = params.shift if params.size > 0
577 options[:headers] = params.shift if params.size > 0
578 options[:client] = self
580 reference = Google::APIClient::Reference.new(options)
581 request = self.generate_request(reference)
582 response = self.transmit(
584 options[:adapter] || self.http_adapter
586 return Google::APIClient::Result.new(reference, request, response)
590 # Same as Google::APIClient#execute, but raises an exception if there was
593 # @see Google::APIClient#execute
594 def execute!(*params)
595 result = self.execute(*params)
596 status, _, _ = result.response
597 if result.data.respond_to?(:error) &&
598 result.data.error.respond_to?(:message)
599 # You're going to get a terrible error message if the response isn't
600 # parsed successfully as an error.
601 error_message = result.data.error.message
602 elsif result.data['error'] && result.data['error']['message']
603 error_message = result.data['error']['message']
605 if status >= 400 && status < 500
607 error_message || "A client error has occurred."
608 elsif status >= 500 && status < 600
610 error_message || "A server error has occurred."
612 raise TransmissionError,
613 error_message || "A transmission error has occurred."
620 require 'google/api_client/version'