lib/json/ld/compact.rb in json-ld-0.9.1 vs lib/json/ld/compact.rb in json-ld-1.0.0

- old
+ new

@@ -1,11 +1,11 @@ module JSON::LD module Compact include Utils ## - # Compact an expanded Array or Hash given an active property and a context. + # This algorithm compacts a JSON-LD document, such that the given context is applied. This must result in shortening any applicable IRIs to terms or compact IRIs, any applicable keywords to keyword aliases, and any applicable JSON-LD values expressed in expanded form to simple values such as strings or numbers. # # @param [Array, Hash] element # @param [String] property (nil) # @return [Array, Hash] def compact(element, property = nil) @@ -14,181 +14,145 @@ else debug("compact") {"property: #{property.inspect}"} end case element when Array - # 1) If value is an array, process each item in value recursively using - # this algorithm, passing copies of the active context and the - # active property. - debug("compact") {"Array #{element.inspect}"} - result = depth {element.map {|v| compact(v, property)}} + debug("") {"Array #{element.inspect}"} + result = depth {element.map {|item| compact(item, property)}.compact} # 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 && @options[:compactArrays] + if result.length == 1 && context.container(property).nil? && @options[:compactArrays] debug("=> extract single element: #{result.first.inspect}") result.first else debug("=> array result: #{result.inspect}") result end when Hash - # 2) Otherwise, if element is an object: - result = {} + # Otherwise element is a JSON object. - if k = %w(@list @set @value).detect {|container| element.has_key?(container)} - debug("compact") {"#{k}: container(#{property}) = #{context.container(property)}"} - end + # @null objects are used in framing + return nil if element.has_key?('@null') - k ||= '@id' if element.keys == ['@id'] - - case k - when '@value', '@id' - # If element has an @value property or element is a node reference, return the result of performing Value Compaction on element using active property. - v = context.compact_value(property, element, :depth => @depth) - debug("compact") {"value optimization for #{property}, return as #{v.inspect}"} - return v - when '@list' - # Otherwise, if the active property has a @container mapping to @list and element has a corresponding @list property, recursively compact that property's value passing a copy of the active context and the active property ensuring that the result is an array with all null values removed. - - # If there already exists a value for active property in element and the full IRI of property is also coerced to @list, return an error. - # FIXME: check for full-iri list coercion - - # Otherwise store the resulting array as value of active property if empty or property otherwise. - compacted_key = context.compact_iri('@list', :position => :predicate, :depth => @depth) - v = depth { compact(element[k], property) } - - # Return either the result as an array, as an object with a key of @list (or appropriate alias from active context - v = [v].compact unless v.is_a?(Array) - unless context.container(property) == '@list' - v = {compacted_key => v} - if element['@index'] - compacted_key = context.compact_iri('@index', :position => :predicate, :depth => @depth) - v[compacted_key] = element['@index'] - end + if element.keys.any? {|k| %w(@id @value).include?(k)} + result = context.compact_value(property, element, :depth => @depth) + unless result.is_a?(Hash) + debug("") {"=> scalar result: #{result.inspect}"} + return result end - debug("compact") {"@list result, return as #{v.inspect}"} - return v end - # Check for property generators before continuing with other elements - # For each term pg in the active context which is a property generator - # Select property generator terms by shortest term - context.mappings.keys.sort.each do |term| - next unless context.mapping(term).is_a?(Array) - # Using the first expanded IRI p associated with the property generator - expanded_iris = context.mapping(term).map(&:to_s) - p = expanded_iris.first.to_s + inside_reverse = property == '@reverse' + result = {} - # Skip to the next property generator term unless p is a property of element - next unless element.has_key?(p) + element.keys.each do |expanded_property| + expanded_value = element[expanded_property] + debug("") {"#{expanded_property}: #{expanded_value.inspect}"} - debug("compact") {"check pg #{term}: #{expanded_iris}"} + if %w(@id @type).include?(expanded_property) + compacted_value = [expanded_value].flatten.compact.map do |expanded_type| + depth {context.compact_iri(expanded_type, :vocab => (expanded_property == '@type'), :depth => @depth)} + end + compacted_value = compacted_value.first if compacted_value.length == 1 - # For each node n which is a value of p in element - node_values = [] - element[p].dup.each do |n| - # For each expanded IRI pi associated with the property generator other than p - next unless expanded_iris[1..-1].all? do |pi| - debug("compact") {"check #{pi} for (#{n.inspect})"} - element.has_key?(pi) && element[pi].any? do |ni| - nodesEquivalent?(n, ni) + al = context.compact_iri(expanded_property, :vocab => true, :quiet => true) + debug(expanded_property) {"result[#{al}] = #{compacted_value.inspect}"} + result[al] = compacted_value + next + end + + if expanded_property == '@reverse' + compacted_value = depth {compact(expanded_value, '@reverse')} + debug("@reverse") {"compacted_value: #{compacted_value.inspect}"} + compacted_value.each do |prop, value| + if context.reverse?(prop) + value = [value] unless value.is_a?(Array) || @options[:compactArrays] + debug("") {"merge #{prop} => #{value.inspect}"} + merge_compacted_value(result, prop, value) + compacted_value.delete(prop) end end - # Remove n as a value of all p and pi in element - debug("compact") {"removed matched value #{n.inspect} from #{expanded_iris.inspect}"} - expanded_iris.each do |pi| - # FIXME: This removes all values equivalent to n, not just the first - element[pi] = element[pi].reject {|ni| nodesEquivalent?(n, ni)} + unless compacted_value.empty? + al = context.compact_iri('@reverse', :quiet => true) + debug("") {"remainder: #{al} => #{compacted_value.inspect}"} + result[al] = compacted_value end + next + end - # Add the result of performing the compaction algorithm on n to pg to output - node_values << n + if expanded_property == '@index' && context.container(property) == '@index' + debug("@index") {"drop @index"} + next end - # If there are node_values, or all the values from expanded_iris are empty, add node_values to result, and remove the expanded_iris as keys from element - if node_values.length > 0 || expanded_iris.all? {|pi| element.has_key?(pi) && element[pi].empty?} - debug("compact") {"compact extracted pg values"} - result[term] = depth { compact(node_values, term)} - result[term] = [result[term]] if !result[term].is_a?(Array) && context.container(term) == '@set' + # Otherwise, if expanded property is @index, @value, or @language: + if %w(@index @value @language).include?(expanded_property) + al = context.compact_iri(expanded_property, :vocab => true, :quiet => true) + debug(expanded_property) {"#{al} => #{expanded_value.inspect}"} + result[al] = expanded_value + next + end - debug("compact") {"remove empty pg keys from element"} - expanded_iris.each do |pi| - debug(" =>") {"#{pi}? #{element.fetch(pi, []).empty?}"} - element.delete(pi) if element.fetch(pi, []).empty? + if expanded_value == [] + item_active_property = depth do + context.compact_iri(expanded_property, + :value => expanded_value, + :vocab => true, + :reverse => inside_reverse, + :depth => @depth) end + + iap = result[item_active_property] ||= [] + result[item_active_property] = [iap] unless iap.is_a?(Array) end - end - # Otherwise, for each property and value in element: - element.each do |key, value| - debug("compact") {"#{key}: #{value.inspect}"} - - if %(@id @type).include?(key) - position = key == '@id' ? :subject : :type - compacted_key = context.compact_iri(key, :position => :predicate, :depth => @depth) - - result[compacted_key] = case value - when String - # If value is a string, the compacted value is the result of performing IRI Compaction on value. - debug {" => compacted string for #{key}"} - context.compact_iri(value, :position => position, :depth => @depth) - when Array - # Otherwise, value must be an array. Perform IRI Compaction on every entry of value. If value contains just one entry, value is set to that entry - compacted_value = value.map {|v2| context.compact_iri(v2, :position => position, :depth => @depth)} - debug {" => compacted value(#{key}): #{compacted_value.inspect}"} - compacted_value = compacted_value.first if compacted_value.length == 1 && @options[:compactArrays] - compacted_value + # At this point, expanded value must be an array due to the Expansion algorithm. + expanded_value.each do |expanded_item| + item_active_property = depth do + context.compact_iri(expanded_property, + :value => expanded_item, + :vocab => true, + :reverse => inside_reverse, + :depth => @depth) end - elsif key == '@index' && context.container(property) == '@index' - # Skip the annotation key if annotations being applied - next - else - if value.empty? - # Make sure that an empty array is preserved - compacted_key = context.compact_iri(key, :position => :predicate, :depth => @depth) - next if compacted_key.nil? - result[compacted_key] = value - next - end + container = context.container(item_active_property) + value = list?(expanded_item) ? expanded_item['@list'] : expanded_item + compacted_item = depth {compact(value, item_active_property)} + debug("") {" => compacted key: #{item_active_property.inspect} for #{compacted_item.inspect}"} - # For each item in value: - value = [value] if key == '@index' && value.is_a?(String) - raise ProcessingError, "found #{value.inspect} for #{key} of #{element.inspect}" unless value.is_a?(Array) - value.each do |item| - compacted_key = context.compact_iri(key, :position => :predicate, :value => item, :depth => @depth) - - # Result for this item, typically the output object itself - item_result = result - item_key = compacted_key - debug {" => compacted key: #{compacted_key.inspect} for #{item.inspect}"} - next if compacted_key.nil? - - # Language maps and annotations - if field = %w(@language @index).detect {|kk| context.container(compacted_key) == kk} - item_result = result[compacted_key] ||= Hash.new - item_key = item[field] - end - - compacted_item = depth {self.compact(item, compacted_key)} - debug {" => compacted value: #{compacted_value.inspect}"} - - case item_result[item_key] - when Array - item_result[item_key] << compacted_item - when nil - if !compacted_value.is_a?(Array) && context.container(compacted_key) == '@set' - compacted_item = [compacted_item].compact - debug {" => as @set: #{compacted_item.inspect}"} + if list?(expanded_item) + compacted_item = [compacted_item] unless compacted_item.is_a?(Array) + unless container == '@list' + al = context.compact_iri('@list', :vocab => true, :quiet => true) + compacted_item = {al => compacted_item} + if expanded_item.has_key?('@index') + key = context.compact_iri('@index', :vocab => true, :quiet => true) + compacted_item[key] = expanded_item['@index'] end - item_result[item_key] = compacted_item else - item_result[item_key] = [item_result[item_key], compacted_item] + raise ProcessingError::CompactionToListOfLists, + "key cannot have more than one list value" if result.has_key?(item_active_property) end end + + if %w(@language @index).include?(container) + map_object = result[item_active_property] ||= {} + compacted_item = compacted_item['@value'] if container == '@language' && value?(compacted_item) + map_key = expanded_item[container] + merge_compacted_value(map_object, map_key, compacted_item) + else + compacted_item = [compacted_item] if + !compacted_item.is_a?(Array) && ( + !@options[:compactArrays] || + %w(@set @list).include?(container) || + %w(@list @graph).include?(expanded_property) + ) + merge_compacted_value(result, item_active_property, compacted_item) + end end end # Re-order result keys r = Hash.ordered @@ -196,40 +160,9 @@ r else # For other types, the compacted value is the element value debug("compact") {element.class.to_s} element - end - end - - private - - # Determines if two nodes are equivalent. - # * Value nodes are equivalent using a deep comparison - # * Arrays are equivalent if they have the same number of elements and each element is equivalent to the matching element - # * Node Defintions/References are equivalent IFF the have the same @id - def nodesEquivalent?(n1, n2) - depth do - r = if n1.is_a?(Array) && n2.is_a?(Array) && n1.length == n2.length - equiv = true - n1.each_with_index do |v1, i| - equiv &&= nodesEquivalent?(v1, n2[i]) if equiv - end - equiv - elsif value?(n1) && value?(n2) - n1 == n2 - elsif list?(n1) - list?(n2) && - n1.fetch('@index', true) == n2.fetch('@index', true) && - nodesEquivalent?(n1['@list'], n2['@list']) - elsif (node?(n1) || node_reference?(n2)) - (node?(n2) || node_reference?(n2)) && n1['@id'] == n2['@id'] - else - false - end - - debug("nodesEquivalent?(#{n1.inspect}, #{n2.inspect}): #{r.inspect}") - r end end end end