Skip to content

Commit c1d11a1

Browse files
committed
Support for lists of lists.
1 parent 164f89f commit c1d11a1

26 files changed

+605
-379
lines changed

lib/json/ld.rb

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ def code
100100
end
101101

102102
class CollidingKeywords < JsonLdError; @code = "colliding keywords"; end
103-
class CompactionToListOfLists < JsonLdError; @code = "compaction to list of lists"; end
104103
class ConflictingIndexes < JsonLdError; @code = "conflicting indexes"; end
105104
class CyclicIRIMapping < JsonLdError; @code = "cyclic IRI mapping"; end
106105
class InvalidBaseIRI < JsonLdError; @code = "invalid base IRI"; end
@@ -133,7 +132,6 @@ class InvalidValueObject < JsonLdError; @code = "invalid value object"; end
133132
class InvalidValueObjectValue < JsonLdError; @code = "invalid value object value"; end
134133
class InvalidVocabMapping < JsonLdError; @code = "invalid vocab mapping"; end
135134
class KeywordRedefinition < JsonLdError; @code = "keyword redefinition"; end
136-
class ListOfLists < JsonLdError; @code = "list of lists"; end
137135
class LoadingDocumentFailed < JsonLdError; @code = "loading document failed"; end
138136
class LoadingRemoteContextFailed < JsonLdError; @code = "loading remote context failed"; end
139137
class MultipleContextLinkHeaders < JsonLdError; @code = "multiple context link headers"; end

lib/json/ld/api.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ def self.frame(input, frame, expanded: false, **options)
375375
log_debug(".frame") {"expanded frame: #{expanded_frame.to_json(JSON_STATE) rescue 'malformed json'}"}
376376

377377
# Get framing nodes from expanded input, replacing Blank Node identifiers as necessary
378-
create_node_map(value, framing_state[:graphMap], graph: '@default')
378+
create_node_map(value, framing_state[:graphMap], active_graph: '@default')
379379

380380
frame_keys = frame.keys.map {|k| context.expand_iri(k, vocab: true, quiet: true)}
381381
if frame_keys.include?('@graph')
@@ -482,7 +482,7 @@ def self.toRdf(input, expanded: false, **options, &block)
482482
#
483483
# The resulting `Array` is either returned or yielded, if a block is given.
484484
#
485-
# @param [Array<RDF::Statement>] input
485+
# @param [RDF::Enumerable] input
486486
# @param [Hash{Symbol => Object}] options
487487
# @option options (see #initialize)
488488
# @option options [Boolean] :useRdfType (false)

lib/json/ld/compact.rb

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ def compact(element, property: nil)
3030
# If element has a single member and the active property has no
3131
# @container mapping to @list or @set, the compacted value is that
3232
# member; otherwise the compacted value is element
33-
if result.length == 1 && !context.as_array?(property) && @options[:compactArrays]
33+
if result.length == 1 &&
34+
!context.as_array?(property) && @options[:compactArrays]
3435
#log_debug("=> extract single element: #{result.first.inspect}")
3536
result.first
3637
else
@@ -51,6 +52,12 @@ def compact(element, property: nil)
5152
end
5253
end
5354

55+
# If expanded property is @list and we're contained within a list container, recursively compact this item to an array
56+
if list?(element) && context.container(property) == %w(@list)
57+
return compact(element['@list'], property: property)
58+
end
59+
60+
5461
inside_reverse = property == '@reverse'
5562
result, nest_result = {}, nil
5663

@@ -183,9 +190,9 @@ def compact(element, property: nil)
183190
compacted_item[key] = expanded_item['@index']
184191
end
185192
else
186-
raise JsonLdError::CompactionToListOfLists,
187-
"key cannot have more than one list value" if nest_result.has_key?(item_active_property)
188-
# Falls through to add list value below
193+
add_value(nest_result, item_active_property, compacted_item,
194+
value_is_array: true, allow_duplicate: true)
195+
next
189196
end
190197
end
191198

lib/json/ld/expand.rb

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,9 @@ def expand(input, active_property, context, ordered: true, framing: false)
3030
# Initialize expanded item to the result of using this algorithm recursively, passing active context, active property, and item as element.
3131
v = expand(v, active_property, context, ordered: ordered, framing: framing)
3232

