diff --git a/bin/google-api b/bin/google-api
index 150a6dd45..71d2811c7 100755
--- a/bin/google-api
+++ b/bin/google-api
@@ -447,7 +447,7 @@ HTML
method.upcase!
request = [method, uri.to_str, headers, [request_body]]
request = client.generate_authenticated_request(:request => request)
- response = client.transmit_request(request)
+ response = client.transmit(request)
status, headers, body = response
puts body
exit(0)
@@ -477,10 +477,13 @@ HTML
parameters['xoauth_requestor_id'] = options[:requestor_id]
end
begin
- response = client.execute(
- method, parameters, request_body, headers
+ result = client.execute(
+ :api_method => method,
+ :parameters => parameters,
+ :merged_body => request_body,
+ :headers => headers
)
- status, headers, body = response
+ status, headers, body = result.response
puts body
exit(0)
rescue ArgumentError => e
diff --git a/examples/buzz.rb b/examples/buzz.rb
index 1f0a5da9d..631136a38 100644
--- a/examples/buzz.rb
+++ b/examples/buzz.rb
@@ -75,10 +75,10 @@ get '/oauth2callback' do
end
get '/' do
- response = @client.execute(
+ result = @client.execute(
@buzz.activities.list,
- 'userId' => '@me', 'scope' => '@consumption', 'alt'=> 'json'
+ {'userId' => '@me', 'scope' => '@consumption', 'alt'=> 'json'}
)
- status, headers, body = response
- [status, {'Content-Type' => 'application/json'}, body]
+ status, _, _ = result.response
+ [status, {'Content-Type' => 'application/json'}, JSON.generate(result.data)]
end
diff --git a/lib/google/api_client.rb b/lib/google/api_client.rb
index 2e062ac4d..725784cab 100644
--- a/lib/google/api_client.rb
+++ b/lib/google/api_client.rb
@@ -20,6 +20,8 @@ require 'stringio'
require 'google/api_client/errors'
require 'google/api_client/environment'
require 'google/api_client/discovery'
+require 'google/api_client/reference'
+require 'google/api_client/result'
module Google
# TODO(bobaman): Document all this stuff.
@@ -65,16 +67,6 @@ module Google
'google-api-ruby-client/' + VERSION::STRING +
' ' + ENV::OS_VERSION
).strip
- # This is mostly a default for the sake of convenience.
- # Unlike most other options, this one may be nil, so we check for
- # the presence of the key rather than checking the value.
- if options.has_key?("parser")
- self.parser = options["parser"]
- else
- require 'google/api_client/parsers/json_parser'
- # NOTE: Do not rely on this default value, as it may change
- self.parser = Google::APIClient::JSONParser
- end
# The writer method understands a few Symbols and will generate useful
# default authentication mechanisms.
self.authorization = options["authorization"] || :oauth_2
@@ -94,33 +86,6 @@ module Google
return self
end
-
- ##
- # Returns the parser used by the client.
- #
- # @return [#serialize, #parse]
- # The parser used by the client. Any object that implements both a
- # #serialize
and a #parse
method may be used.
- # If nil
, no parsing will be done.
- attr_reader :parser
-
- ##
- # Sets the parser used by the client.
- #
- # @param [#serialize, #parse] new_parser
- # The parser used by the client. Any object that implements both a
- # #serialize
and a #parse
method may be used.
- # If nil
, no parsing will be done.
- def parser=(new_parser)
- if new_parser &&
- !new_parser.respond_to?(:serialize) &&
- !new_parser.respond_to?(:parse)
- raise TypeError,
- 'Expected parser object to respond to #serialize and #parse.'
- end
- @parser = new_parser
- end
-
##
# Returns the authorization mechanism used by the client.
#
@@ -280,7 +245,7 @@ module Google
"Expected String or StringIO, got #{discovery_document.class}."
end
@discovery_documents["#{api}:#{version}"] =
- JSON.parse(discovery_document)
+ ::JSON.parse(discovery_document)
end
##
@@ -291,16 +256,21 @@ module Google
return @directory_document ||= (begin
request_uri = self.directory_uri
request = ['GET', request_uri, [], []]
- response = self.transmit_request(request)
+ response = self.transmit(request)
status, headers, body = response
if status == 200 # TODO(bobaman) Better status code handling?
- merged_body = StringIO.new
- body.each do |chunk|
- merged_body.write(chunk)
+ merged_body = body.inject(StringIO.new) do |accu, chunk|
+ accu.write(chunk)
+ accu
end
- merged_body.rewind
- JSON.parse(merged_body.string)
- else
+ ::JSON.parse(merged_body.string)
+ elsif status >= 400 && status < 500
+ raise ClientError,
+ "Could not retrieve discovery document at: #{request_uri}"
+ elsif status >= 500 && status < 600
+ raise ServerError,
+ "Could not retrieve discovery document at: #{request_uri}"
+ elsif status > 600
raise TransmissionError,
"Could not retrieve discovery document at: #{request_uri}"
end
@@ -319,16 +289,21 @@ module Google
return @discovery_documents["#{api}:#{version}"] ||= (begin
request_uri = self.discovery_uri(api, version)
request = ['GET', request_uri, [], []]
- response = self.transmit_request(request)
+ response = self.transmit(request)
status, headers, body = response
if status == 200 # TODO(bobaman) Better status code handling?
- merged_body = StringIO.new
- body.each do |chunk|
- merged_body.write(chunk)
+ merged_body = body.inject(StringIO.new) do |accu, chunk|
+ accu.write(chunk)
+ accu
end
- merged_body.rewind
- JSON.parse(merged_body.string)
- else
+ ::JSON.parse(merged_body.string)
+ elsif status >= 400 && status < 500
+ raise ClientError,
+ "Could not retrieve discovery document at: #{request_uri}"
+ elsif status >= 500 && status < 600
+ raise ServerError,
+ "Could not retrieve discovery document at: #{request_uri}"
+ elsif status > 600
raise TransmissionError,
"Could not retrieve discovery document at: #{request_uri}"
end
@@ -344,7 +319,7 @@ module Google
document_base = self.directory_uri
if self.directory_document && self.directory_document['items']
self.directory_document['items'].map do |discovery_document|
- ::Google::APIClient::API.new(
+ Google::APIClient::API.new(
document_base,
discovery_document
)
@@ -373,7 +348,7 @@ module Google
document_base = self.discovery_uri(api, version)
discovery_document = self.discovery_document(api, version)
if document_base && discovery_document
- ::Google::APIClient::API.new(
+ Google::APIClient::API.new(
document_base,
discovery_document
)
@@ -442,8 +417,6 @@ module Google
# - :version
—
# The service version. Only used if api_method
is a
# String
. Defaults to 'v1'
.
- # - :parser
—
- # The parser for the response.
# - :authorization
—
# The authorization mechanism for the response. Used only if
# :authenticated
is true
.
@@ -457,17 +430,20 @@ module Google
#
# @example
# request = client.generate_request(
- # 'chili.activities.list',
- # {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
+ # :api_method => 'chili.activities.list',
+ # :parameters =>
+ # {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
# )
# method, uri, headers, body = request
- def generate_request(
- api_method, parameters={}, body='', headers=[], options={})
+ def generate_request(options={})
+ # Note: The merge method on a Hash object will coerce an API Reference
+ # object into a Hash and merge with the default options.
options={
- :parser => self.parser,
:version => 'v1',
:authorization => self.authorization
}.merge(options)
+ # The Reference object is going to need this to do method ID lookups.
+ options[:client] = self
# The default value for the :authenticated option depends on whether an
# authorization mechanism has been set.
if options[:authorization]
@@ -475,27 +451,8 @@ module Google
else
options = {:authenticated => false}.merge(options)
end
- if api_method.kind_of?(String) || api_method.kind_of?(Symbol)
- api_method = api_method.to_s
- # This method of guessing the API is unreliable. This will fail for
- # APIs where the first segment of the RPC name does not match the
- # service name. However, this is a fallback mechanism anyway.
- # Developers should be passing in a reference to the method, rather
- # than passing in a string or symbol. This should raise an error
- # in the case of a mismatch.
- api = api_method[/^([^.]+)\./, 1]
- api_method = self.discovered_method(
- api_method, api, options[:version]
- )
- elsif !api_method.kind_of?(::Google::APIClient::Method)
- raise TypeError,
- "Expected String, Symbol, or Google::APIClient::Method, " +
- "got #{api_method.class}."
- end
- unless api_method
- raise ArgumentError, "API method could not be found."
- end
- request = api_method.generate_request(parameters, body, headers)
+ reference = Google::APIClient::Reference.new(options)
+ request = reference.to_request
if options[:authenticated]
request = self.generate_authenticated_request(:request => request)
end
@@ -503,47 +460,13 @@ module Google
end
##
- # Generates a request and transmits it.
+ # Signs a request using the current authorization mechanism.
#
- # @param [Google::APIClient::Method, String] api_method
- # The method object or the RPC name of the method being executed.
- # @param [Hash, Array] parameters
- # The parameters to send to the method.
- # @param [String] body The body of the request.
- # @param [Hash, Array] headers The HTTP headers for the request.
- # @param [Hash] options
- # The configuration parameters for the request.
- # - :version
—
- # The service version. Only used if api_method
is a
- # String
. Defaults to 'v1'
.
- # - :adapter
—
- # The HTTP adapter.
- # - :parser
—
- # The parser for the response.
- # - :authorization
—
- # The authorization mechanism for the response. Used only if
- # :authenticated
is true
.
- # - :authenticated
—
- # true
if the request must be signed or otherwise
- # authenticated, false
- # otherwise. Defaults to true
.
+ # @param [Hash] options The options to pass through.
#
- # @return [Array] The response from the API.
- #
- # @example
- # response = client.execute(
- # 'chili.activities.list',
- # {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
- # )
- # status, headers, body = response
- def execute(api_method, parameters={}, body='', headers=[], options={})
- request = self.generate_request(
- api_method, parameters, body, headers, options
- )
- return self.transmit_request(
- request,
- options[:adapter] || self.http_adapter
- )
+ # @return [Array] The signed or otherwise authenticated request.
+ def generate_authenticated_request(options={})
+ return authorization.generate_authenticated_request(options)
end
##
@@ -553,7 +476,7 @@ module Google
# @param [#transmit] adapter The HTTP adapter.
#
# @return [Array] The response from the server.
- def transmit_request(request, adapter=self.http_adapter)
+ def transmit(request, adapter=self.http_adapter)
if self.user_agent != nil
# If there's no User-Agent header, set one.
method, uri, headers, body = request
@@ -577,13 +500,90 @@ module Google
end
##
- # Signs a request using the current authorization mechanism.
+ # Executes a request, wrapping it in a Result object.
#
- # @param [Hash] options The options to pass through.
+ # @param [Google::APIClient::Method, String] api_method
+ # The method object or the RPC name of the method being executed.
+ # @param [Hash, Array] parameters
+ # The parameters to send to the method.
+ # @param [String] body The body of the request.
+ # @param [Hash, Array] headers The HTTP headers for the request.
+ # @param [Hash] options
+ # The configuration parameters for the request.
+ # - :version
—
+ # The service version. Only used if api_method
is a
+ # String
. Defaults to 'v1'
.
+ # - :adapter
—
+ # The HTTP adapter.
+ # - :authorization
—
+ # The authorization mechanism for the response. Used only if
+ # :authenticated
is true
.
+ # - :authenticated
—
+ # true
if the request must be signed or otherwise
+ # authenticated, false
+ # otherwise. Defaults to true
.
#
- # @return [Array] The signed or otherwise authenticated request.
- def generate_authenticated_request(options={})
- return authorization.generate_authenticated_request(options)
+ # @return [Array] The response from the API.
+ #
+ # @example
+ # request = client.generate_request(
+ # :api_method => 'chili.activities.list',
+ # :parameters =>
+ # {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
+ # )
+ def execute(*params)
+ # This block of code allows us to accept multiple parameter passing
+ # styles, and maintaining backwards compatibility.
+ if params.last.respond_to?(:to_hash) && params.size != 2
+ # Hash options are tricky. If we get two arguments, it's ambiguous
+ # whether to treat them as API parameters or Hash options, but since
+ # it's rare to need to pass in options, we must assume that the
+ # developer wanted to pass API parameters. Prefer using named
+ # parameters to avoid this issue. Unnamed parameters should be
+ # considered syntactic sugar.
+ options = params.pop
+ else
+ options = {}
+ end
+ options[:api_method] = params.shift if params.size > 0
+ options[:parameters] = params.shift if params.size > 0
+ options[:merged_body] = params.shift if params.size > 0
+ options[:headers] = params.shift if params.size > 0
+ options[:client] = self
+
+ reference = Google::APIClient::Reference.new(options)
+ request = self.generate_request(reference)
+ response = self.transmit(
+ request,
+ options[:adapter] || self.http_adapter
+ )
+ return Google::APIClient::Result.new(reference, request, response)
+ end
+
+ ##
+ # Same as Google::APIClient#execute, but raises an exception if there was
+ # an error.
+ #
+ # @see Google::APIClient#execute
+ def execute!(*params)
+ result = self.execute(*params)
+ status, _, _ = result.response
+ if result.data.respond_to?(:error)
+ # You're going to get a terrible error message if the response isn't
+ # parsed successfully as an error.
+ error_message = result.data.error
+ end
+ if status >= 400 && status < 500
+ raise ClientError,
+ error_message || "A client error has occurred."
+ elsif status >= 500 && status < 600
+ raise ServerError,
+ error_message || "A server error has occurred."
+ elsif status > 600
+ raise TransmissionError,
+ error_message || "A transmission error has occurred."
+ end
+ return result
end
end
end
diff --git a/lib/google/api_client/discovery.rb b/lib/google/api_client/discovery.rb
index 4e2f37096..45f348bbe 100644
--- a/lib/google/api_client/discovery.rb
+++ b/lib/google/api_client/discovery.rb
@@ -144,7 +144,7 @@ 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(self.method_base, k, v)
accu
end
)
@@ -158,7 +158,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(self.method_base, k, v)
accu
end
)
@@ -271,7 +271,7 @@ 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(self.method_base, k, v)
accu
end
)
@@ -284,7 +284,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(self.method_base, k, v)
accu
end
)
@@ -378,6 +378,14 @@ module Google
return @discovery_document['id']
end
+ ##
+ # Returns the HTTP method or 'GET' if none is specified.
+ #
+ # @return [String] The HTTP method that will be used in the request.
+ def http_method
+ return @discovery_document['httpMethod'] || 'GET'
+ end
+
##
# Returns the URI template for the method. A parameter list can be
# used to expand this into a URI.
@@ -465,7 +473,7 @@ module Google
if !headers.kind_of?(Array) && !headers.kind_of?(Hash)
raise TypeError, "Expected Hash or Array, got #{headers.class}."
end
- method = @discovery_document['httpMethod'] || 'GET'
+ method = self.http_method
uri = self.generate_uri(parameters)
headers = headers.to_a if headers.kind_of?(Hash)
return [method, uri.to_str, headers, [body]]
diff --git a/lib/google/api_client/errors.rb b/lib/google/api_client/errors.rb
index 00d1995e9..0c6e852b3 100644
--- a/lib/google/api_client/errors.rb
+++ b/lib/google/api_client/errors.rb
@@ -26,5 +26,15 @@ module Google
# invalid parameter values.
class ValidationError < StandardError
end
+
+ ##
+ # A 4xx class HTTP error occurred.
+ class ClientError < TransmissionError
+ end
+
+ ##
+ # A 5xx class HTTP error occurred.
+ class ServerError < TransmissionError
+ end
end
end
diff --git a/lib/google/api_client/parser.rb b/lib/google/api_client/parser.rb
new file mode 100644
index 000000000..e91351673
--- /dev/null
+++ b/lib/google/api_client/parser.rb
@@ -0,0 +1,59 @@
+# 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'
+
+module Google
+ class APIClient
+ module Parser
+ def content_type(content_type)
+ @@content_type_mapping ||= {}
+ @@content_type_mapping[content_type] = self
+ end
+
+ def self.match_content_type(content_type)
+ # TODO(bobaman): Do this more efficiently.
+ mime_type_regexp = /^([^\/]+)(?:\/([^+]+\+)?([^;]+))?(?:;.*)?$/
+ if @@content_type_mapping[content_type]
+ # Exact match
+ return @@content_type_mapping[content_type]
+ else
+ media_type, extension, sub_type =
+ content_type.scan(mime_type_regexp)[0]
+ for pattern, parser in @@content_type_mapping
+ # We want to match on subtype first
+ pattern_media_type, pattern_extension, pattern_sub_type =
+ pattern.scan(mime_type_regexp)[0]
+ next if pattern_extension != nil
+ if media_type == pattern_media_type && sub_type == pattern_sub_type
+ return parser
+ end
+ end
+ for pattern, parser in @@content_type_mapping
+ # We failed to match on the subtype
+ # Try to match only on the media type
+ pattern_media_type, pattern_extension, pattern_sub_type =
+ pattern.scan(mime_type_regexp)[0]
+ next if pattern_extension != nil || pattern_sub_type != nil
+ if media_type == pattern_media_type
+ return parser
+ end
+ end
+ end
+ return nil
+ end
+ end
+ end
+end
diff --git a/lib/google/api_client/parsers/json/error_parser.rb b/lib/google/api_client/parsers/json/error_parser.rb
new file mode 100644
index 000000000..719317a39
--- /dev/null
+++ b/lib/google/api_client/parsers/json/error_parser.rb
@@ -0,0 +1,34 @@
+# 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 'google/api_client/parsers/json_parser'
+
+module Google
+ class APIClient
+ module JSON
+ ##
+ # A module which provides a parser for error responses.
+ class ErrorParser
+ include Google::APIClient::JSONParser
+
+ matches_fields 'error'
+
+ def error
+ return self['error']['message']
+ end
+ end
+ end
+ end
+end
diff --git a/lib/google/api_client/parsers/json/pagination.rb b/lib/google/api_client/parsers/json/pagination.rb
new file mode 100644
index 000000000..cc4e2aa5a
--- /dev/null
+++ b/lib/google/api_client/parsers/json/pagination.rb
@@ -0,0 +1,40 @@
+# 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 'google/api_client/parsers/json_parser'
+
+module Google
+ class APIClient
+ module JSON
+ ##
+ # A module which provides a paginated parser.
+ module Pagination
+ def self.included(parser)
+ parser.class_eval do
+ include Google::APIClient::JSONParser
+ end
+ end
+
+ def next_page_token
+ return self["nextPageToken"]
+ end
+
+ def prev_page_token
+ return self["prevPageToken"]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/google/api_client/parsers/json_parser.rb b/lib/google/api_client/parsers/json_parser.rb
index f686ddfd5..ff828bf33 100644
--- a/lib/google/api_client/parsers/json_parser.rb
+++ b/lib/google/api_client/parsers/json_parser.rb
@@ -14,27 +14,105 @@
require 'json'
+require 'google/api_client/parser'
module Google
class APIClient
##
- # Provides a consistent interface by which to parse request and response
- # content.
- # TODO(mattpok): ensure floats, URLs, dates are parsed correctly
+ # Provides a module which all other parsers should include.
module JSONParser
+ extend Parser
+ content_type 'application/json'
- def self.serialize(hash)
- # JSON parser used can accept arrays as well, but we will limit
- # to only allow hash to JSON string parsing to keep a simple interface
- unless hash.instance_of? Hash
- raise ArgumentError,
- "JSON generate expected a Hash but got a #{hash.class}."
+ module Matcher
+ def conditions
+ @conditions ||= []
+ end
+
+ def matches_kind(kind)
+ self.matches_field_value(:kind, kind)
+ end
+
+ def matches_fields(fields)
+ self.conditions << [:fields, fields]
+ end
+
+ def matches_field_value(field, value)
+ self.conditions << [:field_value, field, value]
end
- return JSON.generate(hash)
end
- def self.parse(json_string)
- return JSON.parse(json_string)
+ def self.parsers
+ @parsers ||= []
+ end
+
+ ##
+ # This method ensures that all parsers auto-register themselves.
+ def self.included(parser)
+ self.parsers << parser
+ parser.extend(Matcher)
+ end
+
+ def initialize(data)
+ @data = data.kind_of?(Hash) ? data : ::JSON.parse(data)
+ end
+
+ def [](key)
+ return self.json[key]
+ end
+
+ def json
+ if @data
+ data = @data
+ elsif self.respond_to?(:data)
+ data = self.data
+ else
+ raise TypeError, "Parser did not provide access to raw data."
+ end
+ return data
+ end
+
+ ##
+ # Matches a parser to the data.
+ def self.match(data)
+ for parser in self.parsers
+ conditions_met = true
+ for condition in (parser.conditions.sort_by { |c| c.size }).reverse
+ condition_type, *params = condition
+ case condition_type
+ when :fields
+ for field in params
+ if !data.has_key?(field)
+ conditions_met = false
+ break
+ end
+ end
+ when :field_values
+ field, value = params
+ if data[field] != value
+ conditions_met = false
+ break
+ end
+ else
+ raise ArgumentError, "Unknown condition type: #{condition_type}"
+ end
+ break if !conditions_met
+ end
+ if conditions_met
+ return parser
+ end
+ end
+ return nil
+ end
+
+ def self.parse(json)
+ data = json.kind_of?(Hash) ? json : ::JSON.parse(json)
+ parser = self.match(data)
+ if parser
+ return parser.new(data)
+ else
+ return data
+ end
end
end
end
diff --git a/lib/google/api_client/reference.rb b/lib/google/api_client/reference.rb
new file mode 100644
index 000000000..97d686709
--- /dev/null
+++ b/lib/google/api_client/reference.rb
@@ -0,0 +1,182 @@
+# 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 'stringio'
+require 'addressable/uri'
+require 'google/api_client/discovery'
+
+module Google
+ class APIClient
+ class Reference
+ def initialize(options={})
+ # We only need this to do lookups on method ID String values
+ # It's optional, but method ID lookups will fail if the client is
+ # omitted.
+ @client = options[:client]
+ @version = options[:version] || 'v1'
+
+ self.api_method = options[:api_method]
+ self.parameters = options[:parameters] || {}
+ self.headers = options[:headers] || []
+ if options[:body]
+ self.body = options[:body]
+ elsif options[:merged_body]
+ self.merged_body = options[:merged_body]
+ else
+ self.merged_body = ''
+ end
+ unless self.api_method
+ self.http_method = options[:http_method] || 'GET'
+ self.uri = options[:uri]
+ end
+ end
+
+ def api_method
+ return @api_method
+ end
+
+ def api_method=(new_api_method)
+ if new_api_method.kind_of?(Google::APIClient::Method) ||
+ new_api_method == nil
+ @api_method = new_api_method
+ elsif new_api_method.respond_to?(:to_str) ||
+ new_api_method.kind_of?(Symbol)
+ unless @client
+ raise ArgumentError,
+ "API method lookup impossible without client instance."
+ end
+ new_api_method = new_api_method.to_s
+ # This method of guessing the API is unreliable. This will fail for
+ # APIs where the first segment of the RPC name does not match the
+ # service name. However, this is a fallback mechanism anyway.
+ # Developers should be passing in a reference to the method, rather
+ # than passing in a string or symbol. This should raise an error
+ # in the case of a mismatch.
+ api = new_api_method[/^([^.]+)\./, 1]
+ @api_method = @client.discovered_method(
+ new_api_method, api, @version
+ )
+ if @api_method
+ # Ditch the client reference, we won't need it again.
+ @client = nil
+ else
+ raise ArgumentError, "API method could not be found."
+ end
+ else
+ raise TypeError,
+ "Expected Google::APIClient::Method, got #{new_api_method.class}."
+ end
+ end
+
+ def parameters
+ return @parameters
+ end
+
+ def parameters=(new_parameters)
+ # No type-checking needed, the Method class handles this.
+ @parameters = new_parameters
+ end
+
+ def body
+ return @body
+ end
+
+ def body=(new_body)
+ if new_body.respond_to?(:each)
+ @body = new_body
+ else
+ raise TypeError, "Expected body to respond to :each."
+ end
+ end
+
+ def merged_body
+ return (self.body.inject(StringIO.new) do |accu, chunk|
+ accu.write(chunk)
+ accu
+ end).string
+ end
+
+ def merged_body=(new_merged_body)
+ if new_merged_body.respond_to?(:string)
+ new_merged_body = new_merged_body.string
+ elsif new_merged_body.respond_to?(:to_str)
+ new_merged_body = new_merged_body.to_str
+ else
+ raise TypeError,
+ "Expected String or StringIO, got #{new_merged_body.class}."
+ end
+ self.body = [new_merged_body]
+ end
+
+ def headers
+ return @headers ||= []
+ end
+
+ def headers=(new_headers)
+ if new_headers.kind_of?(Array) || new_headers.kind_of?(Hash)
+ @headers = new_headers
+ else
+ raise TypeError, "Expected Hash or Array, got #{new_headers.class}."
+ end
+ end
+
+ def http_method
+ return @http_method ||= self.api_method.http_method
+ end
+
+ def http_method=(new_http_method)
+ if new_http_method.kind_of?(Symbol)
+ @http_method = new_http_method.to_s.upcase
+ elsif new_http_method.respond_to?(:to_str)
+ @http_method = new_http_method.to_str.upcase
+ else
+ raise TypeError,
+ "Expected String or Symbol, got #{new_http_method.class}."
+ end
+ end
+
+ def uri
+ return @uri ||= self.api_method.generate_uri(self.parameters)
+ end
+
+ def uri=(new_uri)
+ @uri = Addressable::URI.parse(new_uri)
+ end
+
+ def to_request
+ if self.api_method
+ return self.api_method.generate_request(
+ self.parameters, self.merged_body, self.headers
+ )
+ else
+ return [self.http_method, self.uri, self.headers, self.body]
+ end
+ end
+
+ def to_hash
+ options = {}
+ if self.api_method
+ options[:api_method] = self.api_method
+ options[:parameters] = self.parameters
+ else
+ options[:http_method] = self.http_method
+ options[:uri] = self.uri
+ end
+ options[:headers] = self.headers
+ options[:body] = self.body
+ return options
+ end
+ end
+ end
+end
diff --git a/lib/google/api_client/result.rb b/lib/google/api_client/result.rb
new file mode 100644
index 000000000..f78cb3241
--- /dev/null
+++ b/lib/google/api_client/result.rb
@@ -0,0 +1,116 @@
+# 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 'google/api_client/parsers/json_parser'
+
+module Google
+ class APIClient
+ ##
+ # This class wraps a result returned by an API call.
+ class Result
+ def initialize(reference, request, response)
+ @reference = reference
+ @request = request
+ @response = response
+ end
+
+ attr_reader :reference
+
+ attr_reader :request
+
+ attr_reader :response
+
+ def status
+ return @response[0]
+ end
+
+ def headers
+ return @response[1]
+ end
+
+ def body
+ return @body ||= (begin
+ response_body = @response[2]
+ merged_body = (response_body.inject(StringIO.new) do |accu, chunk|
+ accu.write(chunk)
+ accu
+ end).string
+ end)
+ end
+
+ def data
+ return @data ||= (begin
+ _, 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)
+ end)
+ end
+
+ def pagination_type
+ return :token
+ end
+
+ def page_token_param
+ return "pageToken"
+ end
+
+ def next_page_token
+ if self.data.respond_to?(:next_page_token)
+ return self.data.next_page_token
+ elsif self.data.respond_to?(:[])
+ return self.data["nextPageToken"]
+ else
+ raise TypeError, "Data object did not respond to #next_page_token."
+ end
+ end
+
+ def next_page
+ merged_parameters = Hash[self.reference.parameters].merge({
+ self.page_token_param => self.next_page_token
+ })
+ # Because References can be coerced to Hashes, we can merge them,
+ # preserving all context except the API method parameters that we're
+ # using for pagination.
+ return Google::APIClient::Reference.new(
+ Hash[self.reference].merge(:parameters => merged_parameters)
+ )
+ end
+
+ def prev_page_token
+ if self.data.respond_to?(:prev_page_token)
+ return self.data.prev_page_token
+ elsif self.data.respond_to?(:[])
+ return self.data["prevPageToken"]
+ else
+ raise TypeError, "Data object did not respond to #next_page_token."
+ end
+ end
+
+ def prev_page
+ merged_parameters = Hash[self.reference.parameters].merge({
+ self.page_token_param => self.prev_page_token
+ })
+ # Because References can be coerced to Hashes, we can merge them,
+ # preserving all context except the API method parameters that we're
+ # using for pagination.
+ return Google::APIClient::Reference.new(
+ Hash[self.reference].merge(:parameters => merged_parameters)
+ )
+ end
+ end
+ end
+end
diff --git a/lib/google/api_client/version.rb b/lib/google/api_client/version.rb
index f42feedd0..8ac950717 100644
--- a/lib/google/api_client/version.rb
+++ b/lib/google/api_client/version.rb
@@ -16,8 +16,8 @@
module Google
class APIClient
module VERSION
- MAJOR = 0
- MINOR = 2
+ MAJOR = 1
+ MINOR = 0
TINY = 0
STRING = [MAJOR, MINOR, TINY].join('.')
diff --git a/spec/google/api_client/discovery_spec.rb b/spec/google/api_client/discovery_spec.rb
index bff4685ce..cd4606807 100644
--- a/spec/google/api_client/discovery_spec.rb
+++ b/spec/google/api_client/discovery_spec.rb
@@ -66,6 +66,9 @@ describe Google::APIClient do
describe 'with the prediction API' do
before do
@client.authorization = nil
+ # The prediction API no longer exposes a v1, so we have to be
+ # careful about looking up the wrong API version.
+ @prediction = @client.discovered_api('prediction', 'v1.2')
end
it 'should correctly determine the discovery URI' do
@@ -74,45 +77,39 @@ describe Google::APIClient do
end
it 'should correctly generate API objects' do
- @client.discovered_api('prediction').name.should == 'prediction'
- @client.discovered_api('prediction').version.should == 'v1'
- @client.discovered_api(:prediction).name.should == 'prediction'
- @client.discovered_api(:prediction).version.should == 'v1'
+ @client.discovered_api('prediction', 'v1.2').name.should == 'prediction'
+ @client.discovered_api('prediction', 'v1.2').version.should == 'v1.2'
+ @client.discovered_api(:prediction, 'v1.2').name.should == 'prediction'
+ @client.discovered_api(:prediction, 'v1.2').version.should == 'v1.2'
end
it 'should discover methods' do
@client.discovered_method(
- 'prediction.training.insert', 'prediction'
+ 'prediction.training.insert', 'prediction', 'v1.2'
).name.should == 'insert'
@client.discovered_method(
- :'prediction.training.insert', :prediction
+ :'prediction.training.insert', :prediction, 'v1.2'
).name.should == 'insert'
- end
-
- it 'should discover methods' do
@client.discovered_method(
- 'prediction.training.delete', 'prediction', 'v1.1'
+ 'prediction.training.delete', 'prediction', 'v1.2'
).name.should == 'delete'
end
it 'should not find methods that are not in the discovery document' do
@client.discovered_method(
- 'prediction.training.delete', 'prediction', 'v1'
- ).should == nil
- @client.discovered_method(
- 'prediction.bogus', 'prediction', 'v1'
+ 'prediction.bogus', 'prediction', 'v1.2'
).should == nil
end
it 'should raise an error for bogus methods' do
(lambda do
- @client.discovered_method(42, 'prediction', 'v1')
+ @client.discovered_method(42, 'prediction', 'v1.2')
end).should raise_error(TypeError)
end
it 'should raise an error for bogus methods' do
(lambda do
- @client.generate_request(@client.discovered_api('prediction'))
+ @client.generate_request(@client.discovered_api('prediction', 'v1.2'))
end).should raise_error(TypeError)
end
@@ -123,49 +120,50 @@ describe Google::APIClient do
it 'should generate valid requests' do
request = @client.generate_request(
- 'prediction.training.insert',
- {'data' => '12345', }
+ :api_method => @prediction.training.insert,
+ :parameters => {'data' => '12345', }
)
method, uri, headers, body = request
method.should == 'POST'
uri.should ==
- 'https://www.googleapis.com/prediction/v1/training?data=12345'
+ 'https://www.googleapis.com/prediction/v1.2/training?data=12345'
(headers.inject({}) { |h,(k,v)| h[k]=v; h }).should == {}
body.should respond_to(:each)
end
it 'should generate requests against the correct URIs' do
request = @client.generate_request(
- :'prediction.training.insert',
- {'data' => '12345'}
+ :api_method => @prediction.training.insert,
+ :parameters => {'data' => '12345'}
)
method, uri, headers, body = request
uri.should ==
- 'https://www.googleapis.com/prediction/v1/training?data=12345'
+ 'https://www.googleapis.com/prediction/v1.2/training?data=12345'
end
it 'should generate requests against the correct URIs' do
- prediction = @client.discovered_api('prediction', 'v1')
request = @client.generate_request(
- prediction.training.insert,
- {'data' => '12345'}
+ :api_method => @prediction.training.insert,
+ :parameters => {'data' => '12345'}
)
method, uri, headers, body = request
uri.should ==
- 'https://www.googleapis.com/prediction/v1/training?data=12345'
+ 'https://www.googleapis.com/prediction/v1.2/training?data=12345'
end
it 'should allow modification to the base URIs for testing purposes' do
- prediction = @client.discovered_api('prediction', 'v1')
+ prediction = @client.discovered_api('prediction', 'v1.2')
prediction.method_base =
- 'https://testing-domain.googleapis.com/prediction/v1/'
+ 'https://testing-domain.googleapis.com/prediction/v1.2/'
request = @client.generate_request(
- prediction.training.insert,
- {'data' => '123'}
+ :api_method => prediction.training.insert,
+ :parameters => {'data' => '123'}
)
method, uri, headers, body = request
- uri.should ==
- 'https://testing-domain.googleapis.com/prediction/v1/training?data=123'
+ uri.should == (
+ 'https://testing-domain.googleapis.com/' +
+ 'prediction/v1.2/training?data=123'
+ )
end
it 'should generate OAuth 1 requests' do
@@ -173,8 +171,8 @@ describe Google::APIClient do
@client.authorization.token_credential_key = '12345'
@client.authorization.token_credential_secret = '12345'
request = @client.generate_request(
- 'prediction.training.insert',
- {'data' => '12345'}
+ :api_method => @prediction.training.insert,
+ :parameters => {'data' => '12345'}
)
method, uri, headers, body = request
headers = headers.inject({}) { |h,(k,v)| h[k]=v; h }
@@ -186,8 +184,8 @@ describe Google::APIClient do
@client.authorization = :oauth_2
@client.authorization.access_token = '12345'
request = @client.generate_request(
- 'prediction.training.insert',
- {'data' => '12345'}
+ :api_method => @prediction.training.insert,
+ :parameters => {'data' => '12345'}
)
method, uri, headers, body = request
headers = headers.inject({}) { |h,(k,v)| h[k]=v; h }
@@ -199,24 +197,47 @@ describe Google::APIClient do
@client.authorization = :oauth_1
@client.authorization.token_credential_key = '12345'
@client.authorization.token_credential_secret = '12345'
- response = @client.execute(
- 'prediction.training.insert',
+ result = @client.execute(
+ @prediction.training.insert,
{'data' => '12345'}
)
- status, headers, body = response
+ status, headers, body = result.response
status.should == 401
end
it 'should not be able to execute improperly authorized requests' do
@client.authorization = :oauth_2
@client.authorization.access_token = '12345'
- response = @client.execute(
- 'prediction.training.insert',
+ result = @client.execute(
+ @prediction.training.insert,
{'data' => '12345'}
)
- status, headers, body = response
+ status, headers, body = result.response
status.should == 401
end
+
+ it 'should not be able to execute improperly authorized requests' do
+ (lambda do
+ @client.authorization = :oauth_1
+ @client.authorization.token_credential_key = '12345'
+ @client.authorization.token_credential_secret = '12345'
+ result = @client.execute!(
+ @prediction.training.insert,
+ {'data' => '12345'}
+ )
+ end).should raise_error(Google::APIClient::ClientError)
+ end
+
+ it 'should not be able to execute improperly authorized requests' do
+ (lambda do
+ @client.authorization = :oauth_2
+ @client.authorization.access_token = '12345'
+ result = @client.execute!(
+ @prediction.training.insert,
+ {'data' => '12345'}
+ )
+ end).should raise_error(Google::APIClient::ClientError)
+ end
end
describe 'with the buzz API' do
@@ -251,22 +272,18 @@ describe Google::APIClient do
it 'should fail for string RPC names that do not match API name' do
(lambda do
@client.generate_request(
- 'chili.activities.list',
- {'alt' => 'json'},
- '',
- [],
- {:signed => false}
+ :api_method => 'chili.activities.list',
+ :parameters => {'alt' => 'json'},
+ :authenticated => false
)
end).should raise_error(Google::APIClient::TransmissionError)
end
it 'should generate requests against the correct URIs' do
request = @client.generate_request(
- @buzz.activities.list,
- {'userId' => 'hikingfan', 'scope' => '@public'},
- '',
- [],
- {:signed => false}
+ :api_method => @buzz.activities.list,
+ :parameters => {'userId' => 'hikingfan', 'scope' => '@public'},
+ :authenticated => false
)
method, uri, headers, body = request
uri.should ==
@@ -276,11 +293,9 @@ describe Google::APIClient do
it 'should correctly validate parameters' do
(lambda do
@client.generate_request(
- @buzz.activities.list,
- {'alt' => 'json'},
- '',
- [],
- {:signed => false}
+ :api_method => @buzz.activities.list,
+ :parameters => {'alt' => 'json'},
+ :authenticated => false
)
end).should raise_error(ArgumentError)
end
@@ -288,26 +303,33 @@ describe Google::APIClient do
it 'should correctly validate parameters' do
(lambda do
@client.generate_request(
- @buzz.activities.list,
- {'userId' => 'hikingfan', 'scope' => '@bogus'},
- '',
- [],
- {:signed => false}
+ :api_method => @buzz.activities.list,
+ :parameters => {'userId' => 'hikingfan', 'scope' => '@bogus'},
+ :authenticated => false
)
end).should raise_error(ArgumentError)
end
it 'should be able to execute requests without authorization' do
- response = @client.execute(
+ result = @client.execute(
@buzz.activities.list,
{'alt' => 'json', 'userId' => 'hikingfan', 'scope' => '@public'},
'',
[],
- {:signed => false}
+ :authenticated => false
)
- status, headers, body = response
+ 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
@@ -338,11 +360,8 @@ describe Google::APIClient do
it 'should generate requests against the correct URIs' do
request = @client.generate_request(
- 'latitude.currentLocation.get',
- {},
- '',
- [],
- {:signed => false}
+ :api_method => 'latitude.currentLocation.get',
+ :authenticated => false
)
method, uri, headers, body = request
uri.should ==
@@ -351,11 +370,8 @@ describe Google::APIClient do
it 'should generate requests against the correct URIs' do
request = @client.generate_request(
- @latitude.current_location.get,
- {},
- '',
- [],
- {:signed => false}
+ :api_method => @latitude.current_location.get,
+ :authenticated => false
)
method, uri, headers, body = request
uri.should ==
@@ -363,14 +379,11 @@ describe Google::APIClient do
end
it 'should not be able to execute requests without authorization' do
- response = @client.execute(
- 'latitude.currentLocation.get',
- {},
- '',
- [],
- {:signed => false}
+ result = @client.execute(
+ :api_method => 'latitude.currentLocation.get',
+ :authenticated => false
)
- status, headers, body = response
+ status, headers, body = result.response
status.should == 401
end
end
@@ -403,11 +416,8 @@ describe Google::APIClient do
it 'should generate requests against the correct URIs' do
request = @client.generate_request(
- 'moderator.profiles.get',
- {},
- '',
- [],
- {:signed => false}
+ :api_method => 'moderator.profiles.get',
+ :authenticated => false
)
method, uri, headers, body = request
uri.should ==
@@ -416,11 +426,8 @@ describe Google::APIClient do
it 'should generate requests against the correct URIs' do
request = @client.generate_request(
- @moderator.profiles.get,
- {},
- '',
- [],
- {:signed => false}
+ :api_method => @moderator.profiles.get,
+ :authenticated => false
)
method, uri, headers, body = request
uri.should ==
@@ -428,14 +435,14 @@ describe Google::APIClient do
end
it 'should not be able to execute requests without authorization' do
- response = @client.execute(
+ result = @client.execute(
'moderator.profiles.get',
{},
'',
[],
- {:signed => false}
+ {:authenticated => false}
)
- status, headers, body = response
+ status, headers, body = result.response
status.should == 401
end
end
diff --git a/spec/google/api_client/parsers/json_parser_spec.rb b/spec/google/api_client/parsers/json_parser_spec.rb
index d30242233..c7361ccf6 100644
--- a/spec/google/api_client/parsers/json_parser_spec.rb
+++ b/spec/google/api_client/parsers/json_parser_spec.rb
@@ -16,36 +16,40 @@ require 'spec_helper'
require 'json'
require 'google/api_client/parsers/json_parser'
+require 'google/api_client/parsers/json/error_parser'
+require 'google/api_client/parsers/json/pagination'
-describe Google::APIClient::JSONParser, 'generates json from hash' do
+describe Google::APIClient::JSONParser, 'with error data' do
before do
- @parser = Google::APIClient::JSONParser
- end
-
- it 'should translate simple hash to JSON string' do
- @parser.serialize('test' => 23).should == '{"test":23}'
- end
-
- it 'should translate simple nested into to nested JSON string' do
- @parser.serialize({
- 'test' => 23, 'test2' => {'foo' => 'baz', 12 => 3.14 }
- }).should ==
- '{"test2":{"12":3.14,"foo":"baz"},"test":23}'
- end
-end
-
-describe Google::APIClient::JSONParser, 'parses json string into hash' do
- before do
- @parser = Google::APIClient::JSONParser
- end
-
- it 'should parse simple json string into hash' do
- @parser.parse('{"test":23}').should == {'test' => 23}
- end
-
- it 'should parse nested json object into hash' do
- @parser.parse('{"test":23, "test2":{"bar":"baz", "foo":3.14}}').should == {
- 'test' => 23, 'test2' => {'bar' => 'baz', 'foo' => 3.14}
+ @data = {
+ 'error' => {
+ 'code' => 401,
+ 'message' => 'Token invalid - Invalid AuthSub token.',
+ 'errors' => [
+ {
+ 'location' => 'Authorization',
+ 'domain' => 'global',
+ 'locationType' => 'header',
+ 'reason' => 'authError',
+ 'message' => 'Token invalid - Invalid AuthSub token.'
+ }
+ ]
+ }
}
end
+
+ it 'should correctly match as an error' do
+ parser = Google::APIClient::JSONParser.match(@data)
+ parser.should == Google::APIClient::JSON::ErrorParser
+ end
+
+ it 'should be automatically handled as an error when parsed' do
+ data = Google::APIClient::JSONParser.parse(@data)
+ data.should be_kind_of(Google::APIClient::JSON::ErrorParser)
+ end
+
+ it 'should correctly expose error message' do
+ data = Google::APIClient::JSONParser.parse(@data)
+ data.error.should == 'Token invalid - Invalid AuthSub token.'
+ end
end
diff --git a/spec/google/api_client_spec.rb b/spec/google/api_client_spec.rb
index 30bfa1d10..78b36ba96 100644
--- a/spec/google/api_client_spec.rb
+++ b/spec/google/api_client_spec.rb
@@ -36,7 +36,7 @@ shared_examples_for 'configurable user agent' do
it 'should not allow the user agent to be used with bogus values' do
(lambda do
@client.user_agent = 42
- @client.transmit_request(
+ @client.transmit(
['GET', 'http://www.google.com/', [], []]
)
end).should raise_error(TypeError)
@@ -53,7 +53,7 @@ shared_examples_for 'configurable user agent' do
end
[200, [], ['']]
end
- @client.transmit_request(request, adapter)
+ @client.transmit(request, adapter)
end
end
@@ -63,11 +63,7 @@ describe Google::APIClient do
end
it 'should make its version number available' do
- ::Google::APIClient::VERSION::STRING.should be_instance_of(String)
- end
-
- it 'should use the default JSON parser' do
- @client.parser.should be(Google::APIClient::JSONParser)
+ Google::APIClient::VERSION::STRING.should be_instance_of(String)
end
it 'should default to OAuth 2' do
@@ -104,26 +100,4 @@ describe Google::APIClient do
# TODO
it_should_behave_like 'configurable user agent'
end
-
- describe 'with custom pluggable parser' do
- before do
- class FakeJsonParser
- def serialize(value)
- return "42"
- end
-
- def parse(value)
- return 42
- end
- end
-
- @client.parser = FakeJsonParser.new
- end
-
- it 'should use the custom parser' do
- @client.parser.should be_instance_of(FakeJsonParser)
- end
-
- it_should_behave_like 'configurable user agent'
- end
end