Skip to content

Commit

Permalink
Generate BNode identifiers for unlabled nodes when expanding property…
Browse files Browse the repository at this point in the history
… generator properties.
  • Loading branch information
gkellogg committed Dec 17, 2012
1 parent 4f8f775 commit 0c45ef6
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 31 deletions.
2 changes: 1 addition & 1 deletion lib/json/ld/api.rb
Expand Up @@ -102,7 +102,7 @@ def initialize(input, context, options = {}, &block)
def self.expand(input, context = nil, callback = nil, options = {})
result = nil
API.new(input, context, options) do |api|
result = api.expand(api.value, nil, api.context)
result = api.expand(api.value, nil, api.context, BlankNodeNamer.new("t"))
end

# If, after the algorithm outlined above is run, the resulting element is an
Expand Down
29 changes: 19 additions & 10 deletions lib/json/ld/evaluation_context.rb
Expand Up @@ -247,7 +247,11 @@ def parse(context)
when '@type'
raise InvalidContext::Syntax, "unknown mapping for '@type' to #{value2.inspect}" unless value2.is_a?(String) || value2.nil?
if new_ec.coerce(key) != iri
raise InvalidContext::Syntax, "unknown mapping for '@type' to #{iri.inspect}" unless RDF::URI(iri).absolute? || iri == '@id'
case iri
when '@id', /_:/, RDF::Node
else
raise InvalidContext::Syntax, "unknown mapping for '@type' to #{iri.inspect}" unless (RDF::URI(iri).absolute? rescue false)
end
# Record term coercion
new_ec.set_coerce(key, iri)
end
Expand Down Expand Up @@ -516,12 +520,12 @@ def expand_iri(iri, options = {})
when prefix == '_' && suffix then bnode(suffix)
when iri.to_s[0,1] == "@" then iri
when suffix.to_s[0,2] == '//' then uri(iri)
when mapping = mappings.fetch(prefix, false)
when mapping = mappings.fetch(prefix, false)
if mapping.is_a?(Array)
# Return array of IRIs, if it's a property generator
mapping.map {|m| uri(m + suffix.to_s)}
mapping.map {|m| uri(m.to_s + suffix.to_s)}
else
uri(mapping + suffix.to_s)
uri(mapping.to_s + suffix.to_s)
end
when base then base.join(iri)
when vocab then uri("#{vocab}#{iri}")
Expand Down Expand Up @@ -879,12 +883,17 @@ def dup
private

def uri(value, append = nil)
value = RDF::URI.new(value)
value = value.join(append) if append
value.validate! if @options[:validate]
value.canonicalize! if @options[:canonicalize]
value = RDF::URI.intern(value) if @options[:intern]
value
case value.to_s
when /_:/
RDF::Node.new(value)
else
value = RDF::URI.new(value)
value = value.join(append) if append
value.validate! if @options[:validate]
value.canonicalize! if @options[:canonicalize]
value = RDF::URI.intern(value) if @options[:intern]
value
end
end

# Keep track of allocated BNodes
Expand Down
63 changes: 48 additions & 15 deletions lib/json/ld/expand.rb
Expand Up @@ -10,9 +10,10 @@ module Expand
# @param [Array, Hash] input
# @param [String] active_property
# @param [EvaluationContext] context
# @param [BlankNodeNamer] namer
# @param [Hash{Symbol => Object}] options
# @return [Array, Hash]
def expand(input, active_property, context, options = {})
def expand(input, active_property, context, namer, options = {})
debug("expand") {"input: #{input.inspect}, active_property: #{active_property.inspect}, context: #{context.inspect}"}
result = case input
when Array
Expand All @@ -26,7 +27,7 @@ def expand(input, active_property, context, options = {})
# throw an exception as lists of lists are not allowed.
raise ProcessingError::ListOfLists, "A list may not contain another list" if v.is_a?(Array) && is_list

