Initial implementation of HTTP.
git-svn-id: https://google-api-ruby-client.googlecode.com/svn/trunk@22 c1d61fac-ed7f-fcc1-18f7-ff78120a04ef
This commit is contained in:
parent
a9523bca32
commit
bc844db311
|
@ -12,16 +12,20 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
require 'google/api_client/version'
|
|
||||||
|
|
||||||
module Google #:nodoc:
|
module Google #:nodoc:
|
||||||
##
|
##
|
||||||
# This class manages communication with a single API.
|
# This class manages communication with a single API.
|
||||||
class APIClient
|
class APIClient
|
||||||
|
|
||||||
def initialize(options={})
|
def initialize(options={})
|
||||||
@options = {
|
@options = {
|
||||||
# TODO: What configuration options need to go here?
|
# TODO: What configuration options need to go here?
|
||||||
}.merge(options)
|
}.merge(options)
|
||||||
|
unless @options[:parser]
|
||||||
|
require 'google/api_client/parser/json_parser'
|
||||||
|
# NOTE: Do not rely on this default value, as it may change
|
||||||
|
@options[:parser] = JSONParser.new
|
||||||
|
end
|
||||||
unless @options[:authentication]
|
unless @options[:authentication]
|
||||||
require 'google/api_client/auth/oauth_1'
|
require 'google/api_client/auth/oauth_1'
|
||||||
# NOTE: Do not rely on this default value, as it may change
|
# NOTE: Do not rely on this default value, as it may change
|
||||||
|
@ -32,5 +36,13 @@ module Google #:nodoc:
|
||||||
@options[:transport] = HTTPTransport
|
@options[:transport] = HTTPTransport
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
##
|
||||||
|
# Returns the parser used by the client.
|
||||||
|
def parser
|
||||||
|
return @options[:parser]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
require 'google/api_client/version'
|
||||||
|
|
|
@ -12,60 +12,173 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
require 'google/api_client/parser/json_parser'
|
require 'net/http'
|
||||||
|
require 'net/https'
|
||||||
|
require 'addressable/uri'
|
||||||
|
|
||||||
module Google #:nodoc:
|
module Google #:nodoc:
|
||||||
class APIClient #:nodoc:
|
class APIClient #:nodoc:
|
||||||
|
|
||||||
##
|
##
|
||||||
# Factory for HTTP backed client requests.
|
# Provides a consistent interface by which to make HTTP requests using the
|
||||||
|
# Net::HTTP class.
|
||||||
class HTTPTransport
|
class HTTPTransport
|
||||||
|
ALLOWED_SCHEMES = ["http", "https"]
|
||||||
##
|
METHOD_MAPPING = {
|
||||||
# The default transport configuration values. These may be overridden
|
# RFC 2616
|
||||||
# simply by passing in the same key to the constructor.
|
:options => Net::HTTP::Options,
|
||||||
DEFAULTS = {
|
:get => Net::HTTP::Get,
|
||||||
:parser => :json_parser
|
:head => Net::HTTP::Head,
|
||||||
|
:post => Net::HTTP::Post,
|
||||||
|
:put => Net::HTTP::Put,
|
||||||
|
:delete => Net::HTTP::Delete,
|
||||||
|
:trace => Net::HTTP::Trace,
|
||||||
|
# Other standards supported by Net::HTTP
|
||||||
|
:copy => Net::HTTP::Copy,
|
||||||
|
:lock => Net::HTTP::Lock,
|
||||||
|
:mkcol => Net::HTTP::Mkcol,
|
||||||
|
:move => Net::HTTP::Move,
|
||||||
|
:propfind => Net::HTTP::Propfind,
|
||||||
|
:proppatch => Net::HTTP::Proppatch,
|
||||||
|
:unlock => Net::HTTP::Unlock
|
||||||
}
|
}
|
||||||
|
|
||||||
##
|
##
|
||||||
# The default implementations of various parsers. These may be overriden
|
|
||||||
# simply by passing the same key to the constructor.
|
|
||||||
PARSERS = {
|
|
||||||
:json_parser => JSONParser.new
|
|
||||||
}
|
|
||||||
|
|
||||||
##
|
|
||||||
# Creates a new HTTP request factory.
|
|
||||||
#
|
#
|
||||||
# @param [Hash] options
|
|
||||||
# @return [Google::APIClient::Discovery] The HTTP request factory.
|
|
||||||
def initialize(options={})
|
def initialize(options={})
|
||||||
@options = DEFAULTS.clone
|
# A mapping from authorities to Net::HTTP objects.
|
||||||
@options.merge!(options)
|
@connection_pool = options[:connection_pool] || {}
|
||||||
|
if options[:cert_store]
|
||||||
|
@cert_store = options[:cert_store]
|
||||||
|
else
|
||||||
|
@cert_store = OpenSSL::X509::Store.new
|
||||||
|
@cert_store.set_default_paths
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# first check if user passed a parser then fallback on appropriate default
|
attr_reader :connection_pool
|
||||||
@parser = @options[@options[:parser]] || PARSERS[@options[:parser]]
|
attr_reader :cert_store
|
||||||
unless @parser
|
|
||||||
|
def build_request(method, uri, options={})
|
||||||
|
# No type-checking here, but OK because we check against a whitelist
|
||||||
|
method = method.to_s.downcase.to_sym
|
||||||
|
uri = Addressable::URI.parse(uri).normalize
|
||||||
|
if !METHOD_MAPPING.keys.include?(method)
|
||||||
|
raise ArgumentError, "Unsupported HTTP method: #{method}"
|
||||||
|
end
|
||||||
|
headers = {
|
||||||
|
"Accept" => "application/json;q=1.0, */*;q=0.5"
|
||||||
|
}.merge(options[:headers] || {})
|
||||||
|
|
||||||
|
# TODO(bobaman) More stuff here to handle optional parameters like
|
||||||
|
# form data.
|
||||||
|
|
||||||
|
body = options[:body] || ""
|
||||||
|
if body != ""
|
||||||
|
entity_body_defaults = {
|
||||||
|
"Content-Length" => body.size.to_s,
|
||||||
|
"Content-Type" => "application/json"
|
||||||
|
}
|
||||||
|
headers = entity_body_defaults.merge(headers)
|
||||||
|
end
|
||||||
|
return [method.to_s.upcase, uri.to_s, headers, [body]]
|
||||||
|
end
|
||||||
|
|
||||||
|
def send_request(request)
|
||||||
|
retried = false
|
||||||
|
begin
|
||||||
|
method, uri, headers, body_wrapper = request
|
||||||
|
body = ""
|
||||||
|
body_wrapper.each do |chunk|
|
||||||
|
body += chunk
|
||||||
|
end
|
||||||
|
|
||||||
|
uri = Addressable::URI.parse(uri).normalize
|
||||||
|
connection = self.connect_to(uri)
|
||||||
|
|
||||||
|
# Translate to Net::HTTP request
|
||||||
|
request_class = METHOD_MAPPING[method.to_s.downcase.to_sym]
|
||||||
|
if !request_class
|
||||||
raise ArgumentError,
|
raise ArgumentError,
|
||||||
'Invalid :parser configuration.'
|
"Unsupported HTTP method: #{method.to_s.downcase.to_sym}"
|
||||||
|
end
|
||||||
|
net_http_request = request_class.new(uri.request_uri)
|
||||||
|
for key, value in headers
|
||||||
|
net_http_request[key] = value
|
||||||
|
end
|
||||||
|
net_http_request.body = body
|
||||||
|
response = connection.request(net_http_request)
|
||||||
|
|
||||||
|
response_headers = {}
|
||||||
|
# We want the canonical header name.
|
||||||
|
# Note that Net::HTTP is lossy in that it downcases header names and
|
||||||
|
# then capitalizes them afterwards.
|
||||||
|
# This results in less-than-ideal behavior for headers like 'ETag'.
|
||||||
|
# Not much we can do about it.
|
||||||
|
response.canonical_each do |header, value|
|
||||||
|
response_headers[header] = value
|
||||||
|
end
|
||||||
|
# We use the Rack spec to trivially abstract the response format
|
||||||
|
return [response.code.to_i, response_headers, [response.body]]
|
||||||
|
rescue Errno::EPIPE, IOError, EOFError => e
|
||||||
|
# If there's a problem with the connection, finish and restart
|
||||||
|
if !retried && connection.started?
|
||||||
|
retried = true
|
||||||
|
connection.finish
|
||||||
|
connection.start
|
||||||
|
retry
|
||||||
|
else
|
||||||
|
raise e
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
##
|
##
|
||||||
# Returns configuration of the transport.
|
# Builds a connection to the authority given in the URI using the
|
||||||
|
# appropriate protocol.
|
||||||
#
|
#
|
||||||
# @return [Hash] The configuration options.
|
# @param [Addressable::URI, #to_str] uri The URI to connect to.
|
||||||
def options
|
def connect_to(uri)
|
||||||
return @options
|
uri = Addressable::URI.parse(uri).normalize
|
||||||
end
|
if !ALLOWED_SCHEMES.include?(uri.scheme)
|
||||||
|
raise ArgumentError, "Unsupported protocol: #{uri.scheme}"
|
||||||
##
|
|
||||||
# Returns the parser used by the transport.
|
|
||||||
#
|
|
||||||
# @return The handle to the parser.
|
|
||||||
def parser
|
|
||||||
return @parser
|
|
||||||
end
|
end
|
||||||
|
connection = @connection_pool[uri.site]
|
||||||
|
unless connection
|
||||||
|
connection = Net::HTTP.new(uri.host, uri.inferred_port)
|
||||||
|
end
|
||||||
|
retried = false
|
||||||
|
begin
|
||||||
|
if uri.scheme == 'https' && !connection.started?
|
||||||
|
connection.use_ssl = true
|
||||||
|
if connection.respond_to?(:enable_post_connection_check=)
|
||||||
|
# Deals with a security vulnerability
|
||||||
|
connection.enable_post_connection_check = true
|
||||||
|
end
|
||||||
|
connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
||||||
|
connection.cert_store = @cert_store
|
||||||
|
end
|
||||||
|
unless connection.started?
|
||||||
|
# Since we allow a connection pool to be passed in, we don't
|
||||||
|
# actually know this connection has been started yet.
|
||||||
|
connection.start
|
||||||
|
end
|
||||||
|
rescue Errno::EPIPE, IOError, EOFError => e
|
||||||
|
# If there's a problem with the connection, finish and restart
|
||||||
|
if !retried && connection.started?
|
||||||
|
retried = true
|
||||||
|
connection.finish
|
||||||
|
connection.start
|
||||||
|
retry
|
||||||
|
else
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# Keep a reference to the connection around
|
||||||
|
@connection_pool[uri.site] = connection
|
||||||
|
return connection
|
||||||
|
end
|
||||||
|
protected :connect_to
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,8 +12,6 @@
|
||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
# Used to prevent the class/module from being loaded more than once
|
|
||||||
unless defined? Google::APIClient::VERSION
|
|
||||||
module Google #:nodoc:
|
module Google #:nodoc:
|
||||||
class APIClient #:nodoc:
|
class APIClient #:nodoc:
|
||||||
module VERSION #:nodoc:
|
module VERSION #:nodoc:
|
||||||
|
@ -25,4 +23,3 @@ unless defined? Google::APIClient::VERSION
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
# 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 'spec_helper'
|
||||||
|
|
||||||
|
require 'net/http'
|
||||||
|
require 'net/https'
|
||||||
|
require 'google/api_client/transport/http_transport'
|
||||||
|
|
||||||
|
class AlwaysFail
|
||||||
|
def initialize(*args)
|
||||||
|
raise IOError, "This would never work."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Google::APIClient::HTTPTransport::METHOD_MAPPING[:fail] = AlwaysFail
|
||||||
|
|
||||||
|
describe Google::APIClient::HTTPTransport, 'in the default configuration' do
|
||||||
|
before do
|
||||||
|
@http = Google::APIClient::HTTPTransport.new
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should send a GET request' do
|
||||||
|
request = @http.build_request(:get, "http://www.google.com/")
|
||||||
|
response = @http.send_request(request)
|
||||||
|
status, headers, body = response
|
||||||
|
status.should >= 100
|
||||||
|
body.size.should > 0
|
||||||
|
headers.size.should > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should send a GET request using SSL' do
|
||||||
|
request = @http.build_request(:get, "https://www.google.com/")
|
||||||
|
response = @http.send_request(request)
|
||||||
|
status, headers, body = response
|
||||||
|
status.should >= 100
|
||||||
|
body.size.should > 0
|
||||||
|
headers.size.should > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should send a POST request' do
|
||||||
|
request = @http.build_request(
|
||||||
|
:post, "http://www.google.com/", :body => "A Body."
|
||||||
|
)
|
||||||
|
response = @http.send_request(request)
|
||||||
|
status, headers, body = response
|
||||||
|
status.should >= 100
|
||||||
|
body.size.should > 0
|
||||||
|
headers.size.should > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should send a PUT request' do
|
||||||
|
request = @http.build_request(
|
||||||
|
:put, "http://www.google.com/", :body => "A Body."
|
||||||
|
)
|
||||||
|
response = @http.send_request(request)
|
||||||
|
status, headers, body = response
|
||||||
|
status.should >= 100
|
||||||
|
body.size.should > 0
|
||||||
|
headers.size.should > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should send a DELETE request' do
|
||||||
|
request = @http.build_request(:delete, "http://www.google.com/")
|
||||||
|
response = @http.send_request(request)
|
||||||
|
status, headers, body = response
|
||||||
|
status.should >= 100
|
||||||
|
body.size.should > 0
|
||||||
|
headers.size.should > 0
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should fail to send a FAIL request' do
|
||||||
|
(lambda do
|
||||||
|
request = @http.build_request(:fail, "http://www.google.com/")
|
||||||
|
response = @http.send_request(request)
|
||||||
|
end).should raise_error(IOError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should fail to send a BOGUS request' do
|
||||||
|
(lambda do
|
||||||
|
response = @http.send_request(
|
||||||
|
["BOGUS", "http://www.google.com/", {}, [""]]
|
||||||
|
)
|
||||||
|
end).should raise_error(ArgumentError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should fail to connect to a non-addressable URI' do
|
||||||
|
(lambda do
|
||||||
|
request = @http.build_request(:get, "bogus://www.google.com/")
|
||||||
|
response = @http.send_request(request)
|
||||||
|
end).should raise_error(ArgumentError)
|
||||||
|
end
|
||||||
|
end
|
|
@ -14,50 +14,89 @@
|
||||||
|
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
|
require 'net/http'
|
||||||
|
require 'net/https'
|
||||||
require 'google/api_client/transport/http_transport'
|
require 'google/api_client/transport/http_transport'
|
||||||
require 'google/api_client/parser/json_parser'
|
|
||||||
|
|
||||||
describe Google::APIClient::HTTPTransport, 'with default configuration' do
|
def assemble_body_string(body)
|
||||||
|
body_string = ""
|
||||||
|
body.each do |chunk|
|
||||||
|
body_string += chunk
|
||||||
|
end
|
||||||
|
return body_string
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Google::APIClient::HTTPTransport, 'in the default configuration' do
|
||||||
before do
|
before do
|
||||||
@transport = Google::APIClient::HTTPTransport.new
|
@http = Google::APIClient::HTTPTransport.new
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'should use the default json parser' do
|
it 'should build a valid GET request' do
|
||||||
@transport.parser.should be_instance_of Google::APIClient::JSONParser
|
method, uri, headers, body =
|
||||||
|
@http.build_request(:get, "http://www.example.com/")
|
||||||
|
body_string = assemble_body_string(body)
|
||||||
|
method.should == "GET"
|
||||||
|
uri.should === "http://www.example.com/"
|
||||||
|
headers.keys.should_not include("Content-Length")
|
||||||
|
body_string.should == ""
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should build a valid POST request' do
|
||||||
|
method, uri, headers, body = @http.build_request(
|
||||||
|
:post, "http://www.example.com/", :body => "A body."
|
||||||
|
)
|
||||||
|
body_string = assemble_body_string(body)
|
||||||
|
method.should == "POST"
|
||||||
|
uri.should === "http://www.example.com/"
|
||||||
|
headers["Content-Length"].should == "7"
|
||||||
|
body_string.should == "A body."
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should build a valid PUT request' do
|
||||||
|
method, uri, headers, body = @http.build_request(
|
||||||
|
:put, "http://www.example.com/", :body => "A body."
|
||||||
|
)
|
||||||
|
body_string = assemble_body_string(body)
|
||||||
|
method.should == "PUT"
|
||||||
|
uri.should === "http://www.example.com/"
|
||||||
|
headers["Content-Length"].should == "7"
|
||||||
|
body_string.should == "A body."
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should build a valid DELETE request' do
|
||||||
|
method, uri, headers, body =
|
||||||
|
@http.build_request(:delete, "http://www.example.com/")
|
||||||
|
body_string = assemble_body_string(body)
|
||||||
|
method.should == "DELETE"
|
||||||
|
uri.should === "http://www.example.com/"
|
||||||
|
headers.keys.should_not include("Content-Length")
|
||||||
|
body_string.should == ""
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should not build a BOGUS request' do
|
||||||
|
(lambda do
|
||||||
|
@http.build_request(:bogus, "http://www.example.com/")
|
||||||
|
end).should raise_error(ArgumentError)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe Google::APIClient::HTTPTransport, 'with custom pluggable parser' do
|
describe Google::APIClient::HTTPTransport,
|
||||||
|
'with a certificate store and connection pool' do
|
||||||
before do
|
before do
|
||||||
class FakeJsonParser
|
@http = Google::APIClient::HTTPTransport.new(
|
||||||
end
|
:cert_store => OpenSSL::X509::Store.new,
|
||||||
|
:connection_pool => {
|
||||||
@transport = Google::APIClient::HTTPTransport.new(:json_parser => FakeJsonParser.new)
|
"http://www.example.com" => Net::HTTP.new("www.example.com", 80)
|
||||||
end
|
}
|
||||||
|
|
||||||
it 'should use the custom parser' do
|
|
||||||
@transport.parser.should be_instance_of FakeJsonParser
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe Google::APIClient::HTTPTransport, 'with new parser type' do
|
|
||||||
before do
|
|
||||||
class FakeNewParser
|
|
||||||
end
|
|
||||||
|
|
||||||
@transport = Google::APIClient::HTTPTransport.new(
|
|
||||||
:parser => :new_parser,
|
|
||||||
:new_parser => FakeNewParser.new
|
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'should use new parser type' do
|
it 'should have the correct certificate store' do
|
||||||
@transport.parser.should be_instance_of FakeNewParser
|
# TODO(bobaman) Write a real test
|
||||||
end
|
@http.cert_store.should_not == nil
|
||||||
end
|
end
|
||||||
|
|
||||||
describe Google::APIClient::HTTPTransport, 'with illegal parser config' do
|
it 'should have the correct connection pool' do
|
||||||
it 'should raise ArgumentError' do
|
@http.connection_pool.keys.should include("http://www.example.com")
|
||||||
lambda { Google::APIClient::HTTPTransport.new(:parser => :fakeclass) }.should raise_exception(ArgumentError)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
# 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 'spec_helper'
|
||||||
|
|
||||||
|
require 'google/api_client'
|
||||||
|
require 'google/api_client/version'
|
||||||
|
require 'google/api_client/parser/json_parser'
|
||||||
|
require 'google/api_client/auth/oauth_1'
|
||||||
|
require 'google/api_client/transport/http_transport'
|
||||||
|
|
||||||
|
|
||||||
|
describe Google::APIClient, 'with default configuration' do
|
||||||
|
before do
|
||||||
|
@client = Google::APIClient.new
|
||||||
|
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_instance_of(Google::APIClient::JSONParser)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Google::APIClient, 'with custom pluggable parser' do
|
||||||
|
before do
|
||||||
|
class FakeJsonParser
|
||||||
|
end
|
||||||
|
|
||||||
|
@client = Google::APIClient.new(:parser => FakeJsonParser.new)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'should use the custom parser' do
|
||||||
|
@client.parser.should be_instance_of(FakeJsonParser)
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in New Issue