From 196b73b4a78ab5b44166de3048d8a58b63067602 Mon Sep 17 00:00:00 2001 From: Gregg Kellogg Date: Mon, 19 Feb 2024 12:22:21 -0800 Subject: [PATCH] Add support for RDF 1.2 Triple Terms and deprecation notices for RDF-star quoted triples. Note all RDF 1.2 and RDF-star support remains experimental and may be changed or removed in a future minor or patch version. --- README.md | 9 +++- etc/n-triples.ebnf | 3 +- lib/rdf/mixin/enumerable.rb | 3 +- lib/rdf/mixin/writable.rb | 3 +- lib/rdf/model/dataset.rb | 3 +- lib/rdf/model/graph.rb | 5 ++- lib/rdf/model/statement.rb | 13 +++++- lib/rdf/nquads.rb | 3 +- lib/rdf/ntriples.rb | 6 ++- lib/rdf/ntriples/reader.rb | 32 ++++++++++--- lib/rdf/ntriples/writer.rb | 13 ++++++ lib/rdf/repository.rb | 3 ++ lib/rdf/writer.rb | 23 ++++++++-- spec/model_statement_spec.rb | 9 +++- spec/nquads_spec.rb | 11 ++++- spec/ntriples_spec.rb | 87 ++++++++++++++++++++++++++++++++++-- spec/query_pattern_spec.rb | 2 +- 17 files changed, 201 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index d48225bf..ea24b6d2 100644 --- a/README.md +++ b/README.md @@ -265,9 +265,15 @@ A separate [SPARQL][SPARQL doc] gem builds on basic BGP support to provide full foaf[:name] #=> RDF::URI("http://xmlns.com/foaf/0.1/name") foaf['mbox'] #=> RDF::URI("http://xmlns.com/foaf/0.1/mbox") +## RDF-star CG + +[RDF.rb][] includes provisional support for [RDF-star][] with an N-Triples/N-Quads syntax for quoted triples in the _subject_ or _object_ position. + +Support for RDF-star quoted triples is now deprecated, use RDF 1.2 triple terms instead. + ## RDF 1.2 -[RDF.rb][] includes provisional support for [RDF 1.2][] with an N-Triples/N-Quads syntax for quoted triples in the _subject_ or _object_ position. +[RDF.rb][] includes provisional support for [RDF 1.2][] with an N-Triples/N-Quads syntax for triple terms in the _object_ position. [RDF.rb][] includes provisional support for [RDF 1.2][] directional language-tagged strings, which are literals of type `rdf:dirLangString` having both a `language` and `direction`. Internally, an `RDF::Statement` is treated as another resource, along with `RDF::URI` and `RDF::Node`, which allows an `RDF::Statement` to have a `#subject` or `#object` which is also an `RDF::Statement`. @@ -501,6 +507,7 @@ see or the accompanying {file:UNLICENSE} file. [SPARQL doc]: https://ruby-rdf.github.io/sparql [RDF 1.0]: https://www.w3.org/TR/2004/REC-rdf-concepts-20040210/ [RDF 1.1]: https://www.w3.org/TR/rdf11-concepts/ +[RDF-star]: https://www.w3.org/2021/12/rdf-star.html [RDF 1.2]: https://www.w3.org/TR/rdf12-concepts/ [SPARQL 1.1]: https://www.w3.org/TR/sparql11-query/ [RDF.rb]: https://ruby-rdf.github.io/ diff --git a/etc/n-triples.ebnf b/etc/n-triples.ebnf index 4471ec11..fae8f48d 100644 --- a/etc/n-triples.ebnf +++ b/etc/n-triples.ebnf @@ -2,8 +2,9 @@ ntriplesDoc ::= triple? (EOL triple)* EOL? triple ::= subject predicate object '.' subject ::= IRIREF | BLANK_NODE_LABEL | quotedTriple predicate ::= IRIREF -object ::= IRIREF | BLANK_NODE_LABEL | literal | quotedTriple +object ::= IRIREF | BLANK_NODE_LABEL | literal | tripleTerm | quotedTriple literal ::= STRING_LITERAL_QUOTE ('^^' IRIREF | LANG_DIR )? +tripleTerm ::= '<<' subject predicate object '>>' quotedTriple ::= '<<' subject predicate object '>>' @terminals diff --git a/lib/rdf/mixin/enumerable.rb b/lib/rdf/mixin/enumerable.rb index 7400ffea..03a6fc11 100644 --- a/lib/rdf/mixin/enumerable.rb +++ b/lib/rdf/mixin/enumerable.rb @@ -83,7 +83,8 @@ def to_a # * `:literal_equality' preserves [term-equality](https://www.w3.org/TR/rdf11-concepts/#dfn-literal-term-equality) for literals. Literals are equal only if their lexical values and datatypes are equal, character by character. Literals may be "inlined" to value-space for efficiency only if `:literal_equality` is `false`. # * `:validity` allows a concrete Enumerable implementation to indicate that it does or does not support valididty checking. By default implementations are assumed to support validity checking. # * `:skolemize` supports [Skolemization](https://www.w3.org/wiki/BnodeSkolemization) of an `Enumerable`. Implementations supporting this feature must implement a `#skolemize` method, taking a base URI used for minting URIs for BNodes as stable identifiers and a `#deskolemize` method, also taking a base URI used for turning URIs having that prefix back into the same BNodes which were originally skolemized. - # * `:quoted_triples` supports RDF 1.2 quoted triples. + # * `:rdf_full` supports RDF 1.2 Full profile, including support for embedded Triple Terms. + # * `:quoted_triples` supports RDF-star quoted triples. # * `:base_direction` supports RDF 1.2 directional language-tagged strings. # # @param [Symbol, #to_sym] feature diff --git a/lib/rdf/mixin/writable.rb b/lib/rdf/mixin/writable.rb index 5fe07b86..52331daa 100644 --- a/lib/rdf/mixin/writable.rb +++ b/lib/rdf/mixin/writable.rb @@ -127,7 +127,8 @@ def insert_graph(graph) def insert_statements(statements) each = statements.respond_to?(:each_statement) ? :each_statement : :each statements.__send__(each) do |statement| - if statement.embedded? && respond_to?(:supports?) && !supports?(:quoted_triples) + # FIXME: quoted triples are now deprecated + if statement.embedded? && respond_to?(:supports?) && !(supports?(:quoted_triples) || supports?(:rdf_full)) raise ArgumentError, "Writable does not support quoted triples" end if statement.object && statement.object.literal? && statement.object.direction? && respond_to?(:supports?) && !supports?(:base_direction) diff --git a/lib/rdf/model/dataset.rb b/lib/rdf/model/dataset.rb index f40f3c36..e4045c03 100644 --- a/lib/rdf/model/dataset.rb +++ b/lib/rdf/model/dataset.rb @@ -104,7 +104,8 @@ def isolation_level # @private # @see RDF::Enumerable#supports? def supports?(feature) - return true if %i(graph_name quoted_triples).include?(feature) + # FIXME: quoted triples are now deprecated + return true if %i(graph_name quoted_triples rdf_full).include?(feature) super end diff --git a/lib/rdf/model/graph.rb b/lib/rdf/model/graph.rb index e45f24ad..c049adf4 100644 --- a/lib/rdf/model/graph.rb +++ b/lib/rdf/model/graph.rb @@ -305,8 +305,9 @@ def query_pattern(pattern, **options, &block) # @private # @see RDF::Mutable#insert def insert_statement(statement) - if statement.embedded? && !@data.supports?(:quoted_triples) - raise ArgumentError, "Graph does not support quoted triples" + # FIXME: quoted triples are now deprecated + if statement.embedded? && !(@data.supports?(:quoted_triples) || @data.supports?(:rdf_full)) + raise ArgumentError, "Graph does not support the RDF Full profile" end if statement.object && statement.object.literal? && statement.object.direction? && !@data.supports?(:base_direction) raise ArgumentError, "Graph does not support directional languaged-tagged strings" diff --git a/lib/rdf/model/statement.rb b/lib/rdf/model/statement.rb index 91a0bbd6..dffdd3a4 100644 --- a/lib/rdf/model/statement.rb +++ b/lib/rdf/model/statement.rb @@ -71,7 +71,8 @@ def self.from(statement, graph_name: nil, **options) # @option options [RDF::Term] :graph_name (nil) # Note, in RDF 1.1, a graph name MUST be an {Resource}. # @option options [Boolean] :inferred used as a marker to record that this statement was inferred based on semantic relationships (T-Box). - # @option options [Boolean] :quoted used as a marker to record that this statement quoted and appears as the subject or object of another RDF::Statement. + # @option options [Boolean] :tripleTerm used as a marker to record that this statement appears as the object of another RDF::Statement. + # @option options [Boolean] :quoted used as a marker to record that this statement quoted and appears as the subject or object of another RDF::Statement (deprecated). # @return [RDF::Statement] # # @overload initialize(subject, predicate, object, **options) @@ -84,7 +85,8 @@ def self.from(statement, graph_name: nil, **options) # @option options [RDF::Term] :graph_name (nil) # Note, in RDF 1.1, a graph name MUST be an {Resource}. # @option options [Boolean] :inferred used as a marker to record that this statement was inferred based on semantic relationships (T-Box). - # @option options [Boolean] :quoted used as a marker to record that this statement quoted and appears as the subject or object of another RDF::Statement. + # @option options [Boolean] :tripleTerm used as a marker to record that this statement appears as the object of another RDF::Statement. + # @option options [Boolean] :quoted used as a marker to record that this statement quoted and appears as the subject or object of another RDF::Statement (deprecated). # @return [RDF::Statement] def initialize(subject = nil, predicate = nil, object = nil, options = {}) if subject.is_a?(Hash) @@ -211,6 +213,13 @@ def asserted? ## # @return [Boolean] + def tripleTerm? + !!@options[:tripleTerm] + end + + ## + # @return [Boolean] + # @deprecated Quoted triples are now deprecated def quoted? !!@options[:quoted] end diff --git a/lib/rdf/nquads.rb b/lib/rdf/nquads.rb index 272b26c0..ef4b4aad 100644 --- a/lib/rdf/nquads.rb +++ b/lib/rdf/nquads.rb @@ -71,9 +71,10 @@ def read_triple begin unless blank? || read_comment + # FIXME: quoted triples are now deprecated subject = read_uriref || read_node || read_quotedTriple || fail_subject predicate = read_uriref(intern: true) || fail_predicate - object = read_uriref || read_node || read_literal || read_quotedTriple || fail_object + object = read_uriref || read_node || read_literal || read_tripleTerm || read_quotedTriple || fail_object graph_name = read_uriref || read_node if validate? && !read_eos log_error("Expected end of statement (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError) diff --git a/lib/rdf/ntriples.rb b/lib/rdf/ntriples.rb index f5e526ae..b8247f67 100644 --- a/lib/rdf/ntriples.rb +++ b/lib/rdf/ntriples.rb @@ -15,7 +15,11 @@ module RDF # # "rdf" . # - # ## Quoted Triples + # ## Triple terms + # + # Supports statements as resources using `<<(s p o)>>`. + + # ## Quoted Triples (Deprecated) # # Supports statements as resources using `<>`. # diff --git a/lib/rdf/ntriples/reader.rb b/lib/rdf/ntriples/reader.rb index 4fbb21a1..93fe6d7a 100644 --- a/lib/rdf/ntriples/reader.rb +++ b/lib/rdf/ntriples/reader.rb @@ -70,8 +70,11 @@ class Reader < RDF::Reader LANG_DIR = /@([a-zA-Z]+(?:-[a-zA-Z0-9]+)*(?:--[a-zA-Z]+)?)/.freeze STRING_LITERAL_QUOTE = /"((?:[^\"\\\n\r]|#{ECHAR}|#{UCHAR})*)"/.freeze - ST_START = /^<>/.freeze + TT_START = /^<<\(/.freeze + TT_END = /^\s*\)>>/.freeze + + QT_START = /^<>/.freeze # @see http://www.w3.org/TR/rdf-testcases/#ntrip_grammar COMMENT = /^#\s*(.*)$/.freeze @@ -208,7 +211,7 @@ def read_value begin read_statement rescue RDF::ReaderError - value = read_uriref || read_node || read_literal || read_quotedTriple + value = read_uriref || read_node || read_literal || read_tripleTerm || read_quotedTriple log_recover value end @@ -226,7 +229,7 @@ def read_triple unless blank? || read_comment subject = read_uriref || read_node || read_quotedTriple || fail_subject predicate = read_uriref(intern: true) || fail_predicate - object = read_uriref || read_node || read_literal || read_quotedTriple || fail_object + object = read_uriref || read_node || read_literal || read_tripleTerm || read_quotedTriple || fail_object if validate? && !read_eos log_error("Expected end of statement (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError) @@ -242,12 +245,29 @@ def read_triple ## # @return [RDF::Statement] + def read_tripleTerm + if @options[:rdfstar] && match(TT_START) + subject = read_uriref || read_node || fail_subject + predicate = read_uriref(intern: true) || fail_predicate + object = read_uriref || read_node || read_literal || read_tripleTerm || fail_object + if !match(TT_END) + log_error("Expected end of statement (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError) + end + RDF::Statement.new(subject, predicate, object, tripleTerm: true) + end + end + + ## + # @return [RDF::Statement] + # @deprecated Quoted triples are now deprecated def read_quotedTriple - if @options[:rdfstar] && match(ST_START) + if @options[:rdfstar] && match(QT_START) + warn "[DEPRECATION] RDF-star quoted triples are deprecated and will be removed in a future version.\n" + + "Called from #{Gem.location_of_caller.join(':')}" subject = read_uriref || read_node || read_quotedTriple || fail_subject predicate = read_uriref(intern: true) || fail_predicate object = read_uriref || read_node || read_literal || read_quotedTriple || fail_object - if !match(ST_END) + if !match(QT_END) log_error("Expected end of statement (found: #{current_line.inspect})", lineno: lineno, exception: RDF::ReaderError) end RDF::Statement.new(subject, predicate, object, quoted: true) diff --git a/lib/rdf/ntriples/writer.rb b/lib/rdf/ntriples/writer.rb index 3a1513a7..a69fc94a 100644 --- a/lib/rdf/ntriples/writer.rb +++ b/lib/rdf/ntriples/writer.rb @@ -223,15 +223,28 @@ def format_statement(statement, **options) format_triple(*statement.to_triple, **options) end + ## + # Returns the N-Triples representation of an RDF 1.2 triple term. + # + # @param [RDF::Statement] statement + # @param [Hash{Symbol => Object}] options ({}) + # @return [String] + def format_tripleTerm(statement, **options) + "<<(%s %s %s)>>" % statement.to_a.map { |value| format_term(value, **options) } + end + ## # Returns the N-Triples representation of an RDF-star quoted triple. # # @param [RDF::Statement] statement # @param [Hash{Symbol => Object}] options ({}) # @return [String] + # @deprecated Quoted triples are now deprecated def format_quotedTriple(statement, **options) + # FIXME: quoted triples are now deprecated "<<%s %s %s>>" % statement.to_a.map { |value| format_term(value, **options) } end + ## # Returns the N-Triples representation of a triple. # diff --git a/lib/rdf/repository.rb b/lib/rdf/repository.rb index 69f053bd..c7cfb0ca 100644 --- a/lib/rdf/repository.rb +++ b/lib/rdf/repository.rb @@ -182,6 +182,8 @@ def supports?(feature) when :validity then @options.fetch(:with_validity, true) when :literal_equality then true when :atomic_write then false + when :rdf_full then false + # FIXME: quoted triples are now deprecated when :quoted_triples then false when :base_direction then false when :snapshots then false @@ -270,6 +272,7 @@ def supports?(feature) when :validity then @options.fetch(:with_validity, true) when :literal_equality then true when :atomic_write then true + when :rdf_full then true when :quoted_triples then true when :base_direction then true when :snapshots then true diff --git a/lib/rdf/writer.rb b/lib/rdf/writer.rb index c8d1ee46..a9afbbdc 100644 --- a/lib/rdf/writer.rb +++ b/lib/rdf/writer.rb @@ -518,7 +518,8 @@ def format_term(term, **options) when RDF::Literal then format_literal(term, **options) when RDF::URI then format_uri(term, **options) when RDF::Node then format_node(term, **options) - when RDF::Statement then format_quotedTriple(term, **options) + # FIXME: quoted triples are now deprecated + when RDF::Statement then term.tripleTerm? ? format_tripleTerm(term, **options) : format_quotedTriple(term, **options) else nil end end @@ -566,7 +567,7 @@ def format_list(value, **options) end ## - # Formats a referenced triple. + # Formats a referenced triple term. # # @example # <<

