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.
17 require 'addressable/uri'
18 require 'addressable/template'
20 require 'google/inflection'
21 require 'google/api_client/errors'
26 # A service that has been described by a discovery document.
30 # Creates a description of a particular version of a service.
33 # The identifier for the service. Note that while this frequently
34 # matches the first segment of all of the service's RPC names, this
35 # should not be assumed. There is no requirement that these match.
36 # @param [String] version
37 # The identifier for the service version.
38 # @param [Hash] api_description
39 # The section of the discovery document that applies to this service
42 # @return [Google::APIClient::API] The constructed service object.
43 def initialize(document_base, discovery_document)
44 @document_base = Addressable::URI.parse(document_base)
45 @discovery_document = discovery_document
46 metaclass = (class <<self; self; end)
47 self.resources.each do |resource|
48 method_name = Google::INFLECTOR.underscore(resource.name).to_sym
49 if !self.respond_to?(method_name)
50 metaclass.send(:define_method, method_name) { resource }
53 self.methods.each do |method|
54 method_name = Google::INFLECTOR.underscore(method.name).to_sym
55 if !self.respond_to?(method_name)
56 metaclass.send(:define_method, method_name) { method }
62 # Returns the id of the service.
64 # @return [String] The service id.
66 return @discovery_document['id']
70 # Returns the identifier for the service.
72 # @return [String] The service identifier.
74 return @discovery_document['name']
78 # Returns the version of the service.
80 # @return [String] The service version.
82 return @discovery_document['version']
86 # Returns the parsed section of the discovery document that applies to
87 # this version of the service.
89 # @return [Hash] The service description.
91 return @discovery_document['description']
95 # Returns true if this is the preferred version of this API.
97 # @return [TrueClass, FalseClass]
98 # Whether or not this is the preferred version of this API.
100 return @discovery_document['preferred']
104 # Returns the base URI for the discovery document.
106 # @return [Addressable::URI] The base URI.
107 attr_reader :document_base
110 # Returns the base URI for this version of the service.
112 # @return [Addressable::URI] The base URI that methods are joined to.
114 if @discovery_document['basePath']
115 return @method_base ||= (
117 Addressable::URI.parse(@discovery_document['basePath'])
125 # Updates the hierarchy of resources and methods with the new base.
127 # @param [Addressable::URI, #to_str, String] new_base
128 # The new base URI to use for the service.
129 def method_base=(new_method_base)
130 @method_base = Addressable::URI.parse(new_method_base)
131 self.resources.each do |resource|
132 resource.method_base = @method_base
134 self.methods.each do |method|
135 method.method_base = @method_base
140 # A list of resources available at the root level of this version of the
143 # @return [Array] A list of {Google::APIClient::Resource} objects.
145 return @resources ||= (
146 (@discovery_document['resources'] || []).inject([]) do |accu, (k, v)|
147 accu << Google::APIClient::Resource.new(self.method_base, k, v)
154 # A list of methods available at the root level of this version of the
157 # @return [Array] A list of {Google::APIClient::Method} objects.
159 return @methods ||= (
160 (@discovery_document['methods'] || []).inject([]) do |accu, (k, v)|
161 accu << Google::APIClient::Method.new(self.method_base, k, v)
168 # Converts the service to a flat mapping of RPC names and method objects.
170 # @return [Hash] All methods available on the service.
173 # # Discover available methods
174 # method_names = client.discovered_api('buzz').to_h.keys
176 return @hash ||= (begin
178 self.methods.each do |method|
179 methods_hash[method.id] = method
181 self.resources.each do |resource|
182 methods_hash.merge!(resource.to_h)
189 # Returns a <code>String</code> representation of the service's state.
191 # @return [String] The service's state, as a <code>String</code>.
194 "#<%s:%#0x ID:%s>", self.class.to_s, self.object_id, self.id
200 # A resource that has been described by a discovery document.
204 # Creates a description of a particular version of a resource.
206 # @param [Addressable::URI] base
207 # The base URI for the service.
208 # @param [String] resource_name
209 # The identifier for the resource.
210 # @param [Hash] resource_description
211 # The section of the discovery document that applies to this resource.
213 # @return [Google::APIClient::Resource] The constructed resource object.
214 def initialize(method_base, resource_name, discovery_document)
215 @method_base = method_base
216 @name = resource_name
217 @discovery_document = discovery_document
218 metaclass = (class <<self; self; end)
219 self.resources.each do |resource|
220 method_name = Google::INFLECTOR.underscore(resource.name).to_sym
221 if !self.respond_to?(method_name)
222 metaclass.send(:define_method, method_name) { resource }
225 self.methods.each do |method|
226 method_name = Google::INFLECTOR.underscore(method.name).to_sym
227 if !self.respond_to?(method_name)
228 metaclass.send(:define_method, method_name) { method }
234 # Returns the identifier for the resource.
236 # @return [String] The resource identifier.
240 # Returns the parsed section of the discovery document that applies to
243 # @return [Hash] The resource description.
244 attr_reader :description
247 # Returns the base URI for this resource.
249 # @return [Addressable::URI] The base URI that methods are joined to.
250 attr_reader :method_base
253 # Updates the hierarchy of resources and methods with the new base.
255 # @param [Addressable::URI, #to_str, String] new_base
256 # The new base URI to use for the resource.
257 def method_base=(new_method_base)
258 @method_base = Addressable::URI.parse(new_method_base)
259 self.resources.each do |resource|
260 resource.method_base = @method_base
262 self.methods.each do |method|
263 method.method_base = @method_base
268 # A list of sub-resources available on this resource.
270 # @return [Array] A list of {Google::APIClient::Resource} objects.
272 return @resources ||= (
273 (@discovery_document['resources'] || []).inject([]) do |accu, (k, v)|
274 accu << Google::APIClient::Resource.new(self.method_base, k, v)
281 # A list of methods available on this resource.
283 # @return [Array] A list of {Google::APIClient::Method} objects.
285 return @methods ||= (
286 (@discovery_document['methods'] || []).inject([]) do |accu, (k, v)|
287 accu << Google::APIClient::Method.new(self.method_base, k, v)
294 # Converts the resource to a flat mapping of RPC names and method
297 # @return [Hash] All methods available on the resource.
299 return @hash ||= (begin
301 self.methods.each do |method|
302 methods_hash[method.id] = method
304 self.resources.each do |resource|
305 methods_hash.merge!(resource.to_h)
312 # Returns a <code>String</code> representation of the resource's state.
314 # @return [String] The resource's state, as a <code>String</code>.
317 "#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.name
323 # A method that has been described by a discovery document.
327 # Creates a description of a particular method.
329 # @param [Addressable::URI] method_base
330 # The base URI for the service.
331 # @param [String] method_name
332 # The identifier for the method.
333 # @param [Hash] method_description
334 # The section of the discovery document that applies to this method.
336 # @return [Google::APIClient::Method] The constructed method object.
337 def initialize(method_base, method_name, discovery_document)
338 @method_base = method_base
340 @discovery_document = discovery_document
344 # Returns the identifier for the method.
346 # @return [String] The method identifier.
350 # Returns the parsed section of the discovery document that applies to
353 # @return [Hash] The method description.
354 attr_reader :description
357 # Returns the base URI for the method.
359 # @return [Addressable::URI]
360 # The base URI that this method will be joined to.
361 attr_reader :method_base
364 # Updates the method with the new base.
366 # @param [Addressable::URI, #to_str, String] new_base
367 # The new base URI to use for the method.
368 def method_base=(new_method_base)
369 @method_base = Addressable::URI.parse(new_method_base)
374 # Returns the method ID.
376 # @return [String] The method identifier.
378 return @discovery_document['id']
382 # Returns the HTTP method or 'GET' if none is specified.
384 # @return [String] The HTTP method that will be used in the request.
386 return @discovery_document['httpMethod'] || 'GET'
390 # Returns the URI template for the method. A parameter list can be
391 # used to expand this into a URI.
393 # @return [Addressable::Template] The URI template.
395 # TODO(bobaman) We shouldn't be calling #to_s here, this should be
396 # a join operation on a URI, but we have to treat these as Strings
397 # because of the way the discovery document provides the URIs.
398 # This should be fixed soon.
399 return @uri_template ||= Addressable::Template.new(
400 self.method_base + @discovery_document['path']
405 # Normalizes parameters, converting to the appropriate types.
407 # @param [Hash, Array] parameters
408 # The parameters to normalize.
410 # @return [Hash] The normalized parameters.
411 def normalize_parameters(parameters={})
412 # Convert keys to Strings when appropriate
413 if parameters.kind_of?(Hash) || parameters.kind_of?(Array)
414 # Is a Hash or an Array a better return type? Do we ever need to
415 # worry about the same parameter being sent twice with different
417 parameters = parameters.inject({}) do |accu, (k, v)|
418 k = k.to_s if k.kind_of?(Symbol)
419 k = k.to_str if k.respond_to?(:to_str)
420 unless k.kind_of?(String)
421 raise TypeError, "Expected String, got #{k.class}."
428 "Expected Hash or Array, got #{parameters.class}."
434 # Expands the method's URI template using a parameter list.
436 # @param [Hash, Array] parameters
437 # The parameter list to use.
439 # @return [Addressable::URI] The URI after expansion.
440 def generate_uri(parameters={})
441 parameters = self.normalize_parameters(parameters)
442 self.validate_parameters(parameters)
443 template_variables = self.uri_template.variables
444 uri = self.uri_template.expand(parameters)
445 query_parameters = parameters.reject do |k, v|
446 template_variables.include?(k)
448 if query_parameters.size > 0
449 uri.query_values = (uri.query_values || {}).merge(query_parameters)
451 # Normalization is necessary because of undesirable percent-escaping
452 # during URI template expansion
457 # Generates an HTTP request for this method.
459 # @param [Hash, Array] parameters
460 # The parameters to send.
461 # @param [String, StringIO] body The body for the HTTP request.
462 # @param [Hash, Array] headers The HTTP headers for the request.
464 # @return [Array] The generated HTTP request.
465 def generate_request(parameters={}, body='', headers=[])
466 if body.respond_to?(:string)
468 elsif body.respond_to?(:to_str)
471 raise TypeError, "Expected String or StringIO, got #{body.class}."
473 if !headers.kind_of?(Array) && !headers.kind_of?(Hash)
474 raise TypeError, "Expected Hash or Array, got #{headers.class}."
476 method = self.http_method
477 uri = self.generate_uri(parameters)
478 headers = headers.to_a if headers.kind_of?(Hash)
479 return [method, uri.to_str, headers, [body]]
483 # Returns a <code>Hash</code> of the parameter descriptions for
486 # @return [Hash] The parameter descriptions.
487 def parameter_descriptions
488 @parameter_descriptions ||= (
489 @discovery_document['parameters'] || {}
490 ).inject({}) { |h,(k,v)| h[k]=v; h }
494 # Returns an <code>Array</code> of the parameters for this method.
496 # @return [Array] The parameters.
499 @discovery_document['parameters'] || {}
500 ).inject({}) { |h,(k,v)| h[k]=v; h }).keys
504 # Returns an <code>Array</code> of the required parameters for this
507 # @return [Array] The required parameters.
510 # # A list of all required parameters.
511 # method.required_parameters
512 def required_parameters
513 @required_parameters ||= ((self.parameter_descriptions.select do |k, v|
515 end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
519 # Returns an <code>Array</code> of the optional parameters for this
522 # @return [Array] The optional parameters.
525 # # A list of all optional parameters.
526 # method.optional_parameters
527 def optional_parameters
528 @optional_parameters ||= ((self.parameter_descriptions.reject do |k, v|
530 end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
534 # Verifies that the parameters are valid for this method. Raises an
535 # exception if validation fails.
537 # @param [Hash, Array] parameters
538 # The parameters to verify.
540 # @return [NilClass] <code>nil</code> if validation passes.
541 def validate_parameters(parameters={})
542 parameters = self.normalize_parameters(parameters)
543 required_variables = ((self.parameter_descriptions.select do |k, v|
545 end).inject({}) { |h,(k,v)| h[k]=v; h }).keys
546 missing_variables = required_variables - parameters.keys
547 if missing_variables.size > 0
549 "Missing required parameters: #{missing_variables.join(', ')}."
551 parameters.each do |k, v|
552 if self.parameter_descriptions[k]
553 enum = self.parameter_descriptions[k]['enum']
554 if enum && !enum.include?(v)
556 "Parameter '#{k}' has an invalid value: #{v}. " +
557 "Must be one of #{enum.inspect}."
559 pattern = self.parameter_descriptions[k]['pattern']
561 regexp = Regexp.new("^#{pattern}$")
564 "Parameter '#{k}' has an invalid value: #{v}. " +
565 "Must match: /^#{pattern}$/."
574 # Returns a <code>String</code> representation of the method's state.
576 # @return [String] The method's state, as a <code>String</code>.
580 self.class.to_s, self.object_id, self.id