Added better support for user IP and developer key across all APIs.
[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_uri = self.directory_uri
272         request = ['GET', request_uri, [], []]
273         response = self.transmit(request)
274         status, headers, body = response
275         if status == 200 # TODO(bobaman) Better status code handling?
276           merged_body = body.inject(StringIO.new) do |accu, chunk|
277             accu.write(chunk)
278             accu
279           end
280           ::JSON.parse(merged_body.string)
281         elsif status >= 400 && status < 500
282           raise ClientError,
283             "Could not retrieve discovery document at: #{request_uri}"
284         elsif status >= 500 && status < 600
285           raise ServerError,
286             "Could not retrieve discovery document at: #{request_uri}"
287         elsif status > 600
288           raise TransmissionError,
289             "Could not retrieve discovery document at: #{request_uri}"
290         end
291       end)
292     end
293
294     ##
295     # Returns the parsed discovery document.
296     #
297     # @param [String, Symbol] api The service name.
298     # @param [String] version The desired version of the service.
299     # @return [Hash] The parsed JSON from the discovery document.
300     def discovery_document(api, version=nil)
301       api = api.to_s
302       version = version || 'v1'
303       return @discovery_documents["#{api}:#{version}"] ||= (begin
304         request_uri = self.discovery_uri(api, version)
305         request = ['GET', request_uri, [], []]
306         response = self.transmit(request)
307         status, headers, body = response
308         if status == 200 # TODO(bobaman) Better status code handling?
309           merged_body = body.inject(StringIO.new) do |accu, chunk|
310             accu.write(chunk)
311             accu
312           end
313           ::JSON.parse(merged_body.string)
314         elsif status >= 400 && status < 500
315           raise ClientError,
316             "Could not retrieve discovery document at: #{request_uri}"
317         elsif status >= 500 && status < 600
318           raise ServerError,
319             "Could not retrieve discovery document at: #{request_uri}"
320         elsif status > 600
321           raise TransmissionError,
322             "Could not retrieve discovery document at: #{request_uri}"
323         end
324       end)
325     end
326
327     ##
328     # Returns all APIs published in the directory document.
329     #
330     # @return [Array] The list of available APIs.
331     def discovered_apis
332       @directory_apis ||= (begin
333         document_base = self.directory_uri
334         if self.directory_document && self.directory_document['items']
335           self.directory_document['items'].map do |discovery_document|
336             Google::APIClient::API.new(
337               document_base,
338               discovery_document
339             )
340           end
341         else
342           []
343         end
344       end)
345     end
346
347     ##
348     # Returns the service object for a given service name and service version.
349     #
350     # @param [String, Symbol] api The service name.
351     # @param [String] version The desired version of the service.
352     #
353     # @return [Google::APIClient::API] The service object.
354     def discovered_api(api, version=nil)
355       if !api.kind_of?(String) && !api.kind_of?(Symbol)
356         raise TypeError,
357           "Expected String or Symbol, got #{api.class}."
358       end
359       api = api.to_s
360       version = version || 'v1'
361       return @discovered_apis["#{api}:#{version}"] ||= begin
362         document_base = self.discovery_uri(api, version)
363         discovery_document = self.discovery_document(api, version)
364         if document_base && discovery_document
365           Google::APIClient::API.new(
366             document_base,
367             discovery_document
368           )
369         else
370           nil
371         end
372       end
373     end
374
375     ##
376     # Returns the method object for a given RPC name and service version.
377     #
378     # @param [String, Symbol] rpc_name The RPC name of the desired method.
379     # @param [String] version The desired version of the service.
380     #
381     # @return [Google::APIClient::Method] The method object.
382     def discovered_method(rpc_name, api, version=nil)
383       if !rpc_name.kind_of?(String) && !rpc_name.kind_of?(Symbol)
384         raise TypeError,
385           "Expected String or Symbol, got #{rpc_name.class}."
386       end
387       rpc_name = rpc_name.to_s
388       api = api.to_s
389       version = version || 'v1'
390       service = self.discovered_api(api, version)
391       if service.to_h[rpc_name]
392         return service.to_h[rpc_name]
393       else
394         return nil
395       end
396     end
397
398     ##
399     # Returns the service object with the highest version number.
400     #
401     # @note <em>Warning</em>: This method should be used with great care.
402     # As APIs are updated, minor differences between versions may cause
403     # incompatibilities. Requesting a specific version will avoid this issue.
404     #
405     # @param [String, Symbol] api The name of the service.
406     #
407     # @return [Google::APIClient::API] The service object.
408     def preferred_version(api)
409       if !api.kind_of?(String) && !api.kind_of?(Symbol)
410         raise TypeError,
411           "Expected String or Symbol, got #{api.class}."
412       end
413       api = api.to_s
414       # TODO(bobaman): Update to use directory API.
415       return self.discovered_apis.detect do |a|
416         a.name == api && a.preferred == true
417       end
418     end
419
420     ##
421     # Generates a request.
422     #
423     # @param [Google::APIClient::Method, String] api_method
424     #   The method object or the RPC name of the method being executed.
425     # @param [Hash, Array] parameters
426     #   The parameters to send to the method.
427     # @param [String] body The body of the request.
428     # @param [Hash, Array] headers The HTTP headers for the request.
429     # @param [Hash] options
430     #   The configuration parameters for the request.
431     #   - <code>:version</code> — 
432     #     The service version.  Only used if <code>api_method</code> is a
433     #     <code>String</code>.  Defaults to <code>'v1'</code>.
434     #   - <code>:authorization</code> — 
435     #     The authorization mechanism for the response.  Used only if
436     #     <code>:authenticated</code> is <code>true</code>.
437     #   - <code>:authenticated</code> — 
438     #     <code>true</code> if the request must be signed or otherwise
439     #     authenticated, <code>false</code>
440     #     otherwise.  Defaults to <code>true</code> if an authorization
441     #     mechanism has been set, <code>false</code> otherwise.
442     #
443     # @return [Array] The generated request.
444     #
445     # @example
446     #   request = client.generate_request(
447     #     :api_method => 'chili.activities.list',
448     #     :parameters =>
449     #       {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
450     #   )
451     #   method, uri, headers, body = request
452     def generate_request(options={})
453       # Note: The merge method on a Hash object will coerce an API Reference
454       # object into a Hash and merge with the default options.
455       options={
456         :version => 'v1',
457         :authorization => self.authorization,
458         :key => self.key,
459         :user_ip => self.user_ip
460       }.merge(options)
461       # The Reference object is going to need this to do method ID lookups.
462       options[:client] = self
463       # The default value for the :authenticated option depends on whether an
464       # authorization mechanism has been set.
465       if options[:authorization]
466         options = {:authenticated => true}.merge(options)
467       else
468         options = {:authenticated => false}.merge(options)
469       end
470       reference = Google::APIClient::Reference.new(options)
471       request = reference.to_request
472       if options[:authenticated]
473         request = self.generate_authenticated_request(:request => request)
474       end
475       return request
476     end
477
478     ##
479     # Signs a request using the current authorization mechanism.
480     #
481     # @param [Hash] options The options to pass through.
482     #
483     # @return [Array] The signed or otherwise authenticated request.
484     def generate_authenticated_request(options={})
485       return authorization.generate_authenticated_request(options)
486     end
487
488     ##
489     # Transmits the request using the current HTTP adapter.
490     #
491     # @param [Array] request The request to transmit.
492     # @param [#transmit] adapter The HTTP adapter.
493     #
494     # @return [Array] The response from the server.
495     def transmit(request, adapter=self.http_adapter)
496       if self.user_agent != nil
497         # If there's no User-Agent header, set one.
498         method, uri, headers, body = request
499         unless headers.kind_of?(Enumerable)
500           # We need to use some Enumerable methods, relying on the presence of
501           # the #each method.
502           class <<headers
503             include Enumerable
504           end
505         end
506         if self.user_agent.kind_of?(String)
507           unless headers.any? { |k, v| k.downcase == 'User-Agent'.downcase }
508             headers = headers.to_a.insert(0, ['User-Agent', self.user_agent])
509           end
510         elsif self.user_agent != nil
511           raise TypeError,
512             "Expected User-Agent to be String, got #{self.user_agent.class}"
513         end
514       end
515       adapter.transmit([method, uri, headers, body])
516     end
517
518     ##
519     # Executes a request, wrapping it in a Result object.
520     #
521     # @param [Google::APIClient::Method, String] api_method
522     #   The method object or the RPC name of the method being executed.
523     # @param [Hash, Array] parameters
524     #   The parameters to send to the method.
525     # @param [String] body The body of the request.
526     # @param [Hash, Array] headers The HTTP headers for the request.
527     # @param [Hash] options
528     #   The configuration parameters for the request.
529     #   - <code>:version</code> — 
530     #     The service version.  Only used if <code>api_method</code> is a
531     #     <code>String</code>.  Defaults to <code>'v1'</code>.
532     #   - <code>:adapter</code> — 
533     #     The HTTP adapter.
534     #   - <code>:authorization</code> — 
535     #     The authorization mechanism for the response.  Used only if
536     #     <code>:authenticated</code> is <code>true</code>.
537     #   - <code>:authenticated</code> — 
538     #     <code>true</code> if the request must be signed or otherwise
539     #     authenticated, <code>false</code>
540     #     otherwise.  Defaults to <code>true</code>.
541     #
542     # @return [Array] The response from the API.
543     #
544     # @example
545     #   request = client.generate_request(
546     #     :api_method => 'chili.activities.list',
547     #     :parameters =>
548     #       {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
549     #   )
550     def execute(*params)
551       # This block of code allows us to accept multiple parameter passing
552       # styles, and maintaining some backwards compatibility.
553       #
554       # Note: I'm extremely tempted to deprecate this style of execute call.
555       if params.last.respond_to?(:to_hash) && params.size == 1
556         options = params.pop
557       else
558         options = {}
559       end
560       options[:api_method] = params.shift if params.size > 0
561       options[:parameters] = params.shift if params.size > 0
562       options[:merged_body] = params.shift if params.size > 0
563       options[:headers] = params.shift if params.size > 0
564       options[:client] = self
565
566       reference = Google::APIClient::Reference.new(options)
567       request = self.generate_request(reference)
568       response = self.transmit(
569         request,
570         options[:adapter] || self.http_adapter
571       )
572       return Google::APIClient::Result.new(reference, request, response)
573     end
574
575     ##
576     # Same as Google::APIClient#execute, but raises an exception if there was
577     # an error.
578     #
579     # @see Google::APIClient#execute
580     def execute!(*params)
581       result = self.execute(*params)
582       status, _, _ = result.response
583       if result.data.respond_to?(:error)
584         # You're going to get a terrible error message if the response isn't
585         # parsed successfully as an error.
586         error_message = result.data.error
587       end
588       if status >= 400 && status < 500
589         raise ClientError,
590           error_message || "A client error has occurred."
591       elsif status >= 500 && status < 600
592         raise ServerError,
593           error_message || "A server error has occurred."
594       elsif status > 600
595         raise TransmissionError,
596           error_message || "A transmission error has occurred."
597       end
598       return result
599     end
600   end
601 end
602
603 require 'google/api_client/version'