>>

. @@ -576,8 +577,24 @@ def format_list(value, **options) # @return [String] # @raise [NotImplementedError] unless implemented in subclass # @abstract + def format_tripleTerm(value, **options) + raise NotImplementedError.new("#{self.class}#format_tripleTerm") # override in subclasses + end + + ## + # Formats a referenced quoted triple. + # + # @example + # <<

>>

. + # + # @param [RDF::Statement] value + # @param [Hash{Symbol => Object}] options = ({}) + # @return [String] + # @raise [NotImplementedError] unless implemented in subclass + # @abstract + # @deprecated Quoted Triples are now deprecated in favor of Triple Terms def format_quotedTriple(value, **options) - raise NotImplementedError.new("#{self.class}#format_statement") # override in subclasses + raise NotImplementedError.new("#{self.class}#format_quotedTriple") # override in subclasses end protected diff --git a/spec/model_statement_spec.rb b/spec/model_statement_spec.rb index a159edb8..46558700 100644 --- a/spec/model_statement_spec.rb +++ b/spec/model_statement_spec.rb @@ -75,7 +75,8 @@ it {is_expected.to have_object} its(:object) {is_expected.not_to be_nil} it {is_expected.to be_asserted} - it {is_expected.not_to be_quoted} + it {is_expected.not_to be_tripleTerm} + it {is_expected.not_to be_quoted} # FIXME: quoted triples are deprecated it {is_expected.to be_statement} it {is_expected.not_to be_inferred} its(:terms) {is_expected.to include(s, p, o)} @@ -210,6 +211,12 @@ it {is_expected.to be_inferred} end + context "when marked as tripleTerm" do + subject {RDF::Statement.new(RDF::Node.new, p, o, tripleTerm: true)} + it {is_expected.to be_tripleTerm} + end + + # FIXME: quoted triples are deprecated context "when marked as quoted" do subject {RDF::Statement.new(RDF::Node.new, p, o, quoted: true)} it {is_expected.to be_quoted} diff --git a/spec/nquads_spec.rb b/spec/nquads_spec.rb index 9dd84178..3e5b1dfa 100644 --- a/spec/nquads_spec.rb +++ b/spec/nquads_spec.rb @@ -180,7 +180,8 @@ end end - context "RDF-star" do + # FIXME: quoted triples are deprecated + context "quoted triples" do statements = { "subject-iii": '<< >> .', "subject-iib": '<< _:o1>> .', @@ -205,7 +206,13 @@ statements.each do |name, st| context name do - let(:graph) {RDF::Graph.new << RDF::NQuads::Reader.new(st, rdfstar: true)} + let(:graph) do + g = RDF::Graph.new + expect do + g << RDF::NQuads::Reader.new(st, rdfstar: true) + end.to write('[DEPRECATION]').to(:error) + g + end it "creates two statements" do expect(graph.count).to eql(1) diff --git a/spec/ntriples_spec.rb b/spec/ntriples_spec.rb index 0d42ff09..95ae52fa 100644 --- a/spec/ntriples_spec.rb +++ b/spec/ntriples_spec.rb @@ -432,6 +432,78 @@ end end + context "triple terms" do + ill_statements = { + "subject-iii": '<<( )>> .', + "subject-iib": '<<( _:o1)>> .', + "subject-iil": '<<( "o1")>> .', + "subject-bii": '<<(_:s1 )>> .', + "subject-bib": '<<(_:s1 _:o1)>> .', + "subject-bil": '<<(_:s1 "o")>> .', + "subject-ws": '<<( )>> .', + "recursive-subject": '<<(<<( )>> )>> .', + } + + statements = { + "object-iii": ' <<( )>> .', + "object-iib": ' <<( _:o1)>> .', + "object-iil": ' <<( "o1")>> .', + "object-bii": ' <<(_:s1 )>> .', + "object-bib": ' <<(_:s1 _:o1)>> .', + "object-bil": ' <<(_:s1 "o1")>> .', + "object-ws": ' <<( )>> .', + + "recursive-object": ' <<( <<( )>>)>> .', + } + + context "without rdfstar option" do + it "Raises an error" do + expect do + expect {parse(statements.values.first)}.to raise_error(RDF::ReaderError) + end.to write(:something).to(:error) + end + end + + context "with rdfstar option" do + ill_statements.each do |name, st| + context name do + it "Raises an error" do + expect do + expect {parse(st)}.to raise_error(RDF::ReaderError) + end.to write(:something).to(:error) + end + end + end + + statements.each do |name, st| + context name do + let(:graph) {parse(st, rdfstar: true)} + + it "creates two unquoted statements" do + expect(graph.count).to eql(1) + graph.statements.each do |stmt| + expect(stmt).not_to be_quoted + end + end + + it "has a statement whose object is a statement" do + referencing = graph.statements.first + expect(referencing).to be_a_statement + expect(referencing.object).to be_a_statement + end + + it "statements which are object of another statement are triple terms" do + referencing = graph.statements.first + expect(referencing).to be_a_statement + expect(referencing.object).to be_a_statement + expect(referencing.object).to be_tripleTerm + end + end + end + end + end + + # FIXME: quoted triples are deprecated context "quoted triples" do statements = { "subject-iii": '<< >> .', @@ -459,7 +531,7 @@ context "with rdfstar option" do statements.each do |name, st| context name do - let(:graph) {parse(st, rdfstar: true)} + let(:graph) {parse(st, rdfstar: true, deprecated: true)} it "creates two unquoted statements" do expect(graph.count).to eql(1) @@ -1205,10 +1277,19 @@ def parse(input, **options) options = { validate: false, canonicalize: false, + deprecated: false, }.merge(options) graph = options[:graph] || RDF::Graph.new - RDF::NTriples::Reader.new(input, **options).each do |statement| - graph << statement + if options[:deprecated] + expect do + RDF::NTriples::Reader.new(input, **options).each do |statement| + graph << statement + end + end.to write('[DEPRECATION]').to(:error) + else + RDF::NTriples::Reader.new(input, **options).each do |statement| + graph << statement + end end graph end diff --git a/spec/query_pattern_spec.rb b/spec/query_pattern_spec.rb index 37a68783..bbd1d466 100644 --- a/spec/query_pattern_spec.rb +++ b/spec/query_pattern_spec.rb @@ -282,7 +282,7 @@ end end - context "quoted triples" do + context "triple terms" do let(:s) {RDF::Query::Variable.new(:s)} let(:p) {RDF::Query::Variable.new(:p)} let(:o) {RDF::Query::Variable.new(:o)}