Skip to content

Commit 2d7275c

Browse files
committed
type-scoped contexts disappear when used in sub-objects.
This is for w3c/json-ld-api#89.
1 parent ae6d7d2 commit 2d7275c

File tree

5 files changed

+84
-32
lines changed

5 files changed

+84
-32
lines changed

lib/json/ld/compact.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def compact(element, property: nil, ordered: false)
3030
# If the term definition for active property itself contains a context, use that for compacting values.
3131
input_context = self.context
3232
td = self.context.term_definitions[property] if property
33-
self.context = (td && td.context && self.context.parse(td.context, from_term: property)) || input_context
33+
self.context = (td && td.context && self.context.parse(td.context, from_property: property)) || input_context
3434

3535
case element
3636
when Array
@@ -67,18 +67,20 @@ def compact(element, property: nil, ordered: false)
6767
return compact(element['@list'], property: property, ordered: ordered)
6868
end
6969

70-
7170
inside_reverse = property == '@reverse'
7271
result, nest_result = {}, nil
7372

73+
# Revert any previously type-scoped term definitions
74+
self.context = context.revert_type_scoped_terms
75+
7476
# Apply any context defined on an alias of @type
7577
# If key is @type and any compacted value is a term having a local context, overlay that context.
7678
Array(element['@type']).
7779
map {|expanded_type| context.compact_iri(expanded_type, vocab: true)}.
7880
sort.
7981
each do |term|
8082
term_context = self.context.term_definitions[term].context if context.term_definitions[term]
81-
self.context = context.parse(term_context) if term_context
83+
self.context = context.parse(term_context, from_type: true) if term_context
8284
end
8385

8486
element.keys.opt_sort(ordered: ordered).each do |expanded_property|

lib/json/ld/context.rb

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,14 @@ class TermDefinition
7878
# @return [Boolean]
7979
attr_writer :protected
8080

