1 # Copyright 2010 Google Inc.
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
7 # http://www.apache.org/licenses/LICENSE-2.0
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.
16 require 'addressable/uri'
17 require 'addressable/template'
19 require 'google/inflection'
24 # An exception that is raised if a method is called with missing or
25 # invalid parameter values.
26 class ValidationError < StandardError
30 # A service that has been described by a discovery document.
34 # Creates a description of a particular version of a service.
36 # @param [String] service_name
37 # The identifier for the service. Note that while this frequently
38 # matches the first segment of all of the service's RPC names, this
39 # should not be assumed. There is no requirement that these match.
40 # @param [String] service_version
41 # The identifier for the service version.
42 # @param [Hash] service_description
43 # The section of the discovery document that applies to this service
46 # @return [Google::APIClient::Service] The constructed service object.
47 def initialize(service_name, service_version, service_description)
49 @version = service_version
50 @description = service_description
51 metaclass = (class <<self; self; end)
52 self.resources.each do |resource|
53 method_name = Google::INFLECTOR.underscore(resource.name).to_sym
54 if !self.respond_to?(method_name)
55 metaclass.send(:define_method, method_name) { resource }
58 self.methods.each do |method|
59 method_name = Google::INFLECTOR.underscore(method.name).to_sym
60 if !self.respond_to?(method_name)
61 metaclass.send(:define_method, method_name) { method }
67 # Returns the identifier for the service.
69 # @return [String] The service identifier.
73 # Returns the version of the service.
75 # @return [String] The service version.
79 # Returns the parsed section of the discovery document that applies to
80 # this version of the service.
82 # @return [Hash] The service description.
83 attr_reader :description
86 # Returns the base URI for this version of the service.
88 # @return [Addressable::URI] The base URI that methods are joined to.
90 return @base ||= Addressable::URI.parse(self.description['baseUrl'])
94 # Updates the hierarchy of resources and methods with the new base.
96 # @param [Addressable::URI, #to_str, String] new_base
97 # The new base URI to use for the service.
99 @base = Addressable::URI.parse(new_base)
100 self.resources.each do |resource|
101 resource.base = @base
103 self.methods.each do |method|
109 # A list of resources available at the root level of this version of the
112 # @return [Array] A list of {Google::APIClient::Resource} objects.
114 return @resources ||= (
115 (self.description['resources'] || []).inject([]) do |accu, (k, v)|
116 accu << ::Google::APIClient::Resource.new(self.base, k, v)
123 # A list of methods available at the root level of this version of the
126 # @return [Array] A list of {Google::APIClient::Method} objects.
128 return @methods ||= (
129 (self.description['methods'] || []).inject([]) do |accu, (k, v)|
130 accu << ::Google::APIClient::Method.new(self.base, k, v)
137 # Converts the service to a flat mapping of RPC names and method objects.
139 # @return [Hash] All methods available on the service.
142 # # Discover available methods
143 # method_names = client.discovered_service('buzz').to_h.keys
145 return @hash ||= (begin
147 self.methods.each do |method|
148 methods_hash[method.rpc_name] = method
150 self.resources.each do |resource|
151 methods_hash.merge!(resource.to_h)
158 # Compares two versions of a service.
160 # @param [Object] other The service to compare.
163 # <code>-1</code> if the service is older than <code>other</code>.
164 # <code>0</code> if the service is the same as <code>other</code>.
165 # <code>1</code> if the service is newer than <code>other</code>.
166 # <code>nil</code> if the service cannot be compared to
167 # <code>other</code>.
169 # We can only compare versions of the same service
170 if other.kind_of?(self.class) && self.name == other.name
171 split_version = lambda do |version|
172 dotted_version = version[/^v?(\d+(.\d+)*)-?(.*?)?$/, 1]
173 suffix = version[/^v?(\d+(.\d+)*)-?(.*?)?$/, 3]
174 if dotted_version && suffix
175 [dotted_version.split('.').map { |v| v.to_i }, suffix]
180 self_sortable, self_suffix = split_version.call(self.version)
181 other_sortable, other_suffix = split_version.call(other.version)
182 result = self_sortable <=> other_sortable
185 # If the dotted versions are equal, check the suffix.
186 # An omitted suffix should be sorted after an included suffix.
187 elsif self_suffix == ''
189 elsif other_suffix == ''
192 return self_suffix <=> other_suffix
200 # Returns a <code>String</code> representation of the service's state.
202 # @return [String] The service's state, as a <code>String</code>.
205 "#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.name
211 # A resource that has been described by a discovery document.
215 # Creates a description of a particular version of a resource.
217 # @param [Addressable::URI] base
218 # The base URI for the service.
219 # @param [String] resource_name
220 # The identifier for the resource.
221 # @param [Hash] resource_description
222 # The section of the discovery document that applies to this resource.
224 # @return [Google::APIClient::Resource] The constructed resource object.
225 def initialize(base, resource_name, resource_description)
227 @name = resource_name
228 @description = resource_description
229 metaclass = (class <<self; self; end)
230 self.resources.each do |resource|
231 method_name = Google::INFLECTOR.underscore(resource.name).to_sym
232 if !self.respond_to?(method_name)
233 metaclass.send(:define_method, method_name) { resource }
236 self.methods.each do |method|
237 method_name = Google::INFLECTOR.underscore(method.name).to_sym
238 if !self.respond_to?(method_name)
239 metaclass.send(:define_method, method_name) { method }
245 # Returns the identifier for the resource.
247 # @return [String] The resource identifier.
251 # Returns the parsed section of the discovery document that applies to
254 # @return [Hash] The resource description.
255 attr_reader :description
258 # Returns the base URI for this resource.
260 # @return [Addressable::URI] The base URI that methods are joined to.
264 # Updates the hierarchy of resources and methods with the new base.
266 # @param [Addressable::URI, #to_str, String] new_base
267 # The new base URI to use for the resource.
269 @base = Addressable::URI.parse(new_base)
270 self.resources.each do |resource|
271 resource.base = @base
273 self.methods.each do |method|
279 # A list of sub-resources available on this resource.
281 # @return [Array] A list of {Google::APIClient::Resource} objects.
283 return @resources ||= (
284 (self.description['resources'] || []).inject([]) do |accu, (k, v)|
285 accu << ::Google::APIClient::Resource.new(self.base, k, v)
292 # A list of methods available on this resource.
294 # @return [Array] A list of {Google::APIClient::Method} objects.
296 return @methods ||= (
297 (self.description['methods'] || []).inject([]) do |accu, (k, v)|
298 accu << ::Google::APIClient::Method.new(self.base, k, v)
305 # Converts the resource to a flat mapping of RPC names and method
308 # @return [Hash] All methods available on the resource.
310 return @hash ||= (begin
312 self.methods.each do |method|
313 methods_hash[method.rpc_name] = method
315 self.resources.each do |resource|
316 methods_hash.merge!(resource.to_h)
323 # Returns a <code>String</code> representation of the resource's state.
325 # @return [String] The resource's state, as a <code>String</code>.
328 "#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.name
334 # A method that has been described by a discovery document.
338 # Creates a description of a particular method.
340 # @param [Addressable::URI] base
341 # The base URI for the service.
342 # @param [String] method_name
343 # The identifier for the method.
344 # @param [Hash] method_description
345 # The section of the discovery document that applies to this method.
347 # @return [Google::APIClient::Method] The constructed method object.
348 def initialize(base, method_name, method_description)
351 @description = method_description
355 # Returns the identifier for the method.
357 # @return [String] The method identifier.
361 # Returns the parsed section of the discovery document that applies to
364 # @return [Hash] The method description.
365 attr_reader :description
368 # Returns the base URI for the method.
370 # @return [Addressable::URI]
371 # The base URI that this method will be joined to.
375 # Updates the method with the new base.
377 # @param [Addressable::URI, #to_str, String] new_base
378 # The new base URI to use for the method.
380 @base = Addressable::URI.parse(new_base)
385 # Returns the RPC name for the method.
387 # @return [String] The RPC name.
389 return self.description['rpcName']
393 # Returns the URI template for the method. A parameter list can be
394 # used to expand this into a URI.
396 # @return [Addressable::Template] The URI template.
398 # TODO(bobaman) We shouldn't be calling #to_s here, this should be
399 # a join operation on a URI, but we have to treat these as Strings
400 # because of the way the discovery document provides the URIs.
401 # This should be fixed soon.
402 return @uri_template ||=
403 Addressable::Template.new(base.to_s + self.description['pathUrl'])
407 # Normalizes parameters, converting to the appropriate types.
409 # @param [Hash, Array] parameters
410 # The parameters to normalize.
412 # @return [Hash] The normalized parameters.
413 def normalize_parameters(parameters={})
414 # Convert keys to Strings when appropriate
415 if parameters.kind_of?(Hash) || parameters.kind_of?(Array)
416 # Is a Hash or an Array a better return type? Do we ever need to
417 # worry about the same parameter being sent twice with different
419 parameters = parameters.inject({}) do |accu, (k, v)|
420 k = k.to_s if k.kind_of?(Symbol)
421 k = k.to_str if k.respond_to?(:to_str)
422 unless k.kind_of?(String)
423 raise TypeError, "Expected String, got #{k.class}."
430 "Expected Hash or Array, got #{parameters.class}."
436 # Expands the method's URI template using a parameter list.
438 # @param [Hash, Array] parameters
439 # The parameter list to use.
441 # @return [Addressable::URI] The URI after expansion.
442 def generate_uri(parameters={})
443 parameters = self.normalize_parameters(parameters)
444 self.validate_parameters(parameters)
445 template_variables = self.uri_template.variables
446 uri = self.uri_template.expand(parameters)
447 query_parameters = parameters.reject do |k, v|
448 template_variables.include?(k)
450 if query_parameters.size > 0
451 uri.query_values = (uri.query_values || {}).merge(query_parameters)
453 # Normalization is necessary because of undesirable percent-escaping
454 # during URI template expansion
459 # Generates an HTTP request for this method.
461 # @param [Hash, Array] parameters
462 # The parameters to send.
463 # @param [String, StringIO] body The body for the HTTP request.
464 # @param [Hash, Array] headers The HTTP headers for the request.
466 # @return [Array] The generated HTTP request.
467 def generate_request(parameters={}, body='', headers=[])
468 if body.respond_to?(:string)
470 elsif body.respond_to?(:to_str)
473 raise TypeError, "Expected String or StringIO, got #{body.class}."
475 if !headers.kind_of?(Array) && !headers.kind_of?(Hash)
476 raise TypeError, "Expected Hash or Array, got #{headers.class}."
478 method = self.description['httpMethod'] || 'GET'
479 uri = self.generate_uri(parameters)
480 headers = headers.to_a if headers.kind_of?(Hash)
481 return [method, uri.to_str, headers, [body]]
485 # Returns a <code>Hash</code> of the parameter descriptions for
488 # @return [Hash] The parameter descriptions.
489 def parameter_descriptions
490 @parameter_descriptions ||= (
491 self.description['parameters'] || {}
492 ).inject({}) { |h,(k,v)| h[k]=v; h }
496 # Returns an <code>Array</code> of the parameters for this method.
498 # @return [Array] The parameters.
501 self.description['parameters'] || {}
502 ).inject({}) { |h,(k,v)| h[k]=v; h }).keys
506 # Returns an <code>Array</code> of the required parameters for this
509 # @return [Array] The required parameters.
512 # # A list of all required parameters.
513 # method.required_parameters
514 def required_parameters
515 @required_parameters ||= ((self.parameter_descriptions.select do |k, v|
517 end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
521 # Returns an <code>Array</code> of the optional parameters for this
524 # @return [Array] The optional parameters.
527 # # A list of all optional parameters.
528 # method.optional_parameters
529 def optional_parameters
530 @optional_parameters ||= ((self.parameter_descriptions.reject do |k, v|
532 end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
536 # Verifies that the parameters are valid for this method. Raises an
537 # exception if validation fails.
539 # @param [Hash, Array] parameters
540 # The parameters to verify.
542 # @return [NilClass] <code>nil</code> if validation passes.
543 def validate_parameters(parameters={})
544 parameters = self.normalize_parameters(parameters)
545 required_variables = ((self.parameter_descriptions.select do |k, v|
547 end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
548 missing_variables = required_variables - parameters.keys
549 if missing_variables.size > 0
551 "Missing required parameters: #{missing_variables.join(', ')}."
553 parameters.each do |k, v|
554 if self.parameter_descriptions[k]
555 pattern = self.parameter_descriptions[k]['pattern']
557 regexp = Regexp.new("^#{pattern}$")
560 "Parameter '#{k}' has an invalid value: #{v}. " +
561 "Must match: /^#{pattern}$/."
570 # Returns a <code>String</code> representation of the method's state.
572 # @return [String] The method's state, as a <code>String</code>.
575 "#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.rpc_name