Skip to content

Commit 9de3c5b

Browse files
committed
Add support for compacting named graphs where @container: @graph and @graphId.
1 parent e3f7da7 commit 9de3c5b

File tree

6 files changed

+210
-18
lines changed

6 files changed

+210
-18
lines changed

lib/json/ld.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ module LD
5252
@id
5353
@index
5454
@graph
55+
@graphId
5556
@language
5657
@list
5758
@nest
@@ -106,6 +107,7 @@ class InvalidBaseIRI < JsonLdError; @code = "invalid base IRI"; end
106107
class InvalidContainerMapping < JsonLdError; @code = "invalid container mapping"; end
107108
class InvalidDefaultLanguage < JsonLdError; @code = "invalid default language"; end
108109
class InvalidIdValue < JsonLdError; @code = "invalid @id value"; end
110+
class InvalidGraphIdMapping < JsonLdError; @code = "invalid @graphId mapping"; end
109111
class InvalidIndexValue < JsonLdError; @code = "invalid @index value"; end
110112
class InvalidVersionValue < JsonLdError; @code = "invalid @version value"; end
111113
class InvalidIRIMapping < JsonLdError; @code = "invalid IRI mapping"; end

lib/json/ld/compact.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,11 @@ def compact(element, property: nil)
163163

164164
container = context.container(item_active_property)
165165
as_array = context.as_array?(item_active_property)
166+
graph_id = context.graph_id(item_active_property)
166167

167168
value = case
168169
when list?(expanded_item) then expanded_item['@list']
169-
when simple_graph?(expanded_item) then expanded_item['@graph']
170+
when graph?(expanded_item) then expanded_item['@graph']
170171
else expanded_item
171172
end
172173

@@ -190,11 +191,16 @@ def compact(element, property: nil)
190191
end
191192
end
192193

193-
# handle simple @graph, not the value of a property with @content: @graph
194-
if simple_graph?(expanded_item) && container != '@graph'
194+
# handle @graph, not the value of a property with @content: @graph, or where graph_id does not match
195+
if graph?(expanded_item) && (container != '@graph' || graph_id != expanded_item['@id'])
195196
compacted_item = [compacted_item] unless compacted_item.is_a?(Array)
196197
al = context.compact_iri('@graph', vocab: true, quiet: true)
197198
compacted_item = {al => compacted_item}
199+
if expanded_item['@id']
200+
al = context.compact_iri('@id', vocab: true, quiet: true)
201+
# Do not compact using vocabulary-relative, which is not valid for @id
202+
compacted_item[al] = context.compact_iri(expanded_item['@id'], vocab: false, quiet: true).to_s
203+
end
198204
if expanded_item.has_key?('@index')
199205
key = context.compact_iri('@index', vocab: true, quiet: true)
200206
compacted_item[key] = expanded_item['@index']

lib/json/ld/context.rb

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ class TermDefinition
6262
# @return [Hash{String => Object}]
6363
attr_accessor :context
6464

