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