Remove embedded version #s, use Gemfile
[arvados.git] / lib / google / api_client / reference.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 'addressable/uri'
21 require 'stringio'
22 require 'google/api_client/discovery'
23
24 # TODO - needs some serious cleanup
25
26 module Google
27   class APIClient
28     class Reference
29       MULTIPART_BOUNDARY = "-----------RubyApiMultipartPost".freeze
30
31       def initialize(options={})
32         # We only need this to do lookups on method ID String values
33         # It's optional, but method ID lookups will fail if the client is
34         # omitted.
35         @client = options[:client]
36         @version = options[:version] || 'v1'
37
38         self.connection = options[:connection] || Faraday.default_connection
39         self.authorization = options[:authorization]
40         self.api_method = options[:api_method]
41         self.parameters = options[:parameters] || {}
42         # These parameters are handled differently because they're not
43         # parameters to the API method, but rather to the API system.
44         if self.parameters.kind_of?(Array)
45           if options[:key]
46             self.parameters.reject! { |k, _| k == 'key' }
47             self.parameters << ['key', options[:key]]
48           end
49           if options[:user_ip]
50             self.parameters.reject! { |k, _| k == 'userIp' }
51             self.parameters << ['userIp', options[:user_ip]]
52           end
53         elsif self.parameters.kind_of?(Hash)
54           self.parameters['key'] ||= options[:key] if options[:key]
55           self.parameters['userIp'] ||= options[:user_ip] if options[:user_ip]
56           # Convert to Array, because they're easier to work with when
57           # repeated parameters are an issue.
58           self.parameters = self.parameters.to_a
59         else
60           raise TypeError,
61             "Expected Array or Hash, got #{self.parameters.class}."
62         end
63         self.headers = options[:headers] || {}
64         if options[:media]
65           self.media = options[:media]
66           upload_type = self.parameters.find { |(k, _)| ['uploadType', 'upload_type'].include?(k) }.last
67           case upload_type
68           when "media"
69             if options[:body] || options[:body_object]
70               raise ArgumentError,
71                 "Can not specify body & body object for simple uploads."
72             end
73             self.headers['Content-Type'] ||= self.media.content_type
74             self.body = self.media
75           when "multipart"
76             unless options[:body_object]
77               raise ArgumentError, "Multipart requested but no body object."
78             end
79             # This is all a bit of a hack due to Signet requiring body to be a
80             # string. Ideally, update Signet to delay serialization so we can
81             # just pass streams all the way down through to the HTTP library.
82             metadata = StringIO.new(serialize_body(options[:body_object]))
83             env = {
84               :request_headers => {
85                 'Content-Type' =>
86                   "multipart/related;boundary=#{MULTIPART_BOUNDARY}"
87               },
88               :request => {:boundary => MULTIPART_BOUNDARY}
89             }
90             multipart = Faraday::Request::Multipart.new
91             self.body = multipart.create_multipart(env, [
92               [nil, Faraday::UploadIO.new(
93                 metadata, 'application/json', 'file.json'
94               )],
95               [nil, self.media]])
96             self.headers.update(env[:request_headers])
97           when "resumable"
98             file_length = self.media.length
99             self.headers['X-Upload-Content-Type'] = self.media.content_type
100             self.headers['X-Upload-Content-Length'] = file_length.to_s
101             if options[:body_object]
102               self.headers['Content-Type'] ||= 'application/json'
103               self.body = serialize_body(options[:body_object])
104             else
105               self.body = ''
106             end
107           else
108             raise ArgumentError, "Invalid uploadType for media."
109           end
110         elsif options[:body]
111           self.body = options[:body]
112         elsif options[:body_object]
113           self.headers['Content-Type'] ||= 'application/json'
114           self.body = serialize_body(options[:body_object])
115         else
116           self.body = ''
117         end
118         unless self.api_method
119           self.http_method = options[:http_method] || 'GET'
120           self.uri = options[:uri]
121           unless self.parameters.empty?
122             query_values = (self.uri.query_values(Array) || [])
123             self.uri.query = Addressable::URI.form_encode(
124               (query_values + self.parameters).sort
125             )
126             self.uri.query = nil if self.uri.query == ""
127           end
128         end
129       end
130
131       def serialize_body(body)
132         return body.to_json if body.respond_to?(:to_json)
133         return MultiJson.dump(options[:body_object].to_hash) if body.respond_to?(:to_hash)
134         raise TypeError, 'Could not convert body object to JSON.' +
135                          'Must respond to :to_json or :to_hash.'
136       end
137
138       def media
139         return @media
140       end
141
142       def media=(media)
143         @media = (media)
144       end
145
146       def authorization
147         return @authorization
148       end
149
150       def authorization=(new_authorization)
151         @authorization = new_authorization
152       end
153
154       def connection
155         return @connection
156       end
157
158       def connection=(new_connection)
159         if new_connection.kind_of?(Faraday::Connection)
160           @connection = new_connection
161         else
162           raise TypeError,
163             "Expected Faraday::Connection, got #{new_connection.class}."
164         end
165       end
166
167       def api_method
168         return @api_method
169       end
170
171       def api_method=(new_api_method)
172         if new_api_method.kind_of?(Google::APIClient::Method) ||
173             new_api_method == nil
174           @api_method = new_api_method
175         elsif new_api_method.respond_to?(:to_str) ||
176             new_api_method.kind_of?(Symbol)
177           unless @client
178             raise ArgumentError,
179               "API method lookup impossible without client instance."
180           end
181           new_api_method = new_api_method.to_s
182           # This method of guessing the API is unreliable. This will fail for
183           # APIs where the first segment of the RPC name does not match the
184           # service name. However, this is a fallback mechanism anyway.
185           # Developers should be passing in a reference to the method, rather
186           # than passing in a string or symbol. This should raise an error
187           # in the case of a mismatch.
188           api = new_api_method[/^([^.]+)\./, 1]
189           @api_method = @client.discovered_method(
190             new_api_method, api, @version
191           )
192           if @api_method
193             # Ditch the client reference, we won't need it again.
194             @client = nil
195           else
196             raise ArgumentError, "API method could not be found."
197           end
198         else
199           raise TypeError,
200             "Expected Google::APIClient::Method, got #{new_api_method.class}."
201         end
202       end
203
204       def parameters
205         return @parameters
206       end
207
208       def parameters=(new_parameters)
209         # No type-checking needed, the Method class handles this.
210         @parameters = new_parameters
211       end
212
213       def body
214         return @body
215       end
216
217       def body=(new_body)
218         if new_body.respond_to?(:to_str)
219           @body = new_body.to_str
220         elsif new_body.respond_to?(:read)
221           @body = new_body.read()
222         elsif new_body.respond_to?(:inject)
223           @body = (new_body.inject(StringIO.new) do |accu, chunk|
224             accu.write(chunk)
225             accu
226           end).string
227         else
228           raise TypeError,
229             "Expected body to be String, IO, or Enumerable chunks."
230         end
231       end
232
233       def headers
234         return @headers ||= {}
235       end
236
237       def headers=(new_headers)
238         if new_headers.kind_of?(Array) || new_headers.kind_of?(Hash)
239           @headers = new_headers
240         else
241           raise TypeError, "Expected Hash or Array, got #{new_headers.class}."
242         end
243       end
244
245       def http_method
246         return @http_method ||= self.api_method.http_method
247       end
248
249       def http_method=(new_http_method)
250         if new_http_method.kind_of?(Symbol)
251           @http_method = new_http_method.to_s.upcase
252         elsif new_http_method.respond_to?(:to_str)
253           @http_method = new_http_method.to_str.upcase
254         else
255           raise TypeError,
256             "Expected String or Symbol, got #{new_http_method.class}."
257         end
258       end
259
260       def uri
261         return @uri ||= self.api_method.generate_uri(self.parameters)
262       end
263
264       def uri=(new_uri)
265         @uri = Addressable::URI.parse(new_uri)
266       end
267
268       def to_request
269         if self.api_method
270           return self.api_method.generate_request(
271             self.parameters, self.body, self.headers,
272             :connection => self.connection
273           )
274         else
275           return self.connection.build_request(
276             self.http_method.to_s.downcase.to_sym
277           ) do |req|
278             req.url(Addressable::URI.parse(self.uri).normalize.to_s)
279             req.headers = Faraday::Utils::Headers.new(self.headers)
280             req.body = self.body
281           end
282         end
283       end
284
285       def to_hash
286         options = {}
287         if self.api_method
288           options[:api_method] = self.api_method
289           options[:parameters] = self.parameters
290         else
291           options[:http_method] = self.http_method
292           options[:uri] = self.uri
293         end
294         options[:headers] = self.headers
295         options[:body] = self.body
296         options[:connection] = self.connection
297         unless self.authorization.nil?
298           options[:authorization] = self.authorization
299         end
300         return options
301       end
302     end
303   end
304 end