Skip to content

Commit 8871081

Browse files
committed
Don't overload context PRELOADED with local cache.
Parse contexts without scope, and merge into active context. Improve context merging to look for term protection overrides.
1 parent 5b3c33c commit 8871081

File tree

4 files changed

+81
-68
lines changed

4 files changed

+81
-68
lines changed

lib/json/ld/context.rb

Lines changed: 65 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ class Context
2121
# @return [Hash{Symbol => Context}]
2222
PRELOADED = {}
2323

24+
##
25+
# Defines the maximum number of interned URI references that can be held
26+
# cached in memory at any one time.
27+
CACHE_SIZE = -1 # unlimited by default
28+
2429
class << self
2530
##
2631
# Add preloaded context. In the block form, the context is lazy evaulated on first use.
@@ -335,6 +340,16 @@ def self.parse(local_context, protected: false, override_protected: false, propa
335340
self.new(**options).parse(local_context, protected: false, override_protected: override_protected, propagate: propagate)
336341
end
337342

343+
##
344+
# Class-level cache used for retaining parsed remote contexts.
345+
#
346+
# @return [RDF::Util::Cache]
347+
# @private
348+
def self.cache
349+
require 'rdf/util/cache' unless defined?(::RDF::Util::Cache)
350+
@cache ||= RDF::Util::Cache.new(CACHE_SIZE)
351+
end
352+
338353
##
339354
# Create new evaluation context
340355
# @param [Hash] options
@@ -561,7 +576,7 @@ def parse(local_context,
561576
end
562577
when Context
563578
#log_debug("parse") {"context: #{context.inspect}"}
564-
result = context.dup
579+
result = result.merge(context)
565580
when IO, StringIO
566581
#log_debug("parse") {"io: #{context}"}
567582
# Load context document, if it is an open file
@@ -570,7 +585,6 @@ def parse(local_context,
570585
raise JSON::LD::JsonLdError::InvalidRemoteContext, "Context missing @context key" if @options[:validate] && ctx['@context'].nil?
571586
result = result.dup.parse(ctx["@context"] ? ctx["@context"].dup : {})
572587
result.provided_context = ctx["@context"] if [context] == local_context
573-
result
574588
rescue JSON::ParserError => e
575589
#log_debug("parse") {"Failed to parse @context from remote document at #{context}: #{e.message}"}
576590
raise JSON::LD::JsonLdError::InvalidRemoteContext, "Failed to parse remote context at #{context}: #{e.message}" if @options[:validate]
@@ -582,19 +596,15 @@ def parse(local_context,
582596
# 3.2.1) Set context to the result of resolving value against the base IRI which is established as specified in section 5.1 Establishing a Base URI of [RFC3986]. Only the basic algorithm in section 5.2 of [RFC3986] is used; neither Syntax-Based Normalization nor Scheme-Based Normalization are performed. Characters additionally allowed in IRI references are treated in the same way that unreserved characters are treated in URI references, per section 6.5 of [RFC3987].
583597
context = RDF::URI(result.context_base || options[:base]).join(context)
584598
context_canon = context.canonicalize
585-
context_canon.scheme == 'http' if context_canon.scheme == 'https'
599+
context_canon.scheme = 'http' if context_canon.scheme == 'https'
586600

587601
# If validating a scoped context which has already been loaded, skip to the next one
588602
next if !validate_scoped && remote_contexts.include?(context.to_s)
589603

590604
remote_contexts << context.to_s
591605
raise JsonLdError::ContextOverflow, "#{context}" if remote_contexts.length >= MAX_CONTEXTS_LOADED
592606

593-
context_no_base = result.dup
594-
context_no_base.base = nil
595-
context_no_base.context_base = context.to_s
596-
597-
if PRELOADED[context_canon.to_s]
607+
cached_context = if PRELOADED[context_canon.to_s]
598608
# If we have a cached context, merge it into the current context (result) and use as the new context
599609
#log_debug("parse") {"=> cached_context: #{context_canon.to_s.inspect}"}
600610

@@ -603,10 +613,10 @@ def parse(local_context,
603613
#log_debug("parse") {"=> (call)"}
604614
PRELOADED[context_canon.to_s] = PRELOADED[context_canon.to_s].call
605615
end
606-
context = context_no_base.merge!(PRELOADED[context_canon.to_s])
616+
PRELOADED[context_canon.to_s]
607617
else
608618
# Load context document, if it is a string
609-
begin
619+
Context.cache[context_canon.to_s] ||= begin
610620
context_opts = @options.merge(
611621
profile: 'http://www.w3.org/ns/json-ld#context',
612622
requestProfile: 'http://www.w3.org/ns/json-ld#context',
@@ -615,29 +625,33 @@ def parse(local_context,
615625
JSON::LD::API.loadRemoteDocument(context.to_s, **context_opts) do |remote_doc|
616626
# 3.2.5) Dereference context. If the dereferenced document has no top-level JSON object with an @context member, an invalid remote context has been detected and processing is aborted; otherwise, set context to the value of that member.
617627
raise JsonLdError::InvalidRemoteContext, "#{context}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.has_key?('@context')
618-
context = remote_doc.document['@context']
628+
629+
# Parse stand-alone
630+
ctx = Context.new(**options)
631+
ctx.context_base = context.to_s
632+
ctx.parse(remote_doc.document['@context'], remote_contexts: remote_contexts.dup)
619633
end
620634
rescue JsonLdError::LoadingDocumentFailed => e
621635
#log_debug("parse") {"Failed to retrieve @context from remote document at #{context_no_base.context_base.inspect}: #{e.message}"}
622-
raise JsonLdError::LoadingRemoteContextFailed, "#{context_no_base.context_base}: #{e.message}", e.backtrace
636+
raise JsonLdError::LoadingRemoteContextFailed, "#{context}: #{e.message}", e.backtrace
623637
rescue JsonLdError
624638
raise
625639
rescue StandardError => e
626640
#log_debug("parse") {"Failed to retrieve @context from remote document at #{context_no_base.context_base.inspect}: #{e.message}"}
627-
raise JsonLdError::LoadingRemoteContextFailed, "#{context_no_base.context_base}: #{e.message}", e.backtrace
641+
raise JsonLdError::LoadingRemoteContextFailed, "#{context}: #{e.message}", e.backtrace
628642
end
643+
end.dup()
629644

630-
# 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.
631-
context = context_no_base.parse(context,
632-
remote_contexts: remote_contexts.dup,
633-
protected: protected,
634-
override_protected: override_protected,
635-
propagate: propagate,
636-
validate_scoped: validate_scoped)
637-
PRELOADED[context_canon.to_s] = context.dup
638-
context.provided_context = result.provided_context
645+
# if `protected` is true, update term definitions to be protected
646+
# FIXME: if they were explicitly marked not protected, then this will fail
647+
if protected
648+
cached_context.term_definitions.each {|td| td.protected = true}
639649
end
640-
context.base ||= result.base
650+
651+
# Merge loaded context noting protected term overriding
652+
context = result.merge(cached_context, override_protected: override_protected)
653+
654+
context.previous_context = result unless propagate
641655
result = context
642656
#log_debug("parse") {"=> provided_context: #{context.inspect}"}
643657
when Hash
@@ -711,28 +725,31 @@ def parse(local_context,
711725
# Merge in a context, creating a new context with updates from `context`
712726
#
713727
# @param [Context] context
728+
# @param [Boolean] protected mark resulting context as protected
729+
# @param [Boolean] override_protected Allow or disallow protected terms to be changed
730+
# @param [Boolean]
714731
# @return [Context]
715-
def merge(context)
716-
c = self.dup.merge!(context)
717-
c.instance_variable_set(:@term_definitions, context.term_definitions.dup)
718-
c
719-
end
732+
def merge(context, override_protected: false)
733+
ctx = self.dup
734+
ctx.context_base = context.context_base if context.context_base
735+
ctx.default_language = context.default_language if context.default_language
736+
ctx.default_direction = context.default_direction if context.default_direction
737+
ctx.vocab = context.vocab if context.vocab
738+
ctx.base = context.base if context.base
739+
if !override_protected
740+
ctx.term_definitions.each do |term, definition|
741+
next unless definition.protected? && (other = context.term_definitions[term])
742+
unless definition == other
743+
raise JSON::LD::JsonLdError::ProtectedTermRedefinition, "Attempt to redefine protected term #{term}"
744+
end
745+
end
746+
end
720747

721-
##
722-
# Update context with definitions from `context`
723-
#
724-
# @param [Context] context
725-
# @return [self]
726-
def merge!(context)
727-
# FIXME: if new context removes the default language, this won't do anything
728-
self.default_language = context.default_language if context.default_language
729-
self.vocab = context.vocab if context.vocab
730-
self.base = context.base if context.base
731-
732-
# Merge in Term Definitions
733-
term_definitions.merge!(context.term_definitions)
734-
@inverse_context = nil # Re-build after term definitions set
735-
self
748+
# Add term definitions
749+
context.term_definitions.each do |term, definition|
750+
ctx.term_definitions[term] = definition
751+
end
752+
ctx
736753
end
737754

738755
# The following constants are used to reduce object allocations in #create_term_definition below
@@ -985,7 +1002,10 @@ def create_term_definition(local_context, term, defined,
9851002

9861003
if value.has_key?('@context')
9871004
begin
988-
new_ctx = self.parse(value['@context'], override_protected: true, remote_contexts: remote_contexts, validate_scoped: false)
1005+
new_ctx = self.parse(value['@context'],
1006+
override_protected: true,
1007+
remote_contexts: remote_contexts,
1008+
validate_scoped: false)
9891009
# Record null context in array form
9901010
definition.context = case value['@context']
9911011
when String then new_ctx.context_base
@@ -1838,6 +1858,7 @@ def dup
18381858
ec.instance_eval do
18391859
@term_definitions = that.term_definitions.dup
18401860
@iri_to_term = that.iri_to_term.dup
1861+
@inverse_context = nil
18411862
end
18421863
ec
18431864
end

lib/json/ld/expand.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ module Expand
2929
# Expanding from a map, which could be an `@type` map, so don't clear out context term definitions
3030
# @return [Array<Hash{String => Object}>]
3131
def expand(input, active_property, context, ordered: false, framing: false, from_map: false)
32-
#log_debug("expand") {"input: #{input.inspect}, active_property: #{active_property.inspect}, context: #{context.inspect}"}
32+
log_debug("expand") {"input: #{input.inspect}, active_property: #{active_property.inspect}, context: #{context.inspect}"}
3333
framing = false if active_property == '@default'
3434
expanded_active_property = context.expand_iri(active_property, vocab: true, as_string: true) if active_property
3535

@@ -73,7 +73,7 @@ def expand(input, active_property, context, ordered: false, framing: false, from
7373
# If element contains the key @context, set active context to the result of the Context Processing algorithm, passing active context and the value of the @context key as local context.
7474
if input.has_key?('@context')
7575
context = context.parse(input.delete('@context'))
76-
#log_debug("expand") {"context: #{context.inspect}"}
76+
log_debug("expand") {"context: #{context.inspect}"}
7777
end
7878

7979
# Set the type-scoped context to the context on input, for use later
@@ -102,7 +102,7 @@ def expand(input, active_property, context, ordered: false, framing: false, from
102102
ordered: ordered,
103103
framing: framing)
104104

105-
#log_debug("output object") {output_object.inspect}
105+
log_debug("output object") {output_object.inspect}
106106

107107
# If result contains the key @value:
108108
if value?(output_object)
@@ -161,7 +161,7 @@ def expand(input, active_property, context, ordered: false, framing: false, from
161161
if (expanded_active_property || '@graph') == '@graph' &&
162162
(output_object.key?('@value') || output_object.key?('@list') ||
163163
(output_object.keys - KEY_ID).empty? && !framing)
164-
#log_debug(" =>") { "empty top-level: " + output_object.inspect}
164+
log_debug(" =>") { "empty top-level: " + output_object.inspect}
165165
return nil
166166
end
167167

@@ -181,7 +181,7 @@ def expand(input, active_property, context, ordered: false, framing: false, from
181181
context.expand_value(active_property, input, log_depth: @options[:log_depth])
182182
end
183183

184-
#log_debug {" => #{result.inspect}"}
184+
log_debug {" => #{result.inspect}"}
185185
result
186186
end
187187

spec/compact_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,7 @@
564564
documentUrl: "http://example.com/context")
565565
end
566566
it "uses referenced context" do
567+
JSON::LD::Context.instance_variable_set(:@cache, nil)
567568
input = ::JSON.parse %({
568569
"http://example.com/b": "c"
569570
})

spec/context_spec.rb

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -57,14 +57,14 @@ def containers
5757
context "remote" do
5858

5959
it "retrieves and parses a remote context document" do
60-
JSON::LD::Context::PRELOADED.clear
60+
JSON::LD::Context.instance_variable_set(:@cache, nil)
6161
expect(JSON::LD::API).to receive(:documentLoader).with("http://example.com/context", anything).and_yield(remote_doc)
6262
ec = subject.parse("http://example.com/context")
6363
expect(ec.provided_context).to produce("http://example.com/context", logger)
6464
end
6565

6666
it "fails given a missing remote @context" do
67-
JSON::LD::Context::PRELOADED.clear
67+
JSON::LD::Context.instance_variable_set(:@cache, nil)
6868
expect(JSON::LD::API).to receive(:documentLoader).with("http://example.com/context", anything).and_raise(IOError)
6969
expect {subject.parse("http://example.com/context")}.to raise_error(JSON::LD::JsonLdError::LoadingRemoteContextFailed, %r{http://example.com/context})
7070
end
@@ -116,7 +116,7 @@ def containers
116116
documentUrl: "http://example.com/context",
117117
contentType: "text/html")
118118

119-
JSON::LD::Context::PRELOADED.clear
119+
JSON::LD::Context.instance_variable_set(:@cache, nil)
120120
expect(JSON::LD::API).to receive(:documentLoader).with("http://example.com/context", anything).and_yield(remote_doc)
121121
ec = subject.parse("http://example.com/context")
122122
expect(ec.send(:mappings)).to produce({
@@ -154,7 +154,7 @@ def containers
154154
),
155155
documentUrl: "http://example.com/context",
156156
contentType: "text/html")
157-
JSON::LD::Context::PRELOADED.clear
157+
JSON::LD::Context.instance_variable_set(:@cache, nil)
158158
expect(JSON::LD::API).to receive(:documentLoader).with("http://example.com/context", anything).and_yield(remote_doc)
159159
ec = subject.parse("http://example.com/context")
160160
expect(ec.send(:mappings)).to produce({
@@ -170,7 +170,7 @@ def containers
170170
end
171171

172172
it "parses a referenced context at a relative URI" do
173-
JSON::LD::Context::PRELOADED.clear
173+
JSON::LD::Context.instance_variable_set(:@cache, nil)
174174
rd1 = JSON::LD::API::RemoteDocument.new(%({"@context": "context"}), base_uri: "http://example.com/c1")
175175
expect(JSON::LD::API).to receive(:documentLoader).with("http://example.com/c1", anything).and_yield(rd1)
176176
expect(JSON::LD::API).to receive(:documentLoader).with("http://example.com/context", anything).and_yield(remote_doc)
@@ -185,7 +185,7 @@ def containers
185185

186186
context "remote with local mappings" do
187187
let(:ctx) {["http://example.com/context", {"integer" => "xsd:integer"}]}
188-
before {JSON::LD::Context::PRELOADED.clear}
188+
before {JSON::LD::Context.instance_variable_set(:@cache, nil)}
189189
it "retrieves and parses a remote context document" do
190190
expect(JSON::LD::API).to receive(:documentLoader).with("http://example.com/context", anything).and_yield(remote_doc)
191191
subject.parse(ctx)
@@ -206,7 +206,7 @@ def containers
206206
)
207207
JSON::LD::Context.alias_preloaded("https://example.com/preloaded", "http://example.com/preloaded")
208208
}
209-
after(:all) {JSON::LD::Context::PRELOADED.clear}
209+
after(:all) {JSON::LD::Context.instance_variable_set(:@cache, nil)}
210210

211211
it "does not load referenced context" do
212212
expect(JSON::LD::API).not_to receive(:documentLoader).with(ctx, anything)
@@ -249,6 +249,7 @@ def containers
249249
end
250250

251251
it "merges definitions from remote contexts" do
252+
JSON::LD::Context.instance_variable_set(:@cache, nil)
252253
expect(JSON::LD::API).to receive(:documentLoader).with("http://example.com/context", anything).and_yield(remote_doc)
253254
rd2 = JSON::LD::API::RemoteDocument.new(%q({
254255
"@context": {
@@ -476,7 +477,7 @@ def containers
476477
end
477478

478479
context "@import" do
479-
before(:each) {JSON::LD::Context::PRELOADED.clear}
480+
before(:each) {JSON::LD::Context.instance_variable_set(:@cache, nil)}
480481
it "generates an InvalidImportValue error if not a string" do
481482
expect {subject.parse({'@version' => 1.1, '@import' => true})}.to raise_error(JSON::LD::JsonLdError::InvalidImportValue)
482483
end
@@ -642,18 +643,8 @@ def containers
642643
end
643644
end
644645

645-
describe "#merge!" do
646-
it "updates context with components from new" do
647-
c2 = JSON::LD::Context.parse({'foo' => "http://example.com/"})
648-
cm = context.merge!(c2)
649-
expect(cm).to equal context
650-
expect(cm).not_to equal c2
651-
expect(cm.term_definitions).to eq c2.term_definitions
652-
end
653-
end
654-
655646
describe "#serialize" do
656-
before {JSON::LD::Context::PRELOADED.clear}
647+
before {JSON::LD::Context.instance_variable_set(:@cache, nil)}
657648
it "context document" do
658649
expect(JSON::LD::API).to receive(:documentLoader).with("http://example.com/context", anything).and_yield(remote_doc)
659650
ec = subject.parse("http://example.com/context")

0 commit comments

Comments
 (0)