Major update, primarily to add pagination support.

* Added Reference objects to encapsulate API calls.
* Added Result objects to encapsulate API responses.
* Changed the return value of APIClient#execute to Result.
* Changed the method signature of APIClient#execute to support named params.
* Added APIClient#execute! which throws exceptions on error.
* Added automatic parsing code to better allow for complex nested structures.
* Added error parser.
* Added module for pagination in parsers.
This commit is contained in:
Bob Aman 2011-07-29 18:07:04 -04:00
parent a656c13862
commit f336ab34a7
15 changed files with 821 additions and 306 deletions

View File

@ -447,7 +447,7 @@ HTML
method.upcase! method.upcase!
request = [method, uri.to_str, headers, [request_body]] request = [method, uri.to_str, headers, [request_body]]
request = client.generate_authenticated_request(:request => request) request = client.generate_authenticated_request(:request => request)
response = client.transmit_request(request) response = client.transmit(request)
status, headers, body = response status, headers, body = response
puts body puts body
exit(0) exit(0)
@ -477,10 +477,13 @@ HTML
parameters['xoauth_requestor_id'] = options[:requestor_id] parameters['xoauth_requestor_id'] = options[:requestor_id]
end end
begin begin
response = client.execute( result = client.execute(
method, parameters, request_body, headers :api_method => method,
:parameters => parameters,
:merged_body => request_body,
:headers => headers
) )
status, headers, body = response status, headers, body = result.response
puts body puts body
exit(0) exit(0)
rescue ArgumentError => e rescue ArgumentError => e

View File

@ -75,10 +75,10 @@ get '/oauth2callback' do
end end
get '/' do get '/' do
response = @client.execute( result = @client.execute(
@buzz.activities.list, @buzz.activities.list,
'userId' => '@me', 'scope' => '@consumption', 'alt'=> 'json' {'userId' => '@me', 'scope' => '@consumption', 'alt'=> 'json'}
) )
status, headers, body = response status, _, _ = result.response
[status, {'Content-Type' => 'application/json'}, body] [status, {'Content-Type' => 'application/json'}, JSON.generate(result.data)]
end end

View File