expand(v, active_property, context, options)
expand(v, active_property, context, namer, options)
end.flatten.compact

if is_list && value.any? {|v| v.is_a?(Hash) && v.has_key?('@list')}
Expand All @@ -51,6 +52,7 @@ def expand(input, active_property, context, options = {})
value = input[key]
# Remove property from element expand property according to the steps outlined in IRI Expansion
property = context.expand_iri(key, :position => :predicate, :quiet => true)
property = nil if property.is_a?(Array) && property.empty?

# Set active property to the original un-expanded property if property if not a keyword
active_property = key unless key[0,1] == '@'
Expand All @@ -62,7 +64,6 @@ def expand(input, active_property, context, options = {})
debug(" => ") {"skip nil key"}
next
end
property = property.to_s

expanded_value = case property
when '@id'
Expand Down Expand Up @@ -108,7 +109,7 @@ def expand(input, active_property, context, options = {})
# using this algorithm, passing copies of the active context and active property.
# If the expanded value is not an array, convert it to an array.
value = [value] unless value.is_a?(Array)
value = depth { expand(value, active_property, context, options) }
value = depth { expand(value, active_property, context, namer, options) }

# If property is @list, and any expanded value
# is an object containing an @list property, throw an exception, as lists of lists are not supported
Expand Down Expand Up @@ -153,7 +154,7 @@ def expand(input, active_property, context, options = {})
value.keys.sort.each do |k|
[value[k]].flatten.each do |v|
# Expand the value, adding an '@annotation' key with value equal to the key
expanded_value = depth { expand(v, active_property, context, options) }
expanded_value = depth { expand(v, active_property, context, namer, options) }
next unless expanded_value
expanded_value['@annotation'] ||= k
ary << expanded_value
Expand All @@ -163,7 +164,7 @@ def expand(input, active_property, context, options = {})
ary
else
# Otherwise, expand value recursively using this algorithm, passing copies of the active context and active property.
depth { expand(value, active_property, context, options) }
depth { expand(value, active_property, context, namer, options) }
end
end

Expand All @@ -179,24 +180,38 @@ def expand(input, active_property, context, options = {})
# and the active property has a @container set to @list,
# convert value to an object with an @list property whose value is set to value
# (unless value is already in that form)
if expanded_value && property[0,1] != '@' && context.container(active_property) == '@list' &&
(!expanded_value.is_a?(Hash) || !expanded_value.fetch('@list', false))
debug(" => ") { "convert #{expanded_value.inspect} to list"}
if expanded_value &&
property.to_s[0,1] != '@' &&
context.container(active_property) == '@list' &&
(!expanded_value.is_a?(Hash) || !expanded_value.fetch('@list', false))

debug(" => ") { "convert #{expanded_value.inspect} to list"}
expanded_value = {'@list' => [expanded_value].flatten}
end

# Convert value to array form unless value is null or property is @id, @type, @value, or @language.
if !%(@id @language @type @value @annotation).include?(property) && !expanded_value.is_a?(Array)
if !(property.is_a?(String) && %(@id @language @type @value @annotation).include?(property)) &&
!expanded_value.is_a?(Array)

debug(" => make #{expanded_value.inspect} an array")
expanded_value = [expanded_value]
end

if output_object.has_key?(property)
# If element already contains a property property, append value to the existing value.
output_object[property] += expanded_value
if property.is_a?(Array)
label_blanknodes(expanded_value, namer)
property.map(&:to_s).each do |prop|
# label all blank nodes in value with blank node identifiers by using the Label Blank Nodes Algorithm.
output_object[prop] ||= []
output_object[prop] += expanded_value.dup
end
else
# Otherwise, create a property property with value as value.
output_object[property] = expanded_value
if output_object.has_key?(property.to_s)
# If element already contains a property property, append value to the existing value.
output_object[property.to_s] += expanded_value
else
# Otherwise, create a property property with value as value.
output_object[property.to_s] = expanded_value
end
end
debug {" => #{expanded_value.inspect}"}
end
Expand Down Expand Up @@ -247,5 +262,23 @@ def expand(input, active_property, context, options = {})
debug {" => #{result.inspect}"}
result
end

