From f462322be590be24f07772d46952c6614d806370 Mon Sep 17 00:00:00 2001 From: Bob Aman Date: Thu, 16 Sep 2010 19:12:52 +0000 Subject: [PATCH] URI template expansion works. git-svn-id: https://google-api-ruby-client.googlecode.com/svn/trunk@33 c1d61fac-ed7f-fcc1-18f7-ff78120a04ef --- lib/google/api_client.rb | 138 +++++++++++-- .../api_client/discovery/method_builder.rb | 88 --------- lib/google/api_client/discovery/service.rb | 184 ++++++++++++++++++ 3 files changed, 307 insertions(+), 103 deletions(-) delete mode 100644 lib/google/api_client/discovery/method_builder.rb create mode 100644 lib/google/api_client/discovery/service.rb diff --git a/lib/google/api_client.rb b/lib/google/api_client.rb index 6da8ca616..378c56025 100644 --- a/lib/google/api_client.rb +++ b/lib/google/api_client.rb @@ -12,6 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +require 'httpadapter' +require 'json' + +require 'google/api_client/discovery/service' + module Google #:nodoc: ## # This class manages communication with a single API. @@ -21,11 +26,22 @@ module Google #:nodoc: @options = { # TODO: What configuration options need to go here? }.merge(options) + end + + ## + # Returns the parser used by the client. + def parser unless @options[:parser] require 'google/api_client/parser/json_parser' # NOTE: Do not rely on this default value, as it may change @options[:parser] = JSONParser.new end + return @options[:parser] + end + + ## + # Returns the authorization mechanism used by the client. + def authorization unless @options[:authorization] require 'signet/oauth_1/client' # NOTE: Do not rely on this default value, as it may change @@ -40,34 +56,126 @@ module Google #:nodoc: :client_credential_secret => 'anonymous' ) end - unless @options[:http_adapter] + return @options[:authorization] + end + + ## + # Returns the HTTP adapter used by the client. + def http_adapter + return @options[:http_adapter] ||= (begin require 'httpadapter/adapters/net_http' @options[:http_adapter] = HTTPAdapter::NetHTTPRequestAdapter + end) + end + + def discovery_uri + return @options[:discovery_uri] ||= (begin + if @options[:service] + service_id = @options[:service] + service_version = @options[:service_version] || '1.0' + "http://www.googleapis.com/discovery/0.1/describe" + + "?api=#{service_id}&apiVersion=#{service_version}" + else + raise ArgumentError, + 'Missing required configuration value, :discovery_uri.' + end + end) + end + + def discovery_document + return @discovery_document ||= (begin + request = ['GET', self.discovery_uri, [], []] + response = self.transmit_request(request) + status, headers, body = response + if status == 200 + merged_body = StringIO.new + body.each do |chunk| + merged_body.write(chunk) + end + merged_body.rewind + JSON.parse(merged_body.string) + else + raise TransmissionError, + "Could not retrieve discovery document at: #{self.discovery_uri}" + end + end) + end + + def discovered_services + return @discovered_services ||= (begin + service_names = self.discovery_document['data'].keys() + services = [] + for service_name in service_names + versions = self.discovery_document['data'][service_name] + for version_name in versions.keys() + service_description = + self.discovery_document['data'][service_name][version_name] + service_version = "%.1f" % version_name.gsub(/^v/, '').to_f + services << ::Google::APIClient::Service.new( + service_name, + service_version, + service_description + ) + end + end + services + end) + end + + def discovered_service(service_name, service_version='1.0') + for service in self.discovered_services + if service.name == service_name && + service.version.to_s == service_version.to_s + return service + end end - end - - ## - # Returns the parser used by the client. - def parser - return @options[:parser] - end - - ## - # Returns the authorization mechanism used by the client. - def authorization - return @options[:authorization] + return nil end def build_request(*args, &block) if !args.empty? || block - # Build the request! - # TODO(bobaman): No-op / Debug code! + if args.size != 2 + raise ArgumentError, "wrong number of arguments (#{args.size} for 2)" + end + rpc_name = args[0] + if !rpc_name.kind_of?(String) + raise TypeError, "Expected String, got #{rpc_name.class}." + end + parameters = args[1] + if !parameters.kind_of?(Hash) + raise TypeError, "Expected Hash, got #{parameters.class}." + end + + signature_needed = false + if signature_needed + request = self.sign_request(request) + end return args else require 'google/api_client/discovery/method_builder' return ::Google::APIClient::MethodBuilder.new(self, :build_request) end end + + def transmit_request(request) + ::HTTPAdapter.transmit(request, self.http_adapter) + end + + def sign_request(request) + if self.authorization.respond_to?(:generate_authenticated_request) + return self.authorization.generate_authenticated_request( + :request => request + ) + else + raise TypeError, + 'Expected authorization mechanism to respond to ' + + '#generate_authenticated_request.' + end + end + + def execute_signed_request(*args, &block) + return self.transmit_request(self.build_request(*args, &block)) + end end end diff --git a/lib/google/api_client/discovery/method_builder.rb b/lib/google/api_client/discovery/method_builder.rb deleted file mode 100644 index ed1f69ea7..000000000 --- a/lib/google/api_client/discovery/method_builder.rb +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2010 Google Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Define a BasicObject implementation if one doesn't already exist. -unless defined?(BasicObject) - class BasicObject - instance_methods.each do |m| - if m.to_s !~ /^(?:!=?|==|__.*__|equal\?|instance_(?:eval|exec))$/ - undef_method(m) - end - end - end -end - -module Google #:nodoc: - class APIClient #:nodoc: - ## - # A builder class for assembling requests. - class MethodBuilder < BasicObject - ## - # Creates a new {MethodBuilder}. - # - # @param [Google::APIClient] client - # The client the {MethodBuilder} will use to build requests. - def initialize(client, callback=:build_request) - @segments = [] - @client = client - @callback = callback - end - - ## - # Appends a segment to the builder. - # - # @param [String, Symbol, #to_s] segment The segment to append. - def <<(segment) - @segments << segment.to_s - end - - ## - # Returns the assembled segments. This maps to the rpcName - # field in the discovery document. - # - # @return [String] The RPC name of the method. - def to_s - return @segments.join(".") - end - alias_method :to_str, :to_s - - ## - # The call method will force the builder to finish assembling segments - # and build the request. - # - # @return [Enumerable] - def call(*args, &block) - return @client.build_request(self.to_str, *args, &block) - end - - ## - # Any methods called on the {MethodBuilder} will cause segments to be - # added to the list. - # - # @return [Enumerable, Google::APIClient::MethodBuilder] - # Allows chaining methods to build an endpoint name. The request - # is built when parameters are sent. If an endpoint does not take - # parameters, the {#call} method may be used to terminate the - # sequence. - def method_missing(method, *args, &block) - self << method - if !args.empty? || block - return @client.send(callback, self.to_str, *args, &block) - else - return self - end - end - end - end -end diff --git a/lib/google/api_client/discovery/service.rb b/lib/google/api_client/discovery/service.rb new file mode 100644 index 000000000..0db554fb4 --- /dev/null +++ b/lib/google/api_client/discovery/service.rb @@ -0,0 +1,184 @@ +# Copyright 2010 Google Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 'json' +require 'addressable/uri' +require 'addressable/template' +require 'extlib/inflection' + +module Google #:nodoc: + class APIClient #:nodoc: + class ValidationError < StandardError + end + + class Service + def initialize(service_name, service_version, service_description) + @name = service_name + @version = service_version + @description = service_description + metaclass = (class <String representation of the service's state. + # + # @return [String] The service's state, as a String. + def inspect + sprintf( + "#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.name + ) + end + end + + class Resource + def initialize(base, resource_name, resource_description) + @base = base + @name = resource_name + @description = resource_description + metaclass = (class <String representation of the resource's state. + # + # @return [String] The resource's state, as a String. + def inspect + sprintf( + "#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.name + ) + end + end + + class Method + def initialize(base, method_name, method_description) + @base = base + @name = method_name + @description = method_description + end + + attr_reader :name, :description, :base + + def uri_template + return Addressable::Template.new(base + self.description['pathUrl']) + end + + def generate_uri(parameters={}) + # Convert keys to Strings when appropriate + parameters = parameters.inject({}) do |accu, (k, v)| + k = k.to_s if k.kind_of?(Symbol) + k = k.to_str if k.respond_to?(:to_str) + unless k.kind_of?(String) + raise TypeError, "Expected String, got #{k.class}." + end + accu[k] = v + accu + end + template_variables = self.uri_template.variables + missing_variables = template_variables - parameters.keys + if missing_variables.size > 0 + raise ArgumentError, + "Missing required parameters: #{missing_variables.join(', ')}." + end + uri = self.uri_template.expand(parameters) + query_parameters = parameters.reject do |k, v| + template_variables.include?(k) + end + if query_parameters.size > 0 + uri.query_values = (uri.query_values || {}).merge(query_parameters) + end + # Normalization is necessary because of undesirable percent-escaping + # during URI template expansion + return uri.normalize + end + + ## + # Returns a String representation of the method's state. + # + # @return [String] The method's state, as a String. + def inspect + sprintf( + "#<%s:%#0x NAME:%s>", self.class.to_s, self.object_id, self.name + ) + end + end + end +end