Skip to content

Commit 73d2a5a

Browse files
committed
First pass transliteration of @dlonley's named graph framing code.
1 parent 0e11f03 commit 73d2a5a

File tree

6 files changed

+521
-226
lines changed

6 files changed

+521
-226
lines changed

lib/json/ld/api.rb

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -307,12 +307,12 @@ def self.flatten(input, context, options = {})
307307
log_debug(".flatten") {"expanded input: #{value.to_json(JSON_STATE) rescue 'malformed json'}"}
308308

309309
# Initialize node map to a JSON object consisting of a single member whose key is @default and whose value is an empty JSON object.
310-
graphs = {'@default' => {}}
311-
create_node_map(value, graphs)
310+
graph_maps = {'@default' => {}}
311+
create_node_map(value, graph_maps)
312312

313-
default_graph = graphs['@default']
314-
graphs.keys.kw_sort.reject {|k| k == '@default'}.each do |graph_name|
315-
graph = graphs[graph_name]
313+
default_graph = graph_maps['@default']
314+
graph_maps.keys.kw_sort.reject {|k| k == '@default'}.each do |graph_name|
315+
graph = graph_maps[graph_name]
316316
entry = default_graph[graph_name] ||= {'@id' => graph_name}
317317
nodes = entry['@graph'] ||= []
318318
graph.keys.kw_sort.each do |id|
@@ -402,7 +402,8 @@ def self.frame(input, frame, options = {})
402402
}.merge!(options)
403403

404404
framing_state = {
405-
graphs: {'@default' => {}, '@merged' => {}},
405+
graphMap: {'@default' => {}},
406+
graphStack: [],
406407
subjectStack: [],
407408
link: {},
408409
}
@@ -430,8 +431,18 @@ def self.frame(input, frame, options = {})
430431
log_debug(".frame") {"expanded frame: #{expanded_frame.to_json(JSON_STATE) rescue 'malformed json'}"}
431432

432433
# Get framing nodes from expanded input, replacing Blank Node identifiers as necessary
433-
create_node_map(value, framing_state[:graphs], graph: '@merged')
434-
framing_state[:subjects] = framing_state[:graphs]['@merged']
434+
create_node_map(value, framing_state[:graphMap], graph: '@default')
435+
436+
# If Frame is {'@graph': {}] use only the default graph for matches
437+
if frame.has_key?('@graph') && frame['@graph'].empty?
438+
framing_state[:graph] = '@default'
439+
else
440+
framing_state[:graph] = '@merged'
441+
framing_state[:link]['@merged'] = {}
442+
framing_state[:graphMap]['@merged'] = merge_node_map_graphs(framing_state[:graphMap])
443+
end
444+
445+
framing_state[:subjects] = framing_state[:graphMap][framing_state[:graph]]
435446

436447
result = []
437448
frame(framing_state, framing_state[:subjects].keys.sort, (expanded_frame.first || {}), options.merge(parent: result))

lib/json/ld/expand.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,8 @@ def expand(input, active_property, context, ordered: true)
129129
end
130130
when '@graph'
131131
# If expanded property is @graph, set expanded value to the result of using this algorithm recursively passing active context, @graph for active property, and value for element.
132-
expand(value, '@graph', context, ordered: ordered)
132+
value = expand(value, '@graph', context, ordered: ordered)
133+
value.is_a?(Array) ? value : [value]
133134
when '@value'
134135
# If expanded property is @value and value is not a scalar or null, an invalid value object value error has been detected and processing is aborted. Otherwise, set expanded value to value. If expanded value is null, set the @value member of result to null and continue with the next key from element. Null values need to be preserved in this case as the meaning of an @type member depends on the existence of an @value member.
135136
# If framing, always use array form, unless null

