Skip to content

Commit fbc9f7b

Browse files
committed
Update flatten algorithm to handle embedded nodes and annotation objects.
Note: there remains a problem with consistent Bnode renaming inside embedded objects.
1 parent 98ea808 commit fbc9f7b

File tree

3 files changed

+610
-12
lines changed

3 files changed

+610
-12
lines changed

lib/json/ld/api.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -480,16 +480,16 @@ def self.toRdf(input, expanded: false, **options, &block)
480480
extractAllScripts: true,
481481
}.merge(options)
482482

483-
# Expand input to simplify processing
484-
expanded_input = expanded ? input : API.expand(input, ordered: false, **options)
483+
# Flatten input to simplify processing
484+
flattened_input = API.flatten(input, nil, expanded: expanded, ordered: false, **options)
485485

486-
API.new(expanded_input, nil, **options) do
486+
API.new(flattened_input, nil, **options) do
487487
# 1) Perform the Expansion Algorithm on the JSON-LD input.
488488
# This removes any existing context to allow the given context to be cleanly applied.
489-
log_debug(".toRdf") {"expanded input: #{expanded_input.to_json(JSON_STATE) rescue 'malformed json'}"}
489+
log_debug(".toRdf") {"flattened input: #{flattened_input.to_json(JSON_STATE) rescue 'malformed json'}"}
490490

491491
# Recurse through input
492-
expanded_input.each do |node|
492+
flattened_input.each do |node|
493493
item_to_rdf(node) do |statement|
494494
next if statement.predicate.node? && !options[:produceGeneralizedRdf]
495495

lib/json/ld/flatten.rb

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
# -*- encoding: utf-8 -*-
22
# frozen_string_literal: true
3+
require 'json/canonicalization'
4+
35
module JSON::LD
46
module Flatten
57
include Utils
68

79
##
810
# This algorithm creates a JSON object node map holding an indexed representation of the graphs and nodes represented in the passed expanded document. All nodes that are not uniquely identified by an IRI get assigned a (new) blank node identifier. The resulting node map will have a member for every graph in the document whose value is another object with a member for every node represented in the document. The default graph is stored under the @default member, all other graphs are stored under their graph name.
911
#
12+
# For RDF*/JSON-LD*:
13+
# * Values of `@id` can be an object (embedded node); when these are used as keys in a Node Map, they are serialized as canonical JSON, and de-serialized when flattening.
14+
# * The presence of `@annotation` implies an embedded node and the annotation object is removed from the node/value object in which it appears.
15+
#
1016
# @param [Array, Hash] element
1117
# Expanded JSON-LD input
1218
# @param [Hash] graph_map A map of graph name to subjects
@@ -16,12 +22,15 @@ module Flatten
1622
# Node identifier
1723
# @param [String] active_property (nil)
1824
# Property within current node
25+
# @param [Boolean] reverse (false)
26+
# Processing a reverse relationship
1927
# @param [Array] list (nil)
2028
# Used when property value is a list
2129
def create_node_map(element, graph_map,
2230
active_graph: '@default',
2331
active_subject: nil,
2432
active_property: nil,
33+
reverse: false,
2534
list: nil)
2635
if element.is_a?(Array)
2736
# If element is an array, process each entry in element recursively by passing item for element, node map, active graph, active subject, active property, and list.
@@ -30,13 +39,14 @@ def create_node_map(element, graph_map,
3039
active_graph: active_graph,
3140
active_subject: active_subject,
3241
active_property: active_property,
42+
reverse: false,
3343
list: list)
3444
end
3545
elsif !element.is_a?(Hash)
3646
raise "Expected hash or array to create_node_map, got #{element.inspect}"
3747
else
3848
graph = (graph_map[active_graph] ||= {})
39-
subject_node = !active_subject.is_a?(Hash) && graph[active_subject]
49+
subject_node = !reverse && graph[active_subject.is_a?(Hash) ? active_subject.to_json_c14n : active_subject]
4050