33-
# If the active property is @list or its container mapping is set to @list, the expanded item must not be an array or a list object, otherwise a list of lists error has been detected and processing is aborted.
34-
raise JsonLdError::ListOfLists,
35-
"A list may not contain another list" if
36-
is_list && (v.is_a?(Array) || list?(v))
33+
# If the active property is @list or its container mapping is set to @list and v is an array, change it to a list object
34+
v = {"@list" => v} if is_list && v.is_a?(Array)
35+
3736
case v
3837
when nil then nil
3938
when Array then memo.concat(v)
@@ -283,11 +282,6 @@ def expand_object(input, active_property, context, output_object, ordered:, fram
283282
# Spec FIXME: need to be sure that result is an array
284283
value = as_array(value)
285284

286-
# If expanded value is a list object, a list of lists error has been detected and processing is aborted.
287-
# Spec FIXME: Also look at each object if result is an array
288-
raise JsonLdError::ListOfLists,
289-
"A list may not contain another list" if value.any? {|v| list?(v)}
290-
291285
value
292286
when '@set'
293287
# If expanded property is @set, set expanded value to the result of using this algorithm recursively, passing active context, active property, and value for element.

lib/json/ld/flatten.rb

Lines changed: 99 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -7,111 +7,120 @@ module Flatten
77
##
88
# 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.
99
#
10-
# @param [Array, Hash] input
10+
# @param [Array, Hash] element
1111
# Expanded JSON-LD input
12-
# @param [Hash] graphs A map of graph name to subjects
13-
# @param [String] graph
12+
# @param [Hash] graph_map A map of graph name to subjects
13+
# @param [String] active_graph
1414
# The name of the currently active graph that the processor should use when processing.
15-
# @param [String] name
16-
# The name assigned to the current input if it is a bnode
17-
# @param [Array] list
18-
# List to append to, nil for none
19-
def create_node_map(input, graphs, graph: '@default', name: nil, list: nil)
20-
#log_debug("node_map") {"graph: #{graph}, input: #{input.inspect}, name: #{name}"}
21-
case input
22-
when Array
23-
# If input is an array, process each entry in input recursively by passing item for input, node map, active graph, active subject, active property, and list.
24-
input.map {|o| create_node_map(o, graphs, graph: graph, list: list)}
25-
when Hash
26-
type = input['@type']
27-
if value?(input)
28-
# Rename blanknode @type
29-
input['@type'] = namer.get_name(type) if type && blank_node?(type)
30-
list << input if list
31-
else
32-
# Input is a node definition
15+
# @param [String] active_subject (nil)
16+
# Node identifier
17+
# @param [String] active_property (nil)
18+
# Property within current node
19+
# @param [Array] list (nil)
20+
# Used when property value is a list
21+
def create_node_map(element, graph_map,
22+
active_graph: '@default',
23+
active_subject: nil,
24+
active_property: nil,
25+
list: nil)
26+
log_debug("node_map") {"active_graph: #{active_graph}, element: #{element.inspect}, active_subject: #{active_subject}"}
27+
if element.is_a?(Array)
28+
# 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.
29+
element.map do |o|
30+
create_node_map(o, graph_map,
31+
active_graph: active_graph,
32+
active_subject: active_subject,
33+
active_property: active_property,
34+
list: list)
35+
end
36+
elsif !element.is_a?(Hash)
37+
raise "Expected hash or array to create_node_map, got #{element.inspect}"
38+
else
39+
graph = (graph_map[active_graph] ||= {})
40+
subject_node = graph[active_subject]
3341

34-
# spec requires @type to be named first, so assign names early
35-
Array(type).each {|t| namer.get_name(t) if blank_node?(t)}
42+
# Transform BNode types
43+
if element.has_key?('@type')
44+
element['@type'] = Array(element['@type']).map {|t| blank_node?(t) ? namer.get_name(t) : t}
45+
end
3646

37-
# get name for subject
38-
if name.nil?
39-
name ||= input['@id']
40-
name = namer.get_name(name) if blank_node?(name)
47+
if value?(element)
48+
element['@type'] = element['@type'].first if element ['@type']
49+
if list.nil?
50+
add_value(subject_node, active_property, element, property_is_array: true, allow_duplicate: false)
51+
else
52+
list['@list'] << element
4153
end
54+
elsif list?(element)
55+
result = {'@list' => []}
56+
create_node_map(element['@list'], graph_map,
57+
active_graph: active_graph,
58+
active_subject: active_subject,
59+
active_property: active_property,
60+
list: result)
61+
if list.nil?
62+
add_value(subject_node, active_property, result, property_is_array: true)
63+
else
64+
list['@list'] << result
65+
end
66+
else
67+
# Element is a node object
68+
id = element.delete('@id')
69+
id = namer.get_name(id) if blank_node?(id)
4270

43-
# add subject reference to list
44-
list << {'@id' => name} if list
45-
46-
# create new subject or merge into existing one
47-
subject = (graphs[graph] ||= {})[name] ||= {'@id' => name}
71+
node = graph[id] ||= {'@id' => id}
4872

49-
input.keys.sort.each do |property|
50-
objects = input[property]
51-
case property
52-
when '@id'
53-
# Skip
54-
when '@reverse'
55-
# handle reverse properties
56-
referenced_node, reverse_map = {'@id' => name}, objects
57-
reverse_map.each do |reverse_property, items|
58-
items.each do |item|
59-
item_name = item['@id']
60-
item_name = namer.get_name(item_name) if blank_node?(item_name)
61-
create_node_map(item, graphs, graph: graph, name: item_name)
62-
add_value(graphs[graph][item_name],
63-
reverse_property,
64-
referenced_node,
65-
property_is_array: true,
66-
allow_duplicate: false)
67-
end
68-
end
69-
when '@graph'
70-
graphs[name] ||= {}
71-
create_node_map(objects, graphs, graph: name)
72-
when /^@(?!type)/
73-
# copy non-@type keywords
74-
if property == '@index' && subject['@index']
75-
raise JsonLdError::ConflictingIndexes,
76-
"Element already has index #{subject['@index']} dfferent from #{input['@index']}" if
77-
subject['@index'] != input['@index']
78-
subject['@index'] = input.delete('@index')
79-
end
80-
subject[property] = objects
73+
if active_subject.is_a?(Hash)
74+
# If subject is a hash, then we're processing a reverse-property relationship.
75+
add_value(node, active_property, active_subject, property_is_array: true, allow_duplicate: false)
76+
elsif active_property
77+
reference = {'@id' => id}
78+
if list.nil?
79+
add_value(subject_node, active_property, reference, property_is_array: true, allow_duplicate: false)
8180
else
82-
# if property is a bnode, assign it a new id
83-
property = namer.get_name(property) if blank_node?(property)
84-
85-
add_value(subject, property, [], property_is_array: true) if objects.empty?
81+
list['@list'] << reference
82+
end
83+
end
8684

87-
objects.each do |o|
88-
o = namer.get_name(o) if property == '@type' && blank_node?(o)
85+
if element.has_key?('@type')
86+
add_value(node, '@type', element.delete('@type'), property_is_array: true, allow_duplicate: false)
87+
end
8988

90-
case
91-
when node?(o) || node_reference?(o)
92-
id = o['@id']
93-
id = namer.get_name(id) if blank_node?(id)
89+
if element['@index']
90+
raise JsonLdError::ConflictingIndexes,
91+
"Element already has index #{node['@index']} dfferent from #{element['@index']}" if
92+
node.key?('@index') && node['@index'] != element['@index']
93+
node['@index'] = element.delete('@index')
94+
end
9495

95-
# add reference and recurse
96-
add_value(subject, property, {'@id' => id}, property_is_array: true, allow_duplicate: false)
97-
create_node_map(o, graphs, graph: graph, name: id)
98-
when list?(o)
99-
olist = []
100-
create_node_map(o['@list'], graphs, graph: graph, name: name, list: olist)
101-
o = {'@list' => olist}
102-
add_value(subject, property, o, property_is_array: true, allow_duplicate: true)
103-
else
104-
# handle @value
105-
create_node_map(o, graphs, graph: graph, name: name)
106-
add_value(subject, property, o, property_is_array: true, allow_duplicate: false)
107-
end
96+
if element['@reverse']
97+
referenced_node, reverse_map = {'@id' => id}, element.delete('@reverse')
98+
reverse_map.each do |property, values|
99+
values.each do |value|
100+
create_node_map(value, graph_map,
101+
active_graph: active_graph,
102+
active_subject: referenced_node,
103+
active_property: property)
108104
end
109105
end
110106
end
107+
108+
if element['@graph']
109+
create_node_map(element.delete('@graph'), graph_map,
110+
active_graph: id)
111+
end
112+
113+
element.keys.sort.each do |property|
114+
value = element[property]
115+
116+
property = namer.get_name(property) if blank_node?(property)
117+
node[property] ||= []
118+
create_node_map(value, graph_map,
119+
active_graph: active_graph,
120+
active_subject: id,
121+
active_property: property)
122+
end
111123
end
112-
else
113-
# add non-object to list
114-
list << input if list
115124
end
116125
end
117126

0 commit comments

Comments
 (0)