lib/json/ld/flatten.rb

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,7 @@ def create_node_map(input, graphs, graph: '@default', name: nil, list: nil)
6868
end
6969
when '@graph'
7070
graphs[name] ||= {}
71-
g = graph == '@merged' ? graph : name
72-
create_node_map(objects, graphs, graph: g)
71+
create_node_map(objects, graphs, graph: name)
7372
when /^@(?!type)/
7473
# copy non-@type keywords
7574
if property == '@index' && subject['@index']
@@ -115,5 +114,35 @@ def create_node_map(input, graphs, graph: '@default', name: nil, list: nil)
115114
list << input if list
116115
end
117116
end
117+
118+
private
119+
##
120+
# Merge nodes from all graphs in the graph_map into a new node map
121+
#
122+
# @param [Hash{String => Hash}] graph_map
123+
# @return [Hash]
124+
def merge_node_map_graphs(graph_map)
125+
merged = {}
126+
graph_map.each do |name, node_map|
127+
node_map.each do |id, node|
128+
merged_node = (merged[id] ||= {'@id' => id})
129+
130+
# Iterate over node properties
131+
node.each do |property, values|
132+
if property.start_with?('@')
133+
# Copy keywords
134+
merged_node[property] = node[property].dup
135+
else
136+
# Merge objects
137+
values.each do |value|
138+
add_value(merged_node, property, value.dup, property_is_array: true)
139+
end
140+
end
141+
end
142+
end
143+
end
144+
145+
merged
146+
end
118147
end
119148
end

lib/json/ld/frame.rb

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ module Frame
1919
# The parent property.
2020
# @raise [JSON::LD::InvalidFrame]
2121
def frame(state, subjects, frame, **options)
22+
log_depth do
23+
log_debug("frame") {"subjects: #{subjects.inspect}"}
24+
log_debug("frame") {"frame: #{frame.to_json(JSON_STATE)}"}
25+
log_debug("frame") {"property: #{options[:property].inspect}"}
26+
2227
parent, property = options[:parent], options[:property]
2328
# Validate the frame
2429
validate_frame(frame)
@@ -31,6 +36,9 @@ def frame(state, subjects, frame, **options)
3136
requireAll: get_frame_flag(frame, options, :requireAll),
3237
}
3338

39+
# Get link for current graph
40+
link = state[:link][state[:graph]] ||= {}
41+
3442
# Create a set of matched subjects by filtering subjects by checking the map of flattened subjects against frame
3543
# This gives us a hash of objects indexed by @id
3644
matches = filter_subjects(state, subjects, frame, flags)
@@ -40,35 +48,64 @@ def frame(state, subjects, frame, **options)
4048
subject = matches[id]
4149

4250
# Note: In order to treat each top-level match as a compartmentalized result, clear the unique embedded subjects map when the property is None, which only occurs at the top-level.
43-
state = state.merge(uniqueEmbeds: {}) if property.nil?
51+
if property.nil?
52+
state[:uniqueEmbeds] = {state[:graph] => {}}
53+
else
54+
state[:uniqueEmbeds][state[:graph]] ||= {}
55+
end
4456

45-
if flags[:embed] == '@link' && state[:link].has_key?(id)
57+
if flags[:embed] == '@link' && link.has_key?(id)
4658
# add existing linked subject
47-
add_frame_output(parent, property, state[:link][id])
59+
add_frame_output(parent, property, link[id])
4860
next
4961
end
5062

5163
output = {'@id' => id}
52-
state[:link][id] = output
64+
link[id] = output
5365

5466
# if embed is @never or if a circular reference would be created by an embed, the subject cannot be embedded, just add the reference; note that a circular reference won't occur when the embed flag is `@link` as the above check will short-circuit before reaching this point
55-
if flags[:embed] == '@never' || creates_circular_reference(subject, state[:subjectStack])
67+
if flags[:embed] == '@never' || creates_circular_reference(subject, state[:graph], state[:subjectStack])
5668
add_frame_output(parent, property, output)
5769
next
5870
end
5971

