Skip to content

Commit

Permalink
Support for lists of lists.
Browse files Browse the repository at this point in the history
  • Loading branch information
gkellogg committed Aug 3, 2018
1 parent 164f89f commit c1d11a1
Show file tree
Hide file tree
Showing 26 changed files with 605 additions and 379 deletions.
2 changes: 0 additions & 2 deletions lib/json/ld.rb
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ def code
end

class CollidingKeywords < JsonLdError; @code = "colliding keywords"; end
class CompactionToListOfLists < JsonLdError; @code = "compaction to list of lists"; end
class ConflictingIndexes < JsonLdError; @code = "conflicting indexes"; end
class CyclicIRIMapping < JsonLdError; @code = "cyclic IRI mapping"; end
class InvalidBaseIRI < JsonLdError; @code = "invalid base IRI"; end
Expand Down Expand Up @@ -133,7 +132,6 @@ class InvalidValueObject < JsonLdError; @code = "invalid value object"; end
class InvalidValueObjectValue < JsonLdError; @code = "invalid value object value"; end
class InvalidVocabMapping < JsonLdError; @code = "invalid vocab mapping"; end
class KeywordRedefinition < JsonLdError; @code = "keyword redefinition"; end
class ListOfLists < JsonLdError; @code = "list of lists"; end
class LoadingDocumentFailed < JsonLdError; @code = "loading document failed"; end
class LoadingRemoteContextFailed < JsonLdError; @code = "loading remote context failed"; end
class MultipleContextLinkHeaders < JsonLdError; @code = "multiple context link headers"; end
Expand Down
4 changes: 2 additions & 2 deletions lib/json/ld/api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ def self.frame(input, frame, expanded: false, **options)
log_debug(".frame") {"expanded frame: #{expanded_frame.to_json(JSON_STATE) rescue 'malformed json'}"}

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

frame_keys = frame.keys.map {|k| context.expand_iri(k, vocab: true, quiet: true)}
if frame_keys.include?('@graph')
Expand Down Expand Up @@ -482,7 +482,7 @@ def self.toRdf(input, expanded: false, **options, &block)
#
# The resulting `Array` is either returned or yielded, if a block is given.
#
# @param [Array<RDF::Statement>] input
# @param [RDF::Enumerable] input
# @param [Hash{Symbol => Object}] options
# @option options (see #initialize)
# @option options [Boolean] :useRdfType (false)
Expand Down
15 changes: 11 additions & 4 deletions lib/json/ld/compact.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ def compact(element, property: nil)
# If element has a single member and the active property has no
# @container mapping to @list or @set, the compacted value is that
# member; otherwise the compacted value is element
if result.length == 1 && !context.as_array?(property) && @options[:compactArrays]
if result.length == 1 &&
!context.as_array?(property) && @options[:compactArrays]
#log_debug("=> extract single element: #{result.first.inspect}")
result.first
else
Expand All @@ -51,6 +52,12 @@ def compact(element, property: nil)
end
end

# If expanded property is @list and we're contained within a list container, recursively compact this item to an array
if list?(element) && context.container(property) == %w(@list)
return compact(element['@list'], property: property)
end


inside_reverse = property == '@reverse'
result, nest_result = {}, nil

Expand Down Expand Up @@ -183,9 +190,9 @@ def compact(element, property: nil)
compacted_item[key] = expanded_item['@index']
end
else
raise JsonLdError::CompactionToListOfLists,
"key cannot have more than one list value" if nest_result.has_key?(item_active_property)
# Falls through to add list value below
add_value(nest_result, item_active_property, compacted_item,
value_is_array: true, allow_duplicate: true)
next
end
end

Expand Down
12 changes: 3 additions & 9 deletions lib/json/ld/expand.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,9 @@ def expand(input, active_property, context, ordered: true, framing: false)
# Initialize expanded item to the result of using this algorithm recursively, passing active context, active property, and item as element.
v = expand(v, active_property, context, ordered: ordered, framing: framing)

# 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.
raise JsonLdError::ListOfLists,
"A list may not contain another list" if
is_list && (v.is_a?(Array) || list?(v))
# 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
v = {"@list" => v} if is_list && v.is_a?(Array)

