Mostly doc updates, +remove support for method as string
[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 'faraday'
17 require 'faraday/utils'
18 require 'multi_json'
19 require 'compat/multi_json'
20 require 'stringio'
21
22 require 'google/api_client/version'
23 require 'google/api_client/errors'
24 require 'google/api_client/environment'
25 require 'google/api_client/discovery'
26 require 'google/api_client/reference'
27 require 'google/api_client/result'
28 require 'google/api_client/media'
29 require 'google/api_client/service_account'
30 require 'google/api_client/batch'
31
32 module Google
33   ##
34   # This class manages APIs communication.
35   class APIClient
36     ##
37     # Creates a new Google API client.
38     #
39     # @param [Hash] options The configuration parameters for the client.
40     # @option options [Symbol, #generate_authenticated_request] :authorization
41     #   (:oauth_1)
42     #   The authorization mechanism used by the client.  The following
43     #   mechanisms are supported out-of-the-box:
44     #   <ul>
45     #     <li><code>:two_legged_oauth_1</code></li>
46     #     <li><code>:oauth_1</code></li>
47     #     <li><code>:oauth_2</code></li>
48     #   </ul>
49     # @option options [String] :application_name
50     #   The name of the application using the client.
51     # @option options [String] :application_version
52     #   The version number of the application using the client.
53     # @option options [String] :user_agent
54     #   ("{app_name} google-api-ruby-client/{version} {os_name}/{os_version}")
55     #   The user agent used by the client.  Most developers will want to
56     #   leave this value alone and use the `:application_name` option instead.
57     # @option options [String] :host ("www.googleapis.com")
58     #   The API hostname used by the client. This rarely needs to be changed.
59     # @option options [String] :port (443)
60     #   The port number used by the client. This rarely needs to be changed.
61     # @option options [String] :discovery_path ("/discovery/v1")
62     #   The discovery base path. This rarely needs to be changed.
63     def initialize(options={})
64       # Normalize key to String to allow indifferent access.
65       options = options.inject({}) do |accu, (key, value)|
66         accu[key.to_sym] = value
67         accu
68       end
69       # Almost all API usage will have a host of 'www.googleapis.com'.
70       self.host = options[:host] || 'www.googleapis.com'
71       self.port = options[:port] || 443
72       self.discovery_path = options[:discovery_path] || '/discovery/v1'
73
74       # Most developers will want to leave this value alone and use the
75       # application_name option.
76       application_string = (
77         options[:application_name] ? (
78           "#{options[:application_name]}/" +
79           "#{options[:application_version] || '0.0.0'}"
80         ) : ""
81       )
82       self.user_agent = options[:user_agent] || (
83         "#{application_string} " +
84         "google-api-ruby-client/#{VERSION::STRING} " +
85          ENV::OS_VERSION
86       ).strip
87       # The writer method understands a few Symbols and will generate useful
88       # default authentication mechanisms.
89       self.authorization =
90         options.key?(:authorization) ? options[:authorization] : :oauth_2
91       self.key = options[:key]
92       self.user_ip = options[:user_ip]
93       @discovery_uris = {}
94       @discovery_documents = {}
95       @discovered_apis = {}
96       return self
97     end
98
99     ##
100     # Returns the authorization mechanism used by the client.
101     #
102     # @return [#generate_authenticated_request] The authorization mechanism.
103     attr_reader :authorization
104
105     ##
106     # Sets the authorization mechanism used by the client.
107     #
108     # @param [#generate_authenticated_request] new_authorization
109     #   The new authorization mechanism.
110     def authorization=(new_authorization)
111       case new_authorization
112       when :oauth_1, :oauth
113         require 'signet/oauth_1/client'
114         # NOTE: Do not rely on this default value, as it may change
115         new_authorization = Signet::OAuth1::Client.new(
116           :temporary_credential_uri =>
117             'https://www.google.com/accounts/OAuthGetRequestToken',
118           :authorization_uri =>
119             'https://www.google.com/accounts/OAuthAuthorizeToken',
120           :token_credential_uri =>
121             'https://www.google.com/accounts/OAuthGetAccessToken',
122           :client_credential_key => 'anonymous',
123           :client_credential_secret => 'anonymous'
124         )
125       when :two_legged_oauth_1, :two_legged_oauth
126         require 'signet/oauth_1/client'
127         # NOTE: Do not rely on this default value, as it may change
128         new_authorization = Signet::OAuth1::Client.new(
129           :client_credential_key => nil,
130           :client_credential_secret => nil,
131           :two_legged => true
132         )
133       when :oauth_2
134         require 'signet/oauth_2/client'
135         # NOTE: Do not rely on this default value, as it may change
136         new_authorization = Signet::OAuth2::Client.new(
137           :authorization_uri =>
138             'https://accounts.google.com/o/oauth2/auth',
139           :token_credential_uri =>
140             'https://accounts.google.com/o/oauth2/token'
141         )
142       when nil
143         # No authorization mechanism
144       else
145         if !new_authorization.respond_to?(:generate_authenticated_request)
146           raise TypeError,
147             'Expected authorization mechanism to respond to ' +
148             '#generate_authenticated_request.'
149         end
150       end
151       @authorization = new_authorization
152       return @authorization
153     end
154
155     ##
156     # The application's API key issued by the API console.
157     #
158     # @return [String] The API key.
159     attr_accessor :key
160
161     ##
162     # The IP address of the user this request is being performed on behalf of.
163     #
164     # @return [String] The user's IP address.
165     attr_accessor :user_ip
166
167     ##
168     # The user agent used by the client.
169     #
170     # @return [String]
171     #   The user agent string used in the User-Agent header.
172     attr_accessor :user_agent
173
174     ##
175     # The API hostname used by the client.
176     #
177     # @return [String]
178     #   The API hostname. Should almost always be 'www.googleapis.com'.
179     attr_accessor :host
180
181     ##
182     # The port number used by the client.
183     #
184     # @return [String]
185     #   The port number. Should almost always be 443.
186     attr_accessor :port
187
188     ##
189     # The base path used by the client for discovery.
190     #
191     # @return [String]
192     #   The base path. Should almost always be '/discovery/v1'.
193     attr_accessor :discovery_path
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       return resolve_uri(self.discovery_path + '/apis')
201     end
202
203     ##
204     # Manually registers a URI as a discovery document for a specific version
205     # of an API.
206     #
207     # @param [String, Symbol] api The API name.
208     # @param [String] version The desired version of the API.
209     # @param [Addressable::URI] uri The URI of the discovery document.
210     def register_discovery_uri(api, version, uri)
211       api = api.to_s
212       version = version || 'v1'
213       @discovery_uris["#{api}:#{version}"] = uri
214     end
215
216     ##
217     # Returns the URI for the discovery document.
218     #
219     # @param [String, Symbol] api The API name.
220     # @param [String] version The desired version of the API.
221     # @return [Addressable::URI] The URI of the discovery document.
222     def discovery_uri(api, version=nil)
223       api = api.to_s
224       version = version || 'v1'
225       return @discovery_uris["#{api}:#{version}"] ||= (
226         resolve_uri(
227           self.discovery_path + '/apis/{api}/{version}/rest',
228           'api' => api,
229           'version' => version
230         )
231       )
232     end
233
234     ##
235     # Manually registers a pre-loaded discovery document for a specific version
236     # of an API.
237     #
238     # @param [String, Symbol] api The API name.
239     # @param [String] version The desired version of the API.
240     # @param [String, StringIO] discovery_document
241     #   The contents of the discovery document.
242     def register_discovery_document(api, version, discovery_document)
243       api = api.to_s
244       version = version || 'v1'
245       if discovery_document.kind_of?(StringIO)
246         discovery_document.rewind
247         discovery_document = discovery_document.string
248       elsif discovery_document.respond_to?(:to_str)
249         discovery_document = discovery_document.to_str
250       else
251         raise TypeError,
252           "Expected String or StringIO, got #{discovery_document.class}."
253       end
254       @discovery_documents["#{api}:#{version}"] =
255         MultiJson.load(discovery_document)
256     end
257
258     ##
259     # Returns the parsed directory document.
260     #
261     # @return [Hash] The parsed JSON from the directory document.
262     def directory_document
263       return @directory_document ||= (begin
264         response = self.execute!(
265           :http_method => :get,
266           :uri => self.directory_uri,
267           :authenticated => false
268         )
269         response.data
270       end)
271     end
272
273     ##
274     # Returns the parsed discovery document.
275     #
276     # @param [String, Symbol] api The API name.
277     # @param [String] version The desired version of the API.
278     # @return [Hash] The parsed JSON from the discovery document.
279     def discovery_document(api, version=nil)
280       api = api.to_s
281       version = version || 'v1'
282       return @discovery_documents["#{api}:#{version}"] ||= (begin
283         response = self.execute!(
284           :http_method => :get,
285           :uri => self.discovery_uri(api, version),
286           :authenticated => false
287         )
288         response.data
289       end)
290     end
291
292     ##
293     # Returns all APIs published in the directory document.
294     #
295     # @return [Array] The list of available APIs.
296     def discovered_apis
297       @directory_apis ||= (begin
298         document_base = self.directory_uri
299         if self.directory_document && self.directory_document['items']
300           self.directory_document['items'].map do |discovery_document|
301             Google::APIClient::API.new(
302               document_base,
303               discovery_document
304             )
305           end
306         else
307           []
308         end
309       end)
310     end
311
312     ##
313     # Returns the service object for a given service name and service version.
314     #
315     # @param [String, Symbol] api The API name.
316     # @param [String] version The desired version of the API.
317     #
318     # @return [Google::APIClient::API] The service object.
319     def discovered_api(api, version=nil)
320       if !api.kind_of?(String) && !api.kind_of?(Symbol)
321         raise TypeError,
322           "Expected String or Symbol, got #{api.class}."
323       end
324       api = api.to_s
325       version = version || 'v1'
326       return @discovered_apis["#{api}:#{version}"] ||= begin
327         document_base = self.discovery_uri(api, version)
328         discovery_document = self.discovery_document(api, version)
329         if document_base && discovery_document
330           Google::APIClient::API.new(
331             document_base,
332             discovery_document
333           )
334         else
335           nil
336         end
337       end
338     end
339
340     ##
341     # Returns the method object for a given RPC name and service version.
342     #
343     # @param [String, Symbol] rpc_name The RPC name of the desired method.
344     # @param [String, Symbol] api The API the method is within.
345     # @param [String] version The desired version of the API.
346     #
347     # @return [Google::APIClient::Method] The method object.
348     def discovered_method(rpc_name, api, version=nil)
349       if !rpc_name.kind_of?(String) && !rpc_name.kind_of?(Symbol)
350         raise TypeError,
351           "Expected String or Symbol, got #{rpc_name.class}."
352       end
353       rpc_name = rpc_name.to_s
354       api = api.to_s
355       version = version || 'v1'
356       service = self.discovered_api(api, version)
357       if service.to_h[rpc_name]
358         return service.to_h[rpc_name]
359       else
360         return nil
361       end
362     end
363
364     ##
365     # Returns the service object with the highest version number.
366     #
367     # @note <em>Warning</em>: This method should be used with great care.
368     # As APIs are updated, minor differences between versions may cause
369     # incompatibilities. Requesting a specific version will avoid this issue.
370     #
371     # @param [String, Symbol] api The name of the service.
372     #
373     # @return [Google::APIClient::API] The service object.
374     def preferred_version(api)
375       if !api.kind_of?(String) && !api.kind_of?(Symbol)
376         raise TypeError,
377           "Expected String or Symbol, got #{api.class}."
378       end
379       api = api.to_s
380       return self.discovered_apis.detect do |a|
381         a.name == api && a.preferred == true
382       end
383     end
384
385     ##
386     # Verifies an ID token against a server certificate. Used to ensure that
387     # an ID token supplied by an untrusted client-side mechanism is valid.
388     # Raises an error if the token is invalid or missing.
389     def verify_id_token!
390       require 'jwt'
391       require 'openssl'
392       @certificates ||= {}
393       if !self.authorization.respond_to?(:id_token)
394         raise ArgumentError, (
395           "Current authorization mechanism does not support ID tokens: " +
396           "#{self.authorization.class.to_s}"
397         )
398       elsif !self.authorization.id_token
399         raise ArgumentError, (
400           "Could not verify ID token, ID token missing. " +
401           "Scopes were: #{self.authorization.scope.inspect}"
402         )
403       else
404         check_cached_certs = lambda do
405           valid = false
406           for key, cert in @certificates
407             begin
408               self.authorization.decoded_id_token(cert.public_key)
409               valid = true
410             rescue JWT::DecodeError, Signet::UnsafeOperationError
411               # Expected exception. Ignore, ID token has not been validated.
412             end
413           end
414           valid
415         end
416         if check_cached_certs.call()
417           return true
418         end
419         response = self.execute!(
420           :http_method => :get,
421           :uri => 'https://www.googleapis.com/oauth2/v1/certs',
422           :authenticated => false
423         )
424         @certificates.merge!(
425           Hash[MultiJson.load(response.body).map do |key, cert|
426             [key, OpenSSL::X509::Certificate.new(cert)]
427           end]
428         )
429         if check_cached_certs.call()
430           return true
431         else
432           raise InvalidIDTokenError,
433             "Could not verify ID token against any available certificate."
434         end
435       end
436       return nil
437     end
438
439     ##
440     # Generates a request.
441     #
442     # @option options [Google::APIClient::Method] :api_method
443     #   The method object or the RPC name of the method being executed.
444     # @option options [Hash, Array] :parameters
445     #   The parameters to send to the method.
446     # @option options [Hash, Array] :headers The HTTP headers for the request.
447     # @option options [String] :body The body of the request.
448     # @option options [String] :version ("v1")
449     #   The service version. Only used if `api_method` is a `String`.
450     # @option options [#generate_authenticated_request] :authorization
451     #   The authorization mechanism for the response. Used only if
452     #   `:authenticated` is `true`.
453     # @option options [TrueClass, FalseClass] :authenticated (true)
454     #   `true` if the request must be signed or somehow
455     #   authenticated, `false` otherwise.
456     #
457     # @return [Google::APIClient::Reference] The generated request.
458     #
459     # @example
460     #   request = client.generate_request(
461     #     :api_method => 'plus.activities.list',
462     #     :parameters =>
463     #       {'collection' => 'public', 'userId' => 'me'}
464     #   )
465     def generate_request(options={})
466       options = {
467         :api_client => self
468       }.merge(options)
469       return Google::APIClient::Request.new(options)
470     end
471
472     ##
473     # Executes a request, wrapping it in a Result object.
474     #
475     # @param [Google::APIClient::Request, Hash, Array] params
476     #   Either a Google::APIClient::Request, a Hash, or an Array.
477     #
478     #   If a Google::APIClient::Request, no other parameters are expected.
479     #
480     #   If a Hash, the below parameters are handled. If an Array, the
481     #   parameters are assumed to be in the below order:
482     #
483     #   - (Google::APIClient::Method) api_method:
484     #     The method object or the RPC name of the method being executed.
485     #   - (Hash, Array) parameters:
486     #     The parameters to send to the method.
487     #   - (String) body: The body of the request.
488     #   - (Hash, Array) headers: The HTTP headers for the request.
489     #   - (Hash) options: A set of options for the request, of which:
490     #     - (String) :version (default: "v1") -
491     #       The service version. Only used if `api_method` is a `String`.
492     #     - (#generate_authenticated_request) :authorization (default: true) -
493     #       The authorization mechanism for the response. Used only if
494     #       `:authenticated` is `true`.
495     #     - (TrueClass, FalseClass) :authenticated (default: true) -
496     #       `true` if the request must be signed or somehow
497     #       authenticated, `false` otherwise.
498     #
499     # @return [Google::APIClient::Result] The result from the API, nil if batch.
500     #
501     # @example
502     #   result = client.execute(batch_request)
503     #
504     # @example
505     #   plus = client.discovered_api('plus')
506     #   result = client.execute(
507     #     :api_method => plus.activities.list,
508     #     :parameters => {'collection' => 'public', 'userId' => 'me'}
509     #   )
510     #
511     # @see Google::APIClient#generate_request
512     def execute(*params)
513       if params.last.kind_of?(Google::APIClient::Request) &&
514           params.size == 1
515         request = params.pop
516         options = {}
517       else
518         # This block of code allows us to accept multiple parameter passing
519         # styles, and maintaining some backwards compatibility.
520         #
521         # Note: I'm extremely tempted to deprecate this style of execute call.
522         if params.last.respond_to?(:to_hash) && params.size == 1
523           options = params.pop
524         else
525           options = {}
526         end
527
528         options[:api_method] = params.shift if params.size > 0
529         options[:parameters] = params.shift if params.size > 0
530         options[:body] = params.shift if params.size > 0
531         options[:headers] = params.shift if params.size > 0
532         options[:client] = self
533         request = self.generate_request(options)
534       end
535       
536       request.headers['User-Agent'] ||= '' + self.user_agent unless self.user_agent.nil?
537       request.parameters['key'] ||= self.key unless self.key.nil?
538       request.parameters['userIp'] ||= self.user_ip unless self.user_ip.nil?
539
540       connection = options[:connection] || Faraday.default_connection
541       request.authorization = options[:authorization] || self.authorization unless options[:authenticated] == false
542
543       result = request.send(connection)
544       return result
545     end
546
547     ##
548     # Same as Google::APIClient#execute, but raises an exception if there was
549     # an error.
550     #
551     # @see Google::APIClient#execute
552     def execute!(*params)
553       result = self.execute(*params)
554       if result.error?
555         error_message = result.error_message
556         case result.response.status
557           when 400...500
558             exception_type = ClientError
559             error_message ||= "A client error has occurred."
560           when 500...600
561             exception_type = ServerError
562             error_message ||= "A server error has occurred."
563           else
564             exception_type = TransmissionError
565             error_message ||= "A transmission error has occurred."
566         end
567         raise exception_type, error_message
568       end
569       return result
570     end
571     
572     protected
573     
574     ##
575     # Resolves a URI template against the client's configured base.
576     #
577     # @api private
578     # @param [String, Addressable::URI, Addressable::Template] template
579     #   The template to resolve.
580     # @param [Hash] mapping The mapping that corresponds to the template.
581     # @return [Addressable::URI] The expanded URI.
582     def resolve_uri(template, mapping={})
583       @base_uri ||= Addressable::URI.new(
584         :scheme => 'https',
585         :host => self.host,
586         :port => self.port
587       ).normalize
588       template = if template.kind_of?(Addressable::Template)
589         template.pattern
590       elsif template.respond_to?(:to_str)
591         template.to_str
592       else
593         raise TypeError,
594           "Expected String, Addressable::URI, or Addressable::Template, " +
595           "got #{template.class}."
596       end
597       return Addressable::Template.new(@base_uri + template).expand(mapping)
598     end
599     
600   end
601 end
602
603 require 'google/api_client/version'