Skip to content

Commit

Permalink
Add Client#close to shutdown any HTTP connection and object finaliz…
Browse files Browse the repository at this point in the history
…er to do the same.

Fixes #86.
  • Loading branch information
gkellogg committed Oct 13, 2018
1 parent 204355e commit 64b75fc
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 111 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ This is a [Ruby][] implementation of a [SPARQL][] client for [RDF.rb][].

* [Ruby](http://ruby-lang.org/) (>= 2.2.2)
* [RDF.rb](http://rubygems.org/gems/rdf) (~> 3.0)
* [Net::HTTP::Persistent](http://rubygems.org/gems/net-http-persistent) (>= 1.4)
* [Net::HTTP::Persistent](http://rubygems.org/gems/net-http-persistent) (~> 3.0)
* Soft dependency on [SPARQL](http://rubygems.org/gems/sparql) (~> 3.0)
* Soft dependency on [Nokogiri](http://rubygems.org/gems/nokogiri) (>= 1.7)
* Soft dependency on [Nokogiri](http://rubygems.org/gems/nokogiri) (>= 1.8)

## Installation

Expand Down
24 changes: 19 additions & 5 deletions lib/sparql/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ def initialize(url, options = {}, &block)
@url, @options = RDF::URI.new(url.to_s), options.dup
@headers = @options.delete(:headers) || {}
@http = http_klass(@url.scheme)

# Close the http connection when object is deallocated
ObjectSpace.define_finalizer(self, proc {@http.shutdown if @http.respond_to?(:shutdown)})
end

if block_given?
Expand All @@ -105,6 +108,16 @@ def initialize(url, options = {}, &block)
end
end

##
# Closes a client instance by finishing the connection.
# The client is unavailable for any further data operations; an IOError is raised if such an attempt is made. I/O streams are automatically closed when they are claimed by the garbage collector.
# @return [void] `self`
def close
@http.shutdown if @http
@http = nil
self
end

##
# Executes a boolean `ASK` query.
#
Expand Down Expand Up @@ -288,6 +301,7 @@ def nodes
# @option options [String] :content_type
# @option options [Hash] :headers
# @return [Array<RDF::Query::Solution>]
# @raise [IOError] if connection is closed
# @see http://www.w3.org/TR/sparql11-protocol/#query-operation
def query(query, options = {})
@op = :query
Expand Down Expand Up @@ -315,6 +329,7 @@ def query(query, options = {})
# @option options [String] :content_type
# @option options [Hash] :headers
# @return [void] `self`
# @raise [IOError] if connection is closed
# @see http://www.w3.org/TR/sparql11-protocol/#update-operation
def update(query, options = {})
@op = :update
Expand All @@ -338,6 +353,7 @@ def update(query, options = {})
# @option options [String] :content_type
# @option options [Hash] :headers
# @return [String]
# @raise [IOError] if connection is closed
def response(query, options = {})
headers = options[:headers] || {}
headers['Accept'] = options[:content_type] if options[:content_type]
Expand Down Expand Up @@ -658,11 +674,7 @@ def http_klass(scheme)
value = ENV['https_proxy']
proxy_url = URI.parse(value) unless value.nil? || value.empty?
end
klass = if Net::HTTP::Persistent::VERSION >= '3.0'
Net::HTTP::Persistent.new(name: self.class.to_s, proxy: proxy_url)
else
Net::HTTP::Persistent.new(self.class.to_s, proxy_url)
end
klass = Net::HTTP::Persistent.new(name: self.class.to_s, proxy: proxy_url)
klass.keep_alive = @options[:keep_alive] || 120
klass.read_timeout = @options[:read_timeout] || 60
klass
Expand All @@ -676,6 +688,7 @@ def http_klass(scheme)
# @yield [response]
# @yieldparam [Net::HTTPResponse] response
# @return [Net::HTTPResponse]
# @raise [IOError] if connection is closed
# @see http://www.w3.org/TR/sparql11-protocol/#query-operation
def request(query, headers = {}, &block)
# Make sure an appropriate Accept header is present
Expand All @@ -693,6 +706,7 @@ def request(query, headers = {}, &block)

pre_http_hook(request) if respond_to?(:pre_http_hook)

raise IOError, "Client has been closed" unless @http
response = @http.request(::URI.parse(url.to_s), request)

post_http_hook(response) if respond_to?(:post_http_hook)
Expand Down
2 changes: 1 addition & 1 deletion sparql-client.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Gem::Specification.new do |gem|
gem.required_ruby_version = '>= 2.2.2'
gem.requirements = []
gem.add_runtime_dependency 'rdf', '~> 3.0'
gem.add_runtime_dependency 'net-http-persistent', '>= 2.9', '< 4'
gem.add_runtime_dependency 'net-http-persistent', '~> 3.0'
gem.add_development_dependency 'rdf-spec', '~> 3.0'
gem.add_development_dependency 'sparql', '~> 3.0'
gem.add_development_dependency 'rspec', '~> 3.7'
Expand Down
63 changes: 34 additions & 29 deletions spec/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,81 +55,81 @@ def response(header)
end
end

it "should handle successful response with plain header" do
it "handles successful response with plain header" do
expect(subject).to receive(:request).and_yield response('text/plain')
expect(RDF::Reader).to receive(:for).with(:content_type => 'text/plain').and_call_original
subject.query(query)
end

it "should handle successful response with boolean header" do
it "handles successful response with boolean header" do
expect(subject).to receive(:request).and_yield response(SPARQL::Client::RESULT_BOOL)
expect(subject.query(query)).to be_falsey
end

it "should handle successful response with JSON header" do
it "handles successful response with JSON header" do
expect(subject).to receive(:request).and_yield response(SPARQL::Client::RESULT_JSON)
expect(subject.class).to receive(:parse_json_bindings)
subject.query(query)
end

it "should handle successful response with XML header" do
it "handles successful response with XML header" do
expect(subject).to receive(:request).and_yield response(SPARQL::Client::RESULT_XML)
expect(subject.class).to receive(:parse_xml_bindings)
subject.query(query)
end

it "should handle successful response with CSV header" do
it "handles successful response with CSV header" do
expect(subject).to receive(:request).and_yield response(SPARQL::Client::RESULT_CSV)
expect(subject.class).to receive(:parse_csv_bindings)
subject.query(query)
end

it "should handle successful response with TSV header" do
it "handles successful response with TSV header" do
expect(subject).to receive(:request).and_yield response(SPARQL::Client::RESULT_TSV)
expect(subject.class).to receive(:parse_tsv_bindings)
subject.query(query)
end

it "should handle successful response with overridden XML header" do
it "handles successful response with overridden XML header" do
expect(subject).to receive(:request).and_yield response(SPARQL::Client::RESULT_XML)
expect(subject.class).to receive(:parse_json_bindings)
subject.query(query, :content_type => SPARQL::Client::RESULT_JSON)
end

it "should handle successful response with no content type" do
it "handles successful response with no content type" do
expect(subject).to receive(:request).and_yield response(nil)
expect { subject.query(query) }.not_to raise_error
end

it "should handle successful response with overridden plain header" do
it "handles successful response with overridden plain header" do
expect(subject).to receive(:request).and_yield response('text/plain')
expect(RDF::Reader).to receive(:for).with(:content_type => 'text/turtle').and_call_original
subject.query(query, :content_type => 'text/turtle')
end

it "should handle successful response with custom headers" do
it "handles successful response with custom headers" do
expect(subject).to receive(:request).with(anything, "Authorization" => "Basic XXX==").
and_yield response('text/plain')
subject.query(query, :headers => {"Authorization" => "Basic XXX=="})
end

it "should handle successful response with initial custom headers" do
it "handles successful response with initial custom headers" do
options = {:headers => {"Authorization" => "Basic XXX=="}, :method => :get}
client = SPARQL::Client.new('http://data.linkedmdb.org/sparql', options)
client.instance_variable_set :@http, double(:request => response('text/plain'))
expect(Net::HTTP::Get).to receive(:new).with(anything, hash_including(options[:headers]))
client.query(query)
end

it "should enable overriding the http method" do
it "enables overriding the http method" do
stub_request(:get, "http://data.linkedmdb.org/sparql?query=DESCRIBE%20?kb%20WHERE%20%7B%20?kb%20%3Chttp://data.linkedmdb.org/resource/movie/actor_name%3E%20%22Kevin%20Bacon%22%20.%20%7D").
to_return(:status => 200, :body => "", :headers => { 'Content-Type' => 'application/n-triples'})
allow(subject).to receive(:request_method).with(query).and_return(:get)
expect(subject).to receive(:make_get_request).and_call_original
subject.query(query)
end

it "should support international characters in response body" do
it "supports international characters in response body" do
client = SPARQL::Client.new('http://dbpedia.org/sparql')
json = {
:results => {
Expand All @@ -145,6 +145,11 @@ def response(header)
expect(result[:name].to_s).to eq "東京"
end

it "generates IOError when querying closed client" do
subject.close
expect{ subject.query(ask_query) }.to raise_error IOError
end

context "Redirects" do
before do
WebMock.stub_request(:any, 'http://data.linkedmdb.org/sparql').
Expand All @@ -167,31 +172,31 @@ def response(header)
end

context "Accept Header" do
it "should use application/sparql-results+json for ASK" do
it "uses application/sparql-results+json for ASK" do
WebMock.stub_request(:any, 'http://data.linkedmdb.org/sparql').
to_return(:body => '{}', :status => 200, :headers => { 'Content-Type' => 'application/sparql-results+json'})
subject.query(ask_query)
expect(WebMock).to have_requested(:post, "http://data.linkedmdb.org/sparql").
with(:headers => {'Accept'=>'application/sparql-results+json, application/sparql-results+xml, text/boolean, text/tab-separated-values;q=0.8, text/csv;q=0.2, */*;q=0.1'})
end

it "should use application/n-triples for CONSTRUCT" do
it "uses application/n-triples for CONSTRUCT" do
WebMock.stub_request(:any, 'http://data.linkedmdb.org/sparql').
to_return(:body => '', :status => 200, :headers => { 'Content-Type' => 'application/n-triples'})
subject.query(construct_query)
expect(WebMock).to have_requested(:post, "http://data.linkedmdb.org/sparql").
with(:headers => {'Accept'=>'application/n-triples, text/plain, */*;q=0.1'})
end

it "should use application/n-triples for DESCRIBE" do
it "uses application/n-triples for DESCRIBE" do
WebMock.stub_request(:any, 'http://data.linkedmdb.org/sparql').
to_return(:body => '', :status => 200, :headers => { 'Content-Type' => 'application/n-triples'})
subject.query(describe_query)
expect(WebMock).to have_requested(:post, "http://data.linkedmdb.org/sparql").
with(:headers => {'Accept'=>'application/n-triples, text/plain, */*;q=0.1'})
end

it "should use application/sparql-results+json for SELECT" do
it "uses application/sparql-results+json for SELECT" do
WebMock.stub_request(:any, 'http://data.linkedmdb.org/sparql').
to_return(:body => '{}', :status => 200, :headers => { 'Content-Type' => 'application/sparql-results+json'})
subject.query(select_query)
Expand All @@ -201,21 +206,21 @@ def response(header)
end

context "Alternative Endpoint" do
it "should use the default endpoint if no alternative endpoint is provided" do
it "uses the default endpoint if no alternative endpoint is provided" do
WebMock.stub_request(:any, 'http://data.linkedmdb.org/sparql').
to_return(:body => '', :status => 200)
subject.update(update_query)
expect(WebMock).to have_requested(:post, "http://data.linkedmdb.org/sparql")
end

it "should use the alternative endpoint if provided" do
it "uses the alternative endpoint if provided" do
WebMock.stub_request(:any, 'http://data.linkedmdb.org/alternative').
to_return(:body => '', :status => 200)
subject.update(update_query, { endpoint: "http://data.linkedmdb.org/alternative" })
expect(WebMock).to have_requested(:post, "http://data.linkedmdb.org/alternative")
end

it "should not use the alternative endpoint for a select query" do
it "does not use the alternative endpoint for a select query" do
WebMock.stub_request(:any, 'http://data.linkedmdb.org/sparql').
to_return(:body => '', :status => 200)
WebMock.stub_request(:any, 'http://data.linkedmdb.org/alternative').
Expand Down Expand Up @@ -253,7 +258,7 @@ def response(header)
let(:graph) {RDF::Graph.new << RDF::Statement(RDF::URI('http://example/s'), RDF::URI('http://example/p'), "o")}
subject {SPARQL::Client.new(repo)}

it "should query repository" do
it "queries repository" do
expect(SPARQL).to receive(:execute).with(query, repo, {})
subject.query(query)
end
Expand Down Expand Up @@ -301,7 +306,7 @@ def response(header)
end

context "when parsing XML" do
it "should parse binding results correctly" do
it "parses binding results correctly" do
xml = File.read("spec/fixtures/results.xml")
nodes = {}
solutions = SPARQL::Client::parse_xml_bindings(xml, nodes)
Expand All @@ -317,19 +322,19 @@ def response(header)
expect(solutions[0]["x"]).to eq nodes["r2"]
end

it "should parse boolean true results correctly" do
it "parses boolean true results correctly" do
xml = File.read("spec/fixtures/bool_true.xml")
expect(SPARQL::Client::parse_xml_bindings(xml)).to eq true
end

it "should parse boolean false results correctly" do
it "parses boolean false results correctly" do
xml = File.read("spec/fixtures/bool_false.xml")
expect(SPARQL::Client::parse_xml_bindings(xml)).to eq false
end
end

context "when parsing JSON" do
it "should parse binding results correctly" do
it "parses binding results correctly" do
xml = File.read("spec/fixtures/results.json")
nodes = {}
solutions = SPARQL::Client::parse_json_bindings(xml, nodes)
Expand All @@ -345,19 +350,19 @@ def response(header)
expect(solutions[0]["x"]).to eq nodes["r2"]
end

it "should parse boolean true results correctly" do
it "parses boolean true results correctly" do
json = '{"boolean": true}'
expect(SPARQL::Client::parse_json_bindings(json)).to eq true
end

it "should parse boolean true results correctly" do
it "parses boolean true results correctly" do
json = '{"boolean": false}'
expect(SPARQL::Client::parse_json_bindings(json)).to eq false
end
end

context "when parsing CSV" do
it "should parse binding results correctly" do
it "parses binding results correctly" do
csv = File.read("spec/fixtures/results.csv")
nodes = {}
solutions = SPARQL::Client::parse_csv_bindings(csv, nodes)
Expand All @@ -373,7 +378,7 @@ def response(header)
end

context "when parsing TSV" do
it "should parse binding results correctly" do
it "parses binding results correctly" do
tsv = File.read("spec/fixtures/results.tsv")
nodes = {}
solutions = SPARQL::Client::parse_tsv_bindings(tsv, nodes)
Expand Down
Loading

0 comments on commit 64b75fc

Please sign in to comment.