6072
# if only the last match should be embedded
6173
if flags[:embed] == '@last'
6274
# remove any existing embed
63-
remove_embed(state, id) if state[:uniqueEmbeds].include?(id)
64-
state[:uniqueEmbeds][id] = {
75+
remove_embed(state, id) if state[:uniqueEmbeds][state[:graph]].include?(id)
76+
state[:uniqueEmbeds][state[:graph]][id] = {
6577
parent: parent,
6678
property: property
6779
}
6880
end
6981

7082
# push matching subject onto stack to enable circular embed checks
71-
state[:subjectStack] << subject
83+
state[:subjectStack] << {subject: subject, graph: state[:graph]}
84+
85+
# Subject is also the name of a graph
86+
if state[:graphMap].has_key?(id)
87+
# check frame's "@graph" to see what to do next
88+
# 1. if it doesn't exist and state.graph === "@merged", don't recurse
89+
# 2. if it doesn't exist and state.graph !== "@merged", recurse
90+
# 3. if "@merged" then don't recurse
91+
# 4. if "@default" then don't recurse
92+
# 5. recurse
93+
recurse, subframe = false, nil
94+
if !frame.has_key?('@graph')
95+
recurse, subframe = (state[:graph] != '@merged'), {}
96+
else
97+
subframe = frame['@graph'].first
98+
recurse = !%w(@merged @default).include?(subframe)
99+
subframe = {} unless subframe.is_a?(Hash)
100+
end
101+
102+
if recurse
103+
state[:graphStack].push(state[:graph])
104+
state[:graph] = id
105+
frame(state, state[:graphMap][id].keys, [subframe], options.merge(parent: output, property: '@graph'))
106+
state[:graph] = state[:graphStack].pop
107+
end
108+
end
72109

73110
# iterate over subject properties in order
74111
subject.keys.kw_sort.each do |prop|
@@ -141,6 +178,7 @@ def frame(state, subjects, frame, **options)
141178
# pop matching subject from circular ref-checking stack
142179
state[:subjectStack].pop()
143180
end
181+
end
144182
end
145183

146184
##
@@ -193,7 +231,7 @@ def cleanup_preserve(input)
193231
# @return all of the matched subjects.
194232
def filter_subjects(state, subjects, frame, flags)
195233
subjects.inject({}) do |memo, id|
196-
subject = state[:subjects][id]
234+
subject = state[:graphMap][state[:graph]][id]
197235
memo[id] = subject if filter_subject(subject, frame, state, flags)
198236
memo
199237
end
@@ -306,12 +344,13 @@ def validate_frame(frame)
306344
# Checks the current subject stack to see if embedding the given subject would cause a circular reference.
307345
#
308346
# @param subject_to_embed the subject to embed.
347+
# @param graph the graph the subject to embed is in.
309348
# @param subject_stack the current stack of subjects.
310349
#
311350
# @return true if a circular reference would be created, false if not.
312-
def creates_circular_reference(subject_to_embed, subject_stack)
351+
def creates_circular_reference(subject_to_embed, graph, subject_stack)
313352
subject_stack[0..-2].any? do |subject|
314-
subject['@id'] == subject_to_embed['@id']
353+
subject[:graph] == graph && subject[:subject]['@id'] == subject_to_embed['@id']
315354
end
316355
end
317356

@@ -344,7 +383,7 @@ def get_frame_flag(frame, options, name)
344383
# @param id the @id of the embed to remove.
345384
def remove_embed(state, id)
346385
# get existing embed
347-
embeds = state[:uniqueEmbeds];
386+
embeds = state[:uniqueEmbeds][state[:graph]];
348387
embed = embeds[id];
349388
property = embed[:property];
350389

spec/expand_spec.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,25 @@
4343
{"http://example.com/bar" => [{"@value" => "bar"}]}
4444
]
4545
},
46+
"@graph value (expands to array form)" => {
47+
input: {
48+
"@context" => {"ex" => "http://example.com/"},
49+
"ex:p" => {
50+
"@id" => "ex:Sub1",
51+
"@graph" => {
52+
"ex:q" => "foo"
53+
}
54+
}
55+
},
56+
output: [{
57+
"http://example.com/p" => [{
58+
"@id" => "http://example.com/Sub1",
59+
"@graph" => [{
60+
"http://example.com/q" => [{"@value" => "foo"}],
61+
}]
62+
}]
63+
}]
64+
},
4665
"@type with CURIE" => {
4766
input: {
4867
"@context" => {"ex" => "http://example.com/"},

0 commit comments

Comments
 (0)