Added better handling for errors.
[arvados.git] / lib / google / api_client.rb
1 # Copyright 2010 Google Inc.
2 #
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
6 #
7 #      http://www.apache.org/licenses/LICENSE-2.0
8 #
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.
14
15
16 require 'httpadapter'
17 require 'json'
18 require 'stringio'
19
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'
25
26 module Google
27   # TODO(bobaman): Document all this stuff.
28
29
30   ##
31   # This class manages APIs communication.
32   class APIClient
33     ##
34     # Creates a new Google API client.
35     #
36     # @param [Hash] options The configuration parameters for the client.
37     # @option options [Symbol, #generate_authenticated_request] :authorization
38     #   (:oauth_1)
39     #   The authorization mechanism used by the client.  The following
40     #   mechanisms are supported out-of-the-box:
41     #   <ul>
42     #     <li><code>:two_legged_oauth_1</code></li>
43     #     <li><code>:oauth_1</code></li>
44     #     <li><code>:oauth_2</code></li>
45     #   </ul>
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
59         accu
60       end
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 +
68         ' ' + ENV::OS_VERSION
69       ).strip
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
77       # is trivial.
78       if options["http_adapter"]
79         self.http_adapter = options["http_adapter"]
80       else
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
84       end
85       @discovery_uris = {}
86       @discovery_documents = {}
87       @discovered_apis = {}
88       return self
89     end
90
91     ##
92     # Returns the authorization mechanism used by the client.
93     #
94     # @return [#generate_authenticated_request] The authorization mechanism.
95     attr_reader :authorization
96
97     ##
98     # Sets the authorization mechanism used by the client.
99     #
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'
116         )
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,
123           :two_legged => true
124         )
125       when :oauth_2
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'
133         )
134       when nil
135         # No authorization mechanism
136       else
137         if !new_authorization.respond_to?(:generate_authenticated_request)
138           raise TypeError,
139             'Expected authorization mechanism to respond to ' +
140             '#generate_authenticated_request.'
141         end
142       end
143       @authorization = new_authorization
144       return @authorization
145     end
146
147     ##
148     # The application's API key issued by the API console.
149     #
150     # @return [String] The API key..
151     attr_accessor :key
152
153     ##
154     # The IP address of the user this request is being performed on behalf of.
155     #
156     # @return [String] The user's IP address.
157     attr_accessor :user_ip
158
159     ##
160     # Returns the HTTP adapter used by the client.
161     #
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
166
167     ##
168     # Returns the HTTP adapter used by the client.
169     #
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
176       else
177         raise TypeError, "Expected HTTPAdapter, got #{new_http_adapter.class}."
178       end
179     end
180
181     ##
182     # The API hostname used by the client.
183     #
184     # @return [String]
185     #   The API hostname.  Should almost always be 'www.googleapis.com'.
186     attr_accessor :host
187
188     ##
189     # The user agent used by the client.
190     #
191     # @return [String]
192     #   The user agent string used in the User-Agent header.
193     attr_accessor :user_agent
194
195     ##
196     # Returns the URI for the directory document.
197     #
198     # @return [Addressable::URI] The URI of the directory document.
199     def directory_uri
200       template = Addressable::Template.new(
201         "https://{host}/discovery/v1/apis"
202       )
203       return template.expand({"host" => self.host})
204     end
205
206     ##
207     # Manually registers a URI as a discovery document for a specific version
208     # of an API.
209     #
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)
214       api = api.to_s
215       version = version || 'v1'
216       @discovery_uris["#{api}:#{version}"] = uri
217     end
218
219     ##
220     # Returns the URI for the discovery document.
221     #
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)
226       api = api.to_s
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"
232         )
233         template.expand({
234           "host" => self.host,
235           "api" => api,
236           "version" => version
237         })
238       end)
239     end
240
241     ##
242     # Manually registers a pre-loaded discovery document for a specific version
243     # of an API.
244     #
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)
250       api = api.to_s
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
257       else
258         raise TypeError,
259           "Expected String or StringIO, got #{discovery_document.class}."
260       end
261       @discovery_documents["#{api}:#{version}"] =
262         ::JSON.parse(discovery_document)
263     end
264
265     ##
266     # Returns the parsed directory document.
267     #
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
275         )
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|
281             accu.write(chunk)
282             accu
283           end
284           ::JSON.parse(merged_body.string)
285         elsif status >= 400 && status < 500
286           _, request_uri, _, _ = request
287           raise ClientError,
288             "Could not retrieve discovery document at: #{request_uri}"
289         elsif status >= 500 && status < 600
290           _, request_uri, _, _ = request
291           raise ServerError,
292             "Could not retrieve discovery document at: #{request_uri}"
293         elsif status > 600
294           _, request_uri, _, _ = request
295           raise TransmissionError,
296             "Could not retrieve discovery document at: #{request_uri}"
297         end
298       end)
299     end
300
301     ##
302     # Returns the parsed discovery document.
303     #
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)
308       api = api.to_s
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
315         )
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|
321             accu.write(chunk)
322             accu
323           end
324           ::JSON.parse(merged_body.string)
325         elsif status >= 400 && status < 500
326           _, request_uri, _, _ = request
327           raise ClientError,
328             "Could not retrieve discovery document at: #{request_uri}"
329         elsif status >= 500 && status < 600
330           _, request_uri, _, _ = request
331           raise ServerError,
332             "Could not retrieve discovery document at: #{request_uri}"
333         elsif status > 600
334           _, request_uri, _, _ = request
335           raise TransmissionError,
336             "Could not retrieve discovery document at: #{request_uri}"
337         end
338       end)
339     end
340
341     ##
342     # Returns all APIs published in the directory document.
343     #
344     # @return [Array] The list of available APIs.
345     def discovered_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(
351               document_base,
352               discovery_document
353             )
354           end
355         else
356           []
357         end
358       end)
359     end
360
361     ##
362     # Returns the service object for a given service name and service version.
363     #
364     # @param [String, Symbol] api The service name.
365     # @param [String] version The desired version of the service.
366     #
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)
370         raise TypeError,
371           "Expected String or Symbol, got #{api.class}."
372       end
373       api = api.to_s
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(
380             document_base,
381             discovery_document
382           )
383         else
384           nil
385         end
386       end
387     end
388
389     ##
390     # Returns the method object for a given RPC name and service version.
391     #
392     # @param [String, Symbol] rpc_name The RPC name of the desired method.
393     # @param [String] version The desired version of the service.
394     #
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)
398         raise TypeError,
399           "Expected String or Symbol, got #{rpc_name.class}."
400       end
401       rpc_name = rpc_name.to_s
402       api = api.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]
407       else
408         return nil
409       end
410     end
411
412     ##
413     # Returns the service object with the highest version number.
414     #
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.
418     #
419     # @param [String, Symbol] api The name of the service.
420     #
421     # @return [Google::APIClient::API] The service object.
422     def preferred_version(api)
423       if !api.kind_of?(String) && !api.kind_of?(Symbol)
424         raise TypeError,
425           "Expected String or Symbol, got #{api.class}."
426       end
427       api = api.to_s
428       # TODO(bobaman): Update to use directory API.
429       return self.discovered_apis.detect do |a|
430         a.name == api && a.preferred == true
431       end
432     end
433
434     ##
435     # Generates a request.
436     #
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.
456     #
457     # @return [Array] The generated request.
458     #
459     # @example
460     #   request = client.generate_request(
461     #     :api_method => 'chili.activities.list',
462     #     :parameters =>
463     #       {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
464     #   )
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.
469       options={
470         :version => 'v1',
471         :authorization => self.authorization,
472         :key => self.key,
473         :user_ip => self.user_ip
474       }.merge(options)
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)
481       else
482         options = {:authenticated => false}.merge(options)
483       end
484       reference = Google::APIClient::Reference.new(options)
485       request = reference.to_request
486       if options[:authenticated]
487         request = self.generate_authenticated_request(:request => request)
488       end
489       return request
490     end
491
492     ##
493     # Signs a request using the current authorization mechanism.
494     #
495     # @param [Hash] options The options to pass through.
496     #
497     # @return [Array] The signed or otherwise authenticated request.
498     def generate_authenticated_request(options={})
499       return authorization.generate_authenticated_request(options)
500     end
501
502     ##
503     # Transmits the request using the current HTTP adapter.
504     #
505     # @param [Array] request The request to transmit.
506     # @param [#transmit] adapter The HTTP adapter.
507     #
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
515           # the #each method.
516           class <<headers
517             include Enumerable
518           end
519         end
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])
523           end
524         elsif self.user_agent != nil
525           raise TypeError,
526             "Expected User-Agent to be String, got #{self.user_agent.class}"
527         end
528       end
529       adapter.transmit([method, uri, headers, body])
530     end
531
532     ##
533     # Executes a request, wrapping it in a Result object.
534     #
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> — 
547     #     The HTTP adapter.
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>.
555     #
556     # @return [Array] The response from the API.
557     #
558     # @example
559     #   request = client.generate_request(
560     #     :api_method => 'chili.activities.list',
561     #     :parameters =>
562     #       {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
563     #   )
564     def execute(*params)
565       # This block of code allows us to accept multiple parameter passing
566       # styles, and maintaining some backwards compatibility.
567       #
568       # Note: I'm extremely tempted to deprecate this style of execute call.
569       if params.last.respond_to?(:to_hash) && params.size == 1
570         options = params.pop
571       else
572         options = {}
573       end
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
579
580       reference = Google::APIClient::Reference.new(options)
581       request = self.generate_request(reference)
582       response = self.transmit(
583         request,
584         options[:adapter] || self.http_adapter
585       )
586       return Google::APIClient::Result.new(reference, request, response)
587     end
588
589     ##
590     # Same as Google::APIClient#execute, but raises an exception if there was
591     # an error.
592     #
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']
604       end
605       if status >= 400 && status < 500
606         raise ClientError,
607           error_message || "A client error has occurred."
608       elsif status >= 500 && status < 600
609         raise ServerError,
610           error_message || "A server error has occurred."
611       elsif status > 600
612         raise TransmissionError,
613           error_message || "A transmission error has occurred."
614       end
615       return result
616     end
617   end
618 end
619
620 require 'google/api_client/version'