diff --git a/.gitignore b/.gitignore index 49a35da6..51e06d06 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ doc coverage .bundle /.byebug_history +.ruby-version diff --git a/README.md b/README.md index 0e25169b..ccd63211 100644 --- a/README.md +++ b/README.md @@ -25,70 +25,92 @@ This is a [Ruby][] implementation of a [SPARQL][] client for [RDF.rb][]. ## Examples ### Querying a remote SPARQL endpoint - require 'sparql/client' - sparql = SPARQL::Client.new("http://dbpedia.org/sparql") +```ruby +require 'sparql/client' +sparql = SPARQL::Client.new("http://dbpedia.org/sparql") +``` +### Querying a remote SPARQL endpoint with a specified default graph -### Querying a `RDF::Repository` instance +```ruby +require 'sparql/client' +sparql = SPARQL::Client.new("http://dbpedia.org/sparql", { :graph => "http://dbpedia.org" }) +``` - require 'rdf/trig' - repository = RDF::Repository.load("http://example/dataset.trig") - sparql = SPARQL::Client.new(repository) +### Querying a `RDF::Repository` instance -### Executing a boolean query and outputting the result +```ruby +require 'rdf/trig' +repository = RDF::Repository.load("http://example/dataset.trig") +sparql = SPARQL::Client.new(repository) +``` - # ASK WHERE { ?s ?p ?o } - result = sparql.ask.whether([:s, :p, :o]).true? +### Executing a boolean query and outputting the result - puts result.inspect #=> true or false +```ruby +# ASK WHERE { ?s ?p ?o } +result = sparql.ask.whether([:s, :p, :o]).true? +puts result.inspect #=> true or false +``` ### Executing a tuple query and iterating over the returned solutions - # SELECT * WHERE { ?s ?p ?o } OFFSET 100 LIMIT 10 - query = sparql.select.where([:s, :p, :o]).offset(100).limit(10) +```ruby +# SELECT * WHERE { ?s ?p ?o } OFFSET 100 LIMIT 10 +query = sparql.select.where([:s, :p, :o]).offset(100).limit(10) - query.each_solution do |solution| - puts solution.inspect - end +query.each_solution do |solution| + puts solution.inspect +end +``` ### Executing a graph query and iterating over the returned statements - # CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o } LIMIT 10 - query = sparql.construct([:s, :p, :o]).where([:s, :p, :o]).limit(10) - query.each_statement do |statement| - puts statement.inspect - end +```ruby +# CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o } LIMIT 10 +query = sparql.construct([:s, :p, :o]).where([:s, :p, :o]).limit(10) + +query.each_statement do |statement| + puts statement.inspect +end +``` ### Executing an arbitrary textual SPARQL query string - result = sparql.query("ASK WHERE { ?s ?p ?o }") +```ruby +result = sparql.query("ASK WHERE { ?s ?p ?o }") - puts result.inspect #=> true or false +puts result.inspect #=> true or false +``` ### Inserting data into a graph - # INSERT DATA { "J. Random Hacker" .} - data = RDF::Graph.new do |graph| - graph << [RDF::URI('http://example.org/jhacker'), RDF::Vocab::FOAF.name, "J. Random Hacker"] - end - sparql.insert_data(data) +```ruby +# INSERT DATA { "J. Random Hacker" .} +data = RDF::Graph.new do |graph| + graph << [RDF::URI('http://example.org/jhacker'), RDF::Vocab::FOAF.name, "J. Random Hacker"] +end +sparql.insert_data(data) +``` ### Deleting data from a graph - # DELETE DATA { "J. Random Hacker" .} - data = RDF::Graph.new do |graph| - graph << [RDF::URI('http://example.org/jhacker'), RDF::Vocab::FOAF.name, "J. Random Hacker"] - end - sparql.delete_data(data) +```ruby +# DELETE DATA { "J. Random Hacker" .} +data = RDF::Graph.new do |graph| + graph << [RDF::URI('http://example.org/jhacker'), RDF::Vocab::FOAF.name, "J. Random Hacker"] +end +sparql.delete_data(data) +``` ## Documentation -* {SPARQL::Client} - * {SPARQL::Client::Query} - * {SPARQL::Client::Repository} - * {SPARQL::Client::Update} +* [SPARQL::Client](https://www.rubydoc.info/github/ruby-rdf/sparql-client/SPARQL/Client) + * [SPARQL::Client::Query](https://www.rubydoc.info/github/ruby-rdf/sparql-client/SPARQL/Client/Query) + * [SPARQL::Client::Repository](https://www.rubydoc.info/github/ruby-rdf/sparql-client/SPARQL/Client/Repository) + * [SPARQL::Client::Update](https://www.rubydoc.info/github/ruby-rdf/sparql-client/SPARQL/Client/Update) ## Dependencies diff --git a/VERSION b/VERSION index 4a36342f..cb2b00e4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.0.0 +3.0.1 diff --git a/lib/sparql/client.rb b/lib/sparql/client.rb index 84e3233d..de8ffdc5 100644 --- a/lib/sparql/client.rb +++ b/lib/sparql/client.rb @@ -40,7 +40,7 @@ class ServerError < StandardError; end '*/*;q=0.1' ].join(', ').freeze GRAPH_ALL = ( - RDF::Format.content_types.keys + + RDF::Format.content_types.keys + ['*/*;q=0.1'] ).join(', ').freeze @@ -543,7 +543,7 @@ def parse_rdf_serialization(response, options = {}) if reader = RDF::Reader.for(options) reader.new(response.body) else - raise RDF::ReaderError, "no suitable rdf reader was found." + raise RDF::ReaderError, "no RDF reader was found for #{options}." end end @@ -701,8 +701,8 @@ def request(query, headers = {}, &block) if response.kind_of? Net::HTTPRedirection response = @http.request(::URI.parse(response['location']), request) else - return block_given? ? block.call(response) : response - end + return block_given? ? block.call(response) : response + end end raise ServerError, "Infinite redirect at #{url}. Redirected more than 10 times." end @@ -726,6 +726,7 @@ def request_method(query) def make_get_request(query, headers = {}) url = self.url.dup url.query_values = (url.query_values || {}).merge(:query => query.to_s) + set_url_default_graph url unless @options[:graph].nil? request = Net::HTTP::Get.new(url.request_uri, self.headers.merge(headers)) request end @@ -740,23 +741,56 @@ def make_get_request(query, headers = {}) # @see http://www.w3.org/TR/sparql11-protocol/#query-via-post-urlencoded def make_post_request(query, headers = {}) if @alt_endpoint.nil? - endpoint = url.request_uri + url = self.url.dup + set_url_default_graph url unless @options[:graph].nil? + endpoint = url.request_uri else endpoint = @alt_endpoint end + request = Net::HTTP::Post.new(endpoint, self.headers.merge(headers)) case (self.options[:protocol] || DEFAULT_PROTOCOL).to_s when '1.1' request['Content-Type'] = 'application/sparql-' + (@op || :query).to_s request.body = query.to_s when '1.0' - request.set_form_data((@op || :query) => query.to_s) + form_data = {(@op || :query) => query.to_s} + form_data.merge!( + {:'default-graph-uri' => @options[:graph]} + ) if !@options[:graph].nil? && (@op.eql? :query) + form_data.merge!( + {:'using-graph-uri' => @options[:graph]} + ) if !@options[:graph].nil? && (@op.eql? :update) + request.set_form_data(form_data) else raise ArgumentError, "unknown SPARQL protocol version: #{self.options[:protocol].inspect}" end request end + ## + # Setup url query parameter to use a specified default graph + # + # @see https://www.w3.org/TR/sparql11-protocol/#query-operation + # @see https://www.w3.org/TR/sparql11-protocol/#update-operation + def set_url_default_graph url + if @options[:graph].is_a? Array + graphs = @options[:graph].map {|graph| + CGI::escape(graph) + } + else + graphs = CGI::escape(@options[:graph]) + end + case @op + when :query + url.query_values = (url.query_values || {}) + .merge(:'default-graph-uri' => graphs) + when :update + url.query_values = (url.query_values || {}) + .merge(:'using-graph-uri' => graphs) + end + end + # A query element can be used as a component of a query. It may be initialized with a string, which is wrapped in an appropriate container depending on the type of QueryElement. Implements {#to_s} to property serialize when generating a SPARQL query. class QueryElement attr_reader :elements diff --git a/lib/sparql/client/query.rb b/lib/sparql/client/query.rb index 9a8b8fad..97610544 100644 --- a/lib/sparql/client/query.rb +++ b/lib/sparql/client/query.rb @@ -1,4 +1,4 @@ -module SPARQL; class Client +class SPARQL::Client ## # A SPARQL query builder. # @@ -366,16 +366,38 @@ def slice(start, length) end ## - # @example PREFIX dc: PREFIX foaf: SELECT * WHERE \{ ?s ?p ?o . \} - # query.select. - # prefix(dc: RDF::URI("http://purl.org/dc/elements/1.1/")). - # prefix(foaf: RDF::URI("http://xmlns.com/foaf/0.1/")). - # where([:s, :p, :o]) + # @overload prefix(prefix: uri) + # @example PREFIX dc: PREFIX foaf: SELECT * WHERE \{ ?s ?p ?o . \} + # query.select. + # prefix(dc: RDF::URI("http://purl.org/dc/elements/1.1/")). + # prefix(foaf: RDF::URI("http://xmlns.com/foaf/0.1/")). + # where([:s, :p, :o]) # - # @return [Query] + # @param [RDF::URI] uri + # @param [Symbol, String] prefix + # @return [Query] + # + # @overload prefix(string) + # @example PREFIX dc: PREFIX foaf: SELECT * WHERE \{ ?s ?p ?o . \} + # query.select. + # prefix("dc: "). + # prefix("foaf: "). + # where([:s, :p, :o]) + # + # @param [string] string + # @return [Query] # @see http://www.w3.org/TR/sparql11-query/#prefNames - def prefix(string) - (options[:prefixes] ||= []) << string + def prefix(val) + options[:prefixes] ||= [] + if val.kind_of? String + options[:prefixes] << val + elsif val.kind_of? Hash + val.each do |k, v| + options[:prefixes] << "#{k}: <#{v}>" + end + else + raise ArgumentError, "prefix must be a kind of String or a Hash" + end self end @@ -823,4 +845,4 @@ def to_s end end end -end; end +end diff --git a/lib/sparql/client/repository.rb b/lib/sparql/client/repository.rb index dad6ddac..8bd07821 100644 --- a/lib/sparql/client/repository.rb +++ b/lib/sparql/client/repository.rb @@ -1,4 +1,4 @@ -module SPARQL; class Client +class SPARQL::Client ## # A read-only repository view of a SPARQL endpoint. # @@ -345,4 +345,4 @@ def insert_statement(statement) end end -end; end +end diff --git a/lib/sparql/client/version.rb b/lib/sparql/client/version.rb index 4f877bf6..3d16256c 100644 --- a/lib/sparql/client/version.rb +++ b/lib/sparql/client/version.rb @@ -1,4 +1,4 @@ -module SPARQL; class Client +class SPARQL::Client module VERSION FILE = File.expand_path('../../../../VERSION', __FILE__) MAJOR, MINOR, TINY, EXTRA = File.read(FILE).chomp.split('.') @@ -16,4 +16,4 @@ def self.to_str() STRING end # @return [Array(Integer, Integer, Integer)] def self.to_a() [MAJOR, MINOR, TINY] end end -end; end +end diff --git a/sparql-client.gemspec b/sparql-client.gemspec index dd209dd2..5e7b657c 100755 --- a/sparql-client.gemspec +++ b/sparql-client.gemspec @@ -20,12 +20,7 @@ Gem::Specification.new do |gem| gem.platform = Gem::Platform::RUBY gem.files = %w(AUTHORS CREDITS README.md UNLICENSE VERSION) + Dir.glob('lib/**/*.rb') gem.bindir = %q(bin) - gem.executables = %w() - gem.default_executable = gem.executables.first gem.require_paths = %w(lib) - gem.extensions = %w() - gem.test_files = %w() - gem.has_rdoc = false gem.required_ruby_version = '>= 2.2.2' gem.requirements = [] diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 001846e8..90368232 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -227,6 +227,51 @@ def response(header) end end + context "with multiple Graphs" do + let(:get_graph_client){ SPARQL::Client.new('http://data.linkedmdb.org/sparql', {:method => 'get', :graph => 'http://data.linkedmdb.org/graph1'}) } + let(:post_graph_client10){ SPARQL::Client.new('http://data.linkedmdb.org/sparql', {:method => 'post', :graph => 'http://data.linkedmdb.org/graph1', :protocol => '1.0'}) } + let(:post_graph_client11){ SPARQL::Client.new('http://data.linkedmdb.org/sparql', {:method => 'post', :graph => 'http://data.linkedmdb.org/graph1', :protocol => '1.1'}) } + + it "should create 'query via GET' requests" do + WebMock.stub_request(:get, 'http://data.linkedmdb.org/sparql?default-graph-uri=http%3A%2F%2Fdata.linkedmdb.org%2Fgraph1&query=SELECT%20%3Fkb%20WHERE%20%7B%20%3Fkb%20%3Chttp%3A%2F%2Fdata.linkedmdb.org%2Fresource%2Fmovie%2Factor_name%3E%20%22Kevin%20Bacon%22%20.%20%7D'). + to_return(:body => '{}', :status => 200, :headers => { 'Content-Type' => 'application/sparql-results+json'}) + get_graph_client.query(select_query) + expect(WebMock).to have_requested(:get, "http://data.linkedmdb.org/sparql?default-graph-uri=http%3A%2F%2Fdata.linkedmdb.org%2Fgraph1&query=SELECT%20%3Fkb%20WHERE%20%7B%20%3Fkb%20%3Chttp%3A%2F%2Fdata.linkedmdb.org%2Fresource%2Fmovie%2Factor_name%3E%20%22Kevin%20Bacon%22%20.%20%7D") + end + + it "should create 'query via URL-encoded Post' requests" do + WebMock.stub_request(:post, 'http://data.linkedmdb.org/sparql?default-graph-uri=http%3A%2F%2Fdata.linkedmdb.org%2Fgraph1'). + to_return(:body => '{}', :status => 200, :headers => { 'Content-Type' => 'application/sparql-results+json'}) + post_graph_client10.query(select_query) + expect(WebMock).to have_requested(:post, "http://data.linkedmdb.org/sparql?default-graph-uri=http%3A%2F%2Fdata.linkedmdb.org%2Fgraph1"). + with(:body => "query=SELECT+%3Fkb+WHERE+%7B+%3Fkb+%3Chttp%3A%2F%2Fdata.linkedmdb.org%2Fresource%2Fmovie%2Factor_name%3E+%22Kevin+Bacon%22+.+%7D&default-graph-uri=http%3A%2F%2Fdata.linkedmdb.org%2Fgraph1") + end + + it "should create 'query via Post directly' requests" do + WebMock.stub_request(:post, 'http://data.linkedmdb.org/sparql?default-graph-uri=http%3A%2F%2Fdata.linkedmdb.org%2Fgraph1'). + to_return(:body => '{}', :status => 200, :headers => { 'Content-Type' => 'application/sparql-results+json'}) + post_graph_client11.query(select_query) + expect(WebMock).to have_requested(:post, "http://data.linkedmdb.org/sparql?default-graph-uri=http%3A%2F%2Fdata.linkedmdb.org%2Fgraph1"). + with(:body => select_query) + end + + it "should create requests for 'update via URL-encoded POST'" do + WebMock.stub_request(:post, 'http://data.linkedmdb.org/sparql?using-graph-uri=http%3A%2F%2Fdata.linkedmdb.org%2Fgraph1'). + to_return(:body => '{}', :status => 200) + post_graph_client10.update(update_query) + expect(WebMock).to have_requested(:post, "http://data.linkedmdb.org/sparql?using-graph-uri=http%3A%2F%2Fdata.linkedmdb.org%2Fgraph1"). + with(:body => "update=DELETE+%7B%3Fs+%3Fp+%3Fo%7D+WHERE+%7B%7D&using-graph-uri=http%3A%2F%2Fdata.linkedmdb.org%2Fgraph1") + end + + it "should create requests for 'update via POST directly'" do + WebMock.stub_request(:post, 'http://data.linkedmdb.org/sparql?using-graph-uri=http%3A%2F%2Fdata.linkedmdb.org%2Fgraph1'). + to_return(:body => '{}', :status => 200) + post_graph_client11.update(update_query) + expect(WebMock).to have_requested(:post, "http://data.linkedmdb.org/sparql?using-graph-uri=http%3A%2F%2Fdata.linkedmdb.org%2Fgraph1"). + with(:body => update_query) + end + end + context "Error response" do { "bad request" => {status: 400, error: SPARQL::Client::MalformedQuery }, diff --git a/spec/query_spec.rb b/spec/query_spec.rb index c2da28fc..98ad7645 100644 --- a/spec/query_spec.rb +++ b/spec/query_spec.rb @@ -182,9 +182,25 @@ expect(subject.select.where([:s, :p, :o]).slice(100, 10).to_s).to eq "SELECT * WHERE { ?s ?p ?o . } OFFSET 100 LIMIT 10" end - it "should support PREFIX" do + it "should support string PREFIX" do prefixes = ["dc: ", "foaf: "] - expect(subject.select.prefix(prefixes[0]).prefix(prefixes[1]).where([:s, :p, :o]).to_s).to eq "PREFIX #{prefixes[0]} PREFIX #{prefixes[1]} SELECT * WHERE { ?s ?p ?o . }" + expect(subject.select.prefix(prefixes[0]).prefix(prefixes[1]).where([:s, :p, :o]).to_s).to eq "PREFIX dc: PREFIX foaf: SELECT * WHERE { ?s ?p ?o . }" + end + + it "should support hash PREFIX" do + prefixes = [{dc: RDF::URI("http://purl.org/dc/elements/1.1/")}, {foaf: RDF::URI("http://xmlns.com/foaf/0.1/")}] + expect(subject.select.prefix(prefixes[0]).prefix(prefixes[1]).where([:s, :p, :o]).to_s).to eq "PREFIX dc: PREFIX foaf: SELECT * WHERE { ?s ?p ?o . }" + end + + it "should support multiple values in PREFIX hash" do + expect(subject.select.prefix(dc: RDF::URI("http://purl.org/dc/elements/1.1/"), foaf: RDF::URI("http://xmlns.com/foaf/0.1/")).where([:s, :p, :o]).to_s).to eq "PREFIX dc: PREFIX foaf: SELECT * WHERE { ?s ?p ?o . }" + end + + it "should raise an ArgumentError for invalid PREFIX type" do + inavlid_prefix_types = [RDF::URI('missing prefix hash'), 0, []] + inavlid_prefix_types.each do |invalid_arg| + expect { subject.select.prefix(invalid_arg) }.to raise_error ArgumentError, "prefix must be a kind of String or a Hash" + end end it "should support OPTIONAL" do