protected
# @param [Array, Hash] input
# @param [BlankNodeNamer] namer
def label_blanknodes(element, namer)
if element.is_a?(Array)
element.each {|e| label_blanknodes(e, namer)}
elsif list?(element)
element['@list'].each {|e| label_blanknodes(e, namer)}
elsif element.is_a?(Hash)
element.keys.sort.each do |k|
label_blanknodes(element[k], namer)
end
unless element.has_key?('@id')
element['@id'] = namer.get_name(nil)
end
end
end
end
end
67 changes: 67 additions & 0 deletions spec/expand_spec.rb
Expand Up @@ -763,6 +763,73 @@
end
end

context "property generators" do
{
"expand-0038" => {
:input => {
"@context" => {
"site" => "http://example.com/",
"field_tags" => {
"@id" => [ "site:vocab/field_tags", "http://schema.org/about" ]
},
"field_related" => {
"@id" => [ "site:vocab/field_related", "http://schema.org/about" ]
}
},
"@id" => "site:node/1",
"field_tags" => [
{ "@id" => "site:term/this-is-a-tag" }
],
"field_related" => [
{ "@id" => "site:node/this-is-related-news" }
]
},
:output => [{
"@id" => "http://example.com/node/1",
"http://example.com/vocab/field_related" => [{
"@id" => "http://example.com/node/this-is-related-news"
}],
"http://schema.org/about" => [{
"@id" => "http://example.com/node/this-is-related-news"
}, {
"@id" => "http://example.com/term/this-is-a-tag"
}],
"http://example.com/vocab/field_tags" => [{
"@id" => "http://example.com/term/this-is-a-tag"
}]
}]
},
"generate bnodel ids" => {
:input => {
"@context" => {
"site" => "http://example.com/",
"field_tags" => {
"@id" => [ "site:vocab/field_tags", "http://schema.org/about" ]
}
},
"@id" => "site:node/1",
"field_tags" => { "@type" => "site:term/this-is-a-tag" }
},
:output => [{
"@id" => "http://example.com/node/1",
"http://schema.org/about" => [{
"@id" => "_:t0",
"@type" => ["http://example.com/term/this-is-a-tag"]
}],
"http://example.com/vocab/field_tags" => [{
"@id" => "_:t0",
"@type" => ["http://example.com/term/this-is-a-tag"]
}]
}]
}
}.each do |title, params|
it title do
jld = JSON::LD::API.expand(params[:input], nil, nil, :debug => @debug)
jld.should produce(params[:output], @debug)
end
end
end

context "exceptions" do
{
"@list containing @list" => {
Expand Down
2 changes: 1 addition & 1 deletion spec/flatten_spec.rb
Expand Up @@ -100,7 +100,7 @@
graph = params[:graph] || '@merged'
jld = nil
JSON::LD::API.new(params[:input], nil, :debug => @debug) do |api|
expanded_value = api.expand(api.value, nil, api.context)
expanded_value = api.expand(api.value, nil, JSON::LD::BlankNodeNamer.new("e"), api.context)
api.generate_node_map(expanded_value,
@node_map,
graph,
Expand Down
4 changes: 0 additions & 4 deletions spec/suite_expand_spec.rb
Expand Up @@ -10,10 +10,6 @@
m.entries.each do |t|
specify "#{t.property('input')}: #{t.name}" do
begin
case t.property('input')
when /expand-(0037|0038|0039)/
pending("implementation of property generators")
end
t.debug = ["test: #{t.inspect}", "source: #{t.input.read}"]
t.debug << "context: #{t.context.read}" if t.property('context')
result = JSON::LD::API.expand(t.input, nil, nil,
Expand Down

0 comments on commit 0c45ef6

Please sign in to comment.