diff --git a/lib/google/api_client/discovery.rb b/lib/google/api_client/discovery.rb index 4a72acf60..bb01d67ce 100644 --- a/lib/google/api_client/discovery.rb +++ b/lib/google/api_client/discovery.rb @@ -16,3 +16,4 @@ require 'google/api_client/discovery/api' require 'google/api_client/discovery/resource' require 'google/api_client/discovery/method' +require 'google/api_client/discovery/schema' diff --git a/lib/google/api_client/discovery/api.rb b/lib/google/api_client/discovery/api.rb index 44b245a47..8abea2d04 100644 --- a/lib/google/api_client/discovery/api.rb +++ b/lib/google/api_client/discovery/api.rb @@ -62,7 +62,10 @@ module Google # # @return [String] The service id. def id - return @discovery_document['id'] + return ( + @discovery_document['id'] || + "#{self.name}:#{self.version}" + ) end ## @@ -168,15 +171,47 @@ module Google end end + ## + # A list of schemas available for this version of the API. + # + # @return [Hash] A list of {Google::APIClient::Schema} objects. + def schemas + return @schemas ||= ( + (@discovery_document['schemas'] || []).inject({}) do |accu, (k, v)| + accu[k] = Google::APIClient::Schema.parse(self, v) + accu + end + ) + end + + ## + # Returns a schema for a kind value. + # + # @return [Google::APIClient::Schema] The associated Schema object. + def schema_for_kind(kind) + api_name, schema_name = kind.split('#', 2) + if api_name != self.name + raise ArgumentError, + "The kind does not match this API. " + + "Expected '#{self.name}', got '#{api_name}'." + end + for k, v in self.schemas + return v if k.downcase == schema_name.downcase + end + return nil + end + ## # A list of resources available at the root level of this version of the - # service. + # API. # # @return [Array] A list of {Google::APIClient::Resource} objects. def resources return @resources ||= ( (@discovery_document['resources'] || []).inject([]) do |accu, (k, v)| - accu << Google::APIClient::Resource.new(self.method_base, k, v) + accu << Google::APIClient::Resource.new( + self, self.method_base, k, v + ) accu end ) @@ -184,13 +219,13 @@ module Google ## # A list of methods available at the root level of this version of the - # service. + # API. # # @return [Array] A list of {Google::APIClient::Method} objects. def methods return @methods ||= ( (@discovery_document['methods'] || []).inject([]) do |accu, (k, v)| - accu << Google::APIClient::Method.new(self.method_base, k, v) + accu << Google::APIClient::Method.new(self, self.method_base, k, v) accu end ) diff --git a/lib/google/api_client/discovery/method.rb b/lib/google/api_client/discovery/method.rb index de79bf752..945e4809a 100644 --- a/lib/google/api_client/discovery/method.rb +++ b/lib/google/api_client/discovery/method.rb @@ -35,7 +35,8 @@ module Google # The section of the discovery document that applies to this method. # # @return [Google::APIClient::Method] The constructed method object. - def initialize(method_base, method_name, discovery_document) + def initialize(api, method_base, method_name, discovery_document) + @api = api @method_base = method_base @name = method_name @discovery_document = discovery_document @@ -102,6 +103,32 @@ module Google ) end + ## + # Returns the Schema object for the method's request, if any. + # + # @return [Google::APIClient::Schema] The request schema. + def request_schema + if @discovery_document['request'] + schema_name = @discovery_document['request']['$ref'] + return @api.schemas[schema_name] + else + return nil + end + end + + ## + # Returns the Schema object for the method's response, if any. + # + # @return [Google::APIClient::Schema] The response schema. + def response_schema + if @discovery_document['response'] + schema_name = @discovery_document['response']['$ref'] + return @api.schemas[schema_name] + else + return nil + end + end + ## # Normalizes parameters, converting to the appropriate types. # diff --git a/lib/google/api_client/discovery/resource.rb b/lib/google/api_client/discovery/resource.rb index 6d2d6bddb..649ba3052 100644 --- a/lib/google/api_client/discovery/resource.rb +++ b/lib/google/api_client/discovery/resource.rb @@ -35,7 +35,8 @@ module Google # The section of the discovery document that applies to this resource. # # @return [Google::APIClient::Resource] The constructed resource object. - def initialize(method_base, resource_name, discovery_document) + def initialize(api, method_base, resource_name, discovery_document) + @api = api @method_base = method_base @name = resource_name @discovery_document = discovery_document @@ -95,7 +96,9 @@ module Google def resources return @resources ||= ( (@discovery_document['resources'] || []).inject([]) do |accu, (k, v)| - accu << Google::APIClient::Resource.new(self.method_base, k, v) + accu << Google::APIClient::Resource.new( + @api, self.method_base, k, v + ) accu end ) @@ -108,7 +111,7 @@ module Google def methods return @methods ||= ( (@discovery_document['methods'] || []).inject([]) do |accu, (k, v)| - accu << Google::APIClient::Method.new(self.method_base, k, v) + accu << Google::APIClient::Method.new(@api, self.method_base, k, v) accu end ) diff --git a/lib/google/api_client/discovery/schema.rb b/lib/google/api_client/discovery/schema.rb new file mode 100644 index 000000000..86f7ed397 --- /dev/null +++ b/lib/google/api_client/discovery/schema.rb @@ -0,0 +1,106 @@ +# 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 'time' +require 'json' +require 'base64' +require 'autoparse' +require 'addressable/uri' +require 'addressable/template' + +require 'google/inflection' +require 'google/api_client/errors' + +module Google + class APIClient + module Schema + def self.parse(api, schema_data) + # This method is super-long, but hard to break up due to the + # unavoidable dependence on closures and execution context. + schema_name = schema_data['id'] + + # Due to an oversight, schema IDs may not be URI references. + # TODO(bobaman): Remove this code once this has been resolved. + schema_uri = ( + api.document_base + + (schema_name[0..0] != '#' ? '#' + schema_name : schema_name) + ) + # puts schema_uri + + # Due to an oversight, schema IDs may not be URI references. + # TODO(bobaman): Remove this whole lambda once this has been resolved. + reformat_references = lambda do |data| + # This code is not particularly efficient due to recursive traversal + # and excess object creation, but this hopefully shouldn't be an + # issue since it should only be called only once per schema per + # process. + if data.kind_of?(Hash) && data['$ref'] + reference = data['$ref'] + reference = '#' + reference if reference[0..0] != '#' + data.merge({ + '$ref' => reference + }) + elsif data.kind_of?(Hash) + data.inject({}) do |accu, (key, value)| + if value.kind_of?(Hash) + accu[key] = reformat_references.call(value) + else + accu[key] = value + end + accu + end + else + data + end + end + schema_data = reformat_references.call(schema_data) + # puts schema_data.inspect + + if schema_name + api_name_string = + Google::INFLECTOR.camelize(api.name) + api_version_string = + Google::INFLECTOR.camelize(api.version).gsub('.', '_') + if Google::APIClient::Schema.const_defined?(api_name_string) + api_name = Google::APIClient::Schema.const_get(api_name_string) + else + api_name = Google::APIClient::Schema.const_set( + api_name_string, Module.new + ) + end + if api_name.const_defined?(api_version_string) + api_version = api_name.const_get(api_version_string) + else + api_version = api_name.const_set(api_version_string, Module.new) + end + if api_version.const_defined?(schema_name) + schema_class = api_version.const_get(schema_name) + end + end + + # It's possible the schema has already been defined. If so, don't + # redefine it. This means that reloading a schema which has already + # been loaded into memory is not possible. + unless schema_class + schema_class = AutoParse.generate(schema_data, :uri => schema_uri) + if schema_name + api_version.const_set(schema_name, schema_class) + end + end + return schema_class + end + end + end +end diff --git a/lib/google/api_client/result.rb b/lib/google/api_client/result.rb index f78cb3241..72c396881 100644 --- a/lib/google/api_client/result.rb +++ b/lib/google/api_client/result.rb @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -require 'google/api_client/parsers/json_parser' - module Google class APIClient ## @@ -54,9 +52,27 @@ module Google _, content_type = self.headers.detect do |h, v| h.downcase == 'Content-Type'.downcase end - parser_type = - Google::APIClient::Parser.match_content_type(content_type) - parser_type.parse(self.body) + media_type = content_type[/^([^;]*);?.*$/, 1].strip.downcase + data = self.body + case media_type + when 'application/json' + data = ::JSON.parse(data) + # Strip data wrapper, if present + data = data['data'] if data.has_key?('data') + else + raise ArgumentError, + "Content-Type not supported for parsing: #{media_type}" + end + if @reference.api_method && @reference.api_method.response_schema + # Automatically parse using the schema designated for the + # response of this API method. + data = @reference.api_method.response_schema.new(data) + data + else + # Otherwise, return the raw unparsed value. + # This value must be indexable like a Hash. + data + end end) end diff --git a/spec/google/api_client/discovery_spec.rb b/spec/google/api_client/discovery_spec.rb index 7714e25c6..d0e38e4db 100644 --- a/spec/google/api_client/discovery_spec.rb +++ b/spec/google/api_client/discovery_spec.rb @@ -297,60 +297,54 @@ describe Google::APIClient do end end - describe 'with the buzz API' do + describe 'with the plus API' do before do @client.authorization = nil - @buzz = @client.discovered_api('buzz') + @plus = @client.discovered_api('plus') end it 'should correctly determine the discovery URI' do - @client.discovery_uri('buzz').should === - 'https://www.googleapis.com/discovery/v1/apis/buzz/v1/rest' + @client.discovery_uri('plus').should === + 'https://www.googleapis.com/discovery/v1/apis/plus/v1/rest' end it 'should find APIs that are in the discovery document' do - @client.discovered_api('buzz').name.should == 'buzz' - @client.discovered_api('buzz').version.should == 'v1' - @client.discovered_api(:buzz).name.should == 'buzz' - @client.discovered_api(:buzz).version.should == 'v1' + @client.discovered_api('plus').name.should == 'plus' + @client.discovered_api('plus').version.should == 'v1' + @client.discovered_api(:plus).name.should == 'plus' + @client.discovered_api(:plus).version.should == 'v1' end it 'should find methods that are in the discovery document' do # TODO(bobaman) Fix this when the RPC names are correct @client.discovered_method( - 'chili.activities.list', 'buzz' + 'plus.activities.list', 'plus' ).name.should == 'list' end it 'should not find methods that are not in the discovery document' do - @client.discovered_method('buzz.bogus', 'buzz').should == nil - end - - it 'should fail for string RPC names that do not match API name' do - (lambda do - @client.generate_request( - :api_method => 'chili.activities.list', - :parameters => {'alt' => 'json'}, - :authenticated => false - ) - end).should raise_error(Google::APIClient::TransmissionError) + @client.discovered_method('plus.bogus', 'plus').should == nil end it 'should generate requests against the correct URIs' do request = @client.generate_request( - :api_method => @buzz.activities.list, - :parameters => {'userId' => 'hikingfan', 'scope' => '@public'}, + :api_method => @plus.activities.list, + :parameters => { + 'userId' => '107807692475771887386', 'collection' => 'public' + }, :authenticated => false ) method, uri, headers, body = request - uri.should == - 'https://www.googleapis.com/buzz/v1/activities/hikingfan/@public' + uri.should == ( + 'https://www.googleapis.com/plus/v1/' + + 'people/107807692475771887386/activities/public' + ) end it 'should correctly validate parameters' do (lambda do @client.generate_request( - :api_method => @buzz.activities.list, + :api_method => @plus.activities.list, :parameters => {'alt' => 'json'}, :authenticated => false ) @@ -360,33 +354,14 @@ describe Google::APIClient do it 'should correctly validate parameters' do (lambda do @client.generate_request( - :api_method => @buzz.activities.list, - :parameters => {'userId' => 'hikingfan', 'scope' => '@bogus'}, + :api_method => @plus.activities.list, + :parameters => { + 'userId' => '107807692475771887386', 'collection' => 'bogus' + }, :authenticated => false ) end).should raise_error(ArgumentError) end - - it 'should be able to execute requests without authorization' do - result = @client.execute( - @buzz.activities.list, - {'alt' => 'json', 'userId' => 'hikingfan', 'scope' => '@public'}, - '', - [], - :authenticated => false - ) - status, headers, body = result.response - status.should == 200 - end - - it 'should not be able to execute requests without authorization' do - result = @client.execute( - @buzz.activities.list, - 'alt' => 'json', 'userId' => '@me', 'scope' => '@self' - ) - status, headers, body = result.response - status.should == 401 - end end describe 'with the latitude API' do diff --git a/tasks/gem.rake b/tasks/gem.rake index 09df36570..fb1af805e 100644 --- a/tasks/gem.rake +++ b/tasks/gem.rake @@ -24,6 +24,7 @@ namespace :gem do s.add_runtime_dependency('signet', '~> 0.2.2') s.add_runtime_dependency('addressable', '~> 2.2.2') s.add_runtime_dependency('httpadapter', '~> 1.0.0') + s.add_runtime_dependency('autoparse', '~> 0.2.0') s.add_runtime_dependency('json', '>= 1.4.6') s.add_runtime_dependency('extlib', '>= 0.9.15')