lib/json/ld/evaluation_context.rb in json-ld-0.1.0 vs lib/json/ld/evaluation_context.rb in json-ld-0.1.2
- old
+ new
@@ -2,10 +2,12 @@
require 'json'
require 'bigdecimal'
module JSON::LD
class EvaluationContext # :nodoc:
+ include Utils
+
# The base.
#
# The document base IRI, used for expanding relative IRIs.
#
# @attr_reader [RDF::URI]
@@ -37,21 +39,34 @@
# @attr [Hash{String => String}]
attr :coercions, true
# List coercion
#
- # The @list keyword is used to specify that properties having an array value are to be treated
- # as an ordered list, rather than a normal unordered list
- # @attr [Hash{String => true}]
- attr :lists, true
+ # The @container keyword is used to specify how arrays are to be treated.
+ # A value of @list indicates that arrays of values are to be treated as an ordered list.
+ # A value of @set indicates that arrays are to be treated as unordered and that
+ # singular values are always coerced to an array form on expansion and compaction.
+ # @attr [Hash{String => String}]
+ attr :containers, true
+ # Language coercion
+ #
+ # The @language keyword is used to specify language coercion rules for the data. For each key in the map, the
+ # key is a String representation of the property for which String values will be coerced and
+ # the value is the language to coerce to. If no property-specific language is given,
+ # any default language from the context is used.
+ #
+ # @attr [Hash{String => String}]
+ attr :languages, true
+
# Default language
#
+ #
# This adds a language to plain strings that aren't otherwise coerced
# @attr [String]
- attr :language, true
-
+ attr :default_language, true
+
# Global options used in generating IRIs
# @attr [Hash] options
attr :options, true
# A context provided to us that we can use without re-serializing
@@ -61,40 +76,43 @@
# Create new evaluation context
# @yield [ec]
# @yieldparam [EvaluationContext]
# @return [EvaluationContext]
def initialize(options = {})
- @base = RDF::URI(options[:base_uri]) if options[:base_uri]
+ @base = RDF::URI(options[:base]) if options[:base]
@mappings = {}
@coercions = {}
- @lists = {}
+ @containers = {}
+ @languages = {}
@iri_to_curie = {}
@iri_to_term = {
RDF.to_uri.to_s => "rdf",
RDF::XSD.to_uri.to_s => "xsd"
}
@options = options
# Load any defined prefixes
(options[:prefixes] || {}).each_pair do |k, v|
- @iri_to_term[v.to_s] = k
+ @iri_to_term[v.to_s] = k unless k.nil?
end
debug("init") {"iri_to_term: #{iri_to_term.inspect}"}
yield(self) if block_given?
end
# Create an Evaluation Context using an existing context as a start by parsing the input.
#
- # @param [IO, Array, Hash, String] input
+ # @param [String, #read, Array, Hash, EvaluatoinContext] input
# @return [EvaluationContext] context
# @raise [InvalidContext]
# on a remote context load error, syntax error, or a reference to a term which is not defined.
def parse(context)
case context
+ when nil
+ EvaluationContext.new
when EvaluationContext
debug("parse") {"context: #{context.inspect}"}
context.dup
when IO, StringIO
debug("parse") {"io: #{context}"}
@@ -111,11 +129,11 @@
when String, nil
debug("parse") {"remote: #{context}"}
# Load context document, if it is a string
ec = nil
begin
- open(context.to_s) {|f| ec = parse(f)}
+ RDF::Util::File.open_file(context.to_s) {|f| ec = parse(f)}
ec.provided_context = context
debug("parse") {"=> provided_context: #{context.inspect}"}
ec
rescue Exception => e
debug("parse") {"Failed to retrieve @context from remote document at #{context}: #{e.message}"}
@@ -132,77 +150,87 @@
debug("parse") {"=> provided_context: #{context.inspect}"}
ec
when Hash
new_ec = self.dup
new_ec.provided_context = context
- debug("parse") {"=> provided_context: #{context.inspect}"}
num_updates = 1
while num_updates > 0 do
num_updates = 0
- # Map terms to IRIs first
+ # Map terms to IRIs/keywords first
context.each do |key, value|
# Expand a string value, unless it matches a keyword
debug("parse") {"Hash[#{key}] = #{value.inspect}"}
- if (new_ec.mapping(key) || key) == '@language'
- new_ec.language = value.to_s
+ if key == '@language'
+ new_ec.default_language = value
elsif term_valid?(key)
+ # Remove all coercion information for the property
+ new_ec.set_coerce(key, nil)
+ new_ec.set_container(key, nil)
+ @languages.delete(key)
+
# Extract IRI mapping. This is complicated, as @id may have been aliased
- if value.is_a?(Hash)
- id_key = value.keys.detect {|k| new_ec.mapping(k) == '@id'} || '@id'
- value = value[id_key]
- end
+ value = value.fetch('@id', nil) if value.is_a?(Hash)
raise InvalidContext::Syntax, "unknown mapping for #{key.inspect} to #{value.class}" unless value.is_a?(String) || value.nil?
iri = new_ec.expand_iri(value, :position => :predicate) if value.is_a?(String)
- if iri && new_ec.mappings[key] != iri
+ if iri && new_ec.mappings.fetch(key, nil) != iri
# Record term definition
- new_ec.mapping(key, iri)
+ new_ec.set_mapping(key, iri)
num_updates += 1
+ elsif value.nil?
+ new_ec.set_mapping(key, nil)
end
- elsif !new_ec.expand_iri(key).is_a?(RDF::URI)
+ else
raise InvalidContext::Syntax, "key #{key.inspect} is invalid"
end
end
end
# Next, look for coercion using new_ec
context.each do |key, value|
# Expand a string value, unless it matches a keyword
debug("parse") {"coercion/list: Hash[#{key}] = #{value.inspect}"}
- prop = new_ec.expand_iri(key, :position => :predicate).to_s
case value
when Hash
- # Must have one of @id, @type or @list
- expanded_keys = value.keys.map {|k| new_ec.mapping(k) || k}
- raise InvalidContext::Syntax, "mapping for #{key.inspect} missing one of @id, @type or @list" if (%w(@id @type @list) & expanded_keys).empty?
- raise InvalidContext::Syntax, "unknown mappings for #{key.inspect}: #{value.keys.inspect}" unless (expanded_keys - %w(@id @type @list)).empty?
+ # Must have one of @id, @language, @type or @container
+ raise InvalidContext::Syntax, "mapping for #{key.inspect} missing one of @id, @language, @type or @container" if (%w(@id @language @type @container) & value.keys).empty?
value.each do |key2, value2|
- expanded_key = new_ec.mapping(key2) || key2
iri = new_ec.expand_iri(value2, :position => :predicate) if value2.is_a?(String)
- case expanded_key
+ case key2
when '@type'
raise InvalidContext::Syntax, "unknown mapping for '@type' to #{value2.class}" unless value2.is_a?(String) || value2.nil?
- if new_ec.coerce(prop) != iri
+ if new_ec.coerce(key) != iri
raise InvalidContext::Syntax, "unknown mapping for '@type' to #{iri.inspect}" unless RDF::URI(iri).absolute? || iri == '@id'
# Record term coercion
- debug("parse") {"coerce #{prop.inspect} to #{iri.inspect}"}
- new_ec.coerce(prop, iri)
+ new_ec.set_coerce(key, iri)
end
- when '@list'
- raise InvalidContext::Syntax, "unknown mapping for '@list' to #{value2.class}" unless value2.is_a?(TrueClass) || value2.is_a?(FalseClass)
- if new_ec.list(prop) != value2
- debug("parse") {"list #{prop.inspect} as #{value2.inspect}"}
- new_ec.list(prop, value2)
+ when '@container'
+ raise InvalidContext::Syntax, "unknown mapping for '@container' to #{value2.class}" unless %w(@list @set).include?(value2)
+ if new_ec.container(key) != value2
+ debug("parse") {"container #{key.inspect} as #{value2.inspect}"}
+ new_ec.set_container(key, value2)
end
+ when '@language'
+ if !new_ec.languages.has_key?(key) || new_ec.languages[key] != value2
+ debug("parse") {"language #{key.inspect} as #{value2.inspect}"}
+ new_ec.set_language(key, value2)
+ end
end
end
- when String
+
+ # If value has no @id, create a mapping from key
+ # to the expanded key IRI
+ unless value.has_key?('@id')
+ iri = new_ec.expand_iri(key, :position => :predicate)
+ new_ec.set_mapping(key, iri)
+ end
+ when nil, String
# handled in previous loop
else
- raise InvalidContext::Syntax, "attemp to map #{key.inspect} to #{value.class}"
+ raise InvalidContext::Syntax, "attempt to map #{key.inspect} to #{value.class}"
end
end
new_ec
end
@@ -222,138 +250,195 @@
debug "serlialize: reuse context: #{provided_context.inspect}"
provided_context
else
debug("serlialize: generate context")
debug {"=> context: #{inspect}"}
- ctx = Hash.new
- ctx['@language'] = language.to_s if language
+ ctx = Hash.ordered
+ ctx['@language'] = default_language.to_s if default_language
- # Prefixes
- mappings.keys.sort {|a,b| a.to_s <=> b.to_s}.each do |k|
+ # Mappings
+ mappings.keys.sort{|a, b| a.to_s <=> b.to_s}.each do |k|
next unless term_valid?(k.to_s)
debug {"=> mappings[#{k}] => #{mappings[k]}"}
- ctx[k.to_s] = mappings[k].to_s
+ ctx[k] = mappings[k].to_s
end
- unless coercions.empty? && lists.empty?
+ unless coercions.empty? && containers.empty? && languages.empty?
# Coerce
- (coercions.keys + lists.keys).uniq.sort.each do |k|
- next if ['@type', RDF.type.to_s].include?(k.to_s)
+ (coercions.keys + containers.keys + languages.keys).uniq.sort.each do |k|
+ next if k == '@type'
- k_iri = compact_iri(k, :position => :predicate, :depth => @depth).to_s
- k_prefix = k_iri.split(':').first
-
# Turn into long form
- ctx[k_iri] ||= Hash.new
- if ctx[k_iri].is_a?(String)
- defn = Hash.new
- defn[self.alias("@id")] = ctx[k_iri]
- ctx[k_iri] = defn
+ ctx[k] ||= Hash.ordered
+ if ctx[k].is_a?(String)
+ defn = Hash.ordered
+ defn["@id"] = compact_iri(ctx[k], :position => :subject, :not_term => true)
+ ctx[k] = defn
end
debug {"=> coerce(#{k}) => #{coerce(k)}"}
if coerce(k) && !NATIVE_DATATYPES.include?(coerce(k))
- # If coercion doesn't depend on any prefix definitions, it can be folded into the first context block
- dt = compact_iri(coerce(k), :position => :datatype, :depth => @depth)
+ dt = coerce(k)
+ dt = compact_iri(dt, :position => :datatype) unless dt == '@id'
# Fold into existing definition
- ctx[k_iri][self.alias("@type")] = dt
- debug {"=> reuse datatype[#{k_iri}] => #{dt}"}
+ ctx[k]["@type"] = dt
+ debug {"=> datatype[#{k}] => #{dt}"}
end
- debug {"=> list(#{k}) => #{list(k)}"}
- if list(k)
- # It is not dependent on previously defined terms, fold into existing definition
- ctx[k_iri][self.alias("@list")] = true
- debug {"=> reuse list_range[#{k_iri}] => true"}
+ debug {"=> container(#{k}) => #{container(k)}"}
+ if %w(@list @set).include?(container(k))
+ ctx[k]["@container"] = container(k)
+ debug {"=> container[#{k}] => #{container(k).inspect}"}
end
+
+ debug {"=> language(#{k}) => #{language(k)}"}
+ if language(k) != default_language
+ ctx[k]["@language"] = language(k) ? language(k) : nil
+ debug {"=> language[#{k}] => #{language(k).inspect}"}
+ end
# Remove an empty definition
- ctx.delete(k_iri) if ctx[k_iri].empty?
+ ctx.delete(k) if ctx[k].empty?
end
end
debug {"start_doc: context=#{ctx.inspect}"}
ctx
end
# Return hash with @context, or empty
- r = Hash.new
+ r = Hash.ordered
r['@context'] = use_context unless use_context.nil? || use_context.empty?
r
end
end
##
- # Retrieve term mapping, add it if `value` is provided
+ # Retrieve term mapping
#
# @param [String, #to_s] term
- # @param [RDF::URI, String] value (nil)
#
# @return [RDF::URI, String]
- def mapping(term, value = nil)
+ def mapping(term)
+ @mappings.fetch(term.to_s, nil)
+ end
+
+ ##
+ # Set term mapping
+ #
+ # @param [String] term
+ # @param [RDF::URI, String] value
+ #
+ # @return [RDF::URI, String]
+ def set_mapping(term, value)
+# raise InvalidContext::Syntax, "mapping term #{term.inspect} must be a string" unless term.is_a?(String)
+# raise InvalidContext::Syntax, "mapping value #{value.inspect} must be an RDF::URI" unless value.nil? || value.to_s[0,1] == '@' || value.is_a?(RDF::URI)
+ debug {"map #{term.inspect} to #{value}"} unless @mappings[term] == value
+ iri_to_term.delete(@mappings[term].to_s) if @mappings[term]
if value
- debug {"map #{term.inspect} to #{value}"} unless @mappings[term.to_s] == value
- @mappings[term.to_s] = value
+ @mappings[term] = value
+ @options[:prefixes][term] = value if @options.has_key?(:prefixes)
iri_to_term[value.to_s] = term
+ else
+ @mappings.delete(term)
+ nil
end
- @mappings.has_key?(term.to_s) && @mappings[term.to_s]
end
##
- # Revered term mapping, typically used for finding aliases for keys.
+ # Reverse term mapping, typically used for finding aliases for keys.
#
# Returns either the original value, or a mapping for this value.
#
# @example
# {"@context": {"id": "@id"}, "@id": "foo"} => {"id": "foo"}
#
# @param [RDF::URI, String] value
- # @return [RDF::URI, String]
+ # @return [String]
def alias(value)
- @mappings.invert.fetch(value, value)
+ iri_to_term.fetch(value, value)
end
-
+
##
- # Retrieve term coercion, add it if `value` is provided
+ # Retrieve term coercion
#
- # @param [String] property in full IRI string representation
- # @param [RDF::URI, '@id'] value (nil)
+ # @param [String] property in unexpanded form
#
# @return [RDF::URI, '@id']
- def coerce(property, value = nil)
+ def coerce(property)
# Map property, if it's not an RDF::Value
- debug("coerce") {"map #{property} to #{mapping(property)}"} if mapping(property)
- property = mapping(property) if mapping(property)
return '@id' if [RDF.type, '@type'].include?(property) # '@type' always is an IRI
+ @coercions.fetch(property, nil)
+ end
+
+ ##
+ # Set term coercion
+ #
+ # @param [String] property in unexpanded form
+ # @param [RDF::URI, '@id'] value
+ #
+ # @return [RDF::URI, '@id']
+ def set_coerce(property, value)
+ debug {"coerce #{property.inspect} to #{value.inspect}"} unless @coercions[property.to_s] == value
if value
- debug {"coerce #{property.inspect} to #{value}"} unless @coercions[property.to_s] == value
- @coercions[property.to_s] = value
+ @coercions[property] = value
+ else
+ @coercions.delete(property)
end
- @coercions[property.to_s] if @coercions.has_key?(property.to_s)
end
##
- # Retrieve list mapping, add it if `value` is provided
+ # Retrieve container mapping, add it if `value` is provided
#
- # @param [String] property in full IRI string representation
- # @param [Boolean] value (nil)
+ # @param [String] property in unexpanded form
+ # @return [String]
+ def container(property)
+ @containers.fetch(property.to_s, nil)
+ end
+
+ ##
+ # Set container mapping
+ #
+ # @param [String] property
+ # @param [String] value one of @list, @set or nil
# @return [Boolean]
- def list(property, value = nil)
- unless value.nil?
- debug {"coerce #{property.inspect} to @list"} unless @lists[property.to_s] == value
- @lists[property.to_s] = value
+ def set_container(property, value)
+ return if @containers[property.to_s] == value
+ debug {"coerce #{property.inspect} to #{value.inspect}"}
+ if value
+ @containers[property.to_s] = value
+ else
+ @containers.delete(value)
end
- @lists[property.to_s] && @lists[property.to_s]
end
##
- # Determine if `term` is a suitable term
+ # Retrieve the language associated with a property, or the default language otherwise
+ # @return [String]
+ def language(property)
+ @languages.fetch(property.to_s, @default_language) if !coerce(property)
+ end
+
+ ##
+ # Set language mapping
#
+ # @param [String] property
+ # @param [String] value
+ # @return [String]
+ def set_language(property, value)
+ # Use false for nil language
+ @languages[property.to_s] = value ? value : false
+ end
+
+ ##
+ # Determine if `term` is a suitable term.
+ # Term may be any valid JSON string.
+ #
# @param [String] term
# @return [Boolean]
def term_valid?(term)
- term.empty? || term.match(NC_REGEXP)
+ term.is_a?(String)
end
##
# Expand an IRI. Relative IRIs are expanded against any document base.
#
@@ -366,94 +451,238 @@
# @return [RDF::URI, String] IRI or String, if it's a keyword
# @raise [RDF::ReaderError] if the iri cannot be expanded
# @see http://json-ld.org/spec/latest/json-ld-api/#iri-expansion
def expand_iri(iri, options = {})
return iri unless iri.is_a?(String)
- prefix, suffix = iri.split(":", 2)
- debug("expand_iri") {"prefix: #{prefix.inspect}, suffix: #{suffix.inspect}"}
+ prefix, suffix = iri.split(':', 2)
+ return mapping(iri) if mapping(iri) # If it's an exact match
+ debug("expand_iri") {"prefix: #{prefix.inspect}, suffix: #{suffix.inspect}"} unless options[:quiet]
+ base = self.base unless [:predicate, :datatype].include?(options[:position])
prefix = prefix.to_s
case
- when prefix == '_' then bnode(suffix)
- when iri.to_s[0,1] == "@" then iri
- when mappings.has_key?(prefix) then uri(mappings[prefix] + suffix.to_s)
- when base then base.join(iri)
- else uri(iri)
+ when prefix == '_' && suffix then debug("=> bnode"); bnode(suffix)
+ when iri.to_s[0,1] == "@" then debug("=> keyword"); iri
+ when suffix.to_s[0,2] == '//' then debug("=> iri"); uri(iri)
+ when mappings.has_key?(prefix) then debug("=> curie"); uri(mappings[prefix] + suffix.to_s)
+ when base then debug("=> base"); base.join(iri)
+ else
+ # Otherwise, it must be an absolute IRI
+ u = uri(iri)
+ debug("=> absolute") {"#{u.inspect} abs? #{u.absolute?.inspect}"}
+ u if u.absolute? || [:subject, :object].include?(options[:position])
end
end
##
# Compact an IRI
#
# @param [RDF::URI] iri
# @param [Hash{Symbol => Object}] options ({})
# @option options [:subject, :predicate, :object, :datatype] position
# Useful when determining how to serialize.
+ # @option options [Object] :value
+ # Value, used to select among various maps for the same IRI
+ # @option options [Boolean] :not_term (false)
+ # Don't return a term, but only a CURIE or IRI.
#
# @return [String] compacted form of IRI
# @see http://json-ld.org/spec/latest/json-ld-api/#iri-compaction
def compact_iri(iri, options = {})
- return iri.to_s if [RDF.first, RDF.rest, RDF.nil].include?(iri) # Don't cause these to be compacted
+ # Don't cause these to be compacted
+ return iri.to_s if [RDF.first, RDF.rest, RDF.nil].include?(iri)
+ return self.alias('@type') if options[:position] == :predicate && iri == RDF.type
depth(options) do
- debug {"compact_iri(#{options.inspect}, #{iri.inspect})"}
+ debug {"compact_iri(#{iri.inspect}, #{options.inspect})"}
- result = self.alias('@type') if options[:position] == :predicate && iri == RDF.type
- result ||= get_curie(iri) || self.alias(iri.to_s)
+ value = options.fetch(:value, nil)
+ # Get a list of terms which map to iri
+ terms = mappings.keys.select {|t| mapping(t).to_s == iri}
+
+ # Create an association term map for terms to their associated
+ # term rank.
+ term_map = {}
+
+ # If value is a @list add a term rank for each
+ # term mapping to iri which has @container @list.
+ debug("compact_iri", "#{value.inspect} is a list? #{list?(value).inspect}")
+ if list?(value)
+ list_terms = terms.select {|t| container(t) == '@list'}
+
+ term_map = list_terms.inject({}) do |memo, t|
+ memo[t] = term_rank(t, value)
+ memo
+ end unless list_terms.empty?
+ debug("term map") {"remove zero rank terms: #{term_map.keys.select {|t| term_map[t] == 0}}"} if term_map.any? {|t,r| r == 0}
+ term_map.delete_if {|t, r| r == 0}
+ end
+
+ # Otherwise, value is @value or a native type.
+ # Add a term rank for each term mapping to iri
+ # which does not have @container @list
+ if term_map.empty?
+ non_list_terms = terms.reject {|t| container(t) == '@list'}
+
+ # If value is a @list, exclude from term map those terms
+ # with @container @set
+ non_list_terms.reject {|t| container(t) == '@set'} if list?(value)
+
+ term_map = non_list_terms.inject({}) do |memo, t|
+ memo[t] = term_rank(t, value)
+ memo
+ end unless non_list_terms.empty?
+ debug("term map") {"remove zero rank terms: #{term_map.keys.select {|t| term_map[t] == 0}}"} if term_map.any? {|t,r| r == 0}
+ term_map.delete_if {|t, r| r == 0}
+ end
+
+ # If we don't want terms, remove anything that's not a CURIE or IRI
+ term_map.keep_if {|t, v| t.index(':') } if options.fetch(:not_term, false)
+
+ # Find terms having the greatest term match value
+ least_distance = term_map.values.max
+ terms = term_map.keys.select {|t| term_map[t] == least_distance}
+
+ # If the list of found terms is empty, append a compact IRI for
+ # each term which is a prefix of iri which does not have
+ # @type coercion, @container coercion or @language coercion rules
+ # along with the iri itself.
+ if terms.empty?
+ curies = mappings.keys.map {|k| iri.to_s.sub(mapping(k).to_s, "#{k}:") if
+ iri.to_s.index(mapping(k).to_s) == 0 &&
+ iri.to_s != mapping(k).to_s}.compact
+
+ debug("curies") do
+ curies.map do |c|
+ "#{c}: " +
+ "container: #{container(c).inspect}, " +
+ "coerce: #{coerce(c).inspect}, " +
+ "lang: #{language(c).inspect}"
+ end.inspect
+ end
+
+ terms = curies.select do |curie|
+ container(curie) != '@list' &&
+ coerce(curie).nil? &&
+ language(curie) == default_language
+ end
+
+ debug("curies") {"selected #{terms.inspect}"}
+
+ # If we still don't have any terms and we're using standard_prefixes,
+ # try those, and add to mapping
+ if terms.empty? && @options[:standard_prefixes]
+ terms = RDF::Vocabulary.
+ select {|v| iri.index(v.to_uri.to_s) == 0}.
+ map do |v|
+ prefix = v.__name__.to_s.split('::').last.downcase
+ set_mapping(prefix, v.to_uri.to_s)
+ iri.sub(v.to_uri.to_s, "#{prefix}:").sub(/:$/, '')
+ end
+ debug("curies") {"using standard prefies: #{terms.inspect}"}
+ end
+
+ terms << iri.to_s
+ end
+
+ # Get the first term based on distance and lexecographical order
+ # Prefer terms that don't have @container @set over other terms, unless as set is true
+ terms = terms.sort do |a, b|
+ debug("term sort") {"c(a): #{container(a).inspect}, c(b): #{container(b)}"}
+ if a.length == b.length
+ a <=> b
+ else
+ a.length <=> b.length
+ end
+ end
+ debug("sorted terms") {terms.inspect}
+ result = terms.first
+
debug {"=> #{result.inspect}"}
result
end
end
##
# Expand a value from compacted to expanded form making the context
# unnecessary. This method is used as part of more general expansion
- # and operates on RHS values, using a supplied key to determine @type and @list
+ # and operates on RHS values, using a supplied key to determine @type and @container
# coercion rules.
#
- # @param [RDF::URI] predicate
- # Associated predicate used to find coercion rules
+ # @param [String] property
+ # Associated property used to find coercion rules
# @param [Hash, String] value
# Value (literal or IRI) to be expanded
# @param [Hash{Symbol => Object}] options
#
# @return [Hash] Object representation of value
# @raise [RDF::ReaderError] if the iri cannot be expanded
# @see http://json-ld.org/spec/latest/json-ld-api/#value-expansion
- def expand_value(predicate, value, options = {})
+ def expand_value(property, value, options = {})
depth(options) do
- debug("expand_value") {"predicate: #{predicate}, value: #{value.inspect}, coerce: #{coerce(predicate).inspect}"}
+ debug("expand_value") {"property: #{property.inspect}, value: #{value.inspect}, coerce: #{coerce(property).inspect}"}
result = case value
when TrueClass, FalseClass, RDF::Literal::Boolean
- {"@literal" => value.to_s, "@type" => RDF::XSD.boolean.to_s}
+ case coerce(property)
+ when RDF::XSD.double.to_s
+ {"@value" => value.to_s, "@type" => RDF::XSD.double.to_s}
+ else
+ # Unless there's coercion, to not modify representation
+ value.is_a?(RDF::Literal::Boolean) ? value.object : value
+ end
when Integer, RDF::Literal::Integer
- {"@literal" => value.to_s, "@type" => RDF::XSD.integer.to_s}
- when BigDecimal, RDF::Literal::Decimal
- {"@literal" => value.to_s, "@type" => RDF::XSD.decimal.to_s}
+ case coerce(property)
+ when RDF::XSD.double.to_s
+ {"@value" => RDF::Literal::Double.new(value, :canonicalize => true).to_s, "@type" => RDF::XSD.double.to_s}
+ when RDF::XSD.integer.to_s, nil
+ # Unless there's coercion, to not modify representation
+ value.is_a?(RDF::Literal::Integer) ? value.object : value
+ else
+ res = Hash.ordered
+ res['@value'] = value.to_s
+ res['@type'] = coerce(property)
+ res
+ end
when Float, RDF::Literal::Double
- {"@literal" => value.to_s, "@type" => RDF::XSD.double.to_s}
+ case coerce(property)
+ when RDF::XSD.integer.to_s
+ {"@value" => value.to_int.to_s, "@type" => RDF::XSD.integer.to_s}
+ when RDF::XSD.double.to_s
+ {"@value" => RDF::Literal::Double.new(value, :canonicalize => true).to_s, "@type" => RDF::XSD.double.to_s}
+ when nil
+ # Unless there's coercion, to not modify representation
+ value.is_a?(RDF::Literal::Double) ? value.object : value
+ else
+ res = Hash.ordered
+ res['@value'] = value.to_s
+ res['@type'] = coerce(property)
+ res
+ end
+ when BigDecimal, RDF::Literal::Decimal
+ {"@value" => value.to_s, "@type" => RDF::XSD.decimal.to_s}
when Date, Time, DateTime
l = RDF::Literal(value)
- {"@literal" => l.to_s, "@type" => l.datatype.to_s}
- when RDF::URI
+ {"@value" => l.to_s, "@type" => l.datatype.to_s}
+ when RDF::URI, RDF::Node
{'@id' => value.to_s}
when RDF::Literal
- res = Hash.new
- res['@literal'] = value.to_s
+ res = Hash.ordered
+ res['@value'] = value.to_s
res['@type'] = value.datatype.to_s if value.has_datatype?
res['@language'] = value.language.to_s if value.has_language?
res
else
- case coerce(predicate)
+ case coerce(property)
when '@id'
{'@id' => expand_iri(value, :position => :object).to_s}
when nil
- language ? {"@literal" => value.to_s, "@language" => language.to_s} : value.to_s
+ debug("expand value") {"lang(prop): #{language(property).inspect}, def: #{default_language.inspect}"}
+ language(property) ? {"@value" => value.to_s, "@language" => language(property)} : value.to_s
else
- res = Hash.new
- res['@literal'] = value.to_s
- res['@type'] = coerce(predicate).to_s
+ res = Hash.ordered
+ res['@value'] = value.to_s
+ res['@type'] = coerce(property).to_s
res
end
end
debug {"=> #{result.inspect}"}
@@ -462,56 +691,56 @@
end
##
# Compact a value
#
- # @param [RDF::URI] predicate
- # Associated predicate used to find coercion rules
+ # @param [String] property
+ # Associated property used to find coercion rules
# @param [Hash] value
# Value (literal or IRI), in full object representation, to be compacted
# @param [Hash{Symbol => Object}] options
#
# @return [Hash] Object representation of value
# @raise [ProcessingError] if the iri cannot be expanded
# @see http://json-ld.org/spec/latest/json-ld-api/#value-compaction
- def compact_value(predicate, value, options = {})
- raise ProcessingError::Lossy, "attempt to compact a non-object value" unless value.is_a?(Hash)
+ def compact_value(property, value, options = {})
+ raise ProcessingError::Lossy, "attempt to compact a non-object value: #{value.inspect}" unless value.is_a?(Hash)
depth(options) do
- debug("compact_value") {"predicate: #{predicate.inspect}, value: #{value.inspect}, coerce: #{coerce(predicate).inspect}"}
+ debug("compact_value") {"property: #{property.inspect}, value: #{value.inspect}, coerce: #{coerce(property).inspect}"}
result = case
when %w(boolean integer double).any? {|t| expand_iri(value['@type'], :position => :datatype) == RDF::XSD[t]}
# Compact native type
debug {" (native)"}
- l = RDF::Literal(value['@literal'], :datatype => expand_iri(value['@type'], :position => :datatype))
+ l = RDF::Literal(value['@value'], :datatype => expand_iri(value['@type'], :position => :datatype))
l.canonicalize.object
- when coerce(predicate) == '@id' && value.has_key?('@id')
+ when coerce(property) == '@id' && value.has_key?('@id')
# Compact an @id coercion
debug {" (@id & coerce)"}
compact_iri(value['@id'], :position => :object)
- when value['@type'] && expand_iri(value['@type'], :position => :datatype) == coerce(predicate)
+ when value['@type'] && expand_iri(value['@type'], :position => :datatype) == coerce(property)
# Compact common datatype
- debug {" (@type & coerce) == #{coerce(predicate)}"}
- value['@literal']
+ debug {" (@type & coerce) == #{coerce(property)}"}
+ value['@value']
when value.has_key?('@id')
# Compact an IRI
- value['@id'] = compact_iri(value['@id'], :position => :object)
- debug {" (@id => #{value['@id']})"}
+ value[self.alias('@id')] = compact_iri(value['@id'], :position => :object)
+ debug {" (#{self.alias('@id')} => #{value['@id']})"}
value
- when value['@language'] && value['@language'] == language
+ when value['@language'] && value['@language'] == language(property)
# Compact language
- debug {" (@language) == #{language}"}
- value['@literal']
- when value['@literal'] && !value['@language'] && !value['@type'] && !coerce(predicate) && !language
+ debug {" (@language) == #{language(property).inspect}"}
+ value['@value']
+ when value['@value'] && !value['@language'] && !value['@type'] && !coerce(property) && !default_language
# Compact simple literal to string
- debug {" (@literal && !@language && !@type && !coerce && !language)"}
- value['@literal']
+ debug {" (@value && !@language && !@type && !coerce && !language)"}
+ value['@value']
when value['@type']
# Compact datatype
debug {" (@type)"}
- value['@type'] = compact_iri(value['@type'], :position => :datatype)
+ value[self.alias('@type')] = compact_iri(value['@type'], :position => :datatype)
value
else
# Otherwise, use original value
debug {" (no change)"}
value
@@ -532,23 +761,26 @@
end
end
def inspect
v = %w([EvaluationContext)
+ v << "def_language=#{default_language}"
+ v << "languages[#{languages.keys.length}]=#{languages}"
v << "mappings[#{mappings.keys.length}]=#{mappings}"
v << "coercions[#{coercions.keys.length}]=#{coercions}"
- v << "lists[#{lists.length}]=#{lists}"
+ v << "containers[#{containers.length}]=#{containers}"
v.join(", ") + "]"
end
def dup
# Also duplicate mappings, coerce and list
ec = super
ec.mappings = mappings.dup
ec.coercions = coercions.dup
- ec.lists = lists.dup
- ec.language = language
+ ec.containers = containers.dup
+ ec.languages = languages.dup
+ ec.default_language = default_language
ec.options = options
ec.iri_to_term = iri_to_term.dup
ec.iri_to_curie = iri_to_curie.dup
ec
end
@@ -572,69 +804,61 @@
@@bnode_cache ||= {}
@@bnode_cache[value.to_s] ||= RDF::Node.new(value)
end
##
- # Return a CURIE for the IRI, or nil. Adds namespace of CURIE to defined prefixes
- # @param [RDF::Resource] resource
- # @return [String, nil] value to use to identify IRI
- def get_curie(resource)
- debug {"get_curie(#{resource.inspect})"}
- case resource
- when RDF::Node, /^_:/
- return resource.to_s
+ # Get a "match value" given a term and a value. The value
+ # is lowest when the relative match between the term and the value
+ # is closest.
+ #
+ # @param [String] term
+ # @param [Object] value
+ # @return [Integer]
+ def term_rank(term, value)
+ debug("term rank") { "term: #{term.inspect}, value: #{value.inspect}"}
+ debug("term rank") { "coerce: #{coerce(term).inspect}, lang: #{languages.fetch(term, nil).inspect}"}
+
+ # A term without @language or @type can be used with rank 1 for any value
+ default_term = !coerce(term) && !languages.has_key?(term)
+ debug("term rank") { "default_term: #{default_term.inspect}"}
+
+ rank = case value
+ when TrueClass, FalseClass
+ coerce(term) == RDF::XSD.boolean.to_s ? 3 : (default_term ? 2 : 1)
+ when Integer
+ coerce(term) == RDF::XSD.integer.to_s ? 3 : (default_term ? 2 : 1)
+ when Float
+ coerce(term) == RDF::XSD.double.to_s ? 3 : (default_term ? 2 : 1)
+ when nil
+ # A value of null probably means it's an @id
+ 3
when String
- iri = resource
- resource = RDF::URI(resource)
- return nil unless resource.absolute?
- when RDF::URI
- iri = resource.to_s
- return iri if options[:expand]
+ # When compacting a string, the string has no language, so the term can be used if the term has @language null or it is a default term and there is no default language
+ debug("term rank") {"string: lang: #{languages.fetch(term, false).inspect}, def: #{default_language.inspect}"}
+ !languages.fetch(term, true) || (default_term && !default_language) ? 3 : 0
+ when Hash
+ if list?(value)
+ if value['@list'].empty?
+ # If the @list property is an empty array, if term has @container set to @list, term rank is 1, otherwise 0.
+ container(term) == '@list' ? 1 : 0
+ else
+ # Otherwise, return the sum of the term ranks for every entry in the list.
+ depth {value['@list'].inject(0) {|memo, v| memo + term_rank(term, v)}}
+ end
+ elsif subject?(value) || subject_reference?(value)
+ coerce(term) == '@id' ? 3 : (default_term ? 1 : 0)
+ elsif val_type = value.fetch('@type', nil)
+ coerce(term) == val_type ? 3 : (default_term ? 1 : 0)
+ elsif val_lang = value.fetch('@language', nil)
+ val_lang == language(term) ? 3 : (default_term ? 1 : 0)
+ else
+ default_term ? 3 : 0
+ end
else
- return nil
+ raise ProcessingError, "Unexpected value for term_rank: #{value.inspect}"
end
-
- curie = case
- when iri_to_curie.has_key?(iri)
- return iri_to_curie[iri]
- when u = iri_to_term.keys.detect {|i| iri.index(i.to_s) == 0}
- # Use a defined prefix
- prefix = iri_to_term[u]
- mapping(prefix, u)
- iri.sub(u.to_s, "#{prefix}:").sub(/:$/, '')
- when @options[:standard_prefixes] && vocab = RDF::Vocabulary.detect {|v| iri.index(v.to_uri.to_s) == 0}
- prefix = vocab.__name__.to_s.split('::').last.downcase
- mapping(prefix, vocab.to_uri.to_s)
- iri.sub(vocab.to_uri.to_s, "#{prefix}:").sub(/:$/, '')
- else
- debug "no mapping found for #{iri} in #{iri_to_term.inspect}"
- nil
- end
- iri_to_curie[iri] = curie
- rescue Addressable::URI::InvalidURIError => e
- raise RDF::WriterError, "Invalid IRI #{resource.inspect}: #{e.message}"
- end
-
- # Add debug event to debug array, if specified
- #
- # @param [String] message
- # @yieldreturn [String] appended to message, to allow for lazy-evaulation of message
- def debug(*args)
- return unless ::JSON::LD.debug? || @options[:debug]
- list = args
- list << yield if block_given?
- message = " " * (@depth || 0) * 2 + (list.empty? ? "" : list.join(": "))
- puts message if JSON::LD::debug?
- @options[:debug] << message if @options[:debug].is_a?(Array)
- end
-
- # Increase depth around a method invocation
- def depth(options = {})
- old_depth = @depth || 0
- @depth = (options[:depth] || old_depth) + 1
- ret = yield
- @depth = old_depth
- ret
+ # If term has @container @set, and rank is not 0, increase rank by 1.
+ rank > 0 && container(term) == '@set' ? rank + 1 : rank
end
end
end
\ No newline at end of file