Skip to content

Commit e3f7da7

Browse files
committed
Support expansion and compaction of simple named graphs (i.e., without an @id).
1 parent 4e9cdb2 commit e3f7da7

File tree

7 files changed

+316
-27
lines changed

7 files changed

+316
-27
lines changed

lib/json/ld/compact.rb

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ def compact(element, property: nil)
8282
if expanded_property == '@reverse'
8383
compacted_value = compact(expanded_value, property: '@reverse')
8484
#log_debug("@reverse") {"compacted_value: #{compacted_value.inspect}"}
85+
# handle double-reversed properties
8586
compacted_value.each do |prop, value|
8687
if context.reverse?(prop)
8788
value = [value] if !value.is_a?(Array) &&
@@ -162,10 +163,17 @@ def compact(element, property: nil)
162163

163164
container = context.container(item_active_property)
164165
as_array = context.as_array?(item_active_property)
165-
value = list?(expanded_item) ? expanded_item['@list'] : expanded_item
166+
167+
value = case
168+
when list?(expanded_item) then expanded_item['@list']
169+
when simple_graph?(expanded_item) then expanded_item['@graph']
170+
else expanded_item
171+
end
172+
166173
compacted_item = compact(value, property: item_active_property)
167174
#log_debug("") {" => compacted key: #{item_active_property.inspect} for #{compacted_item.inspect}"}
168175

176+
# handle @list
169177
if list?(expanded_item)
170178
compacted_item = [compacted_item] unless compacted_item.is_a?(Array)
171179
unless container == '@list'
@@ -178,6 +186,18 @@ def compact(element, property: nil)
178186
else
179187
raise JsonLdError::CompactionToListOfLists,
180188
"key cannot have more than one list value" if nest_result.has_key?(item_active_property)
189+
# Falls through to add list value below
190+
end
191+
end
192+
193+
# handle simple @graph, not the value of a property with @content: @graph
194+
if simple_graph?(expanded_item) && container != '@graph'
195+
compacted_item = [compacted_item] unless compacted_item.is_a?(Array)
196+
al = context.compact_iri('@graph', vocab: true, quiet: true)
197+
compacted_item = {al => compacted_item}
198+
if expanded_item.has_key?('@index')
199+
key = context.compact_iri('@index', vocab: true, quiet: true)
200+
compacted_item[key] = expanded_item['@index']
181201
end
182202
end
183203

lib/json/ld/context.rb

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,6 @@ class TermDefinition
4141
# @return ['@index', '@language', '@index', '@type', '@id'] Container mapping
4242
attr_reader :container_mapping
4343

44-
# If container mapping was defined along with @set
45-
# @return [Boolean]
46-
attr_reader :as_set
47-
4844
# @return [String] Term used for nest properties
4945
attr_accessor :nest
5046

@@ -78,7 +74,7 @@ def prefix?; @prefix; end
7874
# @param [String] term
7975
# @param [String] id
8076
# @param [String] type_mapping Type mapping
81-
# @param ['@index', '@language', '@index', '@set', '@type', '@id'] container_mapping
77+
# @param [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] container_mapping
8278
# @param [String] language_mapping
8379
# Language mapping of term, `false` is used if there is explicitly no language mapping for this term
8480
# @param [Boolean] reverse_property
@@ -147,7 +143,7 @@ def to_context_definition(context)
147143
end
148144
end
149145

150-
cm = [container_mapping, ('@set' if as_set)].compact
146+
cm = [container_mapping, ('@set' if as_set?)].compact
151147
cm = cm.first if cm.length == 1
152148
defn['@container'] = cm unless cm.empty?
153149
# Language set as false to be output as null
@@ -168,21 +164,25 @@ def to_rb
168164
%w(id type_mapping container_mapping language_mapping reverse_property nest simple prefix context).each do |acc|
169165
v = instance_variable_get("@#{acc}".to_sym)
170166
v = v.to_s if v.is_a?(RDF::Term)
171-
if acc == 'container_mapping' && as_set
167+
if acc == 'container_mapping' && as_set?
172168
v = v ? [v, '@set'] : '@set'
173169
end
174170
defn << "#{acc}: #{v.inspect}" if v
175171
end
176172
defn.join(', ') + ")"
177173
end
178174

