Removed :nodoc: directives, as they are not understood by YARD.
[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 require 'httpadapter'
16 require 'json'
17
18 require 'google/api_client/discovery'
19
20 module Google
21   # TODO(bobaman): Document all this stuff.
22
23   ##
24   # This class manages communication with a single API.
25   class APIClient
26     ##
27     # An error which is raised when there is an unexpected response or other
28     # transport error that prevents an operation from succeeding.
29     class TransmissionError < StandardError
30     end
31
32     def initialize(options={})
33       @options = {
34         # TODO: What configuration options need to go here?
35       }.merge(options)
36       if !self.authorization.respond_to?(:generate_authenticated_request)
37         raise TypeError,
38           'Expected authorization mechanism to respond to ' +
39           '#generate_authenticated_request.'
40       end
41     end
42
43     ##
44     # Returns the parser used by the client.
45     def parser
46       unless @options[:parser]
47         require 'google/api_client/parsers/json_parser'
48         # NOTE: Do not rely on this default value, as it may change
49         @options[:parser] = JSONParser
50       end
51       return @options[:parser]
52     end
53
54     ##
55     # Returns the authorization mechanism used by the client.
56     def authorization
57       unless @options[:authorization]
58         require 'signet/oauth_1/client'
59         # NOTE: Do not rely on this default value, as it may change
60         @options[:authorization] = Signet::OAuth1::Client.new(
61           :temporary_credential_uri =>
62             'https://www.google.com/accounts/OAuthGetRequestToken',
63           :authorization_uri =>
64             'https://www.google.com/accounts/OAuthAuthorizeToken',
65           :token_credential_uri =>
66             'https://www.google.com/accounts/OAuthGetAccessToken',
67           :client_credential_key => 'anonymous',
68           :client_credential_secret => 'anonymous'
69         )
70       end
71       return @options[:authorization]
72     end
73
74     ##
75     # Returns the HTTP adapter used by the client.
76     def http_adapter
77       return @options[:http_adapter] ||= (begin
78         require 'httpadapter/adapters/net_http'
79         @options[:http_adapter] = HTTPAdapter::NetHTTPRequestAdapter
80       end)
81     end
82
83     ##
84     # Returns the URI for the discovery document.
85     #
86     # @return [Addressable::URI] The URI of the discovery document.
87     def discovery_uri
88       return @options[:discovery_uri] ||= (begin
89         if @options[:service]
90           service_id = @options[:service]
91           service_version = @options[:service_version] || 'v1'
92           Addressable::URI.parse(
93             "http://www.googleapis.com/discovery/0.1/describe" +
94             "?api=#{service_id}"
95           )
96         else
97           raise ArgumentError,
98             'Missing required configuration value, :discovery_uri.'
99         end
100       end)
101     end
102
103     ##
104     # Returns the parsed discovery document.
105     #
106     # @return [Hash] The parsed JSON from the discovery document.
107     def discovery_document
108       return @discovery_document ||= (begin
109         request = ['GET', self.discovery_uri.to_s, [], []]
110         response = self.transmit_request(request)
111         status, headers, body = response
112         if status == 200 # TODO(bobaman) Better status code handling?
113           merged_body = StringIO.new
114           body.each do |chunk|
115             merged_body.write(chunk)
116           end
117           merged_body.rewind
118           JSON.parse(merged_body.string)
119         else
120           raise TransmissionError,
121             "Could not retrieve discovery document at: #{self.discovery_uri}"
122         end
123       end)
124     end
125
126     ##
127     # Returns a list of services this client instance has performed discovery
128     # for.  This may return multiple versions of the same service.
129     #
130     # @return [Array]
131     #   A list of discovered <code>Google::APIClient::Service</code> objects.
132     def discovered_services
133       return @discovered_services ||= (begin
134         service_names = self.discovery_document['data'].keys()
135         services = []
136         for service_name in service_names
137           versions = self.discovery_document['data'][service_name]
138           for service_version in versions.keys()
139             service_description =
140               self.discovery_document['data'][service_name][service_version]
141             services << ::Google::APIClient::Service.new(
142               service_name,
143               service_version,
144               service_description
145             )
146           end
147         end
148         services
149       end)
150     end
151
152     ##
153     # Returns the service object for a given service name and service version.
154     #
155     # @param [String, Symbol] service_name The service name.
156     # @param [String] service_version The desired version of the service.
157     #
158     # @return [Google::APIClient::Service] The service object.
159     def discovered_service(service_name, service_version='v1')
160       if !service_name.kind_of?(String) && !service_name.kind_of?(Symbol)
161         raise TypeError,
162           "Expected String or Symbol, got #{service_name.class}."
163       end
164       service_name = service_name.to_s
165       for service in self.discovered_services
166         if service.name == service_name &&
167             service.version.to_s == service_version.to_s
168           return service
169         end
170       end
171       return nil
172     end
173
174     ##
175     # Returns the method object for a given RPC name and service version.
176     #
177     # @param [String, Symbol] rpc_name The RPC name of the desired method.
178     # @param [String] service_version The desired version of the service.
179     #
180     # @return [Google::APIClient::Method] The method object.
181     def discovered_method(rpc_name, service_version='v1')
182       if !rpc_name.kind_of?(String) && !rpc_name.kind_of?(Symbol)
183         raise TypeError,
184           "Expected String or Symbol, got #{rpc_name.class}."
185       end
186       rpc_name = rpc_name.to_s
187       for service in self.discovered_services
188         # This looks kinda weird, but is not a real problem because there's
189         # almost always only one service, and this is memoized anyhow.
190         if service.version.to_s == service_version.to_s
191           return service.to_h[rpc_name] if service.to_h[rpc_name]
192         end
193       end
194       return nil
195     end
196
197     ##
198     # Returns the service object with the highest version number.
199     #
200     # <em>Warning</em>: This method should be used with great care. As APIs
201     # are updated, minor differences between versions may cause
202     # incompatibilities. Requesting a specific version will avoid this issue.
203     #
204     # @param [String, Symbol] service_name The name of the service.
205     #
206     # @return [Google::APIClient::Service] The service object.
207     def latest_service(service_name)
208       if !service_name.kind_of?(String) && !service_name.kind_of?(Symbol)
209         raise TypeError,
210           "Expected String or Symbol, got #{service_name.class}."
211       end
212       service_name = service_name.to_s
213       versions = {}
214       for service in self.discovered_services
215         next if service.name != service_name
216         sortable_version = service.version.gsub(/^v/, '').split('.').map do |v|
217           v.to_i
218         end
219         versions[sortable_version] = service
220       end
221       return versions[versions.keys.sort.last]
222     end
223
224     ##
225     # Generates a request.
226     #
227     # @param [Google::APIClient::Method, String] api_method
228     #   The method object or the RPC name of the method being executed.
229     # @param [Hash, Array] parameters
230     #   The parameters to send to the method.
231     # @param [String] The body of the request.
232     # @param [Hash, Array] headers The HTTP headers for the request.
233     # @param [Hash] options
234     #   The configuration parameters for the request.
235     #   - <code>:service_version</code> — 
236     #     The service version.  Only used if <code>api_method</code> is a
237     #     <code>String</code>.  Defaults to <code>'v1'</code>.
238     #   - <code>:parser</code> — 
239     #     The parser for the response.
240     #   - <code>:authorization</code> — 
241     #     The authorization mechanism for the response.  Used only if
242     #     <code>:signed</code> is <code>true</code>.
243     #   - <code>:signed</code> — 
244     #     <code>true</code> if the request must be signed, <code>false</code>
245     #     otherwise.  Defaults to <code>true</code>.
246     #
247     # @return [Array] The generated request.
248     def generate_request(
249         api_method, parameters={}, body='', headers=[], options={})
250       options={
251         :signed => true,
252         :parser => self.parser,
253         :service_version => 'v1',
254         :authorization => self.authorization
255       }.merge(options)
256       if api_method.kind_of?(String) || api_method.kind_of?(Symbol)
257         api_method = self.discovered_method(
258           api_method.to_s, options[:service_version]
259         )
260       elsif !api_method.kind_of?(::Google::APIClient::Service)
261         raise TypeError,
262           "Expected String, Symbol, or Google::APIClient::Service, " +
263           "got #{api_method.class}."
264       end
265       unless api_method
266         raise ArgumentError, "API method does not exist."
267       end
268       request = api_method.generate_request(parameters, body, headers)
269       if options[:signed]
270         request = self.sign_request(request, options[:authorization])
271       end
272       return request
273     end
274
275     ##
276     # Generates a request and transmits it.
277     #
278     # @param [Google::APIClient::Method, String] api_method
279     #   The method object or the RPC name of the method being executed.
280     # @param [Hash, Array] parameters
281     #   The parameters to send to the method.
282     # @param [String] The body of the request.
283     # @param [Hash, Array] headers The HTTP headers for the request.
284     # @param [Hash] options
285     #   The configuration parameters for the request.
286     #   - <code>:service_version</code> — 
287     #     The service version.  Only used if <code>api_method</code> is a
288     #     <code>String</code>.  Defaults to <code>'v1'</code>.
289     #   - <code>:adapter</code> — 
290     #     The HTTP adapter.
291     #   - <code>:parser</code> — 
292     #     The parser for the response.
293     #   - <code>:authorization</code> — 
294     #     The authorization mechanism for the response.  Used only if
295     #     <code>:signed</code> is <code>true</code>.
296     #   - <code>:signed</code> — 
297     #     <code>true</code> if the request must be signed, <code>false</code>
298     #     otherwise.  Defaults to <code>true</code>.
299     #
300     # @return [Array] The response from the API.
301     def execute(api_method, parameters={}, body='', headers=[], options={})
302       request = self.generate_request(
303         api_method, parameters, body, headers, options
304       )
305       return self.transmit_request(
306         request,
307         options[:adapter] || self.http_adapter
308       )
309     end
310
311     ##
312     # Transmits the request using the current HTTP adapter.
313     #
314     # @param [Array] request The request to transmit.
315     # @param [#transmit] adapter The HTTP adapter.
316     #
317     # @return [Array] The response from the server.
318     def transmit_request(request, adapter=self.http_adapter)
319       ::HTTPAdapter.transmit(request, adapter)
320     end
321
322     ##
323     # Signs a request using the current authorization mechanism.
324     #
325     # @param [Array] request The request to sign.
326     # @param [#generate_authenticated_request] authorization
327     #   The authorization mechanism.
328     #
329     # @return [Array] The signed request.
330     def sign_request(request, authorization=self.authorization)
331       return authorization.generate_authenticated_request(
332         :request => request
333       )
334     end
335   end
336 end
337
338 require 'google/api_client/version'