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