Skip to content

Commit

Permalink
Add support for RDF 1.2 Triple Terms and deprecation notices for RDF-…
Browse files Browse the repository at this point in the history
…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.
  • Loading branch information
gkellogg committed Feb 19, 2024
1 parent 7eeab69 commit 196b73b
Show file tree
Hide file tree
Showing 17 changed files with 201 additions and 27 deletions.
9 changes: 8 additions & 1 deletion README.md
Expand Up @@ -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`.
Expand Down Expand Up @@ -501,6 +507,7 @@ see <https://unlicense.org/> 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/
Expand Down
3 changes: 2 additions & 1 deletion etc/n-triples.ebnf
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion lib/rdf/mixin/enumerable.rb
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion lib/rdf/mixin/writable.rb
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion lib/rdf/model/dataset.rb
Expand Up @@ -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

Expand Down
5 changes: 3 additions & 2 deletions lib/rdf/model/graph.rb
Expand Up @@ -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"
Expand Down
13 changes: 11 additions & 2 deletions lib/rdf/model/statement.rb
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion lib/rdf/nquads.rb
Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion lib/rdf/ntriples.rb
Expand Up @@ -15,7 +15,11 @@ module RDF
#
# <https://rubygems.org/gems/rdf> <http://purl.org/dc/terms/title> "rdf" .
#
# ## Quoted Triples
# ## Triple terms
#
# Supports statements as resources using `<<(s p o)>>`.

# ## Quoted Triples (Deprecated)
#
# Supports statements as resources using `<<s p o>>`.
#
Expand Down
32 changes: 26 additions & 6 deletions lib/rdf/ntriples/reader.rb
Expand Up @@ -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
ST_END = /^\s*>>/.freeze
TT_START = /^<<\(/.freeze
TT_END = /^\s*\)>>/.freeze

QT_START = /^<</.freeze
QT_END = /^\s*>>/.freeze

# @see http://www.w3.org/TR/rdf-testcases/#ntrip_grammar
COMMENT = /^#\s*(.*)$/.freeze
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions lib/rdf/ntriples/writer.rb
Expand Up @@ -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.
#
Expand Down
3 changes: 3 additions & 0 deletions lib/rdf/repository.rb
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
23 changes: 20 additions & 3 deletions lib/rdf/writer.rb
Expand Up @@ -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
Expand Down Expand Up @@ -566,7 +567,7 @@ def format_list(value, **options)
end

##
# Formats a referenced triple.
# Formats a referenced triple term.
#
# @example
# <<<s> <p> <o>>> <p> <o> .
Expand All @@ -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
# <<<s> <p> <o>>> <p> <o> .
#
# @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
Expand Down
9 changes: 8 additions & 1 deletion spec/model_statement_spec.rb
Expand Up @@ -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)}
Expand Down Expand Up @@ -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}
Expand Down
11 changes: 9 additions & 2 deletions spec/nquads_spec.rb
Expand Up @@ -180,7 +180,8 @@
end
end

context "RDF-star" do
# FIXME: quoted triples are deprecated
context "quoted triples" do
statements = {
"subject-iii": '<<<http://example/s1> <http://example/p1> <http://example/o1>>> <http://example/p> <http://example/o> <http://example/g> .',
"subject-iib": '<<<http://example/s1> <http://example/p1> _:o1>> <http://example/p> <http://example/o> <http://example/g> .',
Expand All @@ -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)
Expand Down

0 comments on commit 196b73b

Please sign in to comment.