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