175+
# If container mapping was defined along with @set
176+
# @return [Boolean]
177+
def as_set?; @as_set || false; end
178+
179179
def inspect
180180
v = %w([TD)
181181
v << "id=#{@id}"
182182
v << "term=#{@term}"
183183
v << "rev" if reverse_property
184184
v << "container=#{container_mapping}" if container_mapping
185-
v << "as_set=#{as_set.inspect}"
185+
v << "as_set=#{as_set?.inspect}"
186186
v << "lang=#{language_mapping.inspect}" unless language_mapping.nil?
187187
v << "type=#{type_mapping}" unless type_mapping.nil?
188188
v << "nest=#{nest.inspect}" unless nest.nil?
@@ -876,21 +876,20 @@ def find_definition(term)
876876
# @param [Term, #to_s] term in unexpanded form
877877
# @return [String]
878878
def container(term)
879-
return '@set' if term == '@graph'
880879
return term if KEYWORDS.include?(term)
881880
term = find_definition(term)
882881
term && term.container_mapping
883882
end
884883

885884
##
886-
# Should values be represented as a set?
885+
# Should values be represented using an array?
887886
#
888887
# @param [Term, #to_s] term in unexpanded form
889888
# @return [Boolean]
890889
def as_array?(term)
891-
return true if term == '@graph' || term == '@list'
890+
return true if CONTEXT_CONTAINER_ARRAY_TERMS.include?(term)
892891
term = find_definition(term)
893-
term && (term.as_set || term.container_mapping == '@list')
892+
term && (term.as_set? || term.container_mapping == '@list')
894893
end
895894

896895
##
@@ -1122,6 +1121,11 @@ def compact_iri(iri, value: nil, vocab: nil, reverse: false, quiet: false, **opt
11221121
tl_value = common_language
11231122
end
11241123
#log_debug("") {"list: containers: #{containers.inspect}, type/language: #{tl.inspect}, type/language value: #{tl_value.inspect}"} unless quiet
1124+
elsif graph?(value)
1125+
# TODO: support `@graphId`?
1126+
# TODO: "@graph@set"?
1127+
containers << '@graph'
1128+
containers << '@set'
11251129
else
11261130
if value?(value)
11271131
if value.has_key?('@language') && !index?(value)
@@ -1442,6 +1446,8 @@ def alias(value)
14421446

14431447
private
14441448

1449+
CONTEXT_CONTAINER_ARRAY_TERMS = %w(@set @list @graph).freeze
1450+
14451451
def uri(value)
14461452
case value.to_s
14471453
when /^_:(.*)$/
@@ -1482,7 +1488,26 @@ def bnode(value = nil)
14821488
#
14831489
# To make use of an inverse context, a list of preferred container mappings and the type mapping or language mapping are gathered for a particular value associated with an IRI. These parameters are then fed to the Term Selection algorithm, which will find the term that most appropriately matches the value's mappings.
14841490
#
1491+
# @example Basic structure of resulting inverse context
1492+
# {
1493+
# "http://example.com/term": {
1494+
# "@language": {
1495+
# "@null": "term",
1496+
# "@none": "term",
1497+
# "en": "term"
1498+
# },
1499+
# "@type": {
1500+
# "@reverse": "term",
1501+
# "@none": "term",
1502+
# "http://datatype": "term"
1503+
# },
1504+
# "@any": {
1505+
# "@none": "term",
1506+
# }
1507+
# }
1508+
# }
14851509
# @return [Hash{String => Hash{String => String}}]
1510+
# @todo May want to include @set along with container to allow selecting terms using @set over those without @set. May require adding some notion of value cardinality to compact_iri
14861511
def inverse_context
14871512
@inverse_context ||= begin
14881513
result = {}
@@ -1491,7 +1516,14 @@ def inverse_context
14911516
a.length == b.length ? (a <=> b) : (a.length <=> b.length)
14921517
end.each do |term|
14931518
next unless td = term_definitions[term]
1494-
container = td.container_mapping || (td.as_set ? '@set' : '@none')
1519+
1520+
container = td.container_mapping || (td.as_set? ? '@set' : '@none')
1521+
# FIXME: Alternative to consider
1522+
## Creates "@language", "@language@set", "@set", or "@none"
1523+
## for each of "@language", "@index", "@type", "@id", "@list", and "@graph"
1524+
#container = td.container_mapping.to_s
1525+
#container += '@set' if td.as_set?
1526+
#container = '@none' if container.empty?
14951527
container_map = result[td.id.to_s] ||= {}
14961528
tl_map = container_map[container] ||= {'@language' => {}, '@type' => {}, '@any' => {}}
14971529
type_map = tl_map['@type']
@@ -1630,7 +1662,7 @@ def check_container(container, local_context, defined, term)
16301662
# Okay
16311663
when '@language', '@index', nil
16321664
# Okay
1633-
when '@type', '@id', nil
1665+
when '@type', '@id', '@graph', nil
16341666
raise JsonLdError::InvalidContainerMapping,
16351667
"unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" if
16361668
(processingMode || 'json-ld-1.0') < 'json-ld-1.1'

lib/json/ld/expand.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,10 +424,19 @@ def expand_object(input, active_property, context, output_object, ordered: false
424424
# If the container mapping associated to key in active context is @list and expanded value is not already a list object, convert expanded value to a list object by first setting it to an array containing only expanded value if it is not already an array, and then by setting it to a JSON object containing the key-value pair @list-expanded value.
425425
if active_context.container(key) == '@list' && !list?(expanded_value)
426426
#log_debug(" => ") { "convert #{expanded_value.inspect} to list"}
427-
expanded_value = {'@list' => [expanded_value].flatten}
427+
expanded_value = [expanded_value] unless expanded_value.is_a?(Array)
428+
expanded_value = {'@list' => expanded_value}
428429
end
429430
#log_debug {" => #{expanded_value.inspect}"}
430431

432+
# convert expanded value to @graph if container specifies it
433+
# FIXME value may be a named graph, as well as a simple graph.
434+
if active_context.container(key) == '@graph' && !graph?(expanded_value)
435+
#log_debug(" => ") { "convert #{expanded_value.inspect} to list"}
436+
expanded_value = [expanded_value] unless expanded_value.is_a?(Array)
437+
expanded_value = {'@graph' => expanded_value}
438+
end
439+
431440
# Otherwise, if the term definition associated to key indicates that it is a reverse property
432441
# Spec FIXME: this is not an otherwise.
433442
if (td = context.term_definitions[key]) && td.reverse_property

lib/json/ld/utils.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,27 @@ def blank_node?(value)
4646
end
4747
end
4848

49+
##
50+
# Is value an expaned @graph?
51+
#
52+
# Note: A value is a simple graph if all of these hold true:
53+
# 1. It is an object.
54+
# 2. It has an `@graph` key.
55+
# 3. It may have '@id' or '@index'
56+
#
57+
# @param [Object] value
58+
# @return [Boolean]
59+
def graph?(value)
60+
value.is_a?(Hash) && (value.keys - UTIL_GRAPH_KEYS) == ['@graph']
61+
end
62+
##
63+
# Is value a simple @graph (lacking @id)?
64+
# @param [Object] value
65+
# @return [Boolean]
66+
def simple_graph?(value)
67+
graph?(value) && !value.has_key?('@id')
68+
end
69+
4970
##
5071
# Is value an expaned @list?
5172
#
@@ -184,6 +205,7 @@ def has_value(subject, property, value)
184205
end
185206

186207
private
208+
UTIL_GRAPH_KEYS = %w(@id @index).freeze
187209

188210
# Merge the last value into an array based for the specified key if hash is not null and value is not already in that array
189211
def merge_value(hash, key, value)

spec/compact_spec.rb

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,110 @@
744744
end
745745
end
746746

747+
context "@container: @graph" do
748+
{
749+
"Compacts simple graph" => {
750+
input: %([{
751+
"http://example.org/input": [{
752+
"@graph": [{
753+
"http://example.org/value": [{"@value": "x"}]
754+
}]
755+
}]
756+
}]),
757+
context: %({
758+
"@vocab": "http://example.org/",
759+
"input": {"@container": "@graph"}
760+
}),
761+
output: %({
762+
"@context": {
763+
"@vocab": "http://example.org/",
764+
"input": {"@container": "@graph"}
765+
},
766+
"input": {
767+
"value": "x"
768+
}
769+
})
770+
},
771+
"Compacts simple graph with @set" => {
772+
input: %([{
773+
"http://example.org/input": [{
774+
"@graph": [{
775+
"http://example.org/value": [{"@value": "x"}]
776+
}]
777+
}]
778+
}]),
779+
context: %({
780+
"@vocab": "http://example.org/",
781+
"input": {"@container": ["@graph", "@set"]}
782+
}),
783+
output: %({
784+
"@context": {
785+
"@vocab": "http://example.org/",
786+
"input": {"@container": ["@graph", "@set"]}
787+
},
788+
"input": [{
789+
"value": "x"
790+
}]
791+
})
792+
},
793+
"Compacts simple graph with @index" => {
794+
input: %([{
795+
"http://example.org/input": [{
796+
"@graph": [{
797+
"http://example.org/value": [{"@value": "x"}]
798+
}],
799+
"@index": "ndx"
800+
}]
801+
}]),
802+
context: %({
803+
"@vocab": "http://example.org/",
804+
"input": {"@container": "@graph"}
805+
}),
806+
output: %({
807+
"@context": {
808+
"@vocab": "http://example.org/",
809+
"input": {"@container": "@graph"}
810+
},
811+
"input": {
812+
"value": "x"
813+
}
814+
})
815+
},
816+
"Does not compacts graph with @id" => {
817+
input: %([{
818+
"http://example.org/input": [{
819+
"@graph": [{
820+
"http://example.org/value": [{"@value": "x"}]
821+
}],
822+
"@id": "http://example.org/id"
823+
}]
824+
}]),
825+
context: %({
826+
"@vocab": "http://example.org/",
827+
"input": {"@container": "@graph"}
828+
}),
829+
output: %({
830+
"@context": {
831+
"@vocab": "http://example.org/",
832+
"input": {
833+
"@container": "@graph"
834+
}
835+
},
836+
"input": {
837+
"@id": "http://example.org/id",
838+
"@graph": [
839+
{
840+
"value": "x"
841+
}
842+
]
843+
}
844+
})
845+
},
846+
}.each_pair do |title, params|
847+
it(title) {run_compact({processingMode: "json-ld-1.1"}.merge(params))}
848+
end
849+
end
850+
747851
context "@nest" do
748852
{
749853
"Indexes to @nest for property with @container: @nest" => {

0 commit comments

Comments
 (0)