diff --git a/.coveralls.yml b/.coveralls.yml new file mode 100644 index 00000000..d45050e6 --- /dev/null +++ b/.coveralls.yml @@ -0,0 +1 @@ +repo_token: Jrom81OgSyBklzReIFjUpSMdZbsoPwaFU diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b0496cf..0e1a0596 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,8 @@ jobs: - 2.5 - 2.6 - 2.7 - #- ruby-head # net-http-persistent + - 3.0 + - ruby-head - jruby steps: - name: Clone repository diff --git a/Gemfile b/Gemfile index 288cc27b..f6fd52d4 100644 --- a/Gemfile +++ b/Gemfile @@ -41,7 +41,6 @@ group :debug do gem 'pry-byebug', platforms: :mri gem 'redcarpet', platforms: :ruby gem 'ruby-prof', platforms: :mri - gem 'awesome_print', github: 'akshaymohite/awesome_print' end group :test do diff --git a/README.md b/README.md index eea71507..3a573da9 100755 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This is a [Ruby][] implementation of [SPARQL][] for [RDF.rb][]. [![Gem Version](https://badge.fury.io/rb/sparql.png)](https://badge.fury.io/rb/sparql) [![Build Status](https://github.com/ruby-rdf/sparql/workflows/CI/badge.svg?branch=develop)](https://github.com/ruby-rdf/sparql/actions?query=workflow%3ACI) -[![Coverage Status](https://coveralls.io/repos/ruby-rdf/sparql/badge.svg)](https://coveralls.io/r/ruby-rdf/sparql) +[![Coverage Status](https://coveralls.io/repos/ruby-rdf/sparql/badge.svg?branch=develop)](https://coveralls.io/r/ruby-rdf/sparql?branch=develop) [![Gitter chat](https://badges.gitter.im/ruby-rdf/rdf.png)](https://gitter.im/ruby-rdf/rdf) ## Features diff --git a/VERSION b/VERSION index 0aec50e6..3ad0595a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -3.1.4 +3.1.5 diff --git a/etc/doap.ttl b/etc/doap.ttl index 7b28712b..c95fd485 100644 --- a/etc/doap.ttl +++ b/etc/doap.ttl @@ -9,9 +9,7 @@ a doap:Project; doap:name "SPARQL"; doap:shortdesc "SPARQL library for Ruby."@en; - doap:description """ - Implements SPARQL grammar parsing to SPARQL Algebra, SPARQL Algebra processing - and includes SPARQL Client for accessing remote repositories."""@en; + doap:description "SPARQL Implements SPARQL 1.1 Query, Update and result formats for the Ruby RDF.rb library suite."@en; doap:implements , , , diff --git a/examples/rdfstar-issue76.rq b/examples/rdfstar-issue76.rq new file mode 100644 index 00000000..fb7eeba6 --- /dev/null +++ b/examples/rdfstar-issue76.rq @@ -0,0 +1,6 @@ +SELECT * +WHERE { + << ?s:marriedTo :Beth >> :at :Kelby . + FILTER (?s = :Peter) . + BIND (<< ?s:marriedTo :Beth >> as ?r) . +} diff --git a/lib/sparql/algebra/expression.rb b/lib/sparql/algebra/expression.rb index 0b86c890..a49b7e20 100644 --- a/lib/sparql/algebra/expression.rb +++ b/lib/sparql/algebra/expression.rb @@ -53,7 +53,7 @@ def self.parse(sse, **options, &block) def self.open(filename, **options, &block) RDF::Util::File.open_file(filename, **options) do |file| options[:base_uri] ||= filename - Expression.parse(file, options, &block) + Expression.parse(file, **options, &block) end end @@ -114,8 +114,11 @@ def self.new(sse, **options) debug(options) {"#{operator.inspect}(#{operands.map(&:inspect).join(',')})"} options.delete_if {|k, v| [:debug, :logger, :depth, :prefixes, :base_uri, :update, :validate].include?(k) } - operands << options unless options.empty? - operator.new(*operands) + begin + operator.new(*operands, **options) + rescue ArgumentError => e + error(options) {"Operator=#{operator.inspect}: #{e}"} + end end ## @@ -332,7 +335,7 @@ def optimize!(**options) # @param [Hash{Symbol => Object}] options ({}) # options passed from query # @return [Expression] `self` - def evaluate(bindings, options = {}) + def evaluate(bindings, **options) self end diff --git a/lib/sparql/algebra/extensions.rb b/lib/sparql/algebra/extensions.rb index 6648630f..060e55cc 100644 --- a/lib/sparql/algebra/extensions.rb +++ b/lib/sparql/algebra/extensions.rb @@ -320,7 +320,7 @@ module RDF::Queryable # @yieldreturn [void] ignored # @return [Enumerator] # @see RDF::Queryable#query_pattern - def query(pattern, options = {}, &block) + def query(pattern, **options, &block) raise TypeError, "#{self} is not queryable" if respond_to?(:queryable?) && !queryable? if pattern.is_a?(SPARQL::Algebra::Operator) && pattern.respond_to?(:execute) diff --git a/lib/sparql/algebra/operator.rb b/lib/sparql/algebra/operator.rb index a7347737..6f66fbdd 100644 --- a/lib/sparql/algebra/operator.rb +++ b/lib/sparql/algebra/operator.rb @@ -336,8 +336,8 @@ def self.arity # @option options [Boolean] :memoize (false) # whether to memoize results for particular operands # @raise [TypeError] if any operand is invalid - def initialize(*operands) - @options = operands.last.is_a?(Hash) ? operands.pop.dup : {} + def initialize(*operands, **options) + @options = options.dup @operands = operands.map! do |operand| case operand when Array @@ -358,7 +358,7 @@ def initialize(*operands) ## # Deep duplicate operands def deep_dup - self.class.new(*operands.map(&:deep_dup), @options) + self.class.new(*operands.map(&:deep_dup), **@options) end ## @@ -745,7 +745,7 @@ class Nullary < Operator ## # @param [Hash{Symbol => Object}] options # any additional options (see {Operator#initialize}) - def initialize(options = {}) + def initialize(**options) super end end # Nullary @@ -764,7 +764,7 @@ class Unary < Operator # the operand # @param [Hash{Symbol => Object}] options # any additional options (see {Operator#initialize}) - def initialize(arg, options = {}) + def initialize(arg, **options) super end end # Unary @@ -785,7 +785,7 @@ class Binary < Operator # the second operand # @param [Hash{Symbol => Object}] options # any additional options (see {Operator#initialize}) - def initialize(arg1, arg2, options = {}) + def initialize(arg1, arg2, **options) super end end # Binary @@ -808,7 +808,7 @@ class Ternary < Operator # the third operand # @param [Hash{Symbol => Object}] options # any additional options (see {Operator#initialize}) - def initialize(arg1, arg2, arg3, options = {}) + def initialize(arg1, arg2, arg3, **options) super end end # Ternary @@ -833,7 +833,7 @@ class Quaternary < Operator # the forth operand # @param [Hash{Symbol => Object}] options # any additional options (see {Operator#initialize}) - def initialize(arg1, arg2, arg3, arg4, options = {}) + def initialize(arg1, arg2, arg3, arg4, **options) super end end # Ternary diff --git a/lib/sparql/algebra/operator/avg.rb b/lib/sparql/algebra/operator/avg.rb index c880ec12..947ed7a3 100644 --- a/lib/sparql/algebra/operator/avg.rb +++ b/lib/sparql/algebra/operator/avg.rb @@ -16,6 +16,13 @@ class Avg < Operator NAME = :avg + def initialize(*operands, **options) + raise ArgumentError, + "avg operator accepts at most one argument with an optional :distinct" if + (operands - %i{distinct}).length != 1 + super + end + ## # The Avg set function calculates the average value for an expression over a group. It is defined in terms of Sum and Count. # diff --git a/lib/sparql/algebra/operator/bgp.rb b/lib/sparql/algebra/operator/bgp.rb index 5993c406..e82c92df 100644 --- a/lib/sparql/algebra/operator/bgp.rb +++ b/lib/sparql/algebra/operator/bgp.rb @@ -22,7 +22,7 @@ class BGP < Operator # @yieldparam [RDF::Query::Solution] solution # @yieldreturn [void] ignored # @return [RDF::Query] - def self.new(*patterns, &block) + def self.new(*patterns, **options, &block) RDF::Query.new(*patterns, graph_name: false, &block) end end # BGP diff --git a/lib/sparql/algebra/operator/graph.rb b/lib/sparql/algebra/operator/graph.rb index 84d528d1..f0cc6625 100644 --- a/lib/sparql/algebra/operator/graph.rb +++ b/lib/sparql/algebra/operator/graph.rb @@ -36,7 +36,7 @@ class Graph < Operator::Binary # @param [Array] patterns # Quads # @return [RDF::Query] - def self.new(name, patterns, options = {}, &block) + def self.new(name, patterns, **options, &block) case patterns when RDF::Query # Record that the argument as a (bgp) for re-serialization back to SSE diff --git a/lib/sparql/algebra/operator/left_join.rb b/lib/sparql/algebra/operator/left_join.rb index 2aafdcb2..98e615b7 100644 --- a/lib/sparql/algebra/operator/left_join.rb +++ b/lib/sparql/algebra/operator/left_join.rb @@ -37,6 +37,10 @@ class LeftJoin < Operator def execute(queryable, **options, &block) filter = operand(2) + raise ArgumentError, + "leftjoin operator accepts at most two arguments with an optional filter" if + operands.length < 2 || operands.length > 3 + debug(options) {"LeftJoin"} left = queryable.query(operand(0), depth: options[:depth].to_i + 1, **options) debug(options) {"=>(leftjoin left) #{left.inspect}"} diff --git a/lib/sparql/algebra/operator/max.rb b/lib/sparql/algebra/operator/max.rb index ce665a3f..64b3969e 100644 --- a/lib/sparql/algebra/operator/max.rb +++ b/lib/sparql/algebra/operator/max.rb @@ -16,6 +16,13 @@ class Max < Operator NAME = :max + def initialize(*operands, **options) + raise ArgumentError, + "max operator accepts at most one argument with an optional :distinct" if + (operands - %i{distinct}).length != 1 + super + end + ## # Max is a SPARQL set function that return the maximum value from a group respectively. # diff --git a/lib/sparql/algebra/operator/min.rb b/lib/sparql/algebra/operator/min.rb index bfe28926..84aef96c 100644 --- a/lib/sparql/algebra/operator/min.rb +++ b/lib/sparql/algebra/operator/min.rb @@ -16,6 +16,13 @@ class Min < Operator NAME = :min + def initialize(*operands, **options) + raise ArgumentError, + "min operator accepts at most one argument with an optional :distinct" if + (operands - %i{distinct}).length != 1 + super + end + ## # Min is a SPARQL set function that return the minimum value from a group respectively. # diff --git a/lib/sparql/algebra/operator/sample.rb b/lib/sparql/algebra/operator/sample.rb index 8d51a2a8..b87046dd 100644 --- a/lib/sparql/algebra/operator/sample.rb +++ b/lib/sparql/algebra/operator/sample.rb @@ -12,11 +12,18 @@ class Operator # (bgp (triple ?s :dec ?o))))))) # # @see https://www.w3.org/TR/sparql11-query/#defn_aggSample - class Sample < Operator::Unary + class Sample < Operator include Aggregate NAME = :sample + def initialize(*operands, **options) + raise ArgumentError, + "sample operator accepts at most one argument with an optional :distinct" if + (operands - %i{distinct}).length != 1 + super + end + ## # Sample is a set function which returns an arbitrary value from the multiset passed to it. # diff --git a/lib/sparql/extensions.rb b/lib/sparql/extensions.rb index 64084bf6..e0bb81c5 100644 --- a/lib/sparql/extensions.rb +++ b/lib/sparql/extensions.rb @@ -25,11 +25,7 @@ module RDF::Queryable # # Used to implement the SPARQL `DESCRIBE` operator. # - # @overload concise_bounded_description(*terms, &block) - # @param [Array] terms - # List of terms to include in the results. - # - # @overload concise_bounded_description(*terms, options, &block) + # @overload concise_bounded_description(*terms, **options, &block) # @param [Array] terms # List of terms to include in the results. # @param [Hash{Symbol => Object}] options @@ -44,16 +40,14 @@ module RDF::Queryable # @return [RDF::Graph] # # @see https://www.w3.org/Submission/CBD/ - def concise_bounded_description(*terms, &block) - options = terms.last.is_a?(Hash) ? terms.pop.dup : {} - + def concise_bounded_description(*terms, **options, &block) graph = options[:graph] || RDF::Graph.new if options[:non_subjects] query_terms = terms.dup # Find terms not in self as a subject and recurse with their subjects - terms.reject {|term| self.first(subject: term)}.each do |term| + terms.reject {|term| self.first({subject: term})}.each do |term| self.query({predicate: term}) do |statement| query_terms << statement.subject end @@ -67,7 +61,7 @@ def concise_bounded_description(*terms, &block) end # Don't consider term if already in graph - terms.reject {|term| graph.first(subject: term)}.each do |term| + terms.reject {|term| graph.first({subject: term})}.each do |term| # Find statements from queryiable with term as a subject self.query({subject: term}) do |statement| yield(statement) if block_given? @@ -84,13 +78,13 @@ def concise_bounded_description(*terms, &block) }, **{}).execute(self).each do |solution| # Recurse to include this subject recurse_opts = options.merge(non_subjects: false, graph: graph) - self.concise_bounded_description(solution[:s], recurse_opts, &block) + self.concise_bounded_description(solution[:s], **recurse_opts, &block) end # Recurse if object is a BNode and it is not already in subjects if statement.object.node? recurse_opts = options.merge(non_subjects: false, graph: graph) - self.concise_bounded_description(statement.object, recurse_opts, &block) + self.concise_bounded_description(statement.object, **recurse_opts, &block) end end end diff --git a/lib/sparql/grammar.rb b/lib/sparql/grammar.rb index aed6e249..340b4e19 100644 --- a/lib/sparql/grammar.rb +++ b/lib/sparql/grammar.rb @@ -292,7 +292,7 @@ def self.parse(query, **options, &block) # @raise [RDF::FormatError] if no reader found for the specified format def self.open(filename, **options, &block) RDF::Util::File.open_file(filename, **options) do |file| - self.parse(file, options, &block) + self.parse(file, **options, &block) end end @@ -326,7 +326,7 @@ def self.valid?(query, **options) # @return [Lexer] # @raise [Lexer::Error] on invalid input def self.tokenize(query, **options, &block) - Lexer.tokenize(query, options, &block) + Lexer.tokenize(query, **options, &block) end end # Grammar end # SPARQL diff --git a/lib/sparql/grammar/parser11.rb b/lib/sparql/grammar/parser11.rb index 4cf4b215..42ca7d51 100644 --- a/lib/sparql/grammar/parser11.rb +++ b/lib/sparql/grammar/parser11.rb @@ -878,10 +878,10 @@ class Parser # [80] Object ::= GraphNode | EmbTP production(:Object) do |input, data, callback| object = data[:GraphNode] || data[:EmbTP] + add_prod_datum(:pattern, data[:pattern]) if object if prod_data[:Verb] add_pattern(:Object, subject: prod_data[:Subject], predicate: prod_data[:Verb], object: object) - add_prod_datum(:pattern, data[:pattern]) elsif prod_data[:VerbPath] add_prod_datum(:path, SPARQL::Algebra::Expression(:path, diff --git a/sparql.gemspec b/sparql.gemspec index 981afc55..fe2fd612 100755 --- a/sparql.gemspec +++ b/sparql.gemspec @@ -18,9 +18,7 @@ Gem::Specification.new do |gem| gem.bindir = %q(bin) gem.executables = %w(sparql) gem.require_paths = %w(lib) - gem.description = %( - Implements SPARQL grammar parsing to SPARQL Algebra, SPARQL Algebra processing - and includes SPARQL Client for accessing remote repositories.) + gem.description = %(SPARQL Implements SPARQL 1.1 Query, Update and result formats for the Ruby RDF.rb library suite.) gem.required_ruby_version = '>= 2.4' gem.requirements = [] @@ -29,7 +27,7 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency 'ebnf', '~> 2.1' gem.add_runtime_dependency 'builder', '~> 3.2' gem.add_runtime_dependency 'sxp', '~> 1.1' - gem.add_runtime_dependency 'sparql-client', '~> 3.1' + gem.add_runtime_dependency 'sparql-client', '~> 3.1', '>= 3.1.2' gem.add_runtime_dependency 'rdf-xsd', '~> 3.1' gem.add_development_dependency 'sinatra', '~> 2.0' diff --git a/spec/grammar/parser_spec.rb b/spec/grammar/parser_spec.rb index c523172f..e4537fb7 100644 --- a/spec/grammar/parser_spec.rb +++ b/spec/grammar/parser_spec.rb @@ -2184,7 +2184,7 @@ def self.variable(id, distinguished = true) def parser(production = nil, **options) @logger = options.fetch(:logger, false) Proc.new do |query| - parser = described_class.new(query, {logger: @logger, resolve_iris: true}.merge(options)) + parser = described_class.new(query, logger: @logger, resolve_iris: true, **options) production ? parser.parse(production) : parser end end diff --git a/spec/suite_helper.rb b/spec/suite_helper.rb index 31bc616b..0c202cef 100644 --- a/spec/suite_helper.rb +++ b/spec/suite_helper.rb @@ -191,8 +191,8 @@ def self.sparql1_1_tests end def self.sparql_star_tests - %w(syntax query update).map do |partial| - "https://w3c.github.io/rdf-star/tests/sparql/manifest-#{partial}.jsonld" + ["syntax/manifest", "manifest-query", "manifest-update"].map do |man| + "https://w3c.github.io/rdf-star/tests/sparql/#{man}.jsonld" end end end \ No newline at end of file diff --git a/spec/support/matchers/generate.rb b/spec/support/matchers/generate.rb index 4346f87c..63f85600 100644 --- a/spec/support/matchers/generate.rb +++ b/spec/support/matchers/generate.rb @@ -1,5 +1,5 @@ require 'rspec/matchers' -require 'awesome_print' +require 'amazing_print' RSpec::Matchers.define :generate do |expected, options| def parser(**options) diff --git a/spec/support/matchers/have_result_set.rb b/spec/support/matchers/have_result_set.rb index be283864..66b5f805 100644 --- a/spec/support/matchers/have_result_set.rb +++ b/spec/support/matchers/have_result_set.rb @@ -1,5 +1,5 @@ require 'rspec/matchers' -require 'awesome_print' +require 'amazing_print' ::RSpec::Matchers.define :have_result_set do |expected| def normalize(soln) diff --git a/spec/support/matchers/solutions.rb b/spec/support/matchers/solutions.rb index de3c2584..449e454c 100644 --- a/spec/support/matchers/solutions.rb +++ b/spec/support/matchers/solutions.rb @@ -1,7 +1,7 @@ require 'rspec/matchers' require 'rdf/isomorphic' require 'rdf/trig' -require 'awesome_print' +require 'amazing_print' # For examining unordered solution sets RSpec::Matchers.define :describe_solutions do |expected_solutions, info| diff --git a/spec/support/matchers/xpath_matcher.rb b/spec/support/matchers/xpath_matcher.rb index 7e6a282f..6e5897de 100644 --- a/spec/support/matchers/xpath_matcher.rb +++ b/spec/support/matchers/xpath_matcher.rb @@ -1,6 +1,6 @@ require 'rspec/matchers' require 'nokogiri' -require 'awesome_print' +require 'amazing_print' RSpec::Matchers.define :have_xpath do |xpath, value| match do |actual| diff --git a/spec/support/models.rb b/spec/support/models.rb index 5f5aab53..0ac165f9 100644 --- a/spec/support/models.rb +++ b/spec/support/models.rb @@ -142,7 +142,7 @@ def expected result.graphs.each do |info| data, format = info[:data], info[:format] if data - RDF::Reader.for(format).new(data, info).each_statement do |st| + RDF::Reader.for(format).new(data, **info).each_statement do |st| st.graph_name = RDF::URI(info[:base_uri]) if info[:base_uri] r << st end