case v
when nil then nil
when Array then memo.concat(v)
Expand Down Expand Up @@ -283,11 +282,6 @@ def expand_object(input, active_property, context, output_object, ordered:, fram
# Spec FIXME: need to be sure that result is an array
value = as_array(value)

# If expanded value is a list object, a list of lists error has been detected and processing is aborted.
# Spec FIXME: Also look at each object if result is an array
raise JsonLdError::ListOfLists,
"A list may not contain another list" if value.any? {|v| list?(v)}

value
when '@set'
# 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.
Expand Down
189 changes: 99 additions & 90 deletions lib/json/ld/flatten.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,111 +7,120 @@ module Flatten
##
# 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.
#
# @param [Array, Hash] input
# @param [Array, Hash] element
# Expanded JSON-LD input
# @param [Hash] graphs A map of graph name to subjects
# @param [String] graph
# @param [Hash] graph_map A map of graph name to subjects
# @param [String] active_graph
# The name of the currently active graph that the processor should use when processing.
# @param [String] name
# The name assigned to the current input if it is a bnode
# @param [Array] list
# List to append to, nil for none
def create_node_map(input, graphs, graph: '@default', name: nil, list: nil)
#log_debug("node_map") {"graph: #{graph}, input: #{input.inspect}, name: #{name}"}
case input
when Array
# 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.
input.map {|o| create_node_map(o, graphs, graph: graph, list: list)}
when Hash
type = input['@type']
if value?(input)
# Rename blanknode @type
input['@type'] = namer.get_name(type) if type && blank_node?(type)
list << input if list
else
# Input is a node definition
# @param [String] active_subject (nil)
# Node identifier
# @param [String] active_property (nil)
# Property within current node
# @param [Array] list (nil)
# Used when property value is a list
def create_node_map(element, graph_map,
active_graph: '@default',
active_subject: nil,
active_property: nil,
list: nil)
log_debug("node_map") {"active_graph: #{active_graph}, element: #{element.inspect}, active_subject: #{active_subject}"}
if element.is_a?(Array)
# 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.
element.map do |o|
create_node_map(o, graph_map,
active_graph: active_graph,
active_subject: active_subject,
active_property: active_property,
list: list)
end
elsif !element.is_a?(Hash)
raise "Expected hash or array to create_node_map, got #{element.inspect}"
else
graph = (graph_map[active_graph] ||= {})
subject_node = graph[active_subject]

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

# get name for subject
if name.nil?
name ||= input['@id']
name = namer.get_name(name) if blank_node?(name)
if value?(element)
element['@type'] = element['@type'].first if element ['@type']
if list.nil?
add_value(subject_node, active_property, element, property_is_array: true, allow_duplicate: false)
else
list['@list'] << element
end
elsif list?(element)
result = {'@list' => []}
create_node_map(element['@list'], graph_map,
active_graph: active_graph,
active_subject: active_subject,
active_property: active_property,
list: result)
if list.nil?
add_value(subject_node, active_property, result, property_is_array: true)
else
list['@list'] << result
end
else
# Element is a node object
id = element.delete('@id')
id = namer.get_name(id) if blank_node?(id)

# add subject reference to list
list << {'@id' => name} if list

# create new subject or merge into existing one
subject = (graphs[graph] ||= {})[name] ||= {'@id' => name}
node = graph[id] ||= {'@id' => id}

input.keys.sort.each do |property|
objects = input[property]
case property
when '@id'
# Skip
when '@reverse'
# handle reverse properties
referenced_node, reverse_map = {'@id' => name}, objects
reverse_map.each do |reverse_property, items|
items.each do |item|
item_name = item['@id']
item_name = namer.get_name(item_name) if blank_node?(item_name)
create_node_map(item, graphs, graph: graph, name: item_name)
add_value(graphs[graph][item_name],
reverse_property,
referenced_node,
property_is_array: true,
allow_duplicate: false)
end
end
when '@graph'
graphs[name] ||= {}
create_node_map(objects, graphs, graph: name)
when /^@(?!type)/
# copy non-@type keywords
if property == '@index' && subject['@index']
raise JsonLdError::ConflictingIndexes,
"Element already has index #{subject['@index']} dfferent from #{input['@index']}" if
subject['@index'] != input['@index']
subject['@index'] = input.delete('@index')
end
subject[property] = objects
if active_subject.is_a?(Hash)
# If subject is a hash, then we're processing a reverse-property relationship.
add_value(node, active_property, active_subject, property_is_array: true, allow_duplicate: false)
elsif active_property
reference = {'@id' => id}
if list.nil?
add_value(subject_node, active_property, reference, property_is_array: true, allow_duplicate: false)
else
# if property is a bnode, assign it a new id
property = namer.get_name(property) if blank_node?(property)

add_value(subject, property, [], property_is_array: true) if objects.empty?
list['@list'] << reference
end
end

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

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

# add reference and recurse
add_value(subject, property, {'@id' => id}, property_is_array: true, allow_duplicate: false)
create_node_map(o, graphs, graph: graph, name: id)
when list?(o)
olist = []
create_node_map(o['@list'], graphs, graph: graph, name: name, list: olist)
o = {'@list' => olist}
add_value(subject, property, o, property_is_array: true, allow_duplicate: true)
else
# handle @value
create_node_map(o, graphs, graph: graph, name: name)
add_value(subject, property, o, property_is_array: true, allow_duplicate: false)
end
if element['@reverse']
referenced_node, reverse_map = {'@id' => id}, element.delete('@reverse')
reverse_map.each do |property, values|
values.each do |value|
create_node_map(value, graph_map,
active_graph: active_graph,
active_subject: referenced_node,
active_property: property)
end
end
end

if element['@graph']
create_node_map(element.delete('@graph'), graph_map,
active_graph: id)
end

element.keys.sort.each do |property|
value = element[property]

property = namer.get_name(property) if blank_node?(property)
node[property] ||= []
create_node_map(value, graph_map,
active_graph: active_graph,
active_subject: id,
active_property: property)
end
end
else
# add non-object to list
list << input if list
end
end

Expand Down
Loading

0 comments on commit c1d11a1

Please sign in to comment.