4151
# Transform BNode types
4252
if element.has_key?('@type')
@@ -45,6 +55,29 @@ def create_node_map(element, graph_map,
4555

4656
if value?(element)
4757
element['@type'] = element['@type'].first if element ['@type']
58+
59+
# 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')
62+
# rdfstar being true is implicit, as it is checked in expansion
63+
as = node_reference?(active_subject) ?
64+
active_subject['@id'] :
65+
active_subject
66+
star_subject = {
67+
"@id" => as,
68+
active_property => [element]
69+
}
70+
71+
# Note that annotation is an array, make the reified subject the id of each member of that array.
72+
annotation = element.delete('@annotation').map do |a|
73+
a.merge('@id' => star_subject)
74+
end
75+
76+
# Invoke recursively using annotation.
77+
create_node_map(annotation, graph_map,
78+
active_graph: active_graph)
79+
end
80+
4881
if list.nil?
4982
add_value(subject_node, active_property, element, property_is_array: true, allow_duplicate: false)
5083
else
@@ -64,13 +97,21 @@ def create_node_map(element, graph_map,
6497
end
6598
else
6699
# Element is a node object
67-
id = element.delete('@id')
68-
id = namer.get_name(id) if blank_node?(id)
100+
ser_id = id = element.delete('@id')
101+
if id.is_a?(Hash)
102+
# recursively rename blank nodes within `id`.
103+
id = rename_embedded(id)
104+
# Index graph using serialized id
105+
ser_id = id.to_json_c14n
106+
elsif blank_node?(id)
107+
ser_id = id = namer.get_name(id)
108+
end
69109

70-
node = graph[id] ||= {'@id' => id}
110+
node = graph[ser_id] ||= {'@id' => id}
71111

72-
if active_subject.is_a?(Hash)
73-
# If subject is a hash, then we're processing a reverse-property relationship.
112+
if reverse
113+
# Note: active_subject is a Hash
114+
# We're processing a reverse-property relationship.
74115
add_value(node, active_property, active_subject, property_is_array: true, allow_duplicate: false)
75116
elsif active_property
76117
reference = {'@id' => id}
@@ -81,6 +122,29 @@ def create_node_map(element, graph_map,
81122
end
82123
end
83124

125+
# For rdfstar, if node contains an `@annotation` member ...
126+
# note: active_subject will not be nil, and may be an object itself.
127+
# XXX: what if we're reversing an annotation?
128+
if element.key?('@annotation')
129+
# rdfstar being true is implicit, as it is checked in expansion
130+
as = node_reference?(active_subject) ?
131+
active_subject['@id'] :
132+
active_subject
133+
star_subject = reverse ?
134+
{"@id" => node['@id'], active_property => [{'@id' => as}]} :
135+
{"@id" => as, active_property => [{'@id' => node['@id']}]}
136+
137+
# Note that annotation is an array, make the reified subject the id of each member of that array.
138+
annotation = element.delete('@annotation').map do |a|
139+
a.merge('@id' => star_subject)
140+
end
141+
142+
# Invoke recursively using annotation.
143+
create_node_map(annotation, graph_map,
144+
active_graph: active_graph,
145+
active_subject: star_subject)
146+
end
147+
84148
if element.has_key?('@type')
85149
add_value(node, '@type', element.delete('@type'), property_is_array: true, allow_duplicate: false)
86150
end
@@ -99,7 +163,8 @@ def create_node_map(element, graph_map,
99163
create_node_map(value, graph_map,
100164
active_graph: active_graph,
101165
active_subject: referenced_node,
102-
active_property: property)
166+
active_property: property,
167+
reverse: true)
103168
end
104169
end
105170
end
@@ -128,7 +193,26 @@ def create_node_map(element, graph_map,
128193
end
129194
end
130195

196+
##
197+
# Rename blank nodes recursively within an embedded object
198+
#
199+
# @param [Object] node
200+
# @return [Hash]
201+
def rename_embedded(node)
202+
case node
203+
when String
204+
blank_node?(node) ? namer.get_name(node) : node
205+
when Array
206+
node.map {|n| rename_embedded(n)}
207+
when Hash
208+
node.inject({}) {|memo, (k, v)| memo.merge(k => rename_embedded(v))}
209+
else
210+
node
211+
end
212+
end
213+
131214
private
215+
132216
##
133217
# Merge nodes from all graphs in the graph_map into a new node map
134218
#

0 commit comments

Comments
 (0)