@ -20,6 +20,8 @@ require 'stringio'
require 'google/api_client/errors' require 'google/api_client/errors'
require 'google/api_client/environment' require 'google/api_client/environment'
require 'google/api_client/discovery' require 'google/api_client/discovery'
require 'google/api_client/reference'
require 'google/api_client/result'
module Google module Google
# TODO(bobaman): Document all this stuff. # TODO(bobaman): Document all this stuff.
@ -65,16 +67,6 @@ module Google
'google-api-ruby-client/' + VERSION::STRING + 'google-api-ruby-client/' + VERSION::STRING +
' ' + ENV::OS_VERSION ' ' + ENV::OS_VERSION
).strip ).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 # The writer method understands a few Symbols and will generate useful
# default authentication mechanisms. # default authentication mechanisms.
self.authorization = options["authorization"] || :oauth_2 self.authorization = options["authorization"] || :oauth_2
@ -94,33 +86,6 @@ module Google
return self return self
end end
##
# Returns the parser used by the client.
#
# @return [#serialize, #parse]
# The parser used by the client. Any object that implements both a
# <code>#serialize</code> and a <code>#parse</code> method may be used.
# If <code>nil</code>, 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
# <code>#serialize</code> and a <code>#parse</code> method may be used.
# If <code>nil</code>, 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. # Returns the authorization mechanism used by the client.
# #
@ -280,7 +245,7 @@ module Google
"Expected String or StringIO, got #{discovery_document.class}." "Expected String or StringIO, got #{discovery_document.class}."
end end
@discovery_documents["#{api}:#{version}"] = @discovery_documents["#{api}:#{version}"] =
JSON.parse(discovery_document) ::JSON.parse(discovery_document)
end end
## ##
@ -291,16 +256,21 @@ module Google
return @directory_document ||= (begin return @directory_document ||= (begin
request_uri = self.directory_uri request_uri = self.directory_uri
request = ['GET', request_uri, [], []] request = ['GET', request_uri, [], []]
response = self.transmit_request(request) response = self.transmit(request)
status, headers, body = response status, headers, body = response
if status == 200 # TODO(bobaman) Better status code handling? if status == 200 # TODO(bobaman) Better status code handling?
merged_body = StringIO.new merged_body = body.inject(StringIO.new) do |accu, chunk|
body.each do |chunk| accu.write(chunk)
merged_body.write(chunk) accu
end end
merged_body.rewind ::JSON.parse(merged_body.string)
JSON.parse(merged_body.string) elsif status >= 400 && status < 500
else 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, raise TransmissionError,
"Could not retrieve discovery document at: #{request_uri}" "Could not retrieve discovery document at: #{request_uri}"
end end
@ -319,16 +289,21 @@ module Google
return @discovery_documents["#{api}:#{version}"] ||= (begin return @discovery_documents["#{api}:#{version}"] ||= (begin
request_uri = self.discovery_uri(api, version) request_uri = self.discovery_uri(api, version)
request = ['GET', request_uri, [], []] request = ['GET', request_uri, [], []]
response = self.transmit_request(request) response = self.transmit(request)
status, headers, body = response status, headers, body = response
if status == 200 # TODO(bobaman) Better status code handling? if status == 200 # TODO(bobaman) Better status code handling?
merged_body = StringIO.new merged_body = body.inject(StringIO.new) do |accu, chunk|
body.each do |chunk| accu.write(chunk)
merged_body.write(chunk) accu
end end
merged_body.rewind ::JSON.parse(merged_body.string)
JSON.parse(merged_body.string) elsif status >= 400 && status < 500
else 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, raise TransmissionError,
"Could not retrieve discovery document at: #{request_uri}" "Could not retrieve discovery document at: #{request_uri}"
end end
@ -344,7 +319,7 @@ module Google
document_base = self.directory_uri document_base = self.directory_uri
if self.directory_document && self.directory_document['items'] if self.directory_document && self.directory_document['items']
self.directory_document['items'].map do |discovery_document| self.directory_document['items'].map do |discovery_document|
::Google::APIClient::API.new( Google::APIClient::API.new(
document_base, document_base,
discovery_document discovery_document
) )
@ -373,7 +348,7 @@ module Google
document_base = self.discovery_uri(api, version) document_base = self.discovery_uri(api, version)
discovery_document = self.discovery_document(api, version) discovery_document = self.discovery_document(api, version)
if document_base && discovery_document if document_base && discovery_document
::Google::APIClient::API.new( Google::APIClient::API.new(
document_base, document_base,
discovery_document discovery_document
) )
@ -442,8 +417,6 @@ module Google
# - <code>:version</code> — # - <code>:version</code> —
# The service version. Only used if <code>api_method</code> is a # The service version. Only used if <code>api_method</code> is a
# <code>String</code>. Defaults to <code>'v1'</code>. # <code>String</code>. Defaults to <code>'v1'</code>.
# - <code>:parser</code> —
# The parser for the response.
# - <code>:authorization</code> — # - <code>:authorization</code> —
# The authorization mechanism for the response. Used only if # The authorization mechanism for the response. Used only if
# <code>:authenticated</code> is <code>true</code>. # <code>:authenticated</code> is <code>true</code>.
@ -457,17 +430,20 @@ module Google
# #
# @example # @example
# request = client.generate_request( # request = client.generate_request(
# 'chili.activities.list', # :api_method => 'chili.activities.list',
# {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'} # :parameters =>
# {'scope' => '@self', 'userId' => '@me', 'alt' => 'json'}
# ) # )
# method, uri, headers, body = request # method, uri, headers, body = request
def generate_request( def generate_request(options={})
api_method, parameters={}, body='', headers=[], 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={ options={
:parser => self.parser,
:version => 'v1', :version => 'v1',
:authorization => self.authorization :authorization => self.authorization
}.merge(options) }.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 # The default value for the :authenticated option depends on whether an
# authorization mechanism has been set. # authorization mechanism has been set.
if options[:authorization] if options[:authorization]
@ -475,27 +451,8 @@ module Google
else else
options = {:authenticated => false}.merge(options) options = {:authenticated => false}.merge(options)
end end
if api_method.kind_of?(String) || api_method.kind_of?(Symbol) reference = Google::APIClient::Reference.new(options)
api_method = api_method.to_s request = reference.to_request
# 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)
if options[:authenticated] if options[:authenticated]
request = self.generate_authenticated_request(:request => request) request = self.generate_authenticated_request(:request => request)
end end
@ -503,47 +460,13 @@ module Google
end end
## ##
# Generates a request and transmits it. # Signs a request using the current authorization mechanism.
# #
# @param [Google::APIClient::Method, String] api_method # @param [Hash] options The options to pass through.
# 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.
# - <code>:version</code> —
# The service version. Only used if <code>api_method</code> is a
# <code>String</code>. Defaults to <code>'v1'</code>.
# - <code>:adapter</code> —
# The HTTP adapter.
# - <code>:parser</code> —
# The parser for the response.
# - <code>:authorization</code> —
# The authorization mechanism for the response. Used only if
# <code>:authenticated</code> is <code>true</code>.
# - <code>:authenticated</code> —
# <code>true</code> if the request must be signed or otherwise
# authenticated, <code>false</code>
# otherwise. Defaults to <code>true</code>.
# #
# @return [Array] The response from the API. # @return [Array] The signed or otherwise authenticated request.
# def generate_authenticated_request(options={})
# @example return authorization.generate_authenticated_request(options)
# 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
)
end end
## ##
@ -553,7 +476,7 @@ module Google
# @param [#transmit] adapter The HTTP adapter. # @param [#transmit] adapter The HTTP adapter.
# #
# @return [Array] The response from the server. # @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 self.user_agent != nil
# If there's no User-Agent header, set one. # If there's no User-Agent header, set one.
method, uri, headers, body = request method, uri, headers, body = request
@ -577,13 +500,90 @@ module Google
end 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.
# - <code>:version</code> —
# The service version. Only used if <code>api_method</code> is a
# <code>String</code>. Defaults to <code>'v1'</code>.
# - <code>:adapter</code> —
# The HTTP adapter.
# - <code>:authorization</code> —
# The authorization mechanism for the response. Used only if
# <code>:authenticated</code> is <code>true</code>.
# - <code>:authenticated</code> —
# <code>true</code> if the request must be signed or otherwise
# authenticated, <code>false</code>
# otherwise. Defaults to <code>true</code>.
# #
# @return [Array] The signed or otherwise authenticated request. # @return [Array] The response from the API.
def generate_authenticated_request(options={}) #
return authorization.generate_authenticated_request(options) # @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 end
end end

View File

@ -144,7 +144,7 @@ module Google
def resources def resources
return @resources ||= ( return @resources ||= (
(@discovery_document['resources'] || []).inject([]) do |accu, (k, v)| (@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 accu
end end
) )
@ -158,7 +158,7 @@ module Google
def methods def methods
return @methods ||= ( return @methods ||= (
(@discovery_document['methods'] || []).inject([]) do |accu, (k, v)| (@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 accu
end end
) )
@ -271,7 +271,7 @@ module Google
def resources def resources
return @resources ||= ( return @resources ||= (
(@discovery_document['resources'] || []).inject([]) do |accu, (k, v)| (@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 accu
end end
) )
@ -284,7 +284,7 @@ module Google
def methods def methods
return @methods ||= ( return @methods ||= (
(@discovery_document['methods'] || []).inject([]) do |accu, (k, v)| (@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 accu
end end
) )
@ -378,6 +378,14 @@ module Google
return @discovery_document['id'] return @discovery_document['id']
end 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 # Returns the URI template for the method. A parameter list can be
# used to expand this into a URI. # used to expand this into a URI.
@ -465,7 +473,7 @@ module Google
if !headers.kind_of?(Array) && !headers.kind_of?(Hash) if !headers.kind_of?(Array) && !headers.kind_of?(Hash)
raise TypeError, "Expected Hash or Array, got #{headers.class}." raise TypeError, "Expected Hash or Array, got #{headers.class}."
end end
method = @discovery_document['httpMethod'] || 'GET' method = self.http_method
uri = self.generate_uri(parameters) uri = self.generate_uri(parameters)
headers = headers.to_a if headers.kind_of?(Hash) headers = headers.to_a if headers.kind_of?(Hash)
return [method, uri.to_str, headers, [body]] return [method, uri.to_str, headers, [body]]

View File

@ -26,5 +26,15 @@ module Google
# invalid parameter values. # invalid parameter values.
class ValidationError < StandardError class ValidationError < StandardError
end end
##
# A 4xx class HTTP error occurred.
class ClientError < TransmissionError
end
##
# A 5xx class HTTP error occurred.
class ServerError < TransmissionError
end
end end
end end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -14,27 +14,105 @@
require 'json' require 'json'
require 'google/api_client/parser'
module Google module Google
class APIClient class APIClient
## ##
# Provides a consistent interface by which to parse request and response # Provides a module which all other parsers should include.
# content.
# TODO(mattpok): ensure floats, URLs, dates are parsed correctly
module JSONParser module JSONParser
extend Parser
content_type 'application/json'
def self.serialize(hash) module Matcher
# JSON parser used can accept arrays as well, but we will limit def conditions
# to only allow hash to JSON string parsing to keep a simple interface @conditions ||= []
unless hash.instance_of? Hash end
raise ArgumentError,
"JSON generate expected a Hash but got a #{hash.class}." 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 end
return JSON.generate(hash)
end end
def self.parse(json_string) def self.parsers
return JSON.parse(json_string) @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 end
end end

View File

@ -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

View File

@ -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

View File

@ -16,8 +16,8 @@
module Google module Google
class APIClient class APIClient
module VERSION module VERSION
MAJOR = 0 MAJOR = 1
MINOR = 2 MINOR = 0
TINY = 0 TINY = 0
STRING = [MAJOR, MINOR, TINY].join('.') STRING = [MAJOR, MINOR, TINY].join('.')

View File

@ -66,6 +66,9 @@ describe Google::APIClient do
describe 'with the prediction API' do describe 'with the prediction API' do
before do before do
@client.authorization = nil @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 end
it 'should correctly determine the discovery URI' do it 'should correctly determine the discovery URI' do
@ -74,45 +77,39 @@ describe Google::APIClient do
end end
it 'should correctly generate API objects' do it 'should correctly generate API objects' do
@client.discovered_api('prediction').name.should == 'prediction' @client.discovered_api('prediction', 'v1.2').name.should == 'prediction'
@client.discovered_api('prediction').version.should == 'v1' @client.discovered_api('prediction', 'v1.2').version.should == 'v1.2'
@client.discovered_api(:prediction).name.should == 'prediction' @client.discovered_api(:prediction, 'v1.2').name.should == 'prediction'
@client.discovered_api(:prediction).version.should == 'v1' @client.discovered_api(:prediction, 'v1.2').version.should == 'v1.2'
end end
it 'should discover methods' do it 'should discover methods' do
@client.discovered_method( @client.discovered_method(
'prediction.training.insert', 'prediction' 'prediction.training.insert', 'prediction', 'v1.2'
).name.should == 'insert' ).name.should == 'insert'
@client.discovered_method( @client.discovered_method(
:'prediction.training.insert', :prediction :'prediction.training.insert', :prediction, 'v1.2'
).name.should == 'insert' ).name.should == 'insert'
end
it 'should discover methods' do
@client.discovered_method( @client.discovered_method(
'prediction.training.delete', 'prediction', 'v1.1' 'prediction.training.delete', 'prediction', 'v1.2'
).name.should == 'delete' ).name.should == 'delete'
end end
it 'should not find methods that are not in the discovery document' do it 'should not find methods that are not in the discovery document' do
@client.discovered_method( @client.discovered_method(
'prediction.training.delete', 'prediction', 'v1' 'prediction.bogus', 'prediction', 'v1.2'
).should == nil
@client.discovered_method(
'prediction.bogus', 'prediction', 'v1'
).should == nil ).should == nil
end end
it 'should raise an error for bogus methods' do it 'should raise an error for bogus methods' do
(lambda do (lambda do
@client.discovered_method(42, 'prediction', 'v1') @client.discovered_method(42, 'prediction', 'v1.2')
end).should raise_error(TypeError) end).should raise_error(TypeError)
end end
it 'should raise an error for bogus methods' do it 'should raise an error for bogus methods' do
(lambda 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).should raise_error(TypeError)
end end
@ -123,49 +120,50 @@ describe Google::APIClient do
it 'should generate valid requests' do it 'should generate valid requests' do
request = @client.generate_request( request = @client.generate_request(
'prediction.training.insert', :api_method => @prediction.training.insert,
{'data' => '12345', } :parameters => {'data' => '12345', }
) )
method, uri, headers, body = request method, uri, headers, body = request
method.should == 'POST' method.should == 'POST'
uri.should == 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 == {} (headers.inject({}) { |h,(k,v)| h[k]=v; h }).should == {}
body.should respond_to(:each) body.should respond_to(:each)
end end
it 'should generate requests against the correct URIs' do it 'should generate requests against the correct URIs' do
request = @client.generate_request( request = @client.generate_request(
:'prediction.training.insert', :api_method => @prediction.training.insert,
{'data' => '12345'} :parameters => {'data' => '12345'}
) )
method, uri, headers, body = request method, uri, headers, body = request
uri.should == uri.should ==
'https://www.googleapis.com/prediction/v1/training?data=12345' 'https://www.googleapis.com/prediction/v1.2/training?data=12345'
end end
it 'should generate requests against the correct URIs' do it 'should generate requests against the correct URIs' do
prediction = @client.discovered_api('prediction', 'v1')
request = @client.generate_request( request = @client.generate_request(
prediction.training.insert, :api_method => @prediction.training.insert,
{'data' => '12345'} :parameters => {'data' => '12345'}
) )
method, uri, headers, body = request method, uri, headers, body = request
uri.should == uri.should ==
'https://www.googleapis.com/prediction/v1/training?data=12345' 'https://www.googleapis.com/prediction/v1.2/training?data=12345'
end end
it 'should allow modification to the base URIs for testing purposes' do 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 = prediction.method_base =
'https://testing-domain.googleapis.com/prediction/v1/' 'https://testing-domain.googleapis.com/prediction/v1.2/'
request = @client.generate_request( request = @client.generate_request(
prediction.training.insert, :api_method => prediction.training.insert,
{'data' => '123'} :parameters => {'data' => '123'}
) )
method, uri, headers, body = request method, uri, headers, body = request
uri.should == uri.should == (
'https://testing-domain.googleapis.com/prediction/v1/training?data=123' 'https://testing-domain.googleapis.com/' +
'prediction/v1.2/training?data=123'
)
end end
it 'should generate OAuth 1 requests' do 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_key = '12345'
@client.authorization.token_credential_secret = '12345' @client.authorization.token_credential_secret = '12345'
request = @client.generate_request( request = @client.generate_request(
'prediction.training.insert', :api_method => @prediction.training.insert,
{'data' => '12345'} :parameters => {'data' => '12345'}
) )
method, uri, headers, body = request method, uri, headers, body = request
headers = headers.inject({}) { |h,(k,v)| h[k]=v; h } headers = headers.inject({}) { |h,(k,v)| h[k]=v; h }
@ -186,8 +184,8 @@ describe Google::APIClient do
@client.authorization = :oauth_2 @client.authorization = :oauth_2
@client.authorization.access_token = '12345' @client.authorization.access_token = '12345'
request = @client.generate_request( request = @client.generate_request(
'prediction.training.insert', :api_method => @prediction.training.insert,
{'data' => '12345'} :parameters => {'data' => '12345'}
) )
method, uri, headers, body = request method, uri, headers, body = request
headers = headers.inject({}) { |h,(k,v)| h[k]=v; h } headers = headers.inject({}) { |h,(k,v)| h[k]=v; h }
@ -199,24 +197,47 @@ describe Google::APIClient do
@client.authorization = :oauth_1 @client.authorization = :oauth_1
@client.authorization.token_credential_key = '12345' @client.authorization.token_credential_key = '12345'
@client.authorization.token_credential_secret = '12345' @client.authorization.token_credential_secret = '12345'
response = @client.execute( result = @client.execute(
'prediction.training.insert', @prediction.training.insert,
{'data' => '12345'} {'data' => '12345'}
) )
status, headers, body = response status, headers, body = result.response
status.should == 401 status.should == 401
end end
it 'should not be able to execute improperly authorized requests' do it 'should not be able to execute improperly authorized requests' do
@client.authorization = :oauth_2 @client.authorization = :oauth_2
@client.authorization.access_token = '12345' @client.authorization.access_token = '12345'
response = @client.execute( result = @client.execute(
'prediction.training.insert', @prediction.training.insert,
{'data' => '12345'} {'data' => '12345'}
) )
status, headers, body = response status, headers, body = result.response
status.should == 401 status.should == 401
end 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 end
describe 'with the buzz API' do 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 it 'should fail for string RPC names that do not match API name' do
(lambda do (lambda do
@client.generate_request( @client.generate_request(
'chili.activities.list', :api_method => 'chili.activities.list',
{'alt' => 'json'}, :parameters => {'alt' => 'json'},
'', :authenticated => false
[],
{:signed => false}
) )
end).should raise_error(Google::APIClient::TransmissionError) end).should raise_error(Google::APIClient::TransmissionError)
end end
it 'should generate requests against the correct URIs' do it 'should generate requests against the correct URIs' do
request = @client.generate_request( request = @client.generate_request(
@buzz.activities.list, :api_method => @buzz.activities.list,
{'userId' => 'hikingfan', 'scope' => '@public'}, :parameters => {'userId' => 'hikingfan', 'scope' => '@public'},
'', :authenticated => false
[],
{:signed => false}
) )
method, uri, headers, body = request method, uri, headers, body = request
uri.should == uri.should ==
@ -276,11 +293,9 @@ describe Google::APIClient do
it 'should correctly validate parameters' do it 'should correctly validate parameters' do
(lambda do (lambda do
@client.generate_request( @client.generate_request(
@buzz.activities.list, :api_method => @buzz.activities.list,
{'alt' => 'json'}, :parameters => {'alt' => 'json'},
'', :authenticated => false
[],
{:signed => false}
) )
end).should raise_error(ArgumentError) end).should raise_error(ArgumentError)
end end
@ -288,26 +303,33 @@ describe Google::APIClient do
it 'should correctly validate parameters' do it 'should correctly validate parameters' do
(lambda do (lambda do
@client.generate_request( @client.generate_request(
@buzz.activities.list, :api_method => @buzz.activities.list,
{'userId' => 'hikingfan', 'scope' => '@bogus'}, :parameters => {'userId' => 'hikingfan', 'scope' => '@bogus'},
'', :authenticated => false
[],
{:signed => false}
) )
end).should raise_error(ArgumentError) end).should raise_error(ArgumentError)
end end
it 'should be able to execute requests without authorization' do it 'should be able to execute requests without authorization' do
response = @client.execute( result = @client.execute(
@buzz.activities.list, @buzz.activities.list,
{'alt' => 'json', 'userId' => 'hikingfan', 'scope' => '@public'}, {'alt' => 'json', 'userId' => 'hikingfan', 'scope' => '@public'},
'', '',
[], [],
{:signed => false} :authenticated => false
) )
status, headers, body = response status, headers, body = result.response
status.should == 200 status.should == 200
end 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 end
describe 'with the latitude API' do describe 'with the latitude API' do
@ -338,11 +360,8 @@ describe Google::APIClient do
it 'should generate requests against the correct URIs' do it 'should generate requests against the correct URIs' do
request = @client.generate_request( request = @client.generate_request(
'latitude.currentLocation.get', :api_method => 'latitude.currentLocation.get',
{}, :authenticated => false
'',
[],
{:signed => false}
) )
method, uri, headers, body = request method, uri, headers, body = request
uri.should == uri.should ==
@ -351,11 +370,8 @@ describe Google::APIClient do
it 'should generate requests against the correct URIs' do it 'should generate requests against the correct URIs' do
request = @client.generate_request( request = @client.generate_request(
@latitude.current_location.get, :api_method => @latitude.current_location.get,
{}, :authenticated => false
'',
[],
{:signed => false}
) )
method, uri, headers, body = request method, uri, headers, body = request
uri.should == uri.should ==
@ -363,14 +379,11 @@ describe Google::APIClient do
end end
it 'should not be able to execute requests without authorization' do it 'should not be able to execute requests without authorization' do
response = @client.execute( result = @client.execute(
'latitude.currentLocation.get', :api_method => 'latitude.currentLocation.get',
{}, :authenticated => false
'',
[],
{:signed => false}
) )
status, headers, body = response status, headers, body = result.response
status.should == 401 status.should == 401
end end
end end
@ -403,11 +416,8 @@ describe Google::APIClient do
it 'should generate requests against the correct URIs' do it 'should generate requests against the correct URIs' do
request = @client.generate_request( request = @client.generate_request(
'moderator.profiles.get', :api_method => 'moderator.profiles.get',
{}, :authenticated => false
'',
[],
{:signed => false}
) )
method, uri, headers, body = request method, uri, headers, body = request
uri.should == uri.should ==
@ -416,11 +426,8 @@ describe Google::APIClient do
it 'should generate requests against the correct URIs' do it 'should generate requests against the correct URIs' do
request = @client.generate_request( request = @client.generate_request(
@moderator.profiles.get, :api_method => @moderator.profiles.get,
{}, :authenticated => false
'',
[],
{:signed => false}
) )
method, uri, headers, body = request method, uri, headers, body = request
uri.should == uri.should ==
@ -428,14 +435,14 @@ describe Google::APIClient do
end end
it 'should not be able to execute requests without authorization' do it 'should not be able to execute requests without authorization' do
response = @client.execute( result = @client.execute(
'moderator.profiles.get', 'moderator.profiles.get',
{}, {},
'', '',
[], [],
{:signed => false} {:authenticated => false}
) )
status, headers, body = response status, headers, body = result.response
status.should == 401 status.should == 401
end end
end end

View File

@ -16,36 +16,40 @@ require 'spec_helper'
require 'json' require 'json'
require 'google/api_client/parsers/json_parser' 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 before do
@parser = Google::APIClient::JSONParser @data = {
end 'error' => {
'code' => 401,
it 'should translate simple hash to JSON string' do 'message' => 'Token invalid - Invalid AuthSub token.',
@parser.serialize('test' => 23).should == '{"test":23}' 'errors' => [
end {
'location' => 'Authorization',
it 'should translate simple nested into to nested JSON string' do 'domain' => 'global',
@parser.serialize({ 'locationType' => 'header',
'test' => 23, 'test2' => {'foo' => 'baz', 12 => 3.14 } 'reason' => 'authError',
}).should == 'message' => 'Token invalid - Invalid AuthSub token.'
'{"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}
} }
end 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 end

View File

@ -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 it 'should not allow the user agent to be used with bogus values' do
(lambda do (lambda do
@client.user_agent = 42 @client.user_agent = 42
@client.transmit_request( @client.transmit(
['GET', 'http://www.google.com/', [], []] ['GET', 'http://www.google.com/', [], []]
) )
end).should raise_error(TypeError) end).should raise_error(TypeError)
@ -53,7 +53,7 @@ shared_examples_for 'configurable user agent' do
end end
[200, [], ['']] [200, [], ['']]
end end
@client.transmit_request(request, adapter) @client.transmit(request, adapter)
end end
end end
@ -63,11 +63,7 @@ describe Google::APIClient do
end end
it 'should make its version number available' do it 'should make its version number available' do
::Google::APIClient::VERSION::STRING.should be_instance_of(String) 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)
end end
it 'should default to OAuth 2' do it 'should default to OAuth 2' do
@ -104,26 +100,4 @@ describe Google::APIClient do
# TODO # TODO
it_should_behave_like 'configurable user agent' it_should_behave_like 'configurable user agent'
end 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 end