google-api-client 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,375 @@
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
+ # Force immediate type-checking and short-cut resolution
37
+ self.parser
38
+ self.authorization
39
+ self.http_adapter
40
+ return self
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
+ #
57
+ # @return [#generate_authenticated_request] The authorization mechanism.
58
+ def authorization
59
+ case @options[:authorization]
60
+ when :oauth_1, :oauth
61
+ require 'signet/oauth_1/client'
62
+ # NOTE: Do not rely on this default value, as it may change
63
+ @options[:authorization] = Signet::OAuth1::Client.new(
64
+ :temporary_credential_uri =>
65
+ 'https://www.google.com/accounts/OAuthGetRequestToken',
66
+ :authorization_uri =>
67
+ 'https://www.google.com/accounts/OAuthAuthorizeToken',
68
+ :token_credential_uri =>
69
+ 'https://www.google.com/accounts/OAuthGetAccessToken',
70
+ :client_credential_key => 'anonymous',
71
+ :client_credential_secret => 'anonymous'
72
+ )
73
+ when nil
74
+ # No authorization mechanism
75
+ else
76
+ if !@options[:authorization].respond_to?(
77
+ :generate_authenticated_request)
78
+ raise TypeError,
79
+ 'Expected authorization mechanism to respond to ' +
80
+ '#generate_authenticated_request.'
81
+ end
82
+ end
83
+ return @options[:authorization]
84
+ end
85
+
86
+ ##
87
+ # Sets the authorization mechanism used by the client.
88
+ #
89
+ # @param [#generate_authenticated_request] new_authorization
90
+ # The new authorization mechanism.
91
+ def authorization=(new_authorization)
92
+ @options[:authorization] = new_authorization
93
+ return self.authorization
94
+ end
95
+
96
+ ##
97
+ # Returns the HTTP adapter used by the client.
98
+ def http_adapter
99
+ return @options[:http_adapter] ||= (begin
100
+ require 'httpadapter/adapters/net_http'
101
+ @options[:http_adapter] = HTTPAdapter::NetHTTPRequestAdapter
102
+ end)
103
+ end
104
+
105
+ ##
106
+ # Returns the URI for the discovery document.
107
+ #
108
+ # @return [Addressable::URI] The URI of the discovery document.
109
+ def discovery_uri
110
+ return @options[:discovery_uri] ||= (begin
111
+ if @options[:service]
112
+ service_id = @options[:service]
113
+ service_version = @options[:service_version] || 'v1'
114
+ Addressable::URI.parse(
115
+ "http://www.googleapis.com/discovery/0.1/describe" +
116
+ "?api=#{service_id}"
117
+ )
118
+ else
119
+ raise ArgumentError,
120
+ 'Missing required configuration value, :discovery_uri.'
121
+ end
122
+ end)
123
+ end
124
+
125
+ ##
126
+ # Returns the parsed discovery document.
127
+ #
128
+ # @return [Hash] The parsed JSON from the discovery document.
129
+ def discovery_document
130
+ return @discovery_document ||= (begin
131
+ request = ['GET', self.discovery_uri.to_s, [], []]
132
+ response = self.transmit_request(request)
133
+ status, headers, body = response
134
+ if status == 200 # TODO(bobaman) Better status code handling?
135
+ merged_body = StringIO.new
136
+ body.each do |chunk|
137
+ merged_body.write(chunk)
138
+ end
139
+ merged_body.rewind
140
+ JSON.parse(merged_body.string)
141
+ else
142
+ raise TransmissionError,
143
+ "Could not retrieve discovery document at: #{self.discovery_uri}"
144
+ end
145
+ end)
146
+ end
147
+
148
+ ##
149
+ # Returns a list of services this client instance has performed discovery
150
+ # for. This may return multiple versions of the same service.
151
+ #
152
+ # @return [Array]
153
+ # A list of discovered <code>Google::APIClient::Service</code> objects.
154
+ def discovered_services
155
+ return @discovered_services ||= (begin
156
+ service_names = self.discovery_document['data'].keys()
157
+ services = []
158
+ for service_name in service_names
159
+ versions = self.discovery_document['data'][service_name]
160
+ for service_version in versions.keys()
161
+ service_description =
162
+ self.discovery_document['data'][service_name][service_version]
163
+ services << ::Google::APIClient::Service.new(
164
+ service_name,
165
+ service_version,
166
+ service_description
167
+ )
168
+ end
169
+ end
170
+ services
171
+ end)
172
+ end
173
+
174
+ ##
175
+ # Returns the service object for a given service name and service version.
176
+ #
177
+ # @param [String, Symbol] service_name The service name.
178
+ # @param [String] service_version The desired version of the service.
179
+ #
180
+ # @return [Google::APIClient::Service] The service object.
181
+ def discovered_service(service_name, service_version='v1')
182
+ if !service_name.kind_of?(String) && !service_name.kind_of?(Symbol)
183
+ raise TypeError,
184
+ "Expected String or Symbol, got #{service_name.class}."
185
+ end
186
+ service_name = service_name.to_s
187
+ for service in self.discovered_services
188
+ if service.name == service_name &&
189
+ service.version.to_s == service_version.to_s
190
+ return service
191
+ end
192
+ end
193
+ return nil
194
+ end
195
+
196
+ ##
197
+ # Returns the method object for a given RPC name and service version.
198
+ #
199
+ # @param [String, Symbol] rpc_name The RPC name of the desired method.
200
+ # @param [String] service_version The desired version of the service.
201
+ #
202
+ # @return [Google::APIClient::Method] The method object.
203
+ def discovered_method(rpc_name, service_version='v1')
204
+ if !rpc_name.kind_of?(String) && !rpc_name.kind_of?(Symbol)
205
+ raise TypeError,
206
+ "Expected String or Symbol, got #{rpc_name.class}."
207
+ end
208
+ rpc_name = rpc_name.to_s
209
+ for service in self.discovered_services
210
+ # This looks kinda weird, but is not a real problem because there's
211
+ # almost always only one service, and this is memoized anyhow.
212
+ if service.version.to_s == service_version.to_s
213
+ return service.to_h[rpc_name] if service.to_h[rpc_name]
214
+ end
215
+ end
216
+ return nil
217
+ end
218
+
219
+ ##
220
+ # Returns the service object with the highest version number.
221
+ #
222
+ # <em>Warning</em>: This method should be used with great care. As APIs
223
+ # are updated, minor differences between versions may cause
224
+ # incompatibilities. Requesting a specific version will avoid this issue.
225
+ #
226
+ # @param [String, Symbol] service_name The name of the service.
227
+ #
228
+ # @return [Google::APIClient::Service] The service object.
229
+ def latest_service_version(service_name)
230
+ if !service_name.kind_of?(String) && !service_name.kind_of?(Symbol)
231
+ raise TypeError,
232
+ "Expected String or Symbol, got #{service_name.class}."
233
+ end
234
+ service_name = service_name.to_s
235
+ return (self.discovered_services.select do |service|
236
+ service.name == service_name
237
+ end).sort.last
238
+ end
239
+
240
+ ##
241
+ # Generates a request.
242
+ #
243
+ # @param [Google::APIClient::Method, String] api_method
244
+ # The method object or the RPC name of the method being executed.
245
+ # @param [Hash, Array] parameters
246
+ # The parameters to send to the method.
247
+ # @param [String] body The body of the request.
248
+ # @param [Hash, Array] headers The HTTP headers for the request.
249
+ # @param [Hash] options
250
+ # The configuration parameters for the request.
251
+ # - <code>:service_version</code> —
252
+ # The service version. Only used if <code>api_method</code> is a
253
+ # <code>String</code>. Defaults to <code>'v1'</code>.
254
+ # - <code>:parser</code> —
255
+ # The parser for the response.
256
+ # - <code>:authorization</code> —
257
+ # The authorization mechanism for the response. Used only if
258
+ # <code>:signed</code> is <code>true</code>.
259
+ # - <code>:signed</code> —
260
+ # <code>true</code> if the request must be signed, <code>false</code>
261
+ # otherwise. Defaults to <code>true</code> if an authorization
262
+ # mechanism has been set, <code>false</code> otherwise.
263
+ #
264
+ # @return [Array] The generated request.
265
+ #
266
+ # @example
267
+ # request = client.generate_request(
268
+ # 'chili.activities.list',
269
+ # {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
270
+ # )
271
+ # method, uri, headers, body = request
272
+ def generate_request(
273
+ api_method, parameters={}, body='', headers=[], options={})
274
+ options={
275
+ :parser => self.parser,
276
+ :service_version => 'v1',
277
+ :authorization => self.authorization
278
+ }.merge(options)
279
+ # The default value for the :signed option depends on whether an
280
+ # authorization mechanism has been set.
281
+ if options[:authorization]
282
+ options = {:signed => true}.merge(options)
283
+ else
284
+ options = {:signed => false}.merge(options)
285
+ end
286
+ if api_method.kind_of?(String) || api_method.kind_of?(Symbol)
287
+ api_method = self.discovered_method(
288
+ api_method.to_s, options[:service_version]
289
+ )
290
+ elsif !api_method.kind_of?(::Google::APIClient::Method)
291
+ raise TypeError,
292
+ "Expected String, Symbol, or Google::APIClient::Method, " +
293
+ "got #{api_method.class}."
294
+ end
295
+ unless api_method
296
+ raise ArgumentError, "API method could not be found."
297
+ end
298
+ request = api_method.generate_request(parameters, body, headers)
299
+ if options[:signed]
300
+ request = self.sign_request(request, options[:authorization])
301
+ end
302
+ return request
303
+ end
304
+
305
+ ##
306
+ # Generates a request and transmits it.
307
+ #
308
+ # @param [Google::APIClient::Method, String] api_method
309
+ # The method object or the RPC name of the method being executed.
310
+ # @param [Hash, Array] parameters
311
+ # The parameters to send to the method.
312
+ # @param [String] body The body of the request.
313
+ # @param [Hash, Array] headers The HTTP headers for the request.
314
+ # @param [Hash] options
315
+ # The configuration parameters for the request.
316
+ # - <code>:service_version</code> —
317
+ # The service version. Only used if <code>api_method</code> is a
318
+ # <code>String</code>. Defaults to <code>'v1'</code>.
319
+ # - <code>:adapter</code> —
320
+ # The HTTP adapter.
321
+ # - <code>:parser</code> —
322
+ # The parser for the response.
323
+ # - <code>:authorization</code> —
324
+ # The authorization mechanism for the response. Used only if
325
+ # <code>:signed</code> is <code>true</code>.
326
+ # - <code>:signed</code> —
327
+ # <code>true</code> if the request must be signed, <code>false</code>
328
+ # otherwise. Defaults to <code>true</code>.
329
+ #
330
+ # @return [Array] The response from the API.
331
+ #
332
+ # @example
333
+ # response = client.execute(
334
+ # 'chili.activities.list',
335
+ # {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
336
+ # )
337
+ # status, headers, body = response
338
+ def execute(api_method, parameters={}, body='', headers=[], options={})
339
+ request = self.generate_request(
340
+ api_method, parameters, body, headers, options
341
+ )
342
+ return self.transmit_request(
343
+ request,
344
+ options[:adapter] || self.http_adapter
345
+ )
346
+ end
347
+
348
+ ##
349
+ # Transmits the request using the current HTTP adapter.
350
+ #
351
+ # @param [Array] request The request to transmit.
352
+ # @param [#transmit] adapter The HTTP adapter.
353
+ #
354
+ # @return [Array] The response from the server.
355
+ def transmit_request(request, adapter=self.http_adapter)
356
+ ::HTTPAdapter.transmit(request, adapter)
357
+ end
358
+
359
+ ##
360
+ # Signs a request using the current authorization mechanism.
361
+ #
362
+ # @param [Array] request The request to sign.
363
+ # @param [#generate_authenticated_request] authorization
364
+ # The authorization mechanism.
365
+ #
366
+ # @return [Array] The signed request.
367
+ def sign_request(request, authorization=self.authorization)
368
+ return authorization.generate_authenticated_request(
369
+ :request => request
370
+ )
371
+ end
372
+ end
373
+ end
374
+
375
+ require 'google/api_client/version'
@@ -0,0 +1,535 @@
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 'json'
16
+ require 'addressable/uri'
17
+ require 'addressable/template'
18
+ require 'extlib/inflection'
19
+
20
+ module Google
21
+ class APIClient
22
+ ##
23
+ # An exception that is raised if a method is called with missing or
24
+ # invalid parameter values.
25
+ class ValidationError < StandardError
26
+ end
27
+
28
+ ##
29
+ # A service that has been described by a discovery document.
30
+ class Service
31
+
32
+ ##
33
+ # Creates a description of a particular version of a service.
34
+ #
35
+ # @param [String] service_name
36
+ # The identifier for the service. Note that while this frequently
37
+ # matches the first segment of all of the service's RPC names, this
38
+ # should not be assumed. There is no requirement that these match.
39
+ # @param [String] service_version
40
+ # The identifier for the service version.
41
+ # @param [Hash] service_description
42
+ # The section of the discovery document that applies to this service
43
+ # version.
44
+ #
45
+ # @return [Google::APIClient::Service] The constructed service object.
46
+ def initialize(service_name, service_version, service_description)
47
+ @name = service_name
48
+ @version = service_version
49
+ @description = service_description
50
+ metaclass = (class <<self; self; end)
51
+ self.resources.each do |resource|
52
+ method_name = Extlib::Inflection.underscore(resource.name).to_sym
53
+ if !self.respond_to?(method_name)
54
+ metaclass.send(:define_method, method_name) { resource }
55
+ end
56
+ end
57
+ self.methods.each do |method|
58
+ method_name = Extlib::Inflection.underscore(method.name).to_sym
59
+ if !self.respond_to?(method_name)
60
+ metaclass.send(:define_method, method_name) { method }
61
+ end
62
+ end
63
+ end
64
+
65
+ ##
66
+ # Returns the identifier for the service.
67
+ #
68
+ # @return [String] The service identifier.
69
+ attr_reader :name
70
+
71
+ ##
72
+ # Returns the version of the service.
73
+ #
74
+ # @return [String] The service version.
75
+ attr_reader :version
76
+
77
+ ##
78
+ # Returns the parsed section of the discovery document that applies to
79
+ # this version of the service.
80
+ #
81
+ # @return [Hash] The service description.
82
+ attr_reader :description
83
+
84
+ ##
85
+ # Returns the base URI for this version of the service.
86
+ #
87
+ # @return [Addressable::URI] The base URI that methods are joined to.
88
+ def base
89
+ return @base ||= Addressable::URI.parse(self.description['baseUrl'])
90
+ end
91
+
92
+ ##
93
+ # A list of resources available at the root level of this version of the
94
+ # service.
95
+ #
96
+ # @return [Array] A list of {Google::APIClient::Resource} objects.
97
+ def resources
98
+ return @resources ||= (
99
+ (self.description['resources'] || []).inject([]) do |accu, (k, v)|
100
+ accu << ::Google::APIClient::Resource.new(self.base, k, v)
101
+ accu
102
+ end
103
+ )
104
+ end
105
+
106
+ ##
107
+ # A list of methods available at the root level of this version of the
108
+ # service.
109
+ #
110
+ # @return [Array] A list of {Google::APIClient::Method} objects.
111
+ def methods
112
+ return @methods ||= (
113
+ (self.description['methods'] || []).inject([]) do |accu, (k, v)|
114
+ accu << ::Google::APIClient::Method.new(self.base, k, v)
115
+ accu
116
+ end
117
+ )
118
+ end
119
+
120
+ ##
121
+ # Converts the service to a flat mapping of RPC names and method objects.
122
+ #
123
+ # @return [Hash] All methods available on the service.
124
+ #
125
+ # @example
126
+ # # Discover available methods
127
+ # method_names = client.discovered_service('buzz').to_h.keys
128
+ def to_h
129
+ return @hash ||= (begin
130
+ methods_hash = {}
131
+ self.methods.each do |method|
132
+ methods_hash[method.rpc_name] = method
133
+ end
134
+ self.resources.each do |resource|
135
+ methods_hash.merge!(resource.to_h)
136
+ end
137
+ methods_hash
138
+ end)
139
+ end
140
+
141
+ ##
142
+ # Compares two versions of a service.
143
+ #
144
+ # @param [Object] other The service to compare.
145
+ #
146
+ # @return [Integer]
147
+ # <code>-1</code> if the service is older than <code>other</code>.
148
+ # <code>0</code> if the service is the same as <code>other</code>.
149
+ # <code>1</code> if the service is newer than <code>other</code>.
150
+ # <code>nil</code> if the service cannot be compared to
151
+ # <code>other</code>.
152
+ def <=>(other)
153
+ # We can only compare versions of the same service
154
+ if other.kind_of?(self.class) && self.name == other.name
155
+ split_version = lambda do |version|
156
+ dotted_version = version[/^v?(\d+(.\d+)*)-?(.*?)?$/, 1]
157
+ suffix = version[/^v?(\d+(.\d+)*)-?(.*?)?$/, 3]
158
+ [dotted_version.split('.').map { |v| v.to_i }, suffix]
159
+ end
160
+ self_sortable, self_suffix = split_version.call(self.version)
161
+ other_sortable, other_suffix = split_version.call(other.version)
162
+ result = self_sortable <=> other_sortable
163
+ if result != 0
164
+ return result
165
+ # If the dotted versions are equal, check the suffix.
166
+ # An omitted suffix should be sorted after an included suffix.
167
+ elsif self_suffix == ''
168
+ return 1
169
+ elsif other_suffix == ''
170
+ return -1
171
+ else
172
+ return self_suffix <=> other_suffix
173
+ end
174
+ else
175
+ return nil
176
+ end
177
+ end
178
+
179
+ ##
180
+ # Returns a <code>String</code> representation of the service's state.
181
+ #
182
+ # @return [String] The service's state, as a <code>String</code>.
183
+ def inspect
184
+ sprintf(
185
+ "#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.name
186
+ )
187
+ end
188
+ end
189
+
190
+ ##
191
+ # A resource that has been described by a discovery document.
192
+ class Resource
193
+
194
+ ##
195
+ # Creates a description of a particular version of a resource.
196
+ #
197
+ # @param [Addressable::URI] base
198
+ # The base URI for the service.
199
+ # @param [String] resource_name
200
+ # The identifier for the resource.
201
+ # @param [Hash] resource_description
202
+ # The section of the discovery document that applies to this resource.
203
+ #
204
+ # @return [Google::APIClient::Resource] The constructed resource object.
205
+ def initialize(base, resource_name, resource_description)
206
+ @base = base
207
+ @name = resource_name
208
+ @description = resource_description
209
+ metaclass = (class <<self; self; end)
210
+ self.resources.each do |resource|
211
+ method_name = Extlib::Inflection.underscore(resource.name).to_sym
212
+ if !self.respond_to?(method_name)
213
+ metaclass.send(:define_method, method_name) { resource }
214
+ end
215
+ end
216
+ self.methods.each do |method|
217
+ method_name = Extlib::Inflection.underscore(method.name).to_sym
218
+ if !self.respond_to?(method_name)
219
+ metaclass.send(:define_method, method_name) { method }
220
+ end
221
+ end
222
+ end
223
+
224
+ ##
225
+ # Returns the identifier for the resource.
226
+ #
227
+ # @return [String] The resource identifier.
228
+ attr_reader :name
229
+
230
+ ##
231
+ # Returns the parsed section of the discovery document that applies to
232
+ # this resource.
233
+ #
234
+ # @return [Hash] The resource description.
235
+ attr_reader :description
236
+
237
+ ##
238
+ # Returns the base URI for this resource.
239
+ #
240
+ # @return [Addressable::URI] The base URI that methods are joined to.
241
+ attr_reader :base
242
+
243
+ ##
244
+ # A list of sub-resources available on this resource.
245
+ #
246
+ # @return [Array] A list of {Google::APIClient::Resource} objects.
247
+ def resources
248
+ return @resources ||= (
249
+ (self.description['resources'] || []).inject([]) do |accu, (k, v)|
250
+ accu << ::Google::APIClient::Resource.new(self.base, k, v)
251
+ accu
252
+ end
253
+ )
254
+ end
255
+
256
+ ##
257
+ # A list of methods available on this resource.
258
+ #
259
+ # @return [Array] A list of {Google::APIClient::Method} objects.
260
+ def methods
261
+ return @methods ||= (
262
+ (self.description['methods'] || []).inject([]) do |accu, (k, v)|
263
+ accu << ::Google::APIClient::Method.new(self.base, k, v)
264
+ accu
265
+ end
266
+ )
267
+ end
268
+
269
+ ##
270
+ # Converts the resource to a flat mapping of RPC names and method
271
+ # objects.
272
+ #
273
+ # @return [Hash] All methods available on the resource.
274
+ def to_h
275
+ return @hash ||= (begin
276
+ methods_hash = {}
277
+ self.methods.each do |method|
278
+ methods_hash[method.rpc_name] = method
279
+ end
280
+ self.resources.each do |resource|
281
+ methods_hash.merge!(resource.to_h)
282
+ end
283
+ methods_hash
284
+ end)
285
+ end
286
+
287
+ ##
288
+ # Returns a <code>String</code> representation of the resource's state.
289
+ #
290
+ # @return [String] The resource's state, as a <code>String</code>.
291
+ def inspect
292
+ sprintf(
293
+ "#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.name
294
+ )
295
+ end
296
+ end
297
+
298
+ ##
299
+ # A method that has been described by a discovery document.
300
+ class Method
301
+
302
+ ##
303
+ # Creates a description of a particular method.
304
+ #
305
+ # @param [Addressable::URI] base
306
+ # The base URI for the service.
307
+ # @param [String] method_name
308
+ # The identifier for the method.
309
+ # @param [Hash] method_description
310
+ # The section of the discovery document that applies to this method.
311
+ #
312
+ # @return [Google::APIClient::Method] The constructed method object.
313
+ def initialize(base, method_name, method_description)
314
+ @base = base
315
+ @name = method_name
316
+ @description = method_description
317
+ end
318
+
319
+ ##
320
+ # Returns the identifier for the method.
321
+ #
322
+ # @return [String] The method identifier.
323
+ attr_reader :name
324
+
325
+ ##
326
+ # Returns the parsed section of the discovery document that applies to
327
+ # this method.
328
+ #
329
+ # @return [Hash] The method description.
330
+ attr_reader :description
331
+
332
+ ##
333
+ # Returns the base URI for the method.
334
+ #
335
+ # @return [Addressable::URI]
336
+ # The base URI that this method will be joined to.
337
+ attr_reader :base
338
+
339
+ ##
340
+ # Returns the RPC name for the method.
341
+ #
342
+ # @return [String] The RPC name.
343
+ def rpc_name
344
+ return self.description['rpcName']
345
+ end
346
+
347
+ ##
348
+ # Returns the URI template for the method. A parameter list can be
349
+ # used to expand this into a URI.
350
+ #
351
+ # @return [Addressable::Template] The URI template.
352
+ def uri_template
353
+ # TODO(bobaman) We shouldn't be calling #to_s here, this should be
354
+ # a join operation on a URI, but we have to treat these as Strings
355
+ # because of the way the discovery document provides the URIs.
356
+ # This should be fixed soon.
357
+ return @uri_template ||=
358
+ Addressable::Template.new(base.to_s + self.description['pathUrl'])
359
+ end
360
+
361
+ ##
362
+ # Normalizes parameters, converting to the appropriate types.
363
+ #
364
+ # @param [Hash, Array] parameters
365
+ # The parameters to normalize.
366
+ #
367
+ # @return [Hash] The normalized parameters.
368
+ def normalize_parameters(parameters={})
369
+ # Convert keys to Strings when appropriate
370
+ if parameters.kind_of?(Hash) || parameters.kind_of?(Array)
371
+ # Is a Hash or an Array a better return type? Do we ever need to
372
+ # worry about the same parameter being sent twice with different
373
+ # values?
374
+ parameters = parameters.inject({}) do |accu, (k, v)|
375
+ k = k.to_s if k.kind_of?(Symbol)
376
+ k = k.to_str if k.respond_to?(:to_str)
377
+ unless k.kind_of?(String)
378
+ raise TypeError, "Expected String, got #{k.class}."
379
+ end
380
+ accu[k] = v
381
+ accu
382
+ end
383
+ else
384
+ raise TypeError,
385
+ "Expected Hash or Array, got #{parameters.class}."
386
+ end
387
+ return parameters
388
+ end
389
+
390
+ ##
391
+ # Expands the method's URI template using a parameter list.
392
+ #
393
+ # @param [Hash, Array] parameters
394
+ # The parameter list to use.
395
+ #
396
+ # @return [Addressable::URI] The URI after expansion.
397
+ def generate_uri(parameters={})
398
+ parameters = self.normalize_parameters(parameters)
399
+ self.validate_parameters(parameters)
400
+ template_variables = self.uri_template.variables
401
+ uri = self.uri_template.expand(parameters)
402
+ query_parameters = parameters.reject do |k, v|
403
+ template_variables.include?(k)
404
+ end
405
+ if query_parameters.size > 0
406
+ uri.query_values = (uri.query_values || {}).merge(query_parameters)
407
+ end
408
+ # Normalization is necessary because of undesirable percent-escaping
409
+ # during URI template expansion
410
+ return uri.normalize
411
+ end
412
+
413
+ ##
414
+ # Generates an HTTP request for this method.
415
+ #
416
+ # @param [Hash, Array] parameters
417
+ # The parameters to send.
418
+ # @param [String, StringIO] body The body for the HTTP request.
419
+ # @param [Hash, Array] headers The HTTP headers for the request.
420
+ #
421
+ # @return [Array] The generated HTTP request.
422
+ def generate_request(parameters={}, body='', headers=[])
423
+ if body.respond_to?(:string)
424
+ body = body.string
425
+ elsif body.respond_to?(:to_str)
426
+ body = body.to_str
427
+ else
428
+ raise TypeError, "Expected String or StringIO, got #{body.class}."
429
+ end
430
+ if !headers.kind_of?(Array) && !headers.kind_of?(Hash)
431
+ raise TypeError, "Expected Hash or Array, got #{headers.class}."
432
+ end
433
+ method = self.description['httpMethod'] || 'GET'
434
+ uri = self.generate_uri(parameters)
435
+ headers = headers.to_a if headers.kind_of?(Hash)
436
+ return [method, uri.to_str, headers, [body]]
437
+ end
438
+
439
+ ##
440
+ # Returns a <code>Hash</code> of the parameter descriptions for
441
+ # this method.
442
+ #
443
+ # @return [Hash] The parameter descriptions.
444
+ def parameter_descriptions
445
+ @parameter_descriptions ||= (
446
+ self.description['parameters'] || {}
447
+ ).inject({}) { |h,(k,v)| h[k]=v; h }
448
+ end
449
+
450
+ ##
451
+ # Returns an <code>Array</code> of the parameters for this method.
452
+ #
453
+ # @return [Array] The parameters.
454
+ def parameters
455
+ @parameters ||= ((
456
+ self.description['parameters'] || {}
457
+ ).inject({}) { |h,(k,v)| h[k]=v; h }).keys
458
+ end
459
+
460
+ ##
461
+ # Returns an <code>Array</code> of the required parameters for this
462
+ # method.
463
+ #
464
+ # @return [Array] The required parameters.
465
+ #
466
+ # @example
467
+ # # A list of all required parameters.
468
+ # method.required_parameters
469
+ def required_parameters
470
+ @required_parameters ||= ((self.parameter_descriptions.select do |k, v|
471
+ v['required']
472
+ end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
473
+ end
474
+
475
+ ##
476
+ # Returns an <code>Array</code> of the optional parameters for this
477
+ # method.
478
+ #
479
+ # @return [Array] The optional parameters.
480
+ #
481
+ # @example
482
+ # # A list of all optional parameters.
483
+ # method.optional_parameters
484
+ def optional_parameters
485
+ @optional_parameters ||= ((self.parameter_descriptions.reject do |k, v|
486
+ v['required']
487
+ end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
488
+ end
489
+
490
+ ##
491
+ # Verifies that the parameters are valid for this method. Raises an
492
+ # exception if validation fails.
493
+ #
494
+ # @param [Hash, Array] parameters
495
+ # The parameters to verify.
496
+ #
497
+ # @return [NilClass] <code>nil</code> if validation passes.
498
+ def validate_parameters(parameters={})
499
+ parameters = self.normalize_parameters(parameters)
500
+ required_variables = ((self.parameter_descriptions.select do |k, v|
501
+ v['required']
502
+ end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
503
+ missing_variables = required_variables - parameters.keys
504
+ if missing_variables.size > 0
505
+ raise ArgumentError,
506
+ "Missing required parameters: #{missing_variables.join(', ')}."
507
+ end
508
+ parameters.each do |k, v|
509
+ if self.parameter_descriptions[k]
510
+ pattern = self.parameter_descriptions[k]['pattern']
511
+ if pattern
512
+ regexp = Regexp.new("^#{pattern}$")
513
+ if v !~ regexp
514
+ raise ArgumentError,
515
+ "Parameter '#{k}' has an invalid value: #{v}. " +
516
+ "Must match: /^#{pattern}$/."
517
+ end
518
+ end
519
+ end
520
+ end
521
+ return nil
522
+ end
523
+
524
+ ##
525
+ # Returns a <code>String</code> representation of the method's state.
526
+ #
527
+ # @return [String] The method's state, as a <code>String</code>.
528
+ def inspect
529
+ sprintf(
530
+ "#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.rpc_name
531
+ )
532
+ end
533
+ end
534
+ end
535
+ end