google-api-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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