81+
# Previous definition defined for this term. This is used for rolling back term definitions based on scoped types.
82+
# @return [TermDefinition]
83+
attr_reader :previous_definition
84+
85+
# Definition is from a type-scoped context.
86+
# @return [TermDefinition]
87+
attr_reader :type_scoped
88+
8189
# This is a simple term definition, not an expanded term definition
8290
# @return [Boolean] simple
8391
def simple?; simple; end
@@ -90,6 +98,8 @@ def prefix?; @prefix; end
9098
# @param [String] term
9199
# @param [String] id
92100
# @param [String] type_mapping Type mapping
101+
# @param [Boolean] type_scoped
102+
# @param [TermDefinition] previous_definition
93103
# @param [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] container_mapping
94104
# @param [String] language_mapping
95105
# Language mapping of term, `false` is used if there is explicitly no language mapping for this term
@@ -104,6 +114,8 @@ def initialize(term,
104114
id: nil,
105115
index: nil,
106116
type_mapping: nil,
117+
previous_definition: nil,
118+
type_scoped: false,
107119
container_mapping: nil,
108120
language_mapping: nil,
109121
reverse_property: false,
@@ -116,6 +128,8 @@ def initialize(term,
116128
@id = id.to_s unless id.nil?
117129
@index = index.to_s unless index.nil?
118130
@type_mapping = type_mapping.to_s unless type_mapping.nil?
131+
@previous_definition = previous_definition
132+
@type_scoped = type_scoped
119133
self.container_mapping = container_mapping
120134
@language_mapping = language_mapping unless language_mapping.nil?
121135
@reverse_property = reverse_property
@@ -241,6 +255,10 @@ def inspect
241255
# @return [Hash{String => TermDefinition}]
242256
attr_reader :term_definitions
243257

258+
# Context has definitions defined from type scopes
259+
# @return [Boolean]
260+
attr_accessor :has_typed_scopes
261+
244262
# @return [Hash{RDF::URI => String}] Reverse mappings from IRI to term only for terms, not CURIEs XXX
245263
attr_accessor :iri_to_term
246264

@@ -279,8 +297,8 @@ def inspect
279297
# @raise [JsonLdError]
280298
# on a remote context load error, syntax error, or a reference to a term which is not defined.
281299
# @return [Context]
282-
def self.parse(local_context, from_term: nil, **options)
283-
self.new(options).parse(local_context, from_term: from_term)
300+
def self.parse(local_context, from_property: false, from_type: false, **options)
301+
self.new(options).parse(local_context, from_property: from_property, from_type: from_type)
284302
end
285303

286304
##
@@ -409,15 +427,16 @@ def vocab=(value)
409427
#
410428
# @param [String, #read, Array, Hash, Context] local_context
411429
# @param [Array<String>] remote_contexts
412-
# @param [String] from_term
413-
# The active term, when expanding. Sealed terms may not be cleared unless from a
414-
# context associated with a term used as a property.
430+
# @param [Boolean] from_property
431+
# Context is created from a scoped context for a property. Sealed terms may not be cleared unless from a context associated with a term used as a property.
432+
# @param [Boolean] from_type
433+
# Context is created from a scoped context for a type. Retains any previously defined term, which can be rolled back when the type context changes.
415434
# @param [RDF::Resource] context_id from context IRI, for sealing terms
416435
# @raise [JsonLdError]
417436
# on a remote context load error, syntax error, or a reference to a term which is not defined.
418437
# @return [Context]
419438
# @see https://www.w3.org/TR/json-ld11-api/index.html#context-processing-algorithm
420-
def parse(local_context, remote_contexts: [], from_term: nil)
439+
def parse(local_context, remote_contexts: [], from_property: false, from_type: false)
421440
result = self.dup
422441
result.provided_context = local_context if self.empty?
423442

@@ -426,8 +445,8 @@ def parse(local_context, remote_contexts: [], from_term: nil)
426445
local_context.each do |context|
427446
case context
428447
when nil
429-
# 3.1 If the `from_term` is not null, and the active context contains protected terms, an error is raised.
430-
if from_term || result.term_definitions.values.none?(&:protected?)
448+
# 3.1 If the `from_property` is not null, and the active context contains protected terms, an error is raised.
449+
if from_property || result.term_definitions.values.none?(&:protected?)
431450
result = Context.new(options)
432451
else
433452
raise JSON::LD::JsonLdError::InvalidContextNullification,
@@ -502,7 +521,7 @@ def parse(local_context, remote_contexts: [], from_term: nil)
502521
end
503522

504523
# 3.2.6) Set context to the result of recursively calling this algorithm, passing context no base for active context, context for local context, and remote contexts.
505-
context = context_no_base.parse(context, remote_contexts: remote_contexts.dup, from_term: from_term)
524+
context = context_no_base.parse(context, remote_contexts: remote_contexts.dup, from_property: from_property, from_type: from_type)
506525
PRELOADED[context_canon.to_s] = context
507526
context.provided_context = result.provided_context
508527
end
@@ -532,7 +551,8 @@ def parse(local_context, remote_contexts: [], from_term: nil)
532551
context.each_key do |key|
533552
# ... where key is not @base, @vocab, @language, or @version
534553
result.create_term_definition(context, key, defined,
535-
from_term: from_term,
554+
from_property: from_property,
555+
from_type: from_type,
536556
protected: context['@protected']) unless NON_TERMDEF_KEYS.include?(key)
537557
end
538558
else
@@ -590,14 +610,15 @@ def merge!(context)
590610
# @param [Hash] local_context
591611
# @param [String] term
592612
# @param [Hash] defined
593-
# @param [String] from_term
594-
# The active term, when expanding. Sealed terms may not be cleared unless from a
595-
# context associated with a term used as a property.
613+
# @param [Boolean] from_property
614+
# Context is created from a scoped context for a property. Sealed terms may not be cleared unless from a context associated with a term used as a property.
615+
# @param [Boolean] from_type
616+
# Context is created from a scoped context for a type. Retains any previously defined term, which can be rolled back when the type context changes.
596617
# @param [Boolean] protected if true, causes all terms to be marked protected
597618
# @raise [JsonLdError]
598619
# Represents a cyclical term dependency
599620
# @see https://www.w3.org/TR/json-ld11-api/index.html#create-term-definition
600-
def create_term_definition(local_context, term, defined, from_term: nil, protected: false)
621+
def create_term_definition(local_context, term, defined, from_property: false, from_type: false, protected: false)
601622
# Expand a string value, unless it matches a keyword
602623
#log_debug("create_term_definition") {"term = #{term.inspect}"}
603624

@@ -627,22 +648,24 @@ def create_term_definition(local_context, term, defined, from_term: nil, protect
627648
value = {'@id' => value} if simple_term
628649

629650
# Remove any existing term definition for term in active context.
630-
if term_definitions[term] && term_definitions[term].protected? && from_term.nil?
651+
previous_definition = term_definitions[term]
652+
if previous_definition && previous_definition.protected? && !from_property
631653
raise JSON::LD::JsonLdError::ProtectedTermRedefinition, "Attempt to redefine protected term #{term}"
632654
else
633-
term_definitions.delete(term) unless term_definitions[term]
655+
term_definitions.delete(term) if previous_definition
634656
end
635657

636658
case value
637659
when nil, ID_NULL_OBJECT
638660
# If value equals null or value is a JSON object containing the key-value pair (@id-null), then set the term definition in active context to null, set the value associated with defined's key term to true, and return.
639661
#log_debug("") {"=> nil"}
640-
term_definitions[term] = TermDefinition.new(term)
662+
term_definitions[term] = TermDefinition.new(term, previous_definition: previous_definition, type_scoped: from_type)
663+
self.has_typed_scopes ||= from_type
641664
defined[term] = true
642665
return
643666
when Hash
644667
#log_debug("") {"Hash[#{term.inspect}] = #{value.inspect}"}
645-
definition = TermDefinition.new(term)
668+
definition = TermDefinition.new(term, previous_definition: previous_definition, type_scoped: from_type)
646669
definition.simple = simple_term
647670

648671
if options[:validate]
@@ -774,7 +797,7 @@ def create_term_definition(local_context, term, defined, from_term: nil, protect
774797

775798
if value.has_key?('@context')
776799
begin
777-
self.parse(value['@context'], from_term: term)
800+
self.parse(value['@context'], from_property: true)
778801
# Record null context in array form
779802
definition.context = value['@context'] ? value['@context'] : [nil]
780803
rescue JsonLdError => e
@@ -809,6 +832,7 @@ def create_term_definition(local_context, term, defined, from_term: nil, protect
809832
end
810833

811834
term_definitions[term] = definition
835+
self.has_typed_scopes ||= from_type
812836
defined[term] = true
813837
else
814838
raise JsonLdError::InvalidTermDefinition, "Term definition for #{term.inspect} is an #{value.class} on term #{term.inspect}"
@@ -934,6 +958,27 @@ def from_vocabulary(graph)
934958
self
935959
end
936960

961+
##
962+
# Revert any type-scoped terms in this context to their previous mappings.
963+
#
964+
# Creates a clone of the context with terms associated with type-scoped contexts reverted to their prior state
965+
def revert_type_scoped_terms
966+
return self unless has_typed_scopes
967+
968+
ctx = self.dup
969+
ctx.term_definitions.each do |term, definition|
970+
if definition.type_scoped
971+
if definition.previous_definition
972+
ctx.term_definitions[term] = definition.previous_definition
973+
else
974+
ctx.term_definitions.delete(term)
975+
end
976+
end
977+
end
978+
979+
ctx
980+
end
981+
937982
# Set term mapping
938983
#
939984
# @param [#to_s] term

lib/json/ld/expand.rb

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ module Expand
2929
# Ensure output objects have keys ordered properly
3030
# @param [Boolean] framing (false)
3131
# Special rules for expanding a frame
32+
# @param [Boolean] from_map
33+
# Expanding from a map, which could be an `@type` map, so don't clear out context term definitions
3234
# @return [Array<Hash{String => Object}>]
33-
def expand(input, active_property, context, ordered: false, framing: false)
35+
def expand(input, active_property, context, ordered: false, framing: false, from_map: false)
3436
#log_debug("expand") {"input: #{input.inspect}, active_property: #{active_property.inspect}, context: #{context.inspect}"}
3537
framing = false if active_property == '@default'
3638
result = case input
@@ -39,7 +41,7 @@ def expand(input, active_property, context, ordered: false, framing: false)
3941
is_list = context.container(active_property) == CONTAINER_LIST
4042
value = input.each_with_object([]) do |v, memo|
4143
# Initialize expanded item to the result of using this algorithm recursively, passing active context, active property, and item as element.
42-
v = expand(v, active_property, context, ordered: ordered, framing: framing)
44+
v = expand(v, active_property, context, ordered: ordered, framing: framing, from_map: from_map)
4345

4446
# If the active property is @list or its container mapping is set to @list and v is an array, change it to a list object
4547
v = {"@list" => v} if is_list && v.is_a?(Array)
@@ -61,12 +63,15 @@ def expand(input, active_property, context, ordered: false, framing: false)
6163

6264
output_object = {}
6365

66+
# Revert any previously type-scoped term definitions
67+
context = context.revert_type_scoped_terms unless from_map
68+
6469
# See if keys mapping to @type have terms with a local context
6570
input.each_pair do |key, val|
6671
next unless context.expand_iri(key, vocab: true) == '@type'
6772
Array(val).sort.each do |term|
6873
term_context = context.term_definitions[term].context if context.term_definitions[term]
69-
context = term_context ? context.parse(term_context) : context
74+
context = term_context ? context.parse(term_context, from_type: true) : context
7075
end
7176
end
7277

@@ -375,7 +380,7 @@ def expand_object(input, active_property, context, output_object, ordered:, fram
375380

376381
# Use a term-specific context, if defined
377382
term_context = context.term_definitions[key].context if context.term_definitions[key]
378-
active_context = term_context ? context.parse(term_context, from_term: key) : context
383+
active_context = term_context ? context.parse(term_context, from_property: true) : context
379384

380385
container = active_context.container(key)
381386
expanded_value = if active_context.coerce(key) == '@json'
@@ -416,13 +421,13 @@ def expand_object(input, active_property, context, output_object, ordered:, fram
416421
keys.each do |k|
417422
# If container mapping in the active context includes @type, and k is a term in the active context having a local context, use that context when expanding values
418423
map_context = active_context.term_definitions[k].context if container.include?('@type') && active_context.term_definitions[k]
419-
map_context = active_context.parse(map_context) if map_context
424+
map_context = active_context.parse(map_context, from_type: true) if map_context
420425
map_context ||= active_context
421426

422427
expanded_k = active_context.expand_iri(k, vocab: true, quiet: true).to_s
423428

424429
# Initialize index value to the result of using this algorithm recursively, passing active context, key as active property, and index value as element.
425-
index_value = expand([value[k]].flatten, key, map_context, ordered: ordered, framing: framing)
430+
index_value = expand([value[k]].flatten, key, map_context, ordered: ordered, framing: framing, from_map: true)
426431
index_value.each do |item|
427432
case container
428433
when CONTAINER_GRAPH_INDEX, CONTAINER_INDEX

spec/compact_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2312,7 +2312,7 @@
23122312
"a": {"type": "Foo", "bar": "baz"}
23132313
}),
23142314
},
2315-
"deep @context affects nested nodes" => {
2315+
"deep @context does not affect nested nodes" => {
23162316
input: %([
23172317
{
23182318
"@type": ["http://example/Foo"],
@@ -2331,7 +2331,7 @@
23312331
"Foo": {"@context": {"baz": {"@type": "@vocab"}}}
23322332
},
23332333
"@type": "Foo",
2334-
"bar": {"baz": "buzz"}
2334+
"bar": {"baz": {"@id": "http://example/buzz"}}
23352335
}),
23362336
},
23372337
"scoped context layers on intemediate contexts" => {

spec/expand_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2605,7 +2605,7 @@
26052605
}
26062606
])
26072607
},
2608-
"deep @context affects nested nodes": {
2608+
"deep @context does not affect nested nodes": {
26092609
input: %({
26102610
"@context": {
26112611
"@vocab": "http://example/",
@@ -2618,7 +2618,7 @@
26182618
{
26192619
"@type": ["http://example/Foo"],
26202620
"http://example/bar": [{
2621-
"http://example/baz": [{"@id": "http://example/buzz"}]
2621+
"http://example/baz": [{"@value": "buzz"}]
26222622
}]
26232623
}
26242624
])

0 commit comments

Comments
 (0)