From e73ce124fa471295253a3f594c26f4d7d5f85440 Mon Sep 17 00:00:00 2001 From: Sergio Gomes Date: Tue, 10 Sep 2013 15:29:36 +0100 Subject: [PATCH 1/4] Initial version of new programming interface --- lib/google/api_client/service.rb | 150 ++++++ lib/google/api_client/service/request.rb | 144 ++++++ lib/google/api_client/service/resource.rb | 40 ++ lib/google/api_client/service/result.rb | 162 ++++++ .../api_client/service/stub_generator.rb | 59 +++ spec/google/api_client/service_spec.rb | 464 ++++++++++++++++++ 6 files changed, 1019 insertions(+) create mode 100755 lib/google/api_client/service.rb create mode 100755 lib/google/api_client/service/request.rb create mode 100755 lib/google/api_client/service/resource.rb create mode 100755 lib/google/api_client/service/result.rb create mode 100755 lib/google/api_client/service/stub_generator.rb create mode 100644 spec/google/api_client/service_spec.rb diff --git a/lib/google/api_client/service.rb b/lib/google/api_client/service.rb new file mode 100755 index 000000000..5e4aaf71c --- /dev/null +++ b/lib/google/api_client/service.rb @@ -0,0 +1,150 @@ +# Copyright 2013 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 'google/api_client' +require 'google/api_client/service/stub_generator' +require 'google/api_client/service/resource' +require 'google/api_client/service/request' +require 'google/api_client/service/result' + +module Google + class APIClient + + ## + # Experimental new programming interface at the API level. + # Hides Google::APIClient. Designed to be easier to use, with less code. + # + # @example + # calendar = Google::APIClient::Service.new('calendar', 'v3') + # result = calendar.events.list('calendarId' => 'primary').execute() + class Service + include Google::APIClient::Service::StubGenerator + + ## + # Creates a new Service. + # + # @param [String, Symbol] api_name + # The name of the API this service will access. + # @param [String, Symbol] api_version + # The version of the API this service will access. + # @param [Hash] options + # The configuration parameters for the service. + # @option options [Symbol, #generate_authenticated_request] :authorization + # (:oauth_1) + # The authorization mechanism used by the client. The following + # mechanisms are supported out-of-the-box: + # + # @option options [Boolean] :auto_refresh_token (true) + # The setting that controls whether or not the api client attempts to + # refresh authorization when a 401 is hit in #execute. If the token does + # not support it, this option is ignored. + # @option options [String] :application_name + # The name of the application using the client. + # @option options [String] :application_version + # The version number of the application using the client. + # @option options [String] :host ("www.googleapis.com") + # The API hostname used by the client. This rarely needs to be changed. + # @option options [String] :port (443) + # The port number used by the client. This rarely needs to be changed. + # @option options [String] :discovery_path ("/discovery/v1") + # The discovery base path. This rarely needs to be changed. + # @option options [String] :ca_file + # Optional set of root certificates to use when validating SSL connections. + # By default, a bundled set of trusted roots will be used. + # @option options [#generate_authenticated_request] :authorization + # The authorization mechanism for requests. Used only if + # `:authenticated` is `true`. + # @option options [TrueClass, FalseClass] :authenticated (default: true) + # `true` if requests must be signed or somehow + # authenticated, `false` otherwise. + # @option options [TrueClass, FalseClass] :gzip (default: true) + # `true` if gzip enabled, `false` otherwise. + # @option options [Faraday] :connection + # A custom connection to be used for all requests. + def initialize(api_name, api_version, options = {}) + @api_name = api_name.to_s + if api_version.nil? + raise ArgumentError, + "API version must be set" + end + @api_version = api_version.to_s + if options && !options.respond_to?(:to_hash) + raise ArgumentError, + "expected options Hash, got #{options.class}" + end + + params = {} + [:application_name, :application_version, :authorization, :host, :port, + :discovery_path, :auto_refresh_token, :key, :user_ip, + :ca_file].each do |option| + if options.include? option + params[option] = options[option] + end + end + + @client = Google::APIClient.new(params) + + @options = options + @api = @client.discovered_api(api_name, api_version) + generate_call_stubs(self, @api) + end + + ## + # Logger for the Service. + # + # @return [Logger] logger instance. + def logger + @client.logger + end + + ## + # Set the Logger for the Service. + def logger=(obj) + @client.logger = obj + end + + ## + # Executes an API request. + # Do not call directly; this method is only used by Request objects when + # executing. + # @param [Google::APIClient::Service::Request] request + # The request to be executed. + def execute(request) + params = {:api_method => request.method, + :parameters => request.parameters} + if request.respond_to? :body + if request.body.respond_to? :to_hash + params[:body_object] = request.body + else + params[:body] = request.body + end + end + if request.respond_to? :media + params[:media] = request.media + end + [:connection, :authenticated, :gzip].each do |option| + if @options.include? option + params[option] = @options[option] + end + end + result = @client.execute(params) + return Google::APIClient::Result.new(request, result) + end + end + end +end diff --git a/lib/google/api_client/service/request.rb b/lib/google/api_client/service/request.rb new file mode 100755 index 000000000..dcbc7e321 --- /dev/null +++ b/lib/google/api_client/service/request.rb @@ -0,0 +1,144 @@ +# Copyright 2013 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. + +module Google + class APIClient + class Service + ## + # Handles an API request. + # This contains a full definition of the request to be made (including + # method name, parameters, body and media). The remote API call can be + # invoked with execute(). + class Request + ## + # Build a request. + # This class should not be directly instantiated in user code; + # instantiation is handled by the stub methods created on Service and + # Resource objects. + # + # @param [Google::APIClient::Service] service + # The parent Service instance that will execute the request. + # @param [Google::APIClient::Method] method + # The Method instance that describes the API method invoked by the + # request. + # @param [Hash] parameters + # A Hash of parameter names and values to be sent in the API call. + def initialize(service, method, parameters) + @service = service + @method = method + @parameters = parameters + @body = nil + @media = nil + + metaclass = (class << self; self; end) + + # If applicable, add "body", "body=" and resource-named methods for + # retrieving and setting the HTTP body for this request. + # Examples of setting the body for files.insert in the Drive API: + # request.body = object + # request.execute + # OR + # request.file = object + # request.execute + # OR + # request.body(object).execute + # OR + # request.file(object).execute + # Examples of retrieving the body for files.insert in the Drive API: + # object = request.body + # OR + # object = request.file + if method.request_schema + body_name = method.request_schema.data['id'].dup + body_name[0] = body_name[0].chr.downcase + body_name_equals = (body_name + '=').to_sym + body_name = body_name.to_sym + + metaclass.send(:define_method, :body) do |*args| + if args.length == 1 + @body = args.first + return self + elsif args.length == 0 + return @body + else + raise ArgumentError, + "wrong number of arguments (#{args.length}; expecting 0 or 1)" + end + end + + metaclass.send(:define_method, :body=) do |body| + @body = body + end + + metaclass.send(:alias_method, body_name, :body) + metaclass.send(:alias_method, body_name_equals, :body=) + end + + # If applicable, add "media" and "media=" for retrieving and setting + # the media object for this request. + # Examples of setting the media object: + # request.media = object + # request.execute + # OR + # request.media(object).execute + # Example of retrieving the media object: + # object = request.media + if method.media_upload + metaclass.send(:define_method, :media) do |*args| + if args.length == 1 + @media = args.first + return self + elsif args.length == 0 + return @media + else + raise ArgumentError, + "wrong number of arguments (#{args.length}; expecting 0 or 1)" + end + end + + metaclass.send(:define_method, :media=) do |media| + @media = media + end + end + end + + ## + # Returns the parent service capable of executing this request. + # + # @return [Google::APIClient::Service] The parent service. + attr_reader :service + + ## + # Returns the Method instance that describes the API method invoked by + # the request. + # + # @return [Google::APIClient::Method] The API method description. + attr_reader :method + + ## + # Contains the Hash of parameter names and values to be sent as the + # parameters for the API call. + # + # @return [Hash] The request parameters. + attr_accessor :parameters + + ## + # Executes the request. + def execute + @service.execute(self) + end + end + end + end +end diff --git a/lib/google/api_client/service/resource.rb b/lib/google/api_client/service/resource.rb new file mode 100755 index 000000000..b493769d4 --- /dev/null +++ b/lib/google/api_client/service/resource.rb @@ -0,0 +1,40 @@ +# Copyright 2013 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. + +module Google + class APIClient + class Service + ## + # Handles an API resource. + # Simple class that contains API methods and/or child resources. + class Resource + include Google::APIClient::Service::StubGenerator + + ## + # Build a resource. + # This class should not be directly instantiated in user code; resources + # are instantiated by the stub generation mechanism on Service creation. + # + # @param [Google::APIClient::Service] service + # The Service instance this resource belongs to. + # @param [Google::APIClient::API, Google::APIClient::Resource] root + # The node corresponding to this resource. + def initialize(service, root) + @service = service + generate_call_stubs(service, root) + end + end + end + end +end diff --git a/lib/google/api_client/service/result.rb b/lib/google/api_client/service/result.rb new file mode 100755 index 000000000..7957ea6a2 --- /dev/null +++ b/lib/google/api_client/service/result.rb @@ -0,0 +1,162 @@ +# Copyright 2013 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. + +module Google + class APIClient + class Service + ## + # Handles an API result. + # Wraps around the Google::APIClient::Result class, making it easier to + # handle the result (e.g. pagination) and keeping it in line with the rest + # of the Service programming interface. + class Result + extend Forwardable + + ## + # Init the result. + # + # @param [Google::APIClient::Service::Request] request + # The original request + # @param [Google::APIClient::Result] base_result + # The base result to be wrapped + def initialize(request, base_result) + @request = request + @base_result = base_result + end + + # @!attribute [r] status + # @return [Fixnum] HTTP status code + # @!attribute [r] headers + # @return [Hash] HTTP response headers + # @!attribute [r] body + # @return [String] HTTP response body + def_delegators :@base_result, :status, :headers, :body + + # @return [Google::APIClient::Service::Request] Original request object + attr_reader :request + + ## + # Get the content type of the response + # @!attribute [r] media_type + # @return [String] + # Value of content-type header + def_delegators :@base_result, :media_type + + ## + # Check if request failed + # + # @!attribute [r] error? + # @return [TrueClass, FalseClass] + # true if result of operation is an error + def_delegators :@base_result, :error? + + ## + # Check if request was successful + # + # @!attribute [r] success? + # @return [TrueClass, FalseClass] + # true if result of operation was successful + def_delegators :@base_result, :success? + + ## + # Extracts error messages from the response body + # + # @!attribute [r] error_message + # @return [String] + # error message, if available + def_delegators :@base_result, :error_message + + ## + # Check for parsable data in response + # + # @!attribute [r] data? + # @return [TrueClass, FalseClass] + # true if body can be parsed + def_delegators :@base_result, :data? + + ## + # Return parsed version of the response body. + # + # @!attribute [r] data + # @return [Object, Hash, String] + # Object if body parsable from API schema, Hash if JSON, raw body if unable to parse + def_delegators :@base_result, :data + + ## + # Pagination scheme used by this request/response + # + # @!attribute [r] pagination_type + # @return [Symbol] + # currently always :token + def_delegators :@base_result, :pagination_type + + ## + # Name of the field that contains the pagination token + # + # @!attribute [r] page_token_param + # @return [String] + # currently always 'pageToken' + def_delegators :@base_result, :page_token_param + + ## + # Get the token used for requesting the next page of data + # + # @!attribute [r] next_page_token + # @return [String] + # next page tokenx = + def_delegators :@base_result, :next_page_token + + ## + # Get the token used for requesting the previous page of data + # + # @!attribute [r] prev_page_token + # @return [String] + # previous page token + def_delegators :@base_result, :prev_page_token + + # @!attribute [r] resumable_upload + def resumable_upload + # TODO(sgomes): implement resumable_upload for Service::Result + raise NotImplementedError + end + + ## + # Build a request for fetching the next page of data + # + # @return [Google::APIClient::Service::Request] + # API request for retrieving next page + def next_page + request = @request.clone + # Make a deep copy of the parameters. + request.parameters = Marshal.load(Marshal.dump(request.parameters)) + request.parameters[page_token_param] = self.next_page_token + return request + end + + ## + # Build a request for fetching the previous page of data + # + # @return [Google::APIClient::Service::Request] + # API request for retrieving previous page + def prev_page + request = @request.clone + # Make a deep copy of the parameters. + request.parameters = Marshal.load(Marshal.dump(request.parameters)) + request.parameters[page_token_param] = self.prev_page_token + return request + end + end + end + end +end diff --git a/lib/google/api_client/service/stub_generator.rb b/lib/google/api_client/service/stub_generator.rb new file mode 100755 index 000000000..37fdc810c --- /dev/null +++ b/lib/google/api_client/service/stub_generator.rb @@ -0,0 +1,59 @@ +# Copyright 2013 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. + +module Google + class APIClient + class Service + ## + # Auxiliary mixin to generate resource and method stubs. + # Used by the Service and Service::Resource classes to generate both + # top-level and nested resources and methods. + module StubGenerator + def generate_call_stubs(service, root) + metaclass = (class << self; self; end) + + # Handle resources. + root.discovered_resources.each do |resource| + method_name = Google::INFLECTOR.underscore(resource.name).to_sym + if !self.respond_to?(method_name) + metaclass.send(:define_method, method_name) do + Google::APIClient::Service::Resource.new(service, resource) + end + end + end + + # Handle methods. + root.discovered_methods.each do |method| + method_name = Google::INFLECTOR.underscore(method.name).to_sym + if !self.respond_to?(method_name) + metaclass.send(:define_method, method_name) do |*args| + if args.length > 1 + raise ArgumentError, + "wrong number of arguments (#{args.length} for 1)" + elsif !args.first.respond_to?(:to_hash) && !args.first.nil? + raise ArgumentError, + "expected parameter Hash, got #{args.first.class}" + else + return Google::APIClient::Service::Request.new( + service, method, args.first + ) + end + end + end + end + end + end + end + end +end diff --git a/spec/google/api_client/service_spec.rb b/spec/google/api_client/service_spec.rb new file mode 100644 index 000000000..43e3a718c --- /dev/null +++ b/spec/google/api_client/service_spec.rb @@ -0,0 +1,464 @@ +# encoding:utf-8 + +# Copyright 2013 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 'spec_helper' + +require 'google/api_client' +require 'google/api_client/service' + +fixtures_path = File.expand_path('../../../fixtures', __FILE__) + +describe Google::APIClient::Service do + include ConnectionHelpers + + APPLICATION_NAME = 'API Client Tests' + + it 'should error out when called without an API name or version' do + (lambda do + Google::APIClient::Service.new + end).should raise_error(ArgumentError) + end + + it 'should error out when called without an API version' do + (lambda do + Google::APIClient::Service.new('foo') + end).should raise_error(ArgumentError) + end + + it 'should error out when the options hash is not a hash' do + (lambda do + Google::APIClient::Service.new('foo', 'v1', 42) + end).should raise_error(ArgumentError) + end + + describe 'with the AdSense Management API' do + + it 'should make a valid call for a method with no parameters' do + conn = stub_connection do |stub| + stub.get('/adsense/v1.3/adclients') do |env| + end + end + adsense = Google::APIClient::Service.new( + 'adsense', + 'v1.3', + { + :application_name => APPLICATION_NAME, + :authenticated => false, + :connection => conn + } + ) + + req = adsense.adclients.list.execute() + conn.verify + end + + it 'should make a valid call for a method with parameters' do + conn = stub_connection do |stub| + stub.get('/adsense/v1.3/adclients/1/adunits') do |env| + end + end + adsense = Google::APIClient::Service.new( + 'adsense', + 'v1.3', + { + :application_name => APPLICATION_NAME, + :authenticated => false, + :connection => conn + } + ) + req = adsense.adunits.list(:adClientId => '1').execute() + end + + it 'should make a valid call for a deep method' do + conn = stub_connection do |stub| + stub.get('/adsense/v1.3/accounts/1/adclients') do |env| + end + end + adsense = Google::APIClient::Service.new( + 'adsense', + 'v1.3', + { + :application_name => APPLICATION_NAME, + :authenticated => false, + :connection => conn + } + ) + req = adsense.accounts.adclients.list(:accountId => '1').execute() + end + + describe 'with no connection' do + before do + @adsense = Google::APIClient::Service.new('adsense', 'v1.3', + {:application_name => APPLICATION_NAME}) + end + + it 'should return a resource when using a valid resource name' do + @adsense.accounts.should be_a(Google::APIClient::Service::Resource) + end + + it 'should throw an error when using an invalid resource name' do + (lambda do + @adsense.invalid_resource + end).should raise_error + end + + it 'should return a request when using a valid method name' do + req = @adsense.adclients.list + req.should be_a(Google::APIClient::Service::Request) + req.method.id.should == 'adsense.adclients.list' + req.parameters.should be_nil + end + + it 'should throw an error when using an invalid method name' do + (lambda do + @adsense.adclients.invalid_method + end).should raise_error + end + + it 'should return a valid request with parameters' do + req = @adsense.adunits.list(:adClientId => '1') + req.should be_a(Google::APIClient::Service::Request) + req.method.id.should == 'adsense.adunits.list' + req.parameters.should_not be_nil + req.parameters[:adClientId].should == '1' + end + end + end + + describe 'with the Prediction API' do + + it 'should make a valid call with an object body' do + conn = stub_connection do |stub| + stub.post('/prediction/v1.5/trainedmodels?project=1') do |env| + env.body.should == '{"id":"1"}' + end + end + prediction = Google::APIClient::Service.new( + 'prediction', + 'v1.5', + { + :application_name => APPLICATION_NAME, + :authenticated => false, + :connection => conn + } + ) + req = prediction.trainedmodels.insert(:project => '1').body({'id' => '1'}).execute() + conn.verify + end + + it 'should make a valid call with a text body' do + conn = stub_connection do |stub| + stub.post('/prediction/v1.5/trainedmodels?project=1') do |env| + env.body.should == '{"id":"1"}' + end + end + prediction = Google::APIClient::Service.new( + 'prediction', + 'v1.5', + { + :application_name => APPLICATION_NAME, + :authenticated => false, + :connection => conn + } + ) + req = prediction.trainedmodels.insert(:project => '1').body('{"id":"1"}').execute() + conn.verify + end + + describe 'with no connection' do + before do + @prediction = Google::APIClient::Service.new('prediction', 'v1.5', + {:application_name => APPLICATION_NAME}) + end + + it 'should return a valid request with a body' do + req = @prediction.trainedmodels.insert(:project => '1').body({'id' => '1'}) + req.should be_a(Google::APIClient::Service::Request) + req.method.id.should == 'prediction.trainedmodels.insert' + req.body.should == {'id' => '1'} + req.parameters.should_not be_nil + req.parameters[:project].should == '1' + end + + it 'should return a valid request with a body when using resource name' do + req = @prediction.trainedmodels.insert(:project => '1').training({'id' => '1'}) + req.should be_a(Google::APIClient::Service::Request) + req.method.id.should == 'prediction.trainedmodels.insert' + req.training.should == {'id' => '1'} + req.parameters.should_not be_nil + req.parameters[:project].should == '1' + end + end + end + + describe 'with the Drive API' do + + before do + @metadata = { + 'title' => 'My movie', + 'description' => 'The best home movie ever made' + } + @file = File.expand_path('files/sample.txt', fixtures_path) + @media = Google::APIClient::UploadIO.new(@file, 'text/plain') + end + + it 'should make a valid call with an object body and media upload' do + conn = stub_connection do |stub| + stub.post('/upload/drive/v1/files?uploadType=multipart') do |env| + env.body.should be_a Faraday::CompositeReadIO + end + end + drive = Google::APIClient::Service.new( + 'drive', + 'v1', + { + :application_name => APPLICATION_NAME, + :authenticated => false, + :connection => conn + } + ) + req = drive.files.insert(:uploadType => 'multipart').body(@metadata).media(@media).execute() + conn.verify + end + + describe 'with no connection' do + before do + @drive = Google::APIClient::Service.new('drive', 'v1', + {:application_name => APPLICATION_NAME}) + end + + it 'should return a valid request with a body and media upload' do + req = @drive.files.insert(:uploadType => 'multipart').body(@metadata).media(@media) + req.should be_a(Google::APIClient::Service::Request) + req.method.id.should == 'drive.files.insert' + req.body.should == @metadata + req.media.should == @media + req.parameters.should_not be_nil + req.parameters[:uploadType].should == 'multipart' + end + + it 'should return a valid request with a body and media upload when using resource name' do + req = @drive.files.insert(:uploadType => 'multipart').file(@metadata).media(@media) + req.should be_a(Google::APIClient::Service::Request) + req.method.id.should == 'drive.files.insert' + req.file.should == @metadata + req.media.should == @media + req.parameters.should_not be_nil + req.parameters[:uploadType].should == 'multipart' + end + end + end +end + + +describe Google::APIClient::Service::Result do + + describe 'with the plus API' do + before do + @plus = Google::APIClient::Service.new('plus', 'v1', + {:application_name => APPLICATION_NAME}) + @reference = Google::APIClient::Reference.new({ + :api_method => @plus.activities.list.method, + :parameters => { + 'userId' => 'me', + 'collection' => 'public', + 'maxResults' => 20 + } + }) + @request = @plus.activities.list(:userId => 'me', :collection => 'public', + :maxResults => 20) + + # Response double + @response = double("response") + @response.stub(:status).and_return(200) + @response.stub(:headers).and_return({ + 'etag' => '12345', + 'x-google-apiary-auth-scopes' => + 'https://www.googleapis.com/auth/plus.me', + 'content-type' => 'application/json; charset=UTF-8', + 'date' => 'Mon, 23 Apr 2012 00:00:00 GMT', + 'cache-control' => 'private, max-age=0, must-revalidate, no-transform', + 'server' => 'GSE', + 'connection' => 'close' + }) + end + + describe 'with a next page token' do + before do + @body = <<-END_OF_STRING + { + "kind": "plus#activityFeed", + "etag": "FOO", + "nextPageToken": "NEXT+PAGE+TOKEN", + "selfLink": "https://www.googleapis.com/plus/v1/people/foo/activities/public?", + "nextLink": "https://www.googleapis.com/plus/v1/people/foo/activities/public?maxResults=20&pageToken=NEXT%2BPAGE%2BTOKEN", + "title": "Plus Public Activity Feed for ", + "updated": "2012-04-23T00:00:00.000Z", + "id": "123456790", + "items": [] + } + END_OF_STRING + @response.stub(:body).and_return(@body) + base_result = Google::APIClient::Result.new(@reference, @response) + @result = Google::APIClient::Service::Result.new(@request, base_result) + end + + it 'should indicate a successful response' do + @result.error?.should be_false + end + + it 'should return the correct next page token' do + @result.next_page_token.should == 'NEXT+PAGE+TOKEN' + end + + it 'generate a correct request when calling next_page' do + next_page_request = @result.next_page + next_page_request.parameters.should include('pageToken') + next_page_request.parameters['pageToken'].should == 'NEXT+PAGE+TOKEN' + @request.parameters.each_pair do |param, value| + next_page_request.parameters[param].should == value + end + end + + it 'should return content type correctly' do + @result.media_type.should == 'application/json' + end + + it 'should return the body correctly' do + @result.body.should == @body + end + + it 'should return the result data correctly' do + @result.data?.should be_true + @result.data.class.to_s.should == + 'Google::APIClient::Schema::Plus::V1::ActivityFeed' + @result.data.kind.should == 'plus#activityFeed' + @result.data.etag.should == 'FOO' + @result.data.nextPageToken.should == 'NEXT+PAGE+TOKEN' + @result.data.selfLink.should == + 'https://www.googleapis.com/plus/v1/people/foo/activities/public?' + @result.data.nextLink.should == + 'https://www.googleapis.com/plus/v1/people/foo/activities/public?' + + 'maxResults=20&pageToken=NEXT%2BPAGE%2BTOKEN' + @result.data.title.should == 'Plus Public Activity Feed for ' + @result.data.id.should == "123456790" + @result.data.items.should be_empty + end + end + + describe 'without a next page token' do + before do + @body = <<-END_OF_STRING + { + "kind": "plus#activityFeed", + "etag": "FOO", + "selfLink": "https://www.googleapis.com/plus/v1/people/foo/activities/public?", + "title": "Plus Public Activity Feed for ", + "updated": "2012-04-23T00:00:00.000Z", + "id": "123456790", + "items": [] + } + END_OF_STRING + @response.stub(:body).and_return(@body) + base_result = Google::APIClient::Result.new(@reference, @response) + @result = Google::APIClient::Service::Result.new(@request, base_result) + end + + it 'should not return a next page token' do + @result.next_page_token.should == nil + end + + it 'should return content type correctly' do + @result.media_type.should == 'application/json' + end + + it 'should return the body correctly' do + @result.body.should == @body + end + + it 'should return the result data correctly' do + @result.data?.should be_true + @result.data.class.to_s.should == + 'Google::APIClient::Schema::Plus::V1::ActivityFeed' + @result.data.kind.should == 'plus#activityFeed' + @result.data.etag.should == 'FOO' + @result.data.selfLink.should == + 'https://www.googleapis.com/plus/v1/people/foo/activities/public?' + @result.data.title.should == 'Plus Public Activity Feed for ' + @result.data.id.should == "123456790" + @result.data.items.should be_empty + end + end + + describe 'with JSON error response' do + before do + @body = <<-END_OF_STRING + { + "error": { + "errors": [ + { + "domain": "global", + "reason": "parseError", + "message": "Parse Error" + } + ], + "code": 400, + "message": "Parse Error" + } + } + END_OF_STRING + @response.stub(:body).and_return(@body) + @response.stub(:status).and_return(400) + base_result = Google::APIClient::Result.new(@reference, @response) + @result = Google::APIClient::Service::Result.new(@request, base_result) + end + + it 'should return error status correctly' do + @result.error?.should be_true + end + + it 'should return the correct error message' do + @result.error_message.should == 'Parse Error' + end + + it 'should return the body correctly' do + @result.body.should == @body + end + end + + describe 'with 204 No Content response' do + before do + @response.stub(:body).and_return('') + @response.stub(:status).and_return(204) + @response.stub(:headers).and_return({}) + base_result = Google::APIClient::Result.new(@reference, @response) + @result = Google::APIClient::Service::Result.new(@request, base_result) + end + + it 'should indicate no data is available' do + @result.data?.should be_false + end + + it 'should return nil for data' do + @result.data.should == nil + end + + it 'should return nil for media_type' do + @result.media_type.should == nil + end + end + end +end From 1e2405093b16a097bf50ac4ee2123408a2efee48 Mon Sep 17 00:00:00 2001 From: Sergio Gomes Date: Wed, 11 Sep 2013 16:51:10 +0100 Subject: [PATCH 2/4] - Improving the Service interface with access to more properties - Adding end-to-end spec - Fixing bugs --- lib/google/api_client/service.rb | 148 +++++++++++++++---------- spec/google/api_client/service_spec.rb | 11 ++ 2 files changed, 102 insertions(+), 57 deletions(-) diff --git a/lib/google/api_client/service.rb b/lib/google/api_client/service.rb index 5e4aaf71c..f7261144d 100755 --- a/lib/google/api_client/service.rb +++ b/lib/google/api_client/service.rb @@ -30,52 +30,56 @@ module Google # result = calendar.events.list('calendarId' => 'primary').execute() class Service include Google::APIClient::Service::StubGenerator + extend Forwardable - ## - # Creates a new Service. - # - # @param [String, Symbol] api_name - # The name of the API this service will access. - # @param [String, Symbol] api_version - # The version of the API this service will access. - # @param [Hash] options - # The configuration parameters for the service. - # @option options [Symbol, #generate_authenticated_request] :authorization - # (:oauth_1) - # The authorization mechanism used by the client. The following - # mechanisms are supported out-of-the-box: - # - # @option options [Boolean] :auto_refresh_token (true) - # The setting that controls whether or not the api client attempts to - # refresh authorization when a 401 is hit in #execute. If the token does - # not support it, this option is ignored. - # @option options [String] :application_name - # The name of the application using the client. - # @option options [String] :application_version - # The version number of the application using the client. - # @option options [String] :host ("www.googleapis.com") - # The API hostname used by the client. This rarely needs to be changed. - # @option options [String] :port (443) - # The port number used by the client. This rarely needs to be changed. - # @option options [String] :discovery_path ("/discovery/v1") - # The discovery base path. This rarely needs to be changed. - # @option options [String] :ca_file - # Optional set of root certificates to use when validating SSL connections. - # By default, a bundled set of trusted roots will be used. - # @option options [#generate_authenticated_request] :authorization - # The authorization mechanism for requests. Used only if - # `:authenticated` is `true`. - # @option options [TrueClass, FalseClass] :authenticated (default: true) - # `true` if requests must be signed or somehow - # authenticated, `false` otherwise. - # @option options [TrueClass, FalseClass] :gzip (default: true) - # `true` if gzip enabled, `false` otherwise. - # @option options [Faraday] :connection - # A custom connection to be used for all requests. + # Cache for discovered APIs. + @@discovered = {} + + ## + # Creates a new Service. + # + # @param [String, Symbol] api_name + # The name of the API this service will access. + # @param [String, Symbol] api_version + # The version of the API this service will access. + # @param [Hash] options + # The configuration parameters for the service. + # @option options [Symbol, #generate_authenticated_request] :authorization + # (:oauth_1) + # The authorization mechanism used by the client. The following + # mechanisms are supported out-of-the-box: + # + # @option options [Boolean] :auto_refresh_token (true) + # The setting that controls whether or not the api client attempts to + # refresh authorization when a 401 is hit in #execute. If the token does + # not support it, this option is ignored. + # @option options [String] :application_name + # The name of the application using the client. + # @option options [String] :application_version + # The version number of the application using the client. + # @option options [String] :host ("www.googleapis.com") + # The API hostname used by the client. This rarely needs to be changed. + # @option options [String] :port (443) + # The port number used by the client. This rarely needs to be changed. + # @option options [String] :discovery_path ("/discovery/v1") + # The discovery base path. This rarely needs to be changed. + # @option options [String] :ca_file + # Optional set of root certificates to use when validating SSL connections. + # By default, a bundled set of trusted roots will be used. + # @option options [#generate_authenticated_request] :authorization + # The authorization mechanism for requests. Used only if + # `:authenticated` is `true`. + # @option options [TrueClass, FalseClass] :authenticated (default: true) + # `true` if requests must be signed or somehow + # authenticated, `false` otherwise. + # @option options [TrueClass, FalseClass] :gzip (default: true) + # `true` if gzip enabled, `false` otherwise. + # @option options [Faraday::Connection] :connection + # A custom connection to be used for all requests. def initialize(api_name, api_version, options = {}) @api_name = api_name.to_s if api_version.nil? @@ -98,35 +102,65 @@ module Google end @client = Google::APIClient.new(params) + @client.logger = options[:logger] if options.include? :logger + + @connection = options[:connection] || @client.connection @options = options - @api = @client.discovered_api(api_name, api_version) + + # Cache discovered APIs in memory. + # Not thread-safe, but the worst that can happen is a cache miss. + unless @api = @@discovered[[api_name, api_version]] + @@discovered[[api_name, api_version]] = @api = @client.discovered_api( + api_name, api_version) + end + generate_call_stubs(self, @api) end ## # Logger for the Service. # - # @return [Logger] logger instance. - def logger - @client.logger - end + # @return [Logger] + # The logger instance. + def_delegators :@client, :logger, :logger= ## - # Set the Logger for the Service. - def logger=(obj) - @client.logger = obj - end + # Returns the authorization mechanism used by the service. + # + # @return [#generate_authenticated_request] The authorization mechanism. + def_delegators :@client, :authorization, :authorization= + + ## + # The setting that controls whether or not the service attempts to + # refresh authorization when a 401 is hit during an API call. + # + # @return [Boolean] + def_delegators :@client, :auto_refresh_token, :auto_refresh_token= + + ## + # The application's API key issued by the API console. + # + # @return [String] The API key. + def_delegators :@client, :key, :key= + + ## + # The Faraday/HTTP connection used by this service. + # + # @return [Faraday::Connection] + attr_accessor :connection ## # Executes an API request. # Do not call directly; this method is only used by Request objects when # executing. + # # @param [Google::APIClient::Service::Request] request # The request to be executed. def execute(request) params = {:api_method => request.method, - :parameters => request.parameters} + :parameters => request.parameters, + :connection => @connection} if request.respond_to? :body if request.body.respond_to? :to_hash params[:body_object] = request.body @@ -137,13 +171,13 @@ module Google if request.respond_to? :media params[:media] = request.media end - [:connection, :authenticated, :gzip].each do |option| + [:authenticated, :gzip].each do |option| if @options.include? option params[option] = @options[option] end end result = @client.execute(params) - return Google::APIClient::Result.new(request, result) + return Google::APIClient::Service::Result.new(request, result) end end end diff --git a/spec/google/api_client/service_spec.rb b/spec/google/api_client/service_spec.rb index 43e3a718c..464ffda21 100644 --- a/spec/google/api_client/service_spec.rb +++ b/spec/google/api_client/service_spec.rb @@ -261,6 +261,17 @@ describe Google::APIClient::Service do end end end + + describe 'with the Discovery API' do + it 'should make a valid end-to-end request' do + discovery = Google::APIClient::Service.new('discovery', 'v1', + {:application_name => APPLICATION_NAME, :authenticated => false}) + result = discovery.apis.get_rest(:api => 'discovery', :version => 'v1').execute + result.should_not be_nil + result.data.name.should == 'discovery' + result.data.version.should == 'v1' + end + end end From 83d411990c88695753d5578afd273a363d7c6bd1 Mon Sep 17 00:00:00 2001 From: Sergio Gomes Date: Fri, 4 Oct 2013 15:56:06 +0100 Subject: [PATCH 3/4] Adding batch support to new service interface --- lib/google/api_client/service.rb | 57 +++++++++----- lib/google/api_client/service/batch.rb | 103 +++++++++++++++++++++++++ spec/google/api_client/service_spec.rb | 97 +++++++++++++++++++++++ 3 files changed, 239 insertions(+), 18 deletions(-) create mode 100644 lib/google/api_client/service/batch.rb diff --git a/lib/google/api_client/service.rb b/lib/google/api_client/service.rb index f7261144d..f538db5ff 100755 --- a/lib/google/api_client/service.rb +++ b/lib/google/api_client/service.rb @@ -17,6 +17,7 @@ require 'google/api_client/service/stub_generator' require 'google/api_client/service/resource' require 'google/api_client/service/request' require 'google/api_client/service/result' +require 'google/api_client/service/batch' module Google class APIClient @@ -150,34 +151,54 @@ module Google # @return [Faraday::Connection] attr_accessor :connection + ## + # Prepares a Google::APIClient::BatchRequest object to make batched calls. + # @param [Array] calls + # Optional array of Google::APIClient::Service::Request to initialize + # the batch request with. + # @param [Proc] block + # Callback for every call's response. Won't be called if a call defined + # a callback of its own. + # + # @yield [Google::APIClient::Service::Result] + # block to be called when result ready + def batch(calls = nil, &block) + Google::APIClient::Service::BatchRequest.new(self, calls, &block) + end + ## # Executes an API request. # Do not call directly; this method is only used by Request objects when # executing. # - # @param [Google::APIClient::Service::Request] request + # @param [Google::APIClient::Service::Request, + # Google::APIClient::Service::BatchCall] request # The request to be executed. def execute(request) - params = {:api_method => request.method, - :parameters => request.parameters, - :connection => @connection} - if request.respond_to? :body - if request.body.respond_to? :to_hash - params[:body_object] = request.body - else - params[:body] = request.body + if request.instance_of? Google::APIClient::Service::Request + params = {:api_method => request.method, + :parameters => request.parameters, + :connection => @connection} + if request.respond_to? :body + if request.body.respond_to? :to_hash + params[:body_object] = request.body + else + params[:body] = request.body + end end - end - if request.respond_to? :media - params[:media] = request.media - end - [:authenticated, :gzip].each do |option| - if @options.include? option - params[option] = @options[option] + if request.respond_to? :media + params[:media] = request.media end + [:authenticated, :gzip].each do |option| + if @options.include? option + params[option] = @options[option] + end + end + result = @client.execute(params) + return Google::APIClient::Service::Result.new(request, result) + elsif request.instance_of? Google::APIClient::Service::BatchRequest + @client.execute(request.base_batch) end - result = @client.execute(params) - return Google::APIClient::Service::Result.new(request, result) end end end diff --git a/lib/google/api_client/service/batch.rb b/lib/google/api_client/service/batch.rb new file mode 100644 index 000000000..7aa749c04 --- /dev/null +++ b/lib/google/api_client/service/batch.rb @@ -0,0 +1,103 @@ +# Copyright 2013 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 'google/api_client/service/result' +require 'google/api_client/batch' + +module Google + class APIClient + class Service + + ## + # Helper class to contain the result of an individual batched call. + # + class BatchedCallResult < Result + # @return [String] UUID of the call + def call_index + return @base_result.response.call_id.to_i - 1 + end + end + + ## + # + # + class BatchRequest + ## + # Creates a new batch request. + # This class shouldn't be instantiated directly, but rather through + # Service.batch. + # + # @param [Array] calls + # List of Google::APIClient::Service::Request to be made. + # @param [Proc] block + # Callback for every call's response. Won't be called if a call + # defined a callback of its own. + # + # @yield [Google::APIClient::Service::Result] + # block to be called when result ready + def initialize(service, calls, &block) + @service = service + @base_batch = Google::APIClient::BatchRequest.new + @global_callback = block if block_given? + + if calls && calls.length > 0 + calls.each do |call| + add(call) + end + end + end + + ## + # Add a new call to the batch request. + # + # @param [Google::APIClient::Service::Request] call + # the call to be added. + # @param [Proc] block + # callback for this call's response. + # + # @return [Google::APIClient::Service::BatchRequest] + # the BatchRequest, for chaining + # + # @yield [Google::APIClient::Service::Result] + # block to be called when result ready + def add(call, &block) + if !block_given? && @global_callback.nil? + raise BatchError, 'Request needs a block' + end + callback = block || @global_callback + base_call = { + :api_method => call.method, + :parameters => call.parameters + } + @base_batch.add(base_call) do |base_result| + result = Google::APIClient::Service::BatchedCallResult.new( + call, base_result) + callback.call(result) + end + return self + end + + ## + # Executes the batch request. + def execute + @service.execute(self) + end + + attr_reader :base_batch + + end + + end + end +end diff --git a/spec/google/api_client/service_spec.rb b/spec/google/api_client/service_spec.rb index 464ffda21..906bf494b 100644 --- a/spec/google/api_client/service_spec.rb +++ b/spec/google/api_client/service_spec.rb @@ -473,3 +473,100 @@ describe Google::APIClient::Service::Result do end end end + +describe Google::APIClient::Service::BatchRequest do + describe 'with the discovery API' do + before do + @discovery = Google::APIClient::Service.new('discovery', 'v1', + {:application_name => APPLICATION_NAME, :authorization => nil}) + end + + describe 'with two valid requests' do + before do + @calls = [ + @discovery.apis.get_rest(:api => 'plus', :version => 'v1'), + @discovery.apis.get_rest(:api => 'discovery', :version => 'v1') + ] + end + + it 'should execute both when using a global callback' do + block_called = 0 + batch = @discovery.batch(@calls) do |result| + block_called += 1 + result.status.should == 200 + end + + batch.execute + block_called.should == 2 + end + + it 'should execute both when using individual callbacks' do + call1_returned, call2_returned = false, false + batch = @discovery.batch + + batch.add(@calls[0]) do |result| + call1_returned = true + result.status.should == 200 + result.call_index.should == 0 + end + + batch.add(@calls[1]) do |result| + call2_returned = true + result.status.should == 200 + result.call_index.should == 1 + end + + batch.execute + call1_returned.should == true + call2_returned.should == true + end + end + + describe 'with a valid request and an invalid one' do + before do + @calls = [ + @discovery.apis.get_rest(:api => 'plus', :version => 'v1'), + @discovery.apis.get_rest(:api => 'invalid', :version => 'invalid') + ] + end + + it 'should execute both when using a global callback' do + block_called = 0 + batch = @discovery.batch(@calls) do |result| + block_called += 1 + if result.call_index == 0 + result.status.should == 200 + else + result.status.should >= 400 + result.status.should < 500 + end + end + + batch.execute + block_called.should == 2 + end + + it 'should execute both when using individual callbacks' do + call1_returned, call2_returned = false, false + batch = @discovery.batch + + batch.add(@calls[0]) do |result| + call1_returned = true + result.status.should == 200 + result.call_index.should == 0 + end + + batch.add(@calls[1]) do |result| + call2_returned = true + result.status.should >= 400 + result.status.should < 500 + result.call_index.should == 1 + end + + batch.execute + call1_returned.should == true + call2_returned.should == true + end + end + end +end \ No newline at end of file From 876dddad36fa1c69ff3d827cfcfb0d01c2fdb1bb Mon Sep 17 00:00:00 2001 From: Sergio Gomes Date: Mon, 7 Oct 2013 18:11:45 +0100 Subject: [PATCH 4/4] Minor doc fix --- lib/google/api_client/service/batch.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/google/api_client/service/batch.rb b/lib/google/api_client/service/batch.rb index 7aa749c04..7a25776a3 100644 --- a/lib/google/api_client/service/batch.rb +++ b/lib/google/api_client/service/batch.rb @@ -23,7 +23,7 @@ module Google # Helper class to contain the result of an individual batched call. # class BatchedCallResult < Result - # @return [String] UUID of the call + # @return [Fixnum] Index of the call def call_index return @base_result.response.call_id.to_i - 1 end