A significant update of the client functionality.
[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/discovery'
22
23 module Google
24   # TODO(bobaman): Document all this stuff.
25
26
27   ##
28   # This class manages APIs communication.
29   class APIClient
30     ##
31     # Creates a new Google API client.
32     #
33     # @param [Hash] options The configuration parameters for the client.
34     # @option options [Symbol, #generate_authenticated_request] :authorization
35     #   (:oauth_1)
36     #   The authorization mechanism used by the client.  The following
37     #   mechanisms are supported out-of-the-box:
38     #   <ul>
39     #     <li><code>:two_legged_oauth_1</code></li>
40     #     <li><code>:oauth_1</code></li>
41     #     <li><code>:oauth_2</code></li>
42     #   </ul>
43     # @option options [String] :host ("www.googleapis.com")
44     #   The API hostname used by the client.  This rarely needs to be changed.
45     # @option options [String] :user_agent ("google-api-ruby-client/{version}")
46     #   The user agent used by the client.  Most developers will want to
47     #   leave this value alone — the API key is the primary mechanism used to
48     #   identify an application.
49     def initialize(options={})
50       # Normalize key to String to allow indifferent access.
51       options = options.inject({}) do |accu, (key, value)|
52         accu[key.to_s] = value
53         accu
54       end
55       # Almost all API usage will have a host of 'www.googleapis.com'.
56       self.host = options["host"] || 'www.googleapis.com'
57       # Most developers will want to leave this value alone.
58       self.user_agent = options["user_agent"] || (
59         'google-api-ruby-client/' + Google::APIClient::VERSION::STRING
60       )
61       # This is mostly a default for the sake of convenience.
62       # Unlike most other options, this one may be nil, so we check for
63       # the presence of the key rather than checking the value.
64       if options.has_key?("parser")
65         self.parser = options["parser"]
66       else
67         require 'google/api_client/parsers/json_parser'
68         # NOTE: Do not rely on this default value, as it may change
69         self.parser = Google::APIClient::JSONParser
70       end
71       # The writer method understands a few Symbols and will generate useful
72       # default authentication mechanisms.
73       self.authorization = options["authorization"] || :oauth_2
74       # The HTTP adapter controls all of the HTTP traffic the client generates.
75       # By default, Net::HTTP is used, but adding support for other clients
76       # is trivial.
77       if options["http_adapter"]
78         self.http_adapter = options["http_adapter"]
79       else
80         require 'httpadapter/adapters/net_http'
81         # NOTE: Do not rely on this default value, as it may change
82         self.http_adapter = HTTPAdapter::NetHTTPAdapter.new
83       end
84       @discovery_uris = {}
85       @discovery_documents = {}
86       @discovered_apis = {}
87       return self
88     end
89
90
91     ##
92     # Returns the parser used by the client.
93     #
94     # @return [#serialize, #parse]
95     #   The parser used by the client.  Any object that implements both a
96     #   <code>#serialize</code> and a <code>#parse</code> method may be used.
97     #   If <code>nil</code>, no parsing will be done.
98     attr_reader :parser
99
100     ##
101     # Sets the parser used by the client.
102     #
103     # @param [#serialize, #parse] new_parser
104     #   The parser used by the client.  Any object that implements both a
105     #   <code>#serialize</code> and a <code>#parse</code> method may be used.
106     #   If <code>nil</code>, no parsing will be done.
107     def parser=(new_parser)
108       if new_parser &&
109           !new_parser.respond_to?(:serialize) &&
110           !new_parser.respond_to?(:parse)
111         raise TypeError,
112           'Expected parser object to respond to #serialize and #parse.'
113       end
114       @parser = new_parser
115     end
116
117     ##
118     # Returns the authorization mechanism used by the client.
119     #
120     # @return [#generate_authenticated_request] The authorization mechanism.
121     attr_reader :authorization
122
123     ##
124     # Sets the authorization mechanism used by the client.
125     #
126     # @param [#generate_authenticated_request] new_authorization
127     #   The new authorization mechanism.
128     def authorization=(new_authorization)
129       case new_authorization
130       when :oauth_1, :oauth
131         require 'signet/oauth_1/client'
132         # NOTE: Do not rely on this default value, as it may change
133         new_authorization = Signet::OAuth1::Client.new(
134           :temporary_credential_uri =>
135             'https://www.google.com/accounts/OAuthGetRequestToken',
136           :authorization_uri =>
137             'https://www.google.com/accounts/OAuthAuthorizeToken',
138           :token_credential_uri =>
139             'https://www.google.com/accounts/OAuthGetAccessToken',
140           :client_credential_key => 'anonymous',
141           :client_credential_secret => 'anonymous'
142         )
143       when :two_legged_oauth_1, :two_legged_oauth
144         require 'signet/oauth_1/client'
145         # NOTE: Do not rely on this default value, as it may change
146         new_authorization = Signet::OAuth1::Client.new(
147           :client_credential_key => nil,
148           :client_credential_secret => nil,
149           :two_legged => true
150         )
151       when :oauth_2
152         require 'signet/oauth_2/client'
153         # NOTE: Do not rely on this default value, as it may change
154         new_authorization = Signet::OAuth2::Client.new(
155           :authorization_uri =>
156             'https://accounts.google.com/o/oauth2/auth',
157           :token_credential_uri =>
158             'https://accounts.google.com/o/oauth2/token'
159         )
160       when nil
161         # No authorization mechanism
162       else
163         if !new_authorization.respond_to?(:generate_authenticated_request)
164           raise TypeError,
165             'Expected authorization mechanism to respond to ' +
166             '#generate_authenticated_request.'
167         end
168       end
169       @authorization = new_authorization
170       return @authorization
171     end
172
173     ##
174     # Returns the HTTP adapter used by the client.
175     #
176     # @return [HTTPAdapter]
177     #   The HTTP adapter object.  The object must include the
178     #   HTTPAdapter module and conform to its interface.
179     attr_reader :http_adapter
180
181     ##
182     # Returns the HTTP adapter used by the client.
183     #
184     # @return [HTTPAdapter]
185     #   The HTTP adapter object.  The object must include the
186     #   HTTPAdapter module and conform to its interface.
187     def http_adapter=(new_http_adapter)
188       if new_http_adapter.kind_of?(HTTPAdapter)
189         @http_adapter = new_http_adapter
190       else
191         raise TypeError, "Expected HTTPAdapter, got #{new_http_adapter.class}."
192       end
193     end
194
195     ##
196     # The API hostname used by the client.
197     #
198     # @return [String]
199     #   The API hostname.  Should almost always be 'www.googleapis.com'.
200     attr_accessor :host
201
202     ##
203     # The user agent used by the client.
204     #
205     # @return [String]
206     #   The user agent string used in the User-Agent header.
207     attr_accessor :user_agent
208
209     ##
210     # Returns the URI for the directory document.
211     #
212     # @return [Addressable::URI] The URI of the directory document.
213     def directory_uri
214       template = Addressable::Template.new(
215         "https://{host}/discovery/v0.3/directory"
216       )
217       return template.expand({
218         "host" => self.host
219       })
220     end
221
222     ##
223     # Manually registers a URI as a discovery document for a specific version
224     # of an API.
225     #
226     # @param [String, Symbol] api The service name.
227     # @param [String] version The desired version of the service.
228     # @param [Addressable::URI] uri The URI of the discovery document.
229     def register_discovery_uri(api, version, uri)
230       api = api.to_s
231       version = version || 'v1'
232       @discovery_uris["#{api}:#{version}"] = uri
233     end
234
235     ##
236     # Returns the URI for the discovery document.
237     #
238     # @param [String, Symbol] api The service name.
239     # @param [String] version The desired version of the service.
240     # @return [Addressable::URI] The URI of the discovery document.
241     def discovery_uri(api, version=nil)
242       api = api.to_s
243       version = version || 'v1'
244       return @discovery_uris["#{api}:#{version}"] ||= (begin
245         template = Addressable::Template.new(
246           "https://{host}/discovery/v0.3/describe/" +
247           "{api}/{version}"
248         )
249         template.expand({
250           "host" => self.host,
251           "api" => api,
252           "version" => version
253         })
254       end)
255     end
256
257     ##
258     # Manually registers a pre-loaded discovery document for a specific version
259     # of an API.
260     #
261     # @param [String, Symbol] api The service name.
262     # @param [String] version The desired version of the service.
263     # @param [String, StringIO] discovery_document
264     #   The contents of the discovery document.
265     def register_discovery_document(api, version, discovery_document)
266       api = api.to_s
267       version = version || 'v1'
268       if discovery_document.kind_of?(StringIO)
269         discovery_document.rewind
270         discovery_document = discovery_document.string
271       elsif discovery_document.respond_to?(:to_str)
272         discovery_document = discovery_document.to_str
273       else
274         raise TypeError,
275           "Expected String or StringIO, got #{discovery_document.class}."
276       end
277       @discovery_documents["#{api}:#{version}"] =
278         JSON.parse(discovery_document)
279     end
280
281     ##
282     # Returns the parsed directory document.
283     #
284     # @return [Hash] The parsed JSON from the directory document.
285     def directory_document
286       return @directory_document ||= (begin
287         request_uri = self.directory_uri
288         request = ['GET', request_uri, [], []]
289         response = self.transmit_request(request)
290         status, headers, body = response
291         if status == 200 # TODO(bobaman) Better status code handling?
292           merged_body = StringIO.new
293           body.each do |chunk|
294             merged_body.write(chunk)
295           end
296           merged_body.rewind
297           JSON.parse(merged_body.string)
298         else
299           raise TransmissionError,
300             "Could not retrieve discovery document at: #{request_uri}"
301         end
302       end)
303     end
304
305     ##
306     # Returns the parsed discovery document.
307     #
308     # @param [String, Symbol] api The service name.
309     # @param [String] version The desired version of the service.
310     # @return [Hash] The parsed JSON from the discovery document.
311     def discovery_document(api, version=nil)
312       api = api.to_s
313       version = version || 'v1'
314       return @discovery_documents["#{api}:#{version}"] ||= (begin
315         request_uri = self.discovery_uri(api, version)
316         request = ['GET', request_uri, [], []]
317         response = self.transmit_request(request)
318         status, headers, body = response
319         if status == 200 # TODO(bobaman) Better status code handling?
320           merged_body = StringIO.new
321           body.each do |chunk|
322             merged_body.write(chunk)
323           end
324           merged_body.rewind
325           JSON.parse(merged_body.string)
326         else
327           raise TransmissionError,
328             "Could not retrieve discovery document at: #{request_uri}"
329         end
330       end)
331     end
332
333     ##
334     # Returns all APIs published in the directory document.
335     #
336     # @return [Array] The list of available APIs.
337     def discovered_apis
338       @directory_apis ||= (begin
339         document_base = self.directory_uri
340         if self.directory_document && self.directory_document['items']
341           self.directory_document['items'].map do |discovery_document|
342             ::Google::APIClient::API.new(
343               document_base,
344               discovery_document
345             )
346           end
347         else
348           []
349         end
350       end)
351     end
352
353     ##
354     # Returns the service object for a given service name and service version.
355     #
356     # @param [String, Symbol] api The service name.
357     # @param [String] version The desired version of the service.
358     #
359     # @return [Google::APIClient::API] The service object.
360     def discovered_api(api, version=nil)
361       if !api.kind_of?(String) && !api.kind_of?(Symbol)
362         raise TypeError,
363           "Expected String or Symbol, got #{api.class}."
364       end
365       api = api.to_s
366       version = version || 'v1'
367       return @discovered_apis["#{api}:#{version}"] ||= begin
368         document_base = self.discovery_uri(api, version)
369         discovery_document = self.discovery_document(api, version)
370         if document_base && discovery_document
371           ::Google::APIClient::API.new(
372             document_base,
373             discovery_document
374           )
375         else
376           nil
377         end
378       end
379     end
380
381     ##
382     # Returns the method object for a given RPC name and service version.
383     #
384     # @param [String, Symbol] rpc_name The RPC name of the desired method.
385     # @param [String] version The desired version of the service.
386     #
387     # @return [Google::APIClient::Method] The method object.
388     def discovered_method(rpc_name, api, version=nil)
389       if !rpc_name.kind_of?(String) && !rpc_name.kind_of?(Symbol)
390         raise TypeError,
391           "Expected String or Symbol, got #{rpc_name.class}."
392       end
393       rpc_name = rpc_name.to_s
394       api = api.to_s
395       version = version || 'v1'
396       service = self.discovered_api(api, version)
397       if service.to_h[rpc_name]
398         return service.to_h[rpc_name]
399       else
400         return nil
401       end
402     end
403
404     ##
405     # Returns the service object with the highest version number.
406     #
407     # @note <em>Warning</em>: This method should be used with great care.
408     # As APIs are updated, minor differences between versions may cause
409     # incompatibilities. Requesting a specific version will avoid this issue.
410     #
411     # @param [String, Symbol] api The name of the service.
412     #
413     # @return [Google::APIClient::API] The service object.
414     def preferred_version(api)
415       if !api.kind_of?(String) && !api.kind_of?(Symbol)
416         raise TypeError,
417           "Expected String or Symbol, got #{api.class}."
418       end
419       api = api.to_s
420       # TODO(bobaman): Update to use directory API.
421       return self.discovered_apis.detect do |a|
422         a.name == api && a.preferred == true
423       end
424     end
425
426     ##
427     # Generates a request.
428     #
429     # @param [Google::APIClient::Method, String] api_method
430     #   The method object or the RPC name of the method being executed.
431     # @param [Hash, Array] parameters
432     #   The parameters to send to the method.
433     # @param [String] body The body of the request.
434     # @param [Hash, Array] headers The HTTP headers for the request.
435     # @param [Hash] options
436     #   The configuration parameters for the request.
437     #   - <code>:version</code> — 
438     #     The service version.  Only used if <code>api_method</code> is a
439     #     <code>String</code>.  Defaults to <code>'v1'</code>.
440     #   - <code>:parser</code> — 
441     #     The parser for the response.
442     #   - <code>:authorization</code> — 
443     #     The authorization mechanism for the response.  Used only if
444     #     <code>:authenticated</code> is <code>true</code>.
445     #   - <code>:authenticated</code> — 
446     #     <code>true</code> if the request must be signed or otherwise
447     #     authenticated, <code>false</code>
448     #     otherwise.  Defaults to <code>true</code> if an authorization
449     #     mechanism has been set, <code>false</code> otherwise.
450     #
451     # @return [Array] The generated request.
452     #
453     # @example
454     #   request = client.generate_request(
455     #     'chili.activities.list',
456     #     {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
457     #   )
458     #   method, uri, headers, body = request
459     def generate_request(
460         api_method, parameters={}, body='', headers=[], options={})
461       options={
462         :parser => self.parser,
463         :version => 'v1',
464         :authorization => self.authorization
465       }.merge(options)
466       # The default value for the :authenticated option depends on whether an
467       # authorization mechanism has been set.
468       if options[:authorization]
469         options = {:authenticated => true}.merge(options)
470       else
471         options = {:authenticated => false}.merge(options)
472       end
473       if api_method.kind_of?(String) || api_method.kind_of?(Symbol)
474         api_method = api_method.to_s
475         # This method of guessing the API is unreliable. This will fail for
476         # APIs where the first segment of the RPC name does not match the
477         # service name. However, this is a fallback mechanism anyway.
478         # Developers should be passing in a reference to the method, rather
479         # than passing in a string or symbol. This should raise an error
480         # in the case of a mismatch.
481         api = api_method[/^([^.]+)\./, 1]
482         api_method = self.discovered_method(
483           api_method, api, options[:version]
484         )
485       elsif !api_method.kind_of?(::Google::APIClient::Method)
486         raise TypeError,
487           "Expected String, Symbol, or Google::APIClient::Method, " +
488           "got #{api_method.class}."
489       end
490       unless api_method
491         raise ArgumentError, "API method could not be found."
492       end
493       request = api_method.generate_request(parameters, body, headers)
494       if options[:authenticated]
495         request = self.generate_authenticated_request(:request => request)
496       end
497       return request
498     end
499
500     ##
501     # Generates a request and transmits it.
502     #
503     # @param [Google::APIClient::Method, String] api_method
504     #   The method object or the RPC name of the method being executed.
505     # @param [Hash, Array] parameters
506     #   The parameters to send to the method.
507     # @param [String] body The body of the request.
508     # @param [Hash, Array] headers The HTTP headers for the request.
509     # @param [Hash] options
510     #   The configuration parameters for the request.
511     #   - <code>:version</code> — 
512     #     The service version.  Only used if <code>api_method</code> is a
513     #     <code>String</code>.  Defaults to <code>'v1'</code>.
514     #   - <code>:adapter</code> — 
515     #     The HTTP adapter.
516     #   - <code>:parser</code> — 
517     #     The parser for the response.
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     #   response = client.execute(
530     #     'chili.activities.list',
531     #     {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
532     #   )
533     #   status, headers, body = response
534     def execute(api_method, parameters={}, body='', headers=[], options={})
535       request = self.generate_request(
536         api_method, parameters, body, headers, options
537       )
538       return self.transmit_request(
539         request,
540         options[:adapter] || self.http_adapter
541       )
542     end
543
544     ##
545     # Transmits the request using the current HTTP adapter.
546     #
547     # @param [Array] request The request to transmit.
548     # @param [#transmit] adapter The HTTP adapter.
549     #
550     # @return [Array] The response from the server.
551     def transmit_request(request, adapter=self.http_adapter)
552       if self.user_agent != nil
553         # If there's no User-Agent header, set one.
554         method, uri, headers, body = request
555         unless headers.kind_of?(Enumerable)
556           # We need to use some Enumerable methods, relying on the presence of
557           # the #each method.
558           class <<headers
559             include Enumerable
560           end
561         end
562         if self.user_agent.kind_of?(String)
563           unless headers.any? { |k, v| k.downcase == 'User-Agent'.downcase }
564             headers = headers.to_a.insert(0, ['User-Agent', self.user_agent])
565           end
566         elsif self.user_agent != nil
567           raise TypeError,
568             "Expected User-Agent to be String, got #{self.user_agent.class}"
569         end
570       end
571       adapter.transmit([method, uri, headers, body])
572     end
573
574     ##
575     # Signs a request using the current authorization mechanism.
576     #
577     # @param [Hash] options The options to pass through.
578     #
579     # @return [Array] The signed or otherwise authenticated request.
580     def generate_authenticated_request(options={})
581       return authorization.generate_authenticated_request(options)
582     end
583   end
584 end
585
586 require 'google/api_client/version'