Merge pull request #3 from robertkaplow/master
[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       # The HTTP adapter controls all of the HTTP traffic the client generates.
74       # By default, Net::HTTP is used, but adding support for other clients
75       # is trivial.
76       if options["http_adapter"]
77         self.http_adapter = options["http_adapter"]
78       else
79         require 'httpadapter/adapters/net_http'
80         # NOTE: Do not rely on this default value, as it may change
81         self.http_adapter = HTTPAdapter::NetHTTPAdapter.new
82       end
83       @discovery_uris = {}
84       @discovery_documents = {}
85       @discovered_apis = {}
86       return self
87     end
88
89     ##
90     # Returns the authorization mechanism used by the client.
91     #
92     # @return [#generate_authenticated_request] The authorization mechanism.
93     attr_reader :authorization
94
95     ##
96     # Sets the authorization mechanism used by the client.
97     #
98     # @param [#generate_authenticated_request] new_authorization
99     #   The new authorization mechanism.
100     def authorization=(new_authorization)
101       case new_authorization
102       when :oauth_1, :oauth
103         require 'signet/oauth_1/client'
104         # NOTE: Do not rely on this default value, as it may change
105         new_authorization = Signet::OAuth1::Client.new(
106           :temporary_credential_uri =>
107             'https://www.google.com/accounts/OAuthGetRequestToken',
108           :authorization_uri =>
109             'https://www.google.com/accounts/OAuthAuthorizeToken',
110           :token_credential_uri =>
111             'https://www.google.com/accounts/OAuthGetAccessToken',
112           :client_credential_key => 'anonymous',
113           :client_credential_secret => 'anonymous'
114         )
115       when :two_legged_oauth_1, :two_legged_oauth
116         require 'signet/oauth_1/client'
117         # NOTE: Do not rely on this default value, as it may change
118         new_authorization = Signet::OAuth1::Client.new(
119           :client_credential_key => nil,
120           :client_credential_secret => nil,
121           :two_legged => true
122         )
123       when :oauth_2
124         require 'signet/oauth_2/client'
125         # NOTE: Do not rely on this default value, as it may change
126         new_authorization = Signet::OAuth2::Client.new(
127           :authorization_uri =>
128             'https://accounts.google.com/o/oauth2/auth',
129           :token_credential_uri =>
130             'https://accounts.google.com/o/oauth2/token'
131         )
132       when nil
133         # No authorization mechanism
134       else
135         if !new_authorization.respond_to?(:generate_authenticated_request)
136           raise TypeError,
137             'Expected authorization mechanism to respond to ' +
138             '#generate_authenticated_request.'
139         end
140       end
141       @authorization = new_authorization
142       return @authorization
143     end
144
145     ##
146     # Returns the HTTP adapter used by the client.
147     #
148     # @return [HTTPAdapter]
149     #   The HTTP adapter object.  The object must include the
150     #   HTTPAdapter module and conform to its interface.
151     attr_reader :http_adapter
152
153     ##
154     # Returns the HTTP adapter used by the client.
155     #
156     # @return [HTTPAdapter]
157     #   The HTTP adapter object.  The object must include the
158     #   HTTPAdapter module and conform to its interface.
159     def http_adapter=(new_http_adapter)
160       if new_http_adapter.kind_of?(HTTPAdapter)
161         @http_adapter = new_http_adapter
162       else
163         raise TypeError, "Expected HTTPAdapter, got #{new_http_adapter.class}."
164       end
165     end
166
167     ##
168     # The API hostname used by the client.
169     #
170     # @return [String]
171     #   The API hostname.  Should almost always be 'www.googleapis.com'.
172     attr_accessor :host
173
174     ##
175     # The user agent used by the client.
176     #
177     # @return [String]
178     #   The user agent string used in the User-Agent header.
179     attr_accessor :user_agent
180
181     ##
182     # Returns the URI for the directory document.
183     #
184     # @return [Addressable::URI] The URI of the directory document.
185     def directory_uri
186       template = Addressable::Template.new(
187         "https://{host}/discovery/v1/apis"
188       )
189       return template.expand({"host" => self.host})
190     end
191
192     ##
193     # Manually registers a URI as a discovery document for a specific version
194     # of an API.
195     #
196     # @param [String, Symbol] api The service name.
197     # @param [String] version The desired version of the service.
198     # @param [Addressable::URI] uri The URI of the discovery document.
199     def register_discovery_uri(api, version, uri)
200       api = api.to_s
201       version = version || 'v1'
202       @discovery_uris["#{api}:#{version}"] = uri
203     end
204
205     ##
206     # Returns the URI for the discovery document.
207     #
208     # @param [String, Symbol] api The service name.
209     # @param [String] version The desired version of the service.
210     # @return [Addressable::URI] The URI of the discovery document.
211     def discovery_uri(api, version=nil)
212       api = api.to_s
213       version = version || 'v1'
214       return @discovery_uris["#{api}:#{version}"] ||= (begin
215         template = Addressable::Template.new(
216           "https://{host}/discovery/v1/apis/" +
217           "{api}/{version}/rest"
218         )
219         template.expand({
220           "host" => self.host,
221           "api" => api,
222           "version" => version
223         })
224       end)
225     end
226
227     ##
228     # Manually registers a pre-loaded discovery document for a specific version
229     # of an API.
230     #
231     # @param [String, Symbol] api The service name.
232     # @param [String] version The desired version of the service.
233     # @param [String, StringIO] discovery_document
234     #   The contents of the discovery document.
235     def register_discovery_document(api, version, discovery_document)
236       api = api.to_s
237       version = version || 'v1'
238       if discovery_document.kind_of?(StringIO)
239         discovery_document.rewind
240         discovery_document = discovery_document.string
241       elsif discovery_document.respond_to?(:to_str)
242         discovery_document = discovery_document.to_str
243       else
244         raise TypeError,
245           "Expected String or StringIO, got #{discovery_document.class}."
246       end
247       @discovery_documents["#{api}:#{version}"] =
248         ::JSON.parse(discovery_document)
249     end
250
251     ##
252     # Returns the parsed directory document.
253     #
254     # @return [Hash] The parsed JSON from the directory document.
255     def directory_document
256       return @directory_document ||= (begin
257         request_uri = self.directory_uri
258         request = ['GET', request_uri, [], []]
259         response = self.transmit(request)
260         status, headers, body = response
261         if status == 200 # TODO(bobaman) Better status code handling?
262           merged_body = body.inject(StringIO.new) do |accu, chunk|
263             accu.write(chunk)
264             accu
265           end
266           ::JSON.parse(merged_body.string)
267         elsif status >= 400 && status < 500
268           raise ClientError,
269             "Could not retrieve discovery document at: #{request_uri}"
270         elsif status >= 500 && status < 600
271           raise ServerError,
272             "Could not retrieve discovery document at: #{request_uri}"
273         elsif status > 600
274           raise TransmissionError,
275             "Could not retrieve discovery document at: #{request_uri}"
276         end
277       end)
278     end
279
280     ##
281     # Returns the parsed discovery document.
282     #
283     # @param [String, Symbol] api The service name.
284     # @param [String] version The desired version of the service.
285     # @return [Hash] The parsed JSON from the discovery document.
286     def discovery_document(api, version=nil)
287       api = api.to_s
288       version = version || 'v1'
289       return @discovery_documents["#{api}:#{version}"] ||= (begin
290         request_uri = self.discovery_uri(api, version)
291         request = ['GET', request_uri, [], []]
292         response = self.transmit(request)
293         status, headers, body = response
294         if status == 200 # TODO(bobaman) Better status code handling?
295           merged_body = body.inject(StringIO.new) do |accu, chunk|
296             accu.write(chunk)
297             accu
298           end
299           ::JSON.parse(merged_body.string)
300         elsif status >= 400 && status < 500
301           raise ClientError,
302             "Could not retrieve discovery document at: #{request_uri}"
303         elsif status >= 500 && status < 600
304           raise ServerError,
305             "Could not retrieve discovery document at: #{request_uri}"
306         elsif status > 600
307           raise TransmissionError,
308             "Could not retrieve discovery document at: #{request_uri}"
309         end
310       end)
311     end
312
313     ##
314     # Returns all APIs published in the directory document.
315     #
316     # @return [Array] The list of available APIs.
317     def discovered_apis
318       @directory_apis ||= (begin
319         document_base = self.directory_uri
320         if self.directory_document && self.directory_document['items']
321           self.directory_document['items'].map do |discovery_document|
322             Google::APIClient::API.new(
323               document_base,
324               discovery_document
325             )
326           end
327         else
328           []
329         end
330       end)
331     end
332
333     ##
334     # Returns the service object for a given service name and service version.
335     #
336     # @param [String, Symbol] api The service name.
337     # @param [String] version The desired version of the service.
338     #
339     # @return [Google::APIClient::API] The service object.
340     def discovered_api(api, version=nil)
341       if !api.kind_of?(String) && !api.kind_of?(Symbol)
342         raise TypeError,
343           "Expected String or Symbol, got #{api.class}."
344       end
345       api = api.to_s
346       version = version || 'v1'
347       return @discovered_apis["#{api}:#{version}"] ||= begin
348         document_base = self.discovery_uri(api, version)
349         discovery_document = self.discovery_document(api, version)
350         if document_base && discovery_document
351           Google::APIClient::API.new(
352             document_base,
353             discovery_document
354           )
355         else
356           nil
357         end
358       end
359     end
360
361     ##
362     # Returns the method object for a given RPC name and service version.
363     #
364     # @param [String, Symbol] rpc_name The RPC name of the desired method.
365     # @param [String] version The desired version of the service.
366     #
367     # @return [Google::APIClient::Method] The method object.
368     def discovered_method(rpc_name, api, version=nil)
369       if !rpc_name.kind_of?(String) && !rpc_name.kind_of?(Symbol)
370         raise TypeError,
371           "Expected String or Symbol, got #{rpc_name.class}."
372       end
373       rpc_name = rpc_name.to_s
374       api = api.to_s
375       version = version || 'v1'
376       service = self.discovered_api(api, version)
377       if service.to_h[rpc_name]
378         return service.to_h[rpc_name]
379       else
380         return nil
381       end
382     end
383
384     ##
385     # Returns the service object with the highest version number.
386     #
387     # @note <em>Warning</em>: This method should be used with great care.
388     # As APIs are updated, minor differences between versions may cause
389     # incompatibilities. Requesting a specific version will avoid this issue.
390     #
391     # @param [String, Symbol] api The name of the service.
392     #
393     # @return [Google::APIClient::API] The service object.
394     def preferred_version(api)
395       if !api.kind_of?(String) && !api.kind_of?(Symbol)
396         raise TypeError,
397           "Expected String or Symbol, got #{api.class}."
398       end
399       api = api.to_s
400       # TODO(bobaman): Update to use directory API.
401       return self.discovered_apis.detect do |a|
402         a.name == api && a.preferred == true
403       end
404     end
405
406     ##
407     # Generates a request.
408     #
409     # @param [Google::APIClient::Method, String] api_method
410     #   The method object or the RPC name of the method being executed.
411     # @param [Hash, Array] parameters
412     #   The parameters to send to the method.
413     # @param [String] body The body of the request.
414     # @param [Hash, Array] headers The HTTP headers for the request.
415     # @param [Hash] options
416     #   The configuration parameters for the request.
417     #   - <code>:version</code> — 
418     #     The service version.  Only used if <code>api_method</code> is a
419     #     <code>String</code>.  Defaults to <code>'v1'</code>.
420     #   - <code>:authorization</code> — 
421     #     The authorization mechanism for the response.  Used only if
422     #     <code>:authenticated</code> is <code>true</code>.
423     #   - <code>:authenticated</code> — 
424     #     <code>true</code> if the request must be signed or otherwise
425     #     authenticated, <code>false</code>
426     #     otherwise.  Defaults to <code>true</code> if an authorization
427     #     mechanism has been set, <code>false</code> otherwise.
428     #
429     # @return [Array] The generated request.
430     #
431     # @example
432     #   request = client.generate_request(
433     #     :api_method => 'chili.activities.list',
434     #     :parameters =>
435     #       {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
436     #   )
437     #   method, uri, headers, body = request
438     def generate_request(options={})
439       # Note: The merge method on a Hash object will coerce an API Reference
440       # object into a Hash and merge with the default options.
441       options={
442         :version => 'v1',
443         :authorization => self.authorization
444       }.merge(options)
445       # The Reference object is going to need this to do method ID lookups.
446       options[:client] = self
447       # The default value for the :authenticated option depends on whether an
448       # authorization mechanism has been set.
449       if options[:authorization]
450         options = {:authenticated => true}.merge(options)
451       else
452         options = {:authenticated => false}.merge(options)
453       end
454       reference = Google::APIClient::Reference.new(options)
455       request = reference.to_request
456       if options[:authenticated]
457         request = self.generate_authenticated_request(:request => request)
458       end
459       return request
460     end
461
462     ##
463     # Signs a request using the current authorization mechanism.
464     #
465     # @param [Hash] options The options to pass through.
466     #
467     # @return [Array] The signed or otherwise authenticated request.
468     def generate_authenticated_request(options={})
469       return authorization.generate_authenticated_request(options)
470     end
471
472     ##
473     # Transmits the request using the current HTTP adapter.
474     #
475     # @param [Array] request The request to transmit.
476     # @param [#transmit] adapter The HTTP adapter.
477     #
478     # @return [Array] The response from the server.
479     def transmit(request, adapter=self.http_adapter)
480       if self.user_agent != nil
481         # If there's no User-Agent header, set one.
482         method, uri, headers, body = request
483         unless headers.kind_of?(Enumerable)
484           # We need to use some Enumerable methods, relying on the presence of
485           # the #each method.
486           class <<headers
487             include Enumerable
488           end
489         end
490         if self.user_agent.kind_of?(String)
491           unless headers.any? { |k, v| k.downcase == 'User-Agent'.downcase }
492             headers = headers.to_a.insert(0, ['User-Agent', self.user_agent])
493           end
494         elsif self.user_agent != nil
495           raise TypeError,
496             "Expected User-Agent to be String, got #{self.user_agent.class}"
497         end
498       end
499       adapter.transmit([method, uri, headers, body])
500     end
501
502     ##
503     # Executes a request, wrapping it in a Result object.
504     #
505     # @param [Google::APIClient::Method, String] api_method
506     #   The method object or the RPC name of the method being executed.
507     # @param [Hash, Array] parameters
508     #   The parameters to send to the method.
509     # @param [String] body The body of the request.
510     # @param [Hash, Array] headers The HTTP headers for the request.
511     # @param [Hash] options
512     #   The configuration parameters for the request.
513     #   - <code>:version</code> — 
514     #     The service version.  Only used if <code>api_method</code> is a
515     #     <code>String</code>.  Defaults to <code>'v1'</code>.
516     #   - <code>:adapter</code> — 
517     #     The HTTP adapter.
518     #   - <code>:authorization</code> — 
519     #     The authorization mechanism for the response.  Used only if
520     #     <code>:authenticated</code> is <code>true</code>.
521     #   - <code>:authenticated</code> — 
522     #     <code>true</code> if the request must be signed or otherwise
523     #     authenticated, <code>false</code>
524     #     otherwise.  Defaults to <code>true</code>.
525     #
526     # @return [Array] The response from the API.
527     #
528     # @example
529     #   request = client.generate_request(
530     #     :api_method => 'chili.activities.list',
531     #     :parameters =>
532     #       {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
533     #   )
534     def execute(*params)
535       # This block of code allows us to accept multiple parameter passing
536       # styles, and maintaining some backwards compatibility.
537       #
538       # Note: I'm extremely tempted to deprecate this style of execute call.
539       if params.last.respond_to?(:to_hash) && params.size == 1
540         options = params.pop
541       else
542         options = {}
543       end
544       options[:api_method] = params.shift if params.size > 0
545       options[:parameters] = params.shift if params.size > 0
546       options[:merged_body] = params.shift if params.size > 0
547       options[:headers] = params.shift if params.size > 0
548       options[:client] = self
549
550       reference = Google::APIClient::Reference.new(options)
551       request = self.generate_request(reference)
552       response = self.transmit(
553         request,
554         options[:adapter] || self.http_adapter
555       )
556       return Google::APIClient::Result.new(reference, request, response)
557     end
558
559     ##
560     # Same as Google::APIClient#execute, but raises an exception if there was
561     # an error.
562     #
563     # @see Google::APIClient#execute
564     def execute!(*params)
565       result = self.execute(*params)
566       status, _, _ = result.response
567       if result.data.respond_to?(:error)
568         # You're going to get a terrible error message if the response isn't
569         # parsed successfully as an error.
570         error_message = result.data.error
571       end
572       if status >= 400 && status < 500
573         raise ClientError,
574           error_message || "A client error has occurred."
575       elsif status >= 500 && status < 600
576         raise ServerError,
577           error_message || "A server error has occurred."
578       elsif status > 600
579         raise TransmissionError,
580           error_message || "A transmission error has occurred."
581       end
582       return result
583     end
584   end
585 end
586
587 require 'google/api_client/version'