65+
# Graph identifier, used with @container @graph
66+
# @return [String]
67+
attr_accessor :graph_id
68+
6569
# This is a simple term definition, not an expanded term definition
6670
# @return [Boolean] simple
6771
def simple?; simple; end
@@ -92,6 +96,7 @@ def initialize(term,
9296
nest: nil,
9397
simple: false,
9498
prefix: nil,
99+
graph_id: nil,
95100
context: nil)
96101
@term = term
97102
@id = id.to_s unless id.nil?
@@ -102,6 +107,7 @@ def initialize(term,
102107
@nest = nest unless nest.nil?
103108
@simple = simple
104109
@prefix = prefix unless prefix.nil?
110+
@graph_i = graph_id unless graph_id.nil?
105111
@context = context unless context.nil?
106112
end
107113

@@ -146,6 +152,7 @@ def to_context_definition(context)
146152
cm = [container_mapping, ('@set' if as_set?)].compact
147153
cm = cm.first if cm.length == 1
148154
defn['@container'] = cm unless cm.empty?
155+
defn['@graphId'] = context.compact_iri(graph_id, vocab: true) if graph_id
149156
# Language set as false to be output as null
150157
defn['@language'] = (@language_mapping ? @language_mapping : nil) unless @language_mapping.nil?
151158
defn['@context'] = @context if @context
@@ -188,6 +195,7 @@ def inspect
188195
v << "nest=#{nest.inspect}" unless nest.nil?
189196
v << "simple=true" if @simple
190197
v << "prefix=#{@prefix.inspect}" unless @prefix.nil?
198+
v << "graph_id=#{@graph_id.inspect}" unless @graph_id.nil?
191199
v << "has-context" unless context.nil?
192200
v.join(" ") + "]"
193201
end
@@ -585,7 +593,7 @@ def create_term_definition(local_context, term, defined)
585593

586594
expected_keys = case processingMode
587595
when "json-ld-1.0", nil then %w(@container @id @language @reverse @type)
588-
else %w(@container @context @id @language @nest @prefix @reverse @type)
596+
else %w(@container @context @graphId @id @language @nest @prefix @reverse @type)
589597
end
590598

591599
extra_keys = value.keys - expected_keys
@@ -716,6 +724,28 @@ def create_term_definition(local_context, term, defined)
716724
end
717725
end
718726

727+
if value.has_key?('@graphId')
728+
graph_id = value['@graphId']
729+
graph_id = case graph_id
730+
when String
731+
begin
732+
expand_iri(graph_id, vocab: true, documentRelative: false, local_context: local_context, defined: defined)
733+
rescue JsonLdError::InvalidIRIMapping
734+
raise JsonLdError::InvalidGraphIdMapping, "invalid mapping for '@graphId': #{graph_id.inspect} on term #{term.inspect}"
735+
end
736+
else
737+
:error
738+
end
739+
unless (graph_id.is_a?(RDF::URI) && graph_id.absolute?) || graph_id.is_a?(RDF::Node)
740+
raise JsonLdError::InvalidGraphIdMapping, "unknown mapping for '@graphId': #{graph_id.inspect} on term #{term.inspect}"
741+
end
742+
unless definition.container_mapping == '@graph'
743+
raise JsonLdError::InvalidGraphIdMapping, "'@graphId' only valid with @container: @graph: #{graph_id.inspect} on term #{term.inspect}"
744+
end
745+
#log_debug("") {"type_mapping: #{type.inspect}"}
746+
definition.graph_id = graph_id.to_s
747+
end
748+
719749
term_definitions[term] = definition
720750
defined[term] = true
721751
else
@@ -893,13 +923,13 @@ def as_array?(term)
893923
end
894924

895925
##
896-
# Retrieve content of a term
926+
# Retrieve graph_id of a term
897927
#
898928
# @param [Term, #to_s] term in unexpanded form
899-
# @return [Hash]
900-
def content(term)
929+
# @return [String]
930+
def graph_id(term)
901931
term = find_definition(term)
902-
term && term.content
932+
term && term.graph_id
903933
end
904934

905935
##
@@ -1126,6 +1156,7 @@ def compact_iri(iri, value: nil, vocab: nil, reverse: false, quiet: false, **opt
11261156
# TODO: "@graph@set"?
11271157
containers << '@graph'
11281158
containers << '@set'
1159+
graph_id = value['@id']
11291160
else
11301161
if value?(value)
11311162
if value.has_key?('@language') && !index?(value)
@@ -1158,7 +1189,7 @@ def compact_iri(iri, value: nil, vocab: nil, reverse: false, quiet: false, **opt
11581189
preferred_values.concat([tl_value, '@none'].compact)
11591190
end
11601191
#log_debug("") {"preferred_values: #{preferred_values.inspect}"} unless quiet
1161-
if p_term = select_term(iri, containers, tl, preferred_values)
1192+
if p_term = select_term(iri, containers, tl, preferred_values, graph_id)
11621193
#log_debug("") {"=> term: #{p_term.inspect}"} unless quiet
11631194
return p_term
11641195
end
@@ -1557,8 +1588,10 @@ def inverse_context
15571588
# indicates whether to look for a term with a matching type mapping or language mapping
15581589
# @param [Array<String>] preferred_values
15591590
# for the type mapping or language mapping
1591+
# @param [String] graph_id
1592+
# @graphId required on term when container matches on @graph
15601593
# @return [String]
1561-
def select_term(iri, containers, type_language, preferred_values)
1594+
def select_term(iri, containers, type_language, preferred_values, graph_id)
15621595
#log_debug("select_term") {
15631596
# "iri: #{iri.inspect}, " +
15641597
# "containers: #{containers.inspect}, " +
@@ -1573,6 +1606,11 @@ def select_term(iri, containers, type_language, preferred_values)
15731606
value_map = tl_map[type_language]
15741607
preferred_values.each do |item|
15751608
next unless value_map.has_key?(item)
1609+
if container == '@graph'
1610+
# Graph names must not exist or exactly match
1611+
td = term_definitions[item]
1612+
next if td && td.graph_id != graph_id
1613+
end
15761614
#log_debug("=>") {value_map[item].inspect}
15771615
return value_map[item]
15781616
end

lib/json/ld/expand.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,11 +435,14 @@ def expand_object(input, active_property, context, output_object, ordered: false
435435
#log_debug(" => ") { "convert #{expanded_value.inspect} to list"}
436436
expanded_value = [expanded_value] unless expanded_value.is_a?(Array)
437437
expanded_value = {'@graph' => expanded_value}
438+
# Add any associated graphId
439+
graph_id = active_context.graph_id(key)
440+
expanded_value['@id'] = context.expand_iri(graph_id, vocab: true).to_s if graph_id
438441
end
439442

440443
# Otherwise, if the term definition associated to key indicates that it is a reverse property
441444
# Spec FIXME: this is not an otherwise.
442-
if (td = context.term_definitions[key]) && td.reverse_property
445+
if context.reverse?(key)
443446
# If result has no @reverse member, create one and initialize its value to an empty JSON object.
444447
reverse_map = output_object['@reverse'] ||= {}
445448
[expanded_value].flatten.each do |item|

spec/compact_spec.rb

Lines changed: 112 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -813,13 +813,36 @@
813813
}
814814
})
815815
},
816-
"Does not compacts graph with @id" => {
816+
"Compacts named graph" => {
817817
input: %([{
818818
"http://example.org/input": [{
819+
"@id": "http://example.org/gid",
819820
"@graph": [{
820821
"http://example.org/value": [{"@value": "x"}]
821-
}],
822-
"@id": "http://example.org/id"
822+
}]
823+
}]
824+
}]),
825+
context: %({
826+
"@vocab": "http://example.org/",
827+
"input": {"@container": "@graph", "@graphId": "gid"}
828+
}),
829+
output: %({
830+
"@context": {
831+
"@vocab": "http://example.org/",
832+
"input": {"@container": "@graph", "@graphId": "gid"}
833+
},
834+
"input": {
835+
"value": "x"
836+
}
837+
})
838+
},
839+
"Does not compact graph with @id without @graphId" => {
840+
input: %([{
841+
"http://example.org/input": [{
842+
"@id": "http://example.org/gid",
843+
"@graph": [{
844+
"http://example.org/value": [{"@value": "x"}]
845+
}]
823846
}]
824847
}]),
825848
context: %({
@@ -829,12 +852,38 @@
829852
output: %({
830853
"@context": {
831854
"@vocab": "http://example.org/",
832-
"input": {
833-
"@container": "@graph"
834-
}
855+
"input": {"@container": "@graph"}
856+
},
857+
"input": {
858+
"@id": "http://example.org/gid",
859+
"@graph": [
860+
{
861+
"value": "x"
862+
}
863+
]
864+
}
865+
})
866+
},
867+
"Does not compacts graph with @id without matching @graphId" => {
868+
input: %([{
869+
"http://example.org/input": [{
870+
"@id": "http://example.org/gid",
871+
"@graph": [{
872+
"http://example.org/value": [{"@value": "x"}]
873+
}]
874+
}]
875+
}]),
876+
context: %({
877+
"@vocab": "http://example.org/",
878+
"input": {"@container": "@graph", "@graphId": "nomatch"}
879+
}),
880+
output: %({
881+
"@context": {
882+
"@vocab": "http://example.org/",
883+
"input": {"@container": "@graph", "@graphId": "nomatch"}
835884
},
836885
"input": {
837-
"@id": "http://example.org/id",
886+
"@id": "http://example.org/gid",
838887
"@graph": [
839888
{
840889
"value": "x"
@@ -843,6 +892,62 @@
843892
}
844893
})
845894
},
895+
"Compacts named graph" => {
896+
input: %([{
897+
"http://example.org/input": [{
898+
"@id": "http://example.org/gid",
899+
"@graph": [{
900+
"http://example.org/value": [{"@value": "x"}]
901+
}]
902+
}]
903+
}]),
904+
context: %({
905+
"@vocab": "http://example.org/",
906+
"input": {"@container": "@graph", "@graphId": "gid"}
907+
}),
908+
output: %({
909+
"@context": {
910+
"@vocab": "http://example.org/",
911+
"input": {"@container": "@graph", "@graphId": "gid"}
912+
},
913+
"input": {
914+
"value": "x"
915+
}
916+
})
917+
},
918+
"Raises error when @graphId is an invalid IRI" => {
919+
input: %([{
920+
"http://example.org/input": [{
921+
"@id": "http://example.org/gid",
922+
"@graph": [{
923+
"http://example.org/value": [{"@value": "x"}]
924+
}]
925+
}]
926+
}]),
927+
context: %({
928+
"input": {
929+
"@id": "http://example.org/",
930+
"@container": "@graph",
931+
"@graphId": "gid"
932+
}
933+
}),
934+
exception: JSON::LD::JsonLdError::InvalidGraphIdMapping
935+
},
936+
"Raises error when @graphId used without @container: @graph" => {
937+
input: %([{
938+
"http://example.org/input": [{
939+
"@id": "http://example.org/gid",
940+
"@graph": [{
941+
"http://example.org/value": [{"@value": "x"}]
942+
}]
943+
}]
944+
}]),
945+
context: %({
946+
"@vocab": "http://example.org/",
947+
"input": {"@container": "@set", "@graphId": "gid"}
948+
}),
949+
exception: JSON::LD::JsonLdError::InvalidGraphIdMapping
950+
},
846951
}.each_pair do |title, params|
847952
it(title) {run_compact({processingMode: "json-ld-1.1"}.merge(params))}
848953
end

spec/expand_spec.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -958,6 +958,44 @@
958958
}]
959959
}])
960960
},
961+
"Creates a @graph container with an @graphId" => {
962+
input: %({
963+
"@context": {
964+
"@vocab": "http://example.org/",
965+
"input": {"@container": "@graph", "@graphId": "gid"}
966+
},
967+
"input": {
968+
"value": "x"
969+
}
970+
}),
971+
output: %([{
972+
"http://example.org/input": [{
973+
"@id": "http://example.org/gid",
974+
"@graph": [{
975+
"http://example.org/value": [{"@value": "x"}]
976+
}]
977+
}]
978+
}])
979+
},
980+
"Creates a @graph container with an blank @graphId" => {
981+
input: %({
982+
"@context": {
983+
"@vocab": "http://example.org/",
984+
"input": {"@container": "@graph", "@graphId": "_:gid"}
985+
},
986+
"input": {
987+
"value": "x"
988+
}
989+
}),
990+
output: %([{
991+
"http://example.org/input": [{
992+
"@id": "_:gid",
993+
"@graph": [{
994+
"http://example.org/value": [{"@value": "x"}]
995+
}]
996+
}]
997+
}])
998+
},
961999
}.each do |title, params|
9621000
it(title) {run_expand({processingMode: "json-ld-1.1"}.merge(params))}
9631001
end

0 commit comments

Comments
 (0)