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