Skip to content

Commit

Permalink
New style reified triples/annotations with postfix/prefix reifiers.
Browse files Browse the repository at this point in the history
  • Loading branch information
gkellogg committed Aug 3, 2024
1 parent d0d30ac commit 7ac9eae
Show file tree
Hide file tree
Showing 7 changed files with 422 additions and 251 deletions.
29 changes: 22 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,23 +38,24 @@ Write a graph to a file:

## RDF 1.2

Both reader and writer include provisional support for [RDF 1.2][] quoted triples.
Both reader and writer include provisional support for [RDF 1.2][] triple terms.

Both reader and writer include 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`.

**Note: This feature is subject to change or elimination as the standards process progresses.**

### Serializing a Graph containing quoted triples
### Serializing a Graph containing reified triples

require 'rdf/turtle'
statement = RDF::Statement(RDF::URI('bob'), RDF::Vocab::FOAF.age, RDF::Literal(23))
graph = RDF::Graph.new << [statement, RDF::URI("ex:certainty"), RDF::Literal(0.9)]
triple = RDF::Statement(RDF::URI('bob'), RDF::Vocab::FOAF.age, RDF::Literal(23))
graph = RDF::Graph.new << [RDF::URI('r'), RDF::URI("ex:certainty"), RDF::Literal(0.9)]
graph << RDF::Statement(RDF::URI('r'), RDF.reifies, triple)
graph.dump(:ttl, validate: false, standard_prefixes: true)
# => '<<<bob> foaf:age 23>> <ex:certainty> 9.0e-1 .'

### Reading a Graph containing quoted triples
### Reading a Graph containing reified triples

By default, the Turtle reader will reject a document containing a subject resource.

Expand All @@ -73,7 +74,7 @@ Readers support a boolean valued `rdfstar` option; only one statement is asserte
graph = RDF::Graph.new do |graph|
RDF::Turtle::Reader.new(ttl, rdfstar: true) {|reader| graph << reader}
end
graph.count #=> 1
graph.count #=> 2

### Reading a Graph containing statement annotations

Expand All @@ -88,7 +89,21 @@ where the subject is the the triple ending with that annotation.
graph = RDF::Graph.new do |graph|
RDF::Turtle::Reader.new(ttl) {|reader| graph << reader}
end
# => RDF::ReaderError
# => RDF::Graph.new do |g|
triple = RDF::Statement.new(RDF::URI('bob'), RDF::FOAF.age, RDF::Literal(23))
bn = RDF::Node.new('anno)
g << triple
g << RDF::Statement.new(bn, RDF.reifies, triple)
g << RDF::Statement.new(bn, RDF::URI("http://example.com/certainty"), RDF::Literal.new(9.0e-1))
end

Annotations can also have a reifier identifier

ttl = %(
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix ex: <http://example.com/> .
<bob> foaf:age 23 ~ ex:anno {| ex:certainty 9.0e-1 |} .
)

Note that this requires the `rdfstar` option to be set.

Expand Down
19 changes: 11 additions & 8 deletions etc/turtle.bnf
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ prefixID ::= '@prefix' PNAME_NS IRIREF '.'
base ::= '@base' IRIREF '.'
sparqlPrefix ::= "PREFIX" PNAME_NS IRIREF
sparqlBase ::= "BASE" IRIREF
triples ::= subject predicateObjectList | blankNodePropertyList predicateObjectList?
triples ::= subject predicateObjectList
| blankNodePropertyList predicateObjectList?
| reifiedTriple predicateObjectList?
predicateObjectList ::= verb objectList (';' (verb objectList)? )*
objectList ::= object annotation* ( ',' object annotation* )*
objectList ::= object annotation ( ',' object annotation )*
verb ::= predicate | 'a'
subject ::= iri | BlankNode | collection | reifier
subject ::= iri | BlankNode | collection
predicate ::= iri
object ::= iri | BlankNode | collection | blankNodePropertyList | literal | tripleTerm | reifier
object ::= iri | BlankNode | collection | blankNodePropertyList | literal | tripleTerm | reifiedTriple
literal ::= RDFLiteral | NumericLiteral | BooleanLiteral
blankNodePropertyList ::= '[' predicateObjectList ']'
collection ::= '(' object* ')'
Expand All @@ -23,10 +25,12 @@ String ::= STRING_LITERAL_QUOTE | STRING_LITERAL_SINGLE_QUOTE
iri ::= IRIREF | PrefixedName
PrefixedName ::= PNAME_LN | PNAME_NS
BlankNode ::= BLANK_NODE_LABEL | ANON
reifier ::= '<<' ((iri | BlankNode) '|' )? subject predicate object '>>'
tripleTerm ::= '<<(' subject predicate ttObject ')>>'
reifier ::= '~' (iri | BlankNode)?
reifiedTriple ::= '<<' ttSubject predicate ttObject reifier* '>>'
tripleTerm ::= '<<(' ttSubject predicate ttObject ')>>'
ttSubject ::= iri | BlankNode
ttObject ::= iri | BlankNode | literal | tripleTerm
annotation ::= '{|' ( (iri | BlankNode) '|' )? predicateObjectList '|}'
annotation ::= (reifier | '{|' predicateObjectList '|}')*

@terminals

Expand All @@ -45,7 +49,6 @@ STRING_LITERAL_LONG_SINGLE_QUOTE ::= "'''" ( ( "'" | "''" )? ( [^'\] | ECHAR |
STRING_LITERAL_LONG_QUOTE ::= '"""' ( ( '"' | '""' )? ( [^"\] | ECHAR | UCHAR ) )* '"""'
UCHAR ::= ( '\u' HEX HEX HEX HEX ) | ( '\U' HEX HEX HEX HEX HEX HEX HEX HEX )
ECHAR ::= ('\' [tbnrf\"'])
NIL ::= '(' WS* ')'
WS ::= #x20 | #x9 | #xD | #xA /* #x20=space #x9=character tabulation #xD=carriage return #xA=new line */
ANON ::= '[' WS* ']'
PN_CHARS_BASE ::= ([A-Z]
Expand Down
141 changes: 74 additions & 67 deletions lib/rdf/turtle/reader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class Reader < RDF::Reader
# String terminals
terminal(nil, %r(
<<\(|\)>>
| [\(\),.;\[\]Aa]
| [\(\),.;~\[\]Aa]
| \^\^
| \{\|
| \|\}
Expand All @@ -38,7 +38,7 @@ class Reader < RDF::Reader

terminal(:PREFIX, PREFIX)
terminal(:BASE, BASE)
terminal(:LANG_DIR, LANG_DIR)
terminal(:LANG_DIR, LANG_DIR)

##
# Reader options
Expand Down Expand Up @@ -340,6 +340,9 @@ def read_triples
# blankNodePropertyList predicateObjectList?
subject = read_blankNodePropertyList || error("Failed to parse blankNodePropertyList", production: :triples, token: @lexer.first)
read_predicateObjectList(subject) || subject
when '<<'
subject = read_reifiedTriple || error("Failed to parse reifiedTriple", production: :triples, token: @lexer.first)
read_predicateObjectList(subject) || subject
else
# subject predicateObjectList
subject = read_subject || error("Failed to parse subject", production: :triples, token: @lexer.first)
Expand Down Expand Up @@ -383,7 +386,7 @@ def read_objectList(subject, predicate)
last_object = object

# If object is followed by annotations, read them.
read_annotations(subject, predicate, object)
read_annotation(subject, predicate, object)

break unless @lexer.first === ','
@lexer.shift while @lexer.first === ','
Expand All @@ -410,15 +413,14 @@ def read_verb
end
end

# subject ::= iri | BlankNode | collection | reifier
# subject ::= iri | BlankNode | collection
#
# @return [RDF::Resource]
def read_subject
prod(:subject) do
read_iri ||
read_BlankNode ||
read_collection ||
read_reifier ||
error( "Expected subject", production: :subject, token: @lexer.first)
end
end
Expand All @@ -427,7 +429,7 @@ def read_subject
# Read object
#
# object ::= iri | BlankNode | collection | blankNodePropertyList
# | literal | tripleTerm | reifier
# | literal | tripleTerm | reifiedTriple
#
# @return [void]
def read_object(subject = nil, predicate = nil)
Expand All @@ -438,7 +440,7 @@ def read_object(subject = nil, predicate = nil)
read_blankNodePropertyList ||
read_literal ||
read_tripleTerm ||
read_reifier
read_reifiedTriple

add_statement(:object, RDF::Statement(subject, predicate, object)) if subject && predicate
object
Expand All @@ -447,38 +449,34 @@ def read_object(subject = nil, predicate = nil)
end

##
# Read reifier
# Read reifiedTriple
#
# reifier ::= '<<' ((iri | BlankNode) '|' )? subject predicate object '>>'
# reifiedTriple ::= '<<' ttSubject predicate ttObject reifier? '>>'
#
# @return [RDF::Term]
def read_reifier
def read_reifiedTriple
return unless @options[:rdfstar]
if @lexer.first.value == '<<'
prod(:reifier) do
prod(:reifiedTriple) do
@lexer.shift # eat <<
# Optional identifier for reifier
id = read_iri || read_BlankNode
if id && @lexer.first.value == '|'
@lexer.shift # eat |
subject = read_subject || error("Failed to parse subject", production: :reifier, token: @lexer.first)
elsif @lexer.first.value == '|'
error("Failed to parse reifier identifier", production: :reifier, token: @lexer.first)
else
# No ID read or missing separator
subject = id || read_subject || error("Failed to parse subject", production: :reifier, token: @lexer.first)
id = bnode
end
predicate = read_verb || error("Failed to parse predicate", production: :reifier, token: @lexer.first)
object = read_object || error("Failed to parse object", production: :reifier, token: @lexer.first)
subject = read_ttSubject || error("Failed to parse subject", production: :reifiedTriple, token: @lexer.first)
predicate = read_verb || error("Failed to parse predicate", production: :reifiedTriple, token: @lexer.first)
object = read_ttObject || error("Failed to parse object", production: :reifiedTriple, token: @lexer.first)
tt = RDF::Statement(subject, predicate, object, tripleTerm: true)

# An optional reifier. If not specified it is a new blank node.
id = if @lexer.first.value == '~'
@lexer.shift
read_iri || read_BlankNode
end || bnode

statement = RDF::Statement(id, RDF.to_uri + 'reifies', tt)
add_statement('reifiedTriple', statement)

unless @lexer.first.value == '>>'
error("Failed to end of triple occurence", production: :reifier, token: @lexer.first)
error("Failed to end of triple occurence", production: :reifiedTriple, token: @lexer.first)
end
@lexer.shift
tt = RDF::Statement(subject, predicate, object, tripleTerm: true)
## XXX replacement for rdf:reifies
statement = RDF::Statement(id, RDF.to_uri + 'reifies', tt)
add_statement('tripleOccurence', statement)
id
end
end
Expand All @@ -487,15 +485,15 @@ def read_reifier
##
# Read triple term
#
# tripleTerm ::= '<<(' subject predicate ttObject ')>>'
# tripleTerm ::= '<<(' ttSubject predicate ttObject ')>>'
#
# @return [RDF::Term]
# @return [RDF::Statement]
def read_tripleTerm
return unless @options[:rdfstar]
if @lexer.first.value == '<<('
prod(:tripleTerm) do
@lexer.shift # eat <<(
subject = read_subject || error("Failed to parse subject", production: :tripleTerm, token: @lexer.first)
subject = read_ttSubject || error("Failed to parse subject", production: :tripleTerm, token: @lexer.first)
predicate = read_verb || error("Failed to parse predicate", production: :tripleTerm, token: @lexer.first)
object = read_ttObject || error("Failed to parse object", production: :tripleTerm, token: @lexer.first)
unless @lexer.first.value == ')>>'
Expand All @@ -508,6 +506,19 @@ def read_tripleTerm
end
end

##
# Read ttSubject
#
# ttSubject::= iri | BlankNode
#
# @return [RDF::Term]
def read_ttSubject
prod(:ttSubject) do
read_iri ||
read_BlankNode
end
end

##
# Read ttObject
#
Expand All @@ -526,46 +537,42 @@ def read_ttObject(subject = nil, predicate = nil)
##
# Read an annotation on a triple
#
# annotation := ('{|' ( (iri | BlankNode) '|' )? predicateObjectList '|}')*
def read_annotations(subject, predicate, object)
# annotation := (reifier | '{|' predicateObjectList '|}')*
#
# The `reifier` becomes the identifier for a subsequent annotation block (if it exists). If there is no reifier, then a blank node is created.
def read_annotation(subject, predicate, object)
error("Unexpected end of file", production: :annotation) unless @lexer.first
while @lexer.first === '{|'
prod(:annotation, %(|})) do
@lexer.shift
# Optional identifier for reifier
tt = RDF::Statement(subject, predicate, object, tripleTerm: true)
id = read_iri || read_BlankNode
if id && @lexer.first.value == '|'
@lexer.shift # eat |
progress("anotation", depth: options[:depth]) {"identifier: #{id.to_ntriples}"}
# Parsed annotation identifier
elsif @lexer.first.value == '|'
# expected annotation identifier
error("Failed to parse annotation identifier", production: :annotation, token: @lexer.first)
elsif id
error("Expected IRI to use as predicate in predicateObjectList",
production: :annotation,
token: id) if id.node?
# Remember any IRI that was read anticipating that it would be an identifier to use as the verb of a prdicateObjectList.
@cached_verb = id if id
# No identifier, use a new blank node
id = bnode
else
# No identifier, use a new blank node
id = bnode
end

statement = RDF::Statement(id, RDF.reifies, tt)
add_statement('annotation', statement)
tt = RDF::Statement(subject, predicate, object, tripleTerm: true)
id = nil

# id becomes subject for predicateObjectList
read_predicateObjectList(id) ||
error("Expected predicateObjectList", production: :annotation, token: @lexer.first)
error("annotation", "Expected closing '|}'") unless @lexer.first === '|}'
@lexer.shift
while %w(~ {|).include? @lexer.first.to_s
if @lexer.first === '~'
prod(:annotation, %(~})) do
@lexer.shift # eat '~'
# Emit any pending reifiedTriple if there was no annotation block
add_statement('annotation', RDF::Statement(id, RDF.reifies, tt)) if id
id = read_iri || read_BlankNode || bnode
end
else
prod(:annotation, %({||})) do
@lexer.shift # eat '{|'
id ||= bnode
# Emit the reifiedTriple
add_statement('annotation', RDF::Statement(id, RDF.reifies, tt))

# id becomes subject for predicateObjectList
read_predicateObjectList(id) ||
error("Expected predicateObjectList", production: :annotation, token: @lexer.first)
error("annotation", "Expected closing '|}'") unless @lexer.first === '|}'
@lexer.shift # eat '|}'
id = nil
end
end
end

# Emit any pending reifiedTriple if there was no annotation block
add_statement('annotation', RDF::Statement(id, RDF.reifies, tt)) if id
end

# @return [RDF::Literal]
Expand Down
2 changes: 1 addition & 1 deletion script/tc
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def run_tc(tc, **options)
else
begin
graph << reader
raise RDF::ReaderError, "quoted triple" if graph.statements.any? {|s| s.to_a.any?(&:statement?)}
raise RDF::ReaderError, "triple term" if graph.statements.any? {|s| s.to_a.any?(&:statement?)}
STDERR.puts "Expected exception" if options[:verbose]
result = "failed"
rescue RDF::ReaderError
Expand Down
2 changes: 1 addition & 1 deletion spec/ntriples_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
else
expect {
graph << reader
raise RDF::ReaderError, "quoted triple" if graph.statements.any? {|s| s.to_a.any?(&:statement?)}
raise RDF::ReaderError, "triple term" if graph.statements.any? {|s| s.to_a.any?(&:statement?)}
#expect(graph.dump(:ntriples).should produce("", t.debug)
}.to raise_error RDF::ReaderError
end
Expand Down
Loading

0 comments on commit 7ac9eae

Please sign in to comment.