lib/json/ld/evaluation_context.rb in json-ld-0.1.2 vs lib/json/ld/evaluation_context.rb in json-ld-0.1.3

- old
+ new

@@ -101,12 +101,11 @@ yield(self) if block_given? end # Create an Evaluation Context using an existing context as a start by parsing the input. # - # @param [String, #read, Array, Hash, EvaluatoinContext] input - # @return [EvaluationContext] context + # @param [String, #read, Array, Hash, EvaluatoinContext] 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 @@ -117,12 +116,11 @@ when IO, StringIO debug("parse") {"io: #{context}"} # Load context document, if it is a string begin ctx = JSON.load(context) - raise JSON::LD::InvalidContext::Syntax, "missing @context" unless ctx.is_a?(Hash) && ctx["@context"] - parse(ctx["@context"]) + parse(ctx["@context"] || {}) rescue JSON::ParserError => e debug("parse") {"Failed to parse @context from remote document at #{context}: #{e.message}"} raise JSON::LD::InvalidContext::Syntax, "Failed to parse remote context at #{context}: #{e.message}" if @options[:validate] self.dup end @@ -159,11 +157,11 @@ # 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 key == '@language' + if key == '@language' && (value.nil? || value.is_a?(String)) 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) @@ -172,11 +170,13 @@ # Extract IRI mapping. This is complicated, as @id may have been aliased 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.fetch(key, nil) != iri + if iri && KEYWORDS.include?(key) + raise InvalidContext::Syntax, "key #{key.inspect} must not be a keyword" + elsif iri && new_ec.mappings.fetch(key, nil) != iri # Record term definition new_ec.set_mapping(key, iri) num_updates += 1 elsif value.nil? new_ec.set_mapping(key, nil) @@ -323,27 +323,24 @@ end ## # Set term mapping # - # @param [String] term + # @param [#to_s] term # @param [RDF::URI, String] value # # @return [RDF::URI, String] def set_mapping(term, value) + term = term.to_s + term_sym = term.empty? ? "" : term.to_sym # 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 + debug {"map #{term.inspect} to #{value.inspect}"} iri_to_term.delete(@mappings[term].to_s) if @mappings[term] - if 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[term] = value + @options[:prefixes][term_sym] = value if @options.has_key?(:prefixes) + iri_to_term[value.to_s] = term end ## # Reverse term mapping, typically used for finding aliases for keys. # @@ -364,11 +361,12 @@ # @param [String] property in unexpanded form # # @return [RDF::URI, '@id'] def coerce(property) # Map property, if it's not an RDF::Value - return '@id' if [RDF.type, '@type'].include?(property) # '@type' always is an IRI + # @type and @graph always is an IRI + return '@id' if [RDF.type, '@type', '@graph'].include?(property) @coercions.fetch(property, nil) end ## # Set term coercion @@ -457,19 +455,18 @@ 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 == '_' && 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) + when prefix == '_' && suffix then bnode(suffix) + when iri.to_s[0,1] == "@" then iri + when suffix.to_s[0,2] == '//' then uri(iri) + when mappings.has_key?(prefix) then uri(mappings[prefix] + suffix.to_s) + when base then 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 ## @@ -485,14 +482,10 @@ # 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 = {}) - # 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(#{iri.inspect}, #{options.inspect})"} value = options.fetch(:value, nil) @@ -545,13 +538,19 @@ # 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") {"mappings: #{mappings.inspect}"} + curies = mappings.keys.map do |k| + debug("curies[#{k}]") {"#{mapping(k).inspect}"} + #debug("curies[#{k}]") {"#{(mapping(k).to_s.length > 0).inspect}, #{iri.to_s.index(mapping(k).to_s)}"} + iri.to_s.sub(mapping(k).to_s, "#{k}:") if + mapping(k).to_s.length > 0 && + iri.to_s.index(mapping(k).to_s) == 0 && + iri.to_s != mapping(k).to_s + end.compact debug("curies") do curies.map do |c| "#{c}: " + "container: #{container(c).inspect}, " + @@ -579,21 +578,28 @@ iri.sub(v.to_uri.to_s, "#{prefix}:").sub(/:$/, '') end debug("curies") {"using standard prefies: #{terms.inspect}"} end - terms << iri.to_s + # If there is a mapping from the complete IRI to null, return null, + # otherwise, return the complete IRI. + if mappings.has_key?(iri.to_s) && !mapping(iri) + debug("iri") {"use nil IRI mapping"} + terms << nil + else + terms << iri.to_s + end 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 + if a.to_s.length == b.to_s.length + a.to_s <=> b.to_s else - a.length <=> b.length + a.to_s.length <=> b.to_s.length end end debug("sorted terms") {terms.inspect} result = terms.first @@ -611,33 +617,43 @@ # @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 + # @option options [Boolean] :native (true) use native representations # # @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(property, value, options = {}) + options = {:native => true}.merge(options) depth(options) do debug("expand_value") {"property: #{property.inspect}, value: #{value.inspect}, coerce: #{coerce(property).inspect}"} result = case value when TrueClass, FalseClass, RDF::Literal::Boolean 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 + if options[:native] + # Unless there's coercion, to not modify representation + {"@value" => (value.is_a?(RDF::Literal::Boolean) ? value.object : value)} + else + {"@value" => value.to_s, "@type" => RDF::XSD.boolean.to_s} + end end when Integer, RDF::Literal::Integer 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 + if options[:native] + {"@value" => value.is_a?(RDF::Literal::Integer) ? value.object : value} + else + {"@value" => value.to_s, "@type" => RDF::XSD.integer.to_s} + end else res = Hash.ordered res['@value'] = value.to_s res['@type'] = coerce(property) res @@ -647,12 +663,16 @@ 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 + if options[:native] + # Unless there's coercion, to not modify representation + {"@value" => value.is_a?(RDF::Literal::Double) ? value.object : value} + else + {"@value" => RDF::Literal::Double.new(value, :canonicalize => true).to_s, "@type" => RDF::XSD.double.to_s} + end else res = Hash.ordered res['@value'] = value.to_s res['@type'] = coerce(property) res @@ -674,11 +694,11 @@ case coerce(property) when '@id' {'@id' => expand_iri(value, :position => :object).to_s} when nil debug("expand value") {"lang(prop): #{language(property).inspect}, def: #{default_language.inspect}"} - language(property) ? {"@value" => value.to_s, "@language" => language(property)} : value.to_s + language(property) ? {"@value" => value.to_s, "@language" => language(property)} : {"@value" => value.to_s} else res = Hash.ordered res['@value'] = value.to_s res['@type'] = coerce(property).to_s res @@ -700,22 +720,23 @@ # @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 + # FIXME: revisit the specification version of this. 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") {"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['@value'], :datatype => expand_iri(value['@type'], :position => :datatype)) - l.canonicalize.object + #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['@value'], :datatype => expand_iri(value['@type'], :position => :datatype)) + # l.canonicalize.object 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(property) @@ -729,14 +750,22 @@ value when value['@language'] && value['@language'] == language(property) # Compact language debug {" (@language) == #{language(property).inspect}"} value['@value'] + when value['@value'] && !value['@value'].is_a?(String) + # Compact simple literal to string + debug {" (@value not string)"} + value['@value'] when value['@value'] && !value['@language'] && !value['@type'] && !coerce(property) && !default_language # Compact simple literal to string debug {" (@value && !@language && !@type && !coerce && !language)"} value['@value'] + when value['@value'] && !value['@language'] && !value['@type'] && !coerce(property) && !language(property) + # Compact simple literal to string + debug {" (@value && !@language && !@type && !coerce && language(property).false)"} + value['@value'] when value['@type'] # Compact datatype debug {" (@type)"} value[self.alias('@type')] = compact_iri(value['@type'], :position => :datatype) value @@ -812,53 +841,49 @@ # # @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}"} + debug("term rank") { + "term: #{term.inspect}, " + + "value: #{value.inspect}, " + + "coerce: #{coerce(term).inspect}, " + + "lang: #{languages.fetch(term, nil).inspect}/#{language(term).inspect} " + + "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 + # value is null + rank = if value.nil? + debug("term rank") { "null value: 3"} 3 - when String - # 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) + elsif 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 value?(value) + val_type = value.fetch('@type', nil) + val_lang = value.fetch('@language', nil) + debug("term rank") {"@val_type: #{val_type.inspect}, val_lang: #{val_lang.inspect}"} + if val_type 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) + elsif !value['@value'].is_a?(String) + default_term ? 2 : 1 + elsif val_lang.nil? + debug("val_lang.nil") {"#{language(term).inspect} && #{coerce(term).inspect}"} + !language(term) && !coerce(term) ? 3 : 0 else - default_term ? 3 : 0 + val_lang == language(term) ? 3 : (default_term ? 1 : 0) end - else - raise ProcessingError, "Unexpected value for term_rank: #{value.inspect}" + else # subject definition/reference + coerce(term) == '@id' ? 3 : (default_term ? 1 : 0) end - # If term has @container @set, and rank is not 0, increase rank by 1. - rank > 0 && container(term) == '@set' ? rank + 1 : rank + debug(" =>") {rank.inspect} + rank end end -end \ No newline at end of file +end