lib/json/ld/compact.rb in json-ld-3.2.4 vs lib/json/ld/compact.rb in json-ld-3.2.5

- old
+ new

@@ -1,356 +1,356 @@ -# -*- encoding: utf-8 -*- # frozen_string_literal: true -module JSON::LD - module Compact - include Utils - # The following constant is used to reduce object allocations in #compact below - CONTAINER_MAPPING_LANGUAGE_INDEX_ID_TYPE = Set.new(%w(@language @index @id @type)).freeze - EXPANDED_PROPERTY_DIRECTION_INDEX_LANGUAGE_VALUE = %w(@direction @index @language @value).freeze +module JSON + module LD + module Compact + include Utils - ## - # 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, RDF::URI] base (nil) - # Ensure output objects have keys ordered properly - # @param [String] property (nil) - # Extra validatation - # @return [Array, Hash] - def compact(element, - base: nil, - property: nil, - log_depth: nil) - # log_debug("compact", depth: log_depth.to_i) {"element: #{element.inspect}, ec: #{context.inspect}"} + # The following constant is used to reduce object allocations in #compact below + CONTAINER_MAPPING_LANGUAGE_INDEX_ID_TYPE = Set.new(%w[@language @index @id @type]).freeze + EXPANDED_PROPERTY_DIRECTION_INDEX_LANGUAGE_VALUE = %w[@direction @index @language @value].freeze - # If the term definition for active property itself contains a context, use that for compacting values. - input_context = self.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, RDF::URI] base (nil) + # Ensure output objects have keys ordered properly + # @param [String] property (nil) + # Extra validatation + # @return [Array, Hash] + def compact(element, + base: nil, + property: nil, + log_depth: nil) + # log_debug("compact", depth: log_depth.to_i) {"element: #{element.inspect}, ec: #{context.inspect}"} - case element - when Array - # log_debug("") {"Array #{element.inspect}"} - result = element.map do |item| - compact(item, base: base, property: property, log_depth: log_depth.to_i + 1) - end.compact + # If the term definition for active property itself contains a context, use that for compacting values. + input_context = context - # 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] - # log_debug("=> extract single element", depth: log_depth.to_i) {result.first.inspect} - result.first - else - # log_debug("=> array result", depth: log_depth.to_i) {result.inspect} - result - end - when Hash - # Otherwise element is a JSON object. + case element + when Array + # log_debug("") {"Array #{element.inspect}"} + result = element.map do |item| + compact(item, base: base, property: property, log_depth: log_depth.to_i + 1) + end.compact - # @null objects are used in framing - return nil if element.key?('@null') + # 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] + # log_debug("=> extract single element", depth: log_depth.to_i) {result.first.inspect} + result.first + else + # log_debug("=> array result", depth: log_depth.to_i) {result.inspect} + result + end + when Hash + # Otherwise element is a JSON object. - # Revert any previously type-scoped (non-preserved) context - if context.previous_context && !element.key?('@value') && element.keys != %w(@id) - # log_debug("revert ec", depth: log_depth.to_i) {"previous context: #{context.previous_context.inspect}"} - self.context = context.previous_context - end + # @null objects are used in framing + return nil if element.key?('@null') - # Look up term definintions from property using the original type-scoped context, if it exists, but apply them to the now current previous context - td = input_context.term_definitions[property] if property - if td && !td.context.nil? - self.context = context.parse(td.context, - override_protected: true) - # log_debug("prop-scoped", depth: log_depth.to_i) {"context: #{self.context.inspect}"} - end + # Revert any previously type-scoped (non-preserved) context + if context.previous_context && !element.key?('@value') && element.keys != %w[@id] + # log_debug("revert ec", depth: log_depth.to_i) {"previous context: #{context.previous_context.inspect}"} + self.context = context.previous_context + end - if (element.key?('@id') || element.key?('@value')) && !element.key?('@annotation') - result = context.compact_value(property, element, base: @options[:base]) - if !result.is_a?(Hash) || context.coerce(property) == '@json' - # log_debug("", depth: log_depth.to_i) {"=> scalar result: #{result.inspect}"} - return result + # Look up term definintions from property using the original type-scoped context, if it exists, but apply them to the now current previous context + td = input_context.term_definitions[property] if property + if td && !td.context.nil? + self.context = context.parse(td.context, + override_protected: true) + # log_debug("prop-scoped", depth: log_depth.to_i) {"context: #{self.context.inspect}"} 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).include?('@list') - return compact(element['@list'], base: base, - property: property, - log_depth: log_depth.to_i + 1) - end + if (element.key?('@id') || element.key?('@value')) && !element.key?('@annotation') + result = context.compact_value(property, element, base: @options[:base]) + if !result.is_a?(Hash) || context.coerce(property) == '@json' + # log_debug("", depth: log_depth.to_i) {"=> scalar result: #{result.inspect}"} + return result + end + end - inside_reverse = property == '@reverse' - result, nest_result = {}, nil + # 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).include?('@list') + return compact(element['@list'], base: base, + property: property, + log_depth: log_depth.to_i + 1) + end - # Apply any context defined on an alias of @type - # If key is @type and any compacted value is a term having a local context, overlay that context. - Array(element['@type']). - map {|expanded_type| context.compact_iri(expanded_type, vocab: true)}. - sort. - each do |term| - term_context = input_context.term_definitions[term].context if input_context.term_definitions[term] - self.context = context.parse(term_context, propagate: false) unless term_context.nil? - # log_debug("type-scoped", depth: log_depth.to_i) {"context: #{self.context.inspect}"} - end + inside_reverse = property == '@reverse' + result = {} + nest_result = nil - element.keys.opt_sort(ordered: @options[:ordered]).each do |expanded_property| - expanded_value = element[expanded_property] - # log_debug("", depth: log_depth.to_i) {"#{expanded_property}: #{expanded_value.inspect}"} + # Apply any context defined on an alias of @type + # If key is @type and any compacted value is a term having a local context, overlay that context. + Array(element['@type']) + .map { |expanded_type| context.compact_iri(expanded_type, vocab: true) } + .sort + .each do |term| + term_context = input_context.term_definitions[term].context if input_context.term_definitions[term] + self.context = context.parse(term_context, propagate: false) unless term_context.nil? + # log_debug("type-scoped", depth: log_depth.to_i) {"context: #{self.context.inspect}"} + end - if expanded_property == '@id' - compacted_value = as_array(expanded_value).map do |expanded_id| - if node?(expanded_id) && @options[:rdfstar] - # This can only really happen for valid RDF-star - compact(expanded_id, base: base, - property: '@id', - log_depth: log_depth.to_i + 1) - else - context.compact_iri(expanded_id, base: @options[:base]) + element.keys.opt_sort(ordered: @options[:ordered]).each do |expanded_property| + expanded_value = element[expanded_property] + # log_debug("", depth: log_depth.to_i) {"#{expanded_property}: #{expanded_value.inspect}"} + + if expanded_property == '@id' + compacted_value = as_array(expanded_value).map do |expanded_id| + if node?(expanded_id) && @options[:rdfstar] + # This can only really happen for valid RDF-star + compact(expanded_id, base: base, + property: '@id', + log_depth: log_depth.to_i + 1) + else + context.compact_iri(expanded_id, base: @options[:base]) + end end + + kw_alias = context.compact_iri('@id', vocab: true) + as_array = compacted_value.length > 1 + compacted_value = compacted_value.first unless as_array + result[kw_alias] = compacted_value + next end - kw_alias = context.compact_iri('@id', vocab: true) - as_array = compacted_value.length > 1 - compacted_value = compacted_value.first unless as_array - result[kw_alias] = compacted_value - next - end + if expanded_property == '@type' + compacted_value = Array(expanded_value).map do |expanded_type| + input_context.compact_iri(expanded_type, vocab: true) + end - if expanded_property == '@type' - compacted_value = Array(expanded_value).map do |expanded_type| - input_context.compact_iri(expanded_type, vocab: true) + kw_alias = context.compact_iri('@type', vocab: true) + as_array = compacted_value.length > 1 || + (context.as_array?(kw_alias) && + !value?(element) && + context.processingMode('json-ld-1.1')) + add_value(result, kw_alias, compacted_value, property_is_array: as_array) + next end - kw_alias = context.compact_iri('@type', vocab: true) - as_array = compacted_value.length > 1 || - (context.as_array?(kw_alias) && - !value?(element) && - context.processingMode('json-ld-1.1')) - add_value(result, kw_alias, compacted_value, property_is_array: as_array) - next - end + if expanded_property == '@reverse' + compacted_value = compact(expanded_value, base: base, + property: '@reverse', + log_depth: log_depth.to_i + 1) + # log_debug("@reverse", depth: log_depth.to_i) {"compacted_value: #{compacted_value.inspect}"} + # handle double-reversed properties + compacted_value.each do |prop, value| + next unless context.reverse?(prop) - if expanded_property == '@reverse' - compacted_value = compact(expanded_value, base: base, - property: '@reverse', - log_depth: log_depth.to_i + 1) - # log_debug("@reverse", depth: log_depth.to_i) {"compacted_value: #{compacted_value.inspect}"} - # handle double-reversed properties - compacted_value.each do |prop, value| - if context.reverse?(prop) add_value(result, prop, value, property_is_array: context.as_array?(prop) || !@options[:compactArrays]) compacted_value.delete(prop) end - end - unless compacted_value.empty? - al = context.compact_iri('@reverse', vocab: true) - # log_debug("", depth: log_depth.to_i) {"remainder: #{al} => #{compacted_value.inspect}"} - result[al] = compacted_value + unless compacted_value.empty? + al = context.compact_iri('@reverse', vocab: true) + # log_debug("", depth: log_depth.to_i) {"remainder: #{al} => #{compacted_value.inspect}"} + result[al] = compacted_value + end + next end - next - end - if expanded_property == '@preserve' - # Compact using `property` - compacted_value = compact(expanded_value, base: base, - property: property, - log_depth: log_depth.to_i + 1) - # log_debug("@preserve", depth: log_depth.to_i) {"compacted_value: #{compacted_value.inspect}"} + if expanded_property == '@preserve' + # Compact using `property` + compacted_value = compact(expanded_value, base: base, + property: property, + log_depth: log_depth.to_i + 1) + # log_debug("@preserve", depth: log_depth.to_i) {"compacted_value: #{compacted_value.inspect}"} - unless compacted_value.is_a?(Array) && compacted_value.empty? - result['@preserve'] = compacted_value + result['@preserve'] = compacted_value unless compacted_value.is_a?(Array) && compacted_value.empty? + next end - next - end - if expanded_property == '@index' && context.container(property).include?('@index') - # log_debug("@index", depth: log_depth.to_i) {"drop @index"} - next - end + if expanded_property == '@index' && context.container(property).include?('@index') + # log_debug("@index", depth: log_depth.to_i) {"drop @index"} + next + end - # Otherwise, if expanded property is @direction, @index, @value, or @language: - if EXPANDED_PROPERTY_DIRECTION_INDEX_LANGUAGE_VALUE.include?(expanded_property) - al = context.compact_iri(expanded_property, vocab: true) - # log_debug(expanded_property, depth: log_depth.to_i) {"#{al} => #{expanded_value.inspect}"} - result[al] = expanded_value - next - end - - if expanded_value.empty? - item_active_property = - context.compact_iri(expanded_property, - value: expanded_value, - vocab: true, - reverse: inside_reverse) - - if nest_prop = context.nest(item_active_property) - result[nest_prop] ||= {} - add_value(result[nest_prop], item_active_property, [], - property_is_array: true) - else - add_value(result, item_active_property, [], - property_is_array: true) + # Otherwise, if expanded property is @direction, @index, @value, or @language: + if EXPANDED_PROPERTY_DIRECTION_INDEX_LANGUAGE_VALUE.include?(expanded_property) + al = context.compact_iri(expanded_property, vocab: true) + # log_debug(expanded_property, depth: log_depth.to_i) {"#{al} => #{expanded_value.inspect}"} + result[al] = expanded_value + next end - end - # At this point, expanded value must be an array due to the Expansion algorithm. - expanded_value.each do |expanded_item| - item_active_property = - context.compact_iri(expanded_property, - value: expanded_item, - vocab: true, - reverse: inside_reverse) + if expanded_value.empty? + item_active_property = + context.compact_iri(expanded_property, + value: expanded_value, + vocab: true, + reverse: inside_reverse) - - nest_result = if nest_prop = context.nest(item_active_property) - # FIXME??: It's possible that nest_prop will be used both for nesting, and for values of @nest - result[nest_prop] ||= {} - else - result + if (nest_prop = context.nest(item_active_property)) + result[nest_prop] ||= {} + add_value(result[nest_prop], item_active_property, [], + property_is_array: true) + else + add_value(result, item_active_property, [], + property_is_array: true) + end end - container = context.container(item_active_property) - as_array = !@options[:compactArrays] || context.as_array?(item_active_property) + # At this point, expanded value must be an array due to the Expansion algorithm. + expanded_value.each do |expanded_item| + item_active_property = + context.compact_iri(expanded_property, + value: expanded_item, + vocab: true, + reverse: inside_reverse) - value = case - when list?(expanded_item) then expanded_item['@list'] - when graph?(expanded_item) then expanded_item['@graph'] - else expanded_item - end + nest_result = if (nest_prop = context.nest(item_active_property)) + # FIXME??: It's possible that nest_prop will be used both for nesting, and for values of @nest + result[nest_prop] ||= {} + else + result + end - compacted_item = compact(value, base: base, - property: item_active_property, - log_depth: log_depth.to_i + 1) - # log_debug("", depth: log_depth.to_i) {" => compacted key: #{item_active_property.inspect} for #{compacted_item.inspect}"} + container = context.container(item_active_property) + as_array = !@options[:compactArrays] || context.as_array?(item_active_property) - # handle @list - if list?(expanded_item) - compacted_item = as_array(compacted_item) - unless container.include?('@list') - al = context.compact_iri('@list', vocab: true) - compacted_item = {al => compacted_item} - if expanded_item.key?('@index') - key = context.compact_iri('@index', vocab: true) - compacted_item[key] = expanded_item['@index'] - end + value = if list?(expanded_item) + expanded_item['@list'] + elsif graph?(expanded_item) + expanded_item['@graph'] else - add_value(nest_result, item_active_property, compacted_item, - value_is_array: true, allow_duplicate: true) + expanded_item + end + + compacted_item = compact(value, base: base, + property: item_active_property, + log_depth: log_depth.to_i + 1) + # log_debug("", depth: log_depth.to_i) {" => compacted key: #{item_active_property.inspect} for #{compacted_item.inspect}"} + + # handle @list + if list?(expanded_item) + compacted_item = as_array(compacted_item) + if container.include?('@list') + add_value(nest_result, item_active_property, compacted_item, + value_is_array: true, allow_duplicate: true) next + else + al = context.compact_iri('@list', vocab: true) + compacted_item = { al => compacted_item } + if expanded_item.key?('@index') + key = context.compact_iri('@index', vocab: true) + compacted_item[key] = expanded_item['@index'] + end + end end - end - # Graph object compaction cases: - if graph?(expanded_item) - if container.include?('@graph') && - (container.include?('@id') || container.include?('@index') && simple_graph?(expanded_item)) - # container includes @graph and @id - map_object = nest_result[item_active_property] ||= {} - # If there is no @id, create a blank node identifier to use as an index - map_key = if container.include?('@id') && expanded_item['@id'] - context.compact_iri(expanded_item['@id'], base: @options[:base]) - elsif container.include?('@index') && expanded_item['@index'] - context.compact_iri(expanded_item['@index'], vocab: true) + # Graph object compaction cases: + if graph?(expanded_item) + if container.include?('@graph') && + (container.include?('@id') || (container.include?('@index') && simple_graph?(expanded_item))) + # container includes @graph and @id + map_object = nest_result[item_active_property] ||= {} + # If there is no @id, create a blank node identifier to use as an index + map_key = if container.include?('@id') && expanded_item['@id'] + context.compact_iri(expanded_item['@id'], base: @options[:base]) + elsif container.include?('@index') && expanded_item['@index'] + context.compact_iri(expanded_item['@index'], vocab: true) + else + context.compact_iri('@none', vocab: true) + end + add_value(map_object, map_key, compacted_item, + property_is_array: as_array) + elsif container.include?('@graph') && simple_graph?(expanded_item) + # container includes @graph but not @id or @index and value is a simple graph object + if compacted_item.is_a?(Array) && compacted_item.length > 1 + # Mutple objects in the same graph can't be represented directly, as they would be interpreted as two different graphs. Need to wrap in @included. + included_key = context.compact_iri('@included', vocab: true) + compacted_item = { included_key => compacted_item } + end + # Drop through, where compacted_item will be added + add_value(nest_result, item_active_property, compacted_item, + property_is_array: as_array) else - context.compact_iri('@none', vocab: true) + # container does not include @graph or otherwise does not match one of the previous cases, redo compacted_item + al = context.compact_iri('@graph', vocab: true) + compacted_item = { al => compacted_item } + if expanded_item['@id'] + al = context.compact_iri('@id', vocab: true) + compacted_item[al] = context.compact_iri(expanded_item['@id'], vocab: false) + end + if expanded_item.key?('@index') + key = context.compact_iri('@index', vocab: true) + compacted_item[key] = expanded_item['@index'] + end + add_value(nest_result, item_active_property, compacted_item, + property_is_array: as_array) end + elsif container.intersect?(CONTAINER_MAPPING_LANGUAGE_INDEX_ID_TYPE) && !container.include?('@graph') + map_object = nest_result[item_active_property] ||= {} + c = container.first + container_key = context.compact_iri(c, vocab: true) + compacted_item = if container.include?('@id') + map_key = compacted_item[container_key] + compacted_item.delete(container_key) + compacted_item + elsif container.include?('@index') + index_key = context.term_definitions[item_active_property].index || '@index' + if index_key == '@index' + map_key = expanded_item['@index'] + else + index_key = context.expand_iri(index_key, vocab: true) + container_key = context.compact_iri(index_key, vocab: true) + map_key, *others = Array(compacted_item[container_key]) + if map_key.is_a?(String) + case others.length + when 0 then compacted_item.delete(container_key) + when 1 then compacted_item[container_key] = others.first + else compacted_item[container_key] = others + end + else + map_key = context.compact_iri('@none', vocab: true) + end + end + # Note, if compacted_item is a node reference and key is @id-valued, then this could be compacted further. + compacted_item + elsif container.include?('@language') + map_key = expanded_item['@language'] + value?(expanded_item) ? expanded_item['@value'] : compacted_item + elsif container.include?('@type') + map_key, *types = Array(compacted_item[container_key]) + case types.length + when 0 then compacted_item.delete(container_key) + when 1 then compacted_item[container_key] = types.first + else compacted_item[container_key] = types + end + + # if compacted_item contains a single entry who's key maps to @id, then recompact the item without @type + if compacted_item.keys.length == 1 && expanded_item.key?('@id') + compacted_item = compact({ '@id' => expanded_item['@id'] }, + base: base, + property: item_active_property, + log_depth: log_depth.to_i + 1) + end + compacted_item + end + map_key ||= context.compact_iri('@none', vocab: true) add_value(map_object, map_key, compacted_item, property_is_array: as_array) - elsif container.include?('@graph') && simple_graph?(expanded_item) - # container includes @graph but not @id or @index and value is a simple graph object - if compacted_item.is_a?(Array) && compacted_item.length > 1 - # Mutple objects in the same graph can't be represented directly, as they would be interpreted as two different graphs. Need to wrap in @included. - included_key = context.compact_iri('@included', vocab: true) - compacted_item = {included_key => compacted_item} - end - # Drop through, where compacted_item will be added - add_value(nest_result, item_active_property, compacted_item, - property_is_array: as_array) else - # container does not include @graph or otherwise does not match one of the previous cases, redo compacted_item - al = context.compact_iri('@graph', vocab: true) - compacted_item = {al => compacted_item} - if expanded_item['@id'] - al = context.compact_iri('@id', vocab: true) - compacted_item[al] = context.compact_iri(expanded_item['@id'], vocab: false) - end - if expanded_item.key?('@index') - key = context.compact_iri('@index', vocab: true) - compacted_item[key] = expanded_item['@index'] - end add_value(nest_result, item_active_property, compacted_item, property_is_array: as_array) end - elsif container.intersect?(CONTAINER_MAPPING_LANGUAGE_INDEX_ID_TYPE) && !container.include?('@graph') - map_object = nest_result[item_active_property] ||= {} - c = container.first - container_key = context.compact_iri(c, vocab: true) - compacted_item = case - when container.include?('@id') - map_key = compacted_item[container_key] - compacted_item.delete(container_key) - compacted_item - when container.include?('@index') - index_key = context.term_definitions[item_active_property].index || '@index' - if index_key == '@index' - map_key = expanded_item['@index'] - else - index_key = context.expand_iri(index_key, vocab: true) - container_key = context.compact_iri(index_key, vocab: true) - map_key, *others = Array(compacted_item[container_key]) - if map_key.is_a?(String) - case others.length - when 0 then compacted_item.delete(container_key) - when 1 then compacted_item[container_key] = others.first - else compacted_item[container_key] = others - end - else - map_key = context.compact_iri('@none', vocab: true) - end - end - # Note, if compacted_item is a node reference and key is @id-valued, then this could be compacted further. - compacted_item - when container.include?('@language') - map_key = expanded_item['@language'] - value?(expanded_item) ? expanded_item['@value'] : compacted_item - when container.include?('@type') - map_key, *types = Array(compacted_item[container_key]) - case types.length - when 0 then compacted_item.delete(container_key) - when 1 then compacted_item[container_key] = types.first - else compacted_item[container_key] = types - end - - # if compacted_item contains a single entry who's key maps to @id, then recompact the item without @type - if compacted_item.keys.length == 1 && expanded_item.keys.include?('@id') - compacted_item = compact({'@id' => expanded_item['@id']}, - base: base, - property: item_active_property, - log_depth: log_depth.to_i + 1) - end - compacted_item - end - map_key ||= context.compact_iri('@none', vocab: true) - add_value(map_object, map_key, compacted_item, - property_is_array: as_array) - else - add_value(nest_result, item_active_property, compacted_item, - property_is_array: as_array) end end - end - result - else - # For other types, the compacted value is the element value - # log_debug("compact", depth: log_depth.to_i) {element.class.to_s} - element + result + else + # For other types, the compacted value is the element value + # log_debug("compact", depth: log_depth.to_i) {element.class.to_s} + element + end + ensure + self.context = input_context end - - ensure - self.context = input_context end end end