Skip to content

Commit 1d4afaa

Browse files
committed
Partial updates for new RDF 1.2 model using triple terms, and reifiers.
1 parent 88ba1fd commit 1d4afaa

File tree

9 files changed

+129
-75
lines changed

9 files changed

+129
-75
lines changed

lib/json/ld.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ module LD
7373
@propagate
7474
@protected
7575
@preserve
76-
@reifier
76+
@reifies
7777
@requireAll
7878
@reverse
7979
@set
@@ -163,6 +163,7 @@ class InvalidSetOrListObject < JsonLdError; @code = 'invalid set or list object'
163163
class InvalidStreamingKeyOrder < JsonLdError; @code = 'invalid streaming key order' end
164164
class InvalidTermDefinition < JsonLdError; @code = 'invalid term definition'; end
165165
class InvalidTripleTerm < JsonLdError; @code = 'invalid triple term'; end
166+
class InvalidReification < JsonLdError; @code = 'invalid reification'; end
166167
class InvalidTypedValue < JsonLdError; @code = 'invalid typed value'; end
167168
class InvalidTypeMapping < JsonLdError; @code = 'invalid type mapping'; end
168169
class InvalidTypeValue < JsonLdError; @code = 'invalid type value'; end

lib/json/ld/expand.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ def expand_object(input, active_property, context, output_object,
299299
end
300300

301301
expanded_value = case expanded_property
302-
when '@annotation', '@reifies'
302+
when '@annotation'
303303
# Skip unless rdfstar option is set
304304
next unless @options[:rdfstar]
305305

@@ -477,6 +477,16 @@ def expand_object(input, active_property, context, output_object,
477477
# Skip unless rdfstar option is set
478478
next unless @options[:rdfstar]
479479

480+
# Result may have multiple reifications.
481+
rei_nodes = as_array(expand(value, nil, context, log_depth: log_depth.to_i + 1))
482+
rei_nodes.each do |rei_node|
483+
statements = to_enum(:item_to_rdf, rei_node)
484+
unless statements.count >= 1
485+
raise JsonLdError::InvalidReification,
486+
"Reification with #{statements.size.to_i} statements"
487+
end
488+
end
489+
480490
as_array(expand(value, '@reifies', context,
481491
framing: framing,
482492
log_depth: log_depth.to_i + 1))

lib/json/ld/flatten.rb

Lines changed: 86 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def create_node_map(element, graph_map,
4646
raise "Expected hash or array to create_node_map, got #{element.inspect}"
4747
else
4848
graph = (graph_map[active_graph] ||= {})
49-
subject_node = !reverse && graph[active_subject.is_a?(Hash) ? active_subject.to_json_c14n : active_subject]
49+
subject_node = graph[active_subject]
5050

5151
# Transform BNode types
5252
if element.key?('@type')
@@ -57,27 +57,26 @@ def create_node_map(element, graph_map,
5757
element['@type'] = element['@type'].first if element['@type']
5858

5959
# For rdfstar, if value contains an `@annotation` member ...
60-
# note: active_subject will not be nil, and may be an object itself.
61-
if element.key?('@annotation')
60+
# note: active_subject will not be nil.
61+
if annotation = element.delete('@annotation')
6262
# rdfstar being true is implicit, as it is checked in expansion
6363
as = if node_reference?(active_subject)
6464
active_subject['@id']
6565
else
6666
active_subject
6767
end
68-
star_subject = {
69-
"@id" => as,
70-
active_property => [element]
71-
}
68+
69+
reification = {'@id' => as, active_property => [element]}
7270

7371
# Note that annotation is an array, make the reified subject the id of each member of that array.
74-
annotation = element.delete('@annotation').map do |a|
75-
a.merge('@id' => star_subject)
76-
end
72+
annotation.each do |a|
73+
# XXX may be zero or more reifiers; use bnode for now.
74+
reifier = namer.get_name
75+
a = a.merge('@id' => reifier, '@reifies' => reification)
7776

78-
# Invoke recursively using annotation.
79-
create_node_map(annotation, graph_map,
80-
active_graph: active_graph)
77+
# Invoke recursively using annotation.
78+
create_node_map(a, graph_map, active_graph: active_graph, active_subject: reifier)
79+
end
8180
end
8281

8382
if list.nil?
@@ -106,20 +105,13 @@ def create_node_map(element, graph_map,
106105
end
107106
else
108107
# Element is a node object
109-
ser_id = id = element.delete('@id')
110-
if id.is_a?(Hash)
111-
# Index graph using serialized id
112-
ser_id = id.to_json_c14n
113-
raise "Can't happen"
114-
elsif id.nil?
115-
ser_id = id = namer.get_name
116-
end
108+
id = element.delete('@id')
109+
id = namer.get_name(id) if blank_node?(id)
117110

118-
node = graph[ser_id] ||= { '@id' => id }
111+
node = graph[id] ||= {'@id' => id}
119112

120-
if reverse
121-
# NOTE: active_subject is a Hash
122-
# We're processing a reverse-property relationship.
113+
if active_subject.is_a?(Hash)
114+
# If subject is a hash, then we're processing a reverse-property relationship.
123115
add_value(node, active_property, active_subject, property_is_array: true, allow_duplicate: false)
124116
elsif active_property
125117
reference = { '@id' => id }
@@ -131,30 +123,31 @@ def create_node_map(element, graph_map,
131123
end
132124

133125
# For rdfstar, if node contains an `@annotation` member ...
134-
# note: active_subject will not be nil, and may be an object itself.
126+
# note: active_subject will not be nil
135127
# XXX: what if we're reversing an annotation?
136-
if element.key?('@annotation')
128+
if annotation = element.delete('@annotation')
137129
# rdfstar being true is implicit, as it is checked in expansion
138130
as = if node_reference?(active_subject)
139131
active_subject['@id']
140132
else
141133
active_subject
142134
end
143-
star_subject = if reverse
144-
{ "@id" => node['@id'], active_property => [{ '@id' => as }] }
145-
else
146-
{ "@id" => as, active_property => [{ '@id' => node['@id'] }] }
147-
end
135+
136+
reification = {'@id' => as, active_property => [{ '@id' => node['@id'] }]}
148137

149138
# Note that annotation is an array, make the reified subject the id of each member of that array.
150-
annotation = element.delete('@annotation').map do |a|
151-
a.merge('@id' => star_subject)
139+
annotation.each do |a|
140+
# XXX may be zero or more reifiers; use bnode for now.
141+
reifier = namer.get_name
142+
a = a.merge('@id' => reifier, '@reifies' => reification)
143+
144+
# Invoke recursively using annotation.
145+
create_node_map(a, graph_map, active_graph: active_graph, active_subject: reifier)
152146
end
147+
end
153148

154-
# Invoke recursively using annotation.
155-
create_node_map(annotation, graph_map,
156-
active_graph: active_graph,
157-
active_subject: star_subject)
149+
if element.key?('@reifies')
150+
add_value(node, '@reifies', element.delete('@reifies'), property_is_array: true, allow_duplicate: false)
158151
end
159152

160153
if element.key?('@type')
@@ -210,45 +203,68 @@ def create_node_map(element, graph_map,
210203
##
211204
# Create annotations
212205
#
213-
# Updates a node map from which annotations have been folded into embedded triples to re-extract the annotations.
206+
# Updates a node map from which annotations have been folded into reified triples to re-extract the annotations.
214207
#
215-
# Map entries where the key is of the form of a canonicalized JSON object are used to find keys with the `@id` and property components. If found, the original map entry is removed and entries added to an `@annotation` property of the associated value.
208+
# Map entries having an `@reifies` key are used to find map entries that have a key based on the reification `@id` and a matching value. If found, the original map entry is removed and entries added to an `@annotation` property of the associated value.
216209
#
217-
# * Keys which are of the form of a canonicalized JSON object are examined in inverse order of length.
218-
# * Deserialize the key into a map, and re-serialize the value of `@id`.
219-
# * If the map contains an entry with that value (after re-canonicalizing, as appropriate), and the associated antry has a item which matches the non-`@id` item from the map, the node is used to create an `@annotation` entry within that value.
210+
# * If the map contains an entry with that value, and the associated antry has a item which matches the non-`@id` item from the map, the node is used to create an `@annotation` entry within that value.
220211
#
221212
# @param [Hash{String => Hash}] node_map
222213
# @return [Hash{String => Hash}]
223214
def create_annotations(node_map)
224-
node_map.keys
225-
.select { |k| k.start_with?('{') }
226-
.sort_by(&:length)
227-
.reverse_each do |key|
228-
annotation = node_map[key]
229-
# Deserialize key, and re-serialize the `@id` value.
230-
emb = annotation['@id'].dup
231-
id = emb.delete('@id')
232-
property, value = emb.to_a.first
233-
234-
# If id is a map, set it to the result of canonicalizing that value, otherwise to itself.
235-
id = id.to_json_c14n if id.is_a?(Hash)
236-
237-
next unless node_map.key?(id)
238-
239-
# If node map has an entry for id and that entry contains the same property and value from entry:
240-
node = node_map[id]
241-
242-
next unless node.key?(property)
243-
244-
node[property].each do |emb_value|
245-
next unless emb_value == value.first
246-
247-
node_map.delete(key)
248-
annotation.delete('@id')
249-
add_value(emb_value, '@annotation', annotation, property_is_array: true) unless
250-
annotation.empty?
215+
node_map
216+
.select {|_, node| node.key?('@reifies')}
217+
.each do |key, node|
218+
219+
reif_id = node['@id']
220+
reifs = node['@reifies']
221+
raise "expected the value of `@reifies` to be an array: #{reifs.inspect}" unless
222+
reifs.is_a?(Array)
223+
224+
# The node has properties other than `@id` and `@reifies`
225+
annotation = node.dup.delete_if {|k, _| %w(@id @reifies).include?(k)}
226+
227+
reifs.each do |reif|
228+
# node is a reification which _may_ relate to a value elsewhere in node_map
229+
raise "expected the value of `@reifies` to be an array: #{reifs.inspect}" unless
230+
reifs.is_a?(Array)
231+
target_id = reif['@id']
232+
target_node = node_map[target_id]
233+
next unless target_node
234+
235+
# The reification should have just `@id` and an additional property
236+
reif_prop = (reif.keys - %w(@id)).first
237+
raise "expected reification to have a non-id key: #{node.keys.inspect}" unless
238+
reif_prop
239+
reif_values = reif[reif_prop]
240+
# There should be only a single value
241+
raise "expected a single reifiation property value: #{reif}" unless
242+
reif_values.length == 1
243+
244+
reif_value = reif_values.first
245+
246+
# If target_node has the matching property and a matching value
247+
target_values = target_node[reif_prop]
248+
next unless target_values
249+
250+
# target_values must be an array
251+
raise "expected target propery value to have an array value: #{target_values.inspect}" unless
252+
target_values.is_a?(Array)
253+
254+
target_values.each do |t_value|
255+
next unless t_value == reif_value
256+
257+
# Add annotation to the identified value
258+
t_value['@annotation'] ||= []
259+
t_value['@annotation'] << {'@id' => reif_id}.merge(annotation)
260+
261+
# This consumes the reification
262+
node['@reifies'] = node['@reifies'] - [reif]
263+
end
251264
end
265+
266+
# If all reifications are consumed, remove the reification
267+
node_map.delete(reif_id) if node['@reifies'].empty?
252268
end
253269
end
254270

lib/json/ld/from_rdf.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ def from_statements(dataset, useRdfType: false, useNativeTypes: false, extendedR
7474
next
7575
end
7676

77+
# If predicate equals rdf:reifies and object is a triple term, append the @reifies of that triple term.
78+
if statement.predicate == RDF.reifies && statement.object.tripleTerm?
79+
reification = resource_representation(statement.object, useNativeTypes, extendedRepresentation)
80+
merge_value(node, '@reifies', reification['@triple'])
81+
next
82+
end
83+
7784
# Set value to the result of using the RDF to Object Conversion algorithm, passing object, rdfDirection, and use native types.
7885
value = resource_representation(statement.object, useNativeTypes, extendedRepresentation)
7986

lib/json/ld/to_rdf.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ def item_to_rdf(item, graph_name: nil, tripleTerm: false, &block)
9494
# log_debug("item_to_rdf") {"subject: #{subject.to_ntriples rescue 'malformed rdf'}"}
9595
item.each do |property, values|
9696
case property
97+
when '@reifies'
98+
next unless @options[:rdfstar]
99+
100+
# Each value of @reifies returns a single triple
101+
as_array(item['@reifies']).each do |rei|
102+
item_to_rdf(rei, graph_name: graph_name, tripleTerm: true) do |rei|
103+
yield RDF::Statement(subject, RDF.reifies, rei, graph_name: graph_name, tripleTerm: tripleTerm)
104+
end
105+
end
97106
when '@type'
98107
# If property is @type, construct triple as an RDF Triple composed of id, rdf:type, and object from values where id and object are represented either as IRIs or Blank Nodes
99108
values.each do |v|

lib/json/ld/utils.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ def triple_term?(value)
9797
value.is_a?(Hash) && value.key?('@triple')
9898
end
9999

100+
##
101+
# Is value a reification?
102+
#
103+
# @param [Object] value
104+
# @return [Boolean]
105+
def reification?(value)
106+
value.is_a?(Hash) && value.key?('@reifies')
107+
end
108+
100109
##
101110
# Is value a literal?
102111
#

script/tc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def run_tc(man, tc, options)
130130
# toRdf/e075 is hard to test, but verified manually
131131
output == tc.expect ? 'passed' : (tc.input_loc.include?('e075') ? 'passed' : 'failed')
132132
else
133-
expected = RDF::Repository.new << RDF::NQuads::Reader.new(tc.expect, rdfstar: tc.options[:rdfstar], validate: false, logger: [])
133+
expected = RDF::Repository.new << RDF::NQuads::Reader.new(tc.expect, rdfstar: tc.options[:rdfstar], validate: false)
134134
output.isomorphic?(expected) ? 'passed' : 'failed'
135135
end
136136
rescue RDF::ReaderError, JSON::LD::JsonLdError
@@ -179,6 +179,8 @@ def run_tc(man, tc, options)
179179
"failed"
180180
end
181181
end
182+
ensure
183+
STDERR.puts options[:logger].to_s if options[:verbose]
182184
end
183185

184186
#options[:output].puts("\nOutput:\n" + output) unless options[:quiet]
@@ -200,7 +202,7 @@ def run_tc(man, tc, options)
200202
puts "#{" test result:" unless options[:quiet]} #{result}"
201203
end
202204

203-
logger = Logger.new(STDERR)
205+
logger = RDF::Spec.logger
204206
logger.level = Logger::WARN
205207
logger.formatter = lambda {|severity, datetime, progname, msg| "#{severity}: #{msg}\n"}
206208

spec/rdfstar_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
describe m.name do
1818
m.entries.each do |t|
1919
specify "#{t.property('@id')}: #{t.name}#{' (negative test)' unless t.positiveTest?}" do
20+
pending "annotation folding" if t.name.include?("(with @annotation)")
2021
t.options[:ordered] = false
2122
expect { t.run self }.not_to write.to(:error)
2223
end

spec/writer_spec.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,6 @@
321321

322322
expect(parse(jsonld, format: :jsonld, **t.options)).to be_equivalent_graph(repo, t)
323323
rescue RDF::WriterError => e
324-
#require 'byebug'; byebug
325324
fail e.message + logger.to_s
326325
end
327326
end

0 commit comments

Comments
 (0)