-
Notifications
You must be signed in to change notification settings - Fork 27
/
Copy pathcontext.rb
2368 lines (2110 loc) · 99.9 KB
/
context.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# frozen_string_literal: true
require 'json'
require 'bigdecimal'
require 'set'
require 'rdf/util/cache'
module JSON
module LD
class Context
include Utils
include RDF::Util::Logger
##
# Preloaded contexts.
# To avoid runtime context parsing and downloading, contexts may be pre-loaded by implementations.
# @return [Hash{Symbol => Context}]
PRELOADED = {}
# Initial contexts, defined on first access
INITIAL_CONTEXTS = {}
##
# Defines the maximum number of interned URI references that can be held
# cached in memory at any one time.
CACHE_SIZE = 100 # unlimited by default
class << self
##
# Add preloaded context. In the block form, the context is lazy evaulated on first use.
# @param [String, RDF::URI] url
# @param [Context] context (nil)
# @yieldreturn [Context]
def add_preloaded(url, context = nil, &block)
PRELOADED[url.to_s.freeze] = context || block
end
##
# Alias a previousliy loaded context
# @param [String, RDF::URI] a
# @param [String, RDF::URI] url
def alias_preloaded(a, url)
PRELOADED[a.to_s.freeze] = PRELOADED[url.to_s.freeze]
end
end
begin
# Attempt to load this to avoid unnecessary context fetches
require 'json/ld/preloaded'
rescue LoadError
# Silently allow this to fail
end
# The base.
#
# @return [RDF::URI] Current base IRI, used for expanding relative IRIs.
attr_reader :base
# @return [RDF::URI] base IRI of the context, if loaded remotely.
attr_accessor :context_base
# Term definitions
# @return [Hash{String => TermDefinition}]
attr_reader :term_definitions
# @return [Hash{RDF::URI => String}] Reverse mappings from IRI to term only for terms, not CURIEs XXX
attr_accessor :iri_to_term
# Previous definition for this context. This is used for rolling back type-scoped contexts.
# @return [Context]
attr_accessor :previous_context
# Context is property-scoped
# @return [Boolean]
attr_accessor :property_scoped
# Default language
#
# This adds a language to plain strings that aren't otherwise coerced
# @return [String]
attr_reader :default_language
# Default direction
#
# This adds a direction to plain strings that aren't otherwise coerced
# @return ["lrt", "rtl"]
attr_reader :default_direction
# Default vocabulary
#
# Sets the default vocabulary used for expanding terms which
# aren't otherwise absolute IRIs
# @return [RDF::URI]
attr_reader :vocab
# @return [Hash{Symbol => Object}] Global options used in generating IRIs
attr_accessor :options
# @return [BlankNodeNamer]
attr_accessor :namer
##
# Create a new context by parsing a context.
#
# @see #initialize
# @see #parse
# @param [String, #read, Array, Hash, Context] local_context
# @param [String, #to_s] base (nil)
# The Base IRI to use when expanding the document. This overrides the value of `input` if it is a _IRI_. If not specified and `input` is not an _IRI_, the base IRI defaults to the current document IRI if in a browser context, or the empty string if there is no document context.
# @param [Boolean] override_protected (false)
# Protected terms may be cleared.
# @param [Boolean] propagate (true)
# If false, retains any previously defined term, which can be rolled back when the descending into a new node object changes.
# @raise [JsonLdError]
# on a remote context load error, syntax error, or a reference to a term which is not defined.
# @return [Context]
def self.parse(local_context,
base: nil,
override_protected: false,
propagate: true,
**options)
c = new(**options)
if local_context.respond_to?(:empty?) && local_context.empty?
c
else
c.parse(local_context,
base: base,
override_protected: override_protected,
propagate: propagate)
end
end
##
# Class-level cache used for retaining parsed remote contexts.
#
# @return [RDF::Util::Cache]
# @private
def self.cache
@cache ||= RDF::Util::Cache.new(CACHE_SIZE)
end
##
# Class-level cache inverse contexts.
#
# @return [RDF::Util::Cache]
# @private
def self.inverse_cache
@inverse_cache ||= RDF::Util::Cache.new(CACHE_SIZE)
end
##
# @private
# Allow caching of well-known contexts
def self.new(**options)
if (options.keys - %i[
compactArrays
documentLoader
extractAllScripts
ordered
processingMode
validate
]).empty?
# allow caching
key = options.hash
INITIAL_CONTEXTS[key] ||= begin
context = JSON::LD::Context.allocate
context.send(:initialize, **options)
context.freeze
context.term_definitions.freeze
context
end
else
# Don't try to cache
context = JSON::LD::Context.allocate
context.send(:initialize, **options)
context
end
end
##
# Create new evaluation context
# @param [Hash] options
# @option options [Hash{Symbol => String}] :prefixes
# See `RDF::Reader#initialize`
# @option options [String, #to_s] :vocab
# Initial value for @vocab
# @option options [String, #to_s] :language
# Initial value for @langauge
# @yield [ec]
# @yieldparam [Context]
# @return [Context]
def initialize(**options)
@processingMode = 'json-ld-1.0' if options[:processingMode] == 'json-ld-1.0'
@term_definitions = {}
@iri_to_term = {
RDF.to_uri.to_s => "rdf",
RDF::XSD.to_uri.to_s => "xsd"
}
@namer = BlankNodeMapper.new("t")
@options = options
# Load any defined prefixes
(options[:prefixes] || {}).each_pair do |k, v|
next if k.nil?
@iri_to_term[v.to_s] = k
@term_definitions[k.to_s] = TermDefinition.new(k, id: v.to_s, simple: true, prefix: true)
end
self.vocab = options[:vocab] if options[:vocab]
self.default_language = options[:language] if /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/.match?(options[:language])
@term_definitions = options[:term_definitions] if options[:term_definitions]
# log_debug("init") {"iri_to_term: #{iri_to_term.inspect}"}
yield(self) if block_given?
end
# Create an Evaluation Context
#
# When processing a JSON-LD data structure, each processing rule is applied using information provided by the active context. This section describes how to produce an active context.
#
# The active context contains the active term definitions which specify how properties and values have to be interpreted as well as the current base IRI, the vocabulary mapping and the default language. Each term definition consists of an IRI mapping, a boolean flag reverse property, an optional type mapping or language mapping, and an optional container mapping. A term definition can not only be used to map a term to an IRI, but also to map a term to a keyword, in which case it is referred to as a keyword alias.
#
# When processing, the active context is initialized without any term definitions, vocabulary mapping, or default language. If a local context is encountered during processing, a new active context is created by cloning the existing active context. Then the information from the local context is merged into the new active context. Given that local contexts may contain references to remote contexts, this includes their retrieval.
#
#
# @param [String, #read, Array, Hash, Context] local_context
# @param [String, #to_s] base
# The Base IRI to use when expanding the document. This overrides the value of `input` if it is a _IRI_. If not specified and `input` is not an _IRI_, the base IRI defaults to the current document IRI if in a browser context, or the empty string if there is no document context.
# @param [Boolean] override_protected Protected terms may be cleared.
# @param [Boolean] propagate (true)
# If false, retains any previously defined term, which can be rolled back when the descending into a new node object changes.
# @param [Array<String>] remote_contexts ([])
# @param [Boolean] validate_scoped (true).
# Validate scoped context, loading if necessary.
# If false, do not load scoped contexts.
# @raise [JsonLdError]
# on a remote context load error, syntax error, or a reference to a term which is not defined.
# @return [Context]
# @see https://www.w3.org/TR/json-ld11-api/index.html#context-processing-algorithm
def parse(local_context,
base: nil,
override_protected: false,
propagate: true,
remote_contexts: [],
validate_scoped: true)
result = dup
# Early check for @propagate, which can only appear in a local context
propagate = local_context.is_a?(Hash) ? local_context.fetch('@propagate', propagate) : propagate
result.previous_context ||= result.dup unless propagate
local_context = as_array(local_context)
log_depth do
local_context.each do |context|
case context
when nil, false
# 3.1 If the `override_protected` is false, and the active context contains protected terms, an error is raised.
if override_protected || result.term_definitions.values.none?(&:protected?)
null_context = Context.new(**options)
null_context.previous_context = result unless propagate
result = null_context
else
raise JSON::LD::JsonLdError::InvalidContextNullification,
"Attempt to clear a context with protected terms"
end
when Context
# log_debug("parse") {"context: #{context.inspect}"}
result = result.merge(context)
when IO, StringIO
# log_debug("parse") {"io: #{context}"}
# Load context document, if it is an open file
begin
ctx = load_context(context, **@options)
if @options[:validate] && ctx['@context'].nil?
raise JSON::LD::JsonLdError::InvalidRemoteContext,
"Context missing @context key"
end
result = result.parse(ctx["@context"] || {})
rescue JSON::ParserError => e
log_info("parse") { "Failed to parse @context from remote document at #{context}: #{e.message}" }
if @options[:validate]
raise JSON::LD::JsonLdError::InvalidRemoteContext,
"Failed to parse remote context at #{context}: #{e.message}"
end
self
end
when String, RDF::URI
# log_debug("parse") {"remote: #{context}, base: #{result.context_base || result.base}"}
# 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].
context = RDF::URI(result.context_base || base).join(context)
context_canon = context.canonicalize
context_canon.scheme = 'http' if context_canon.scheme == 'https'
# If validating a scoped context which has already been loaded, skip to the next one
next if !validate_scoped && remote_contexts.include?(context.to_s)
remote_contexts << context.to_s
raise JsonLdError::ContextOverflow, context.to_s if remote_contexts.length >= MAX_CONTEXTS_LOADED
cached_context = if PRELOADED[context_canon.to_s]
# If we have a cached context, merge it into the current context (result) and use as the new context
# log_debug("parse") {"=> cached_context: #{context_canon.to_s.inspect}"}
# If this is a Proc, then replace the entry with the result of running the Proc
if PRELOADED[context_canon.to_s].respond_to?(:call)
# log_debug("parse") {"=> (call)"}
PRELOADED[context_canon.to_s] = PRELOADED[context_canon.to_s].call
end
PRELOADED[context_canon.to_s].context_base ||= context_canon.to_s
PRELOADED[context_canon.to_s]
else
# Load context document, if it is a string
Context.cache[context_canon.to_s] ||= begin
context_opts = @options.merge(
profile: 'http://www.w3.org/ns/json-ld#context',
requestProfile: 'http://www.w3.org/ns/json-ld#context',
base: nil
)
# context_opts.delete(:headers)
JSON::LD::API.loadRemoteDocument(context.to_s, **context_opts) do |remote_doc|
# 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.
unless remote_doc.document.is_a?(Hash) && remote_doc.document.key?('@context')
raise JsonLdError::InvalidRemoteContext,
context.to_s
end
# Parse stand-alone
ctx = Context.new(unfrozen: true, **options).dup
ctx.context_base = context.to_s
ctx = ctx.parse(remote_doc.document['@context'], remote_contexts: remote_contexts.dup)
ctx.context_base = context.to_s # In case it was altered
ctx.instance_variable_set(:@base, nil)
ctx
end
rescue JsonLdError::LoadingDocumentFailed => e
log_info("parse") do
"Failed to retrieve @context from remote document at #{context_canon.inspect}: #{e.message}"
end
raise JsonLdError::LoadingRemoteContextFailed, "#{context}: #{e.message}", e.backtrace
rescue JsonLdError
raise
rescue StandardError => e
log_info("parse") do
"Failed to retrieve @context from remote document at #{context_canon.inspect}: #{e.message}"
end
raise JsonLdError::LoadingRemoteContextFailed, "#{context}: #{e.message}", e.backtrace
end
end
# Merge loaded context noting protected term overriding
context = result.merge(cached_context, override_protected: override_protected)
context.previous_context = self unless propagate
result = context
when Hash
context = context.dup # keep from modifying a hash passed as a param
# This counts on hash elements being processed in order
{
'@version' => :processingMode=,
'@import' => nil,
'@base' => :base=,
'@direction' => :default_direction=,
'@language' => :default_language=,
'@propagate' => :propagate=,
'@vocab' => :vocab=
}.each do |key, setter|
next unless context.key?(key)
if key == '@import'
# Retrieve remote context and merge the remaining context object into the result.
if result.processingMode("json-ld-1.0")
raise JsonLdError::InvalidContextEntry,
"@import may only be used in 1.1 mode}"
end
unless context['@import'].is_a?(String)
raise JsonLdError::InvalidImportValue,
"@import must be a string: #{context['@import'].inspect}"
end
import_loc = RDF::URI(result.context_base || base).join(context['@import'])
begin
context_opts = @options.merge(
profile: 'http://www.w3.org/ns/json-ld#context',
requestProfile: 'http://www.w3.org/ns/json-ld#context',
base: nil
)
context_opts.delete(:headers)
# FIXME: should cache this, but ContextCache is for parsed contexts
JSON::LD::API.loadRemoteDocument(import_loc, **context_opts) do |remote_doc|
# Dereference import_loc. 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.
unless remote_doc.document.is_a?(Hash) && remote_doc.document.key?('@context')
raise JsonLdError::InvalidRemoteContext,
import_loc.to_s
end
import_context = remote_doc.document['@context']
import_context.delete('@base')
unless import_context.is_a?(Hash)
raise JsonLdError::InvalidRemoteContext,
"#{import_context.to_json} must be an object"
end
if import_context.key?('@import')
raise JsonLdError::InvalidContextEntry,
"#{import_context.to_json} must not include @import entry"
end
context.delete(key)
context = import_context.merge(context)
end
rescue JsonLdError::LoadingDocumentFailed => e
raise JsonLdError::LoadingRemoteContextFailed, "#{import_loc}: #{e.message}", e.backtrace
rescue JsonLdError
raise
rescue StandardError => e
raise JsonLdError::LoadingRemoteContextFailed, "#{import_loc}: #{e.message}", e.backtrace
end
else
result.send(setter, context[key], remote_contexts: remote_contexts)
end
context.delete(key)
end
defined = {}
# For each key-value pair in context invoke the Create Term Definition subalgorithm, passing result for active context, context for local context, key, and defined
context.each_key do |key|
# ... where key is not @base, @vocab, @language, or @version
next if NON_TERMDEF_KEYS.include?(key)
result.create_term_definition(context, key, defined,
base: base,
override_protected: override_protected,
protected: context['@protected'],
remote_contexts: remote_contexts.dup,
validate_scoped: validate_scoped)
end
else
# 3.3) If context is not a JSON object, an invalid local context error has been detected and processing is aborted.
raise JsonLdError::InvalidLocalContext, "must be a URL, JSON object or array of same: #{context.inspect}"
end
end
end
result
end
##
# Merge in a context, creating a new context with updates from `context`
#
# @param [Context] context
# @param [Boolean] override_protected Allow or disallow protected terms to be changed
# @return [Context]
def merge(context, override_protected: false)
ctx = Context.new(term_definitions: term_definitions, standard_prefixes: options[:standard_prefixes])
ctx.context_base = context.context_base || context_base
ctx.default_language = context.default_language || default_language
ctx.default_direction = context.default_direction || default_direction
ctx.vocab = context.vocab || vocab
ctx.base = base unless base.nil?
unless override_protected
ctx.term_definitions.each do |term, definition|
next unless definition.protected? && (other = context.term_definitions[term])
unless definition == other
raise JSON::LD::JsonLdError::ProtectedTermRedefinition, "Attempt to redefine protected term #{term}"
end
end
end
# Add term definitions
context.term_definitions.each do |term, definition|
ctx.term_definitions[term] = definition
end
ctx
end
# The following constants are used to reduce object allocations in #create_term_definition below
ID_NULL_OBJECT = { '@id' => nil }.freeze
NON_TERMDEF_KEYS = Set.new(%w[@base @direction @language @protected @version @vocab]).freeze
JSON_LD_10_EXPECTED_KEYS = Set.new(%w[@container @id @language @reverse @type]).freeze
JSON_LD_11_EXPECTED_KEYS = Set.new(%w[@context @direction @index @nest @prefix @protected]).freeze
JSON_LD_EXPECTED_KEYS = (JSON_LD_10_EXPECTED_KEYS + JSON_LD_11_EXPECTED_KEYS).freeze
JSON_LD_10_TYPE_VALUES = Set.new(%w[@id @vocab]).freeze
JSON_LD_11_TYPE_VALUES = Set.new(%w[@json @none]).freeze
PREFIX_URI_ENDINGS = Set.new(%w(: / ? # [ ] @)).freeze
##
# Create Term Definition
#
# Term definitions are created by parsing the information in the given local context for the given term. If the given term is a compact IRI, it may omit an IRI mapping by depending on its prefix having its own term definition. If the prefix is a key in the local context, then its term definition must first be created, through recursion, before continuing. Because a term definition can depend on other term definitions, a mechanism must be used to detect cyclical dependencies. The solution employed here uses a map, defined, that keeps track of whether or not a term has been defined or is currently in the process of being defined. This map is checked before any recursion is attempted.
#
# After all dependencies for a term have been defined, the rest of the information in the local context for the given term is taken into account, creating the appropriate IRI mapping, container mapping, and type mapping or language mapping for the term.
#
# @param [Hash] local_context
# @param [String] term
# @param [Hash] defined
# @param [String, RDF::URI] base for resolving document-relative IRIs
# @param [Boolean] protected if true, causes all terms to be marked protected
# @param [Boolean] override_protected Protected terms may be cleared.
# @param [Array<String>] remote_contexts
# @param [Boolean] validate_scoped (true).
# Validate scoped context, loading if necessary.
# If false, do not load scoped contexts.
# @raise [JsonLdError]
# Represents a cyclical term dependency
# @see https://www.w3.org/TR/json-ld11-api/index.html#create-term-definition
def create_term_definition(local_context, term, defined,
base: nil,
override_protected: false,
protected: nil,
remote_contexts: [],
validate_scoped: true)
# Expand a string value, unless it matches a keyword
# log_debug("create_term_definition") {"term = #{term.inspect}"}
# If defined contains the key term, then the associated value must be true, indicating that the term definition has already been created, so return. Otherwise, a cyclical term definition has been detected, which is an error.
case defined[term]
when TrueClass then return
when nil
defined[term] = false
else
raise JsonLdError::CyclicIRIMapping, "Cyclical term dependency found: #{term.inspect}"
end
# Initialize value to a the value associated with the key term in local context.
value = local_context.fetch(term, false)
simple_term = value.is_a?(String) || value.nil?
# Since keywords cannot be overridden, term must not be a keyword. Otherwise, an invalid value has been detected, which is an error.
if term == '@type' &&
value.is_a?(Hash) &&
!value.empty? &&
processingMode("json-ld-1.1") &&
(value.keys - %w[@container @protected]).empty? &&
value.fetch('@container', '@set') == '@set'
# thes are the only cases were redefining a keyword is allowed
elsif KEYWORDS.include?(term) # TODO: anything that looks like a keyword
raise JsonLdError::KeywordRedefinition, "term must not be a keyword: #{term.inspect}" if
@options[:validate]
elsif term.to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
warn "Terms beginning with '@' are reserved for future use and ignored: #{term}."
return
elsif !term_valid?(term) && @options[:validate]
raise JsonLdError::InvalidTermDefinition, "term is invalid: #{term.inspect}"
end
value = { '@id' => value } if simple_term
# Remove any existing term definition for term in active context.
previous_definition = term_definitions[term]
if previous_definition&.protected? && !override_protected
# Check later to detect identical redefinition
elsif previous_definition
term_definitions.delete(term)
end
unless value.is_a?(Hash)
raise JsonLdError::InvalidTermDefinition,
"Term definition for #{term.inspect} is an #{value.class} on term #{term.inspect}"
end
# log_debug("") {"Hash[#{term.inspect}] = #{value.inspect}"}
definition = TermDefinition.new(term)
definition.simple = simple_term
expected_keys = case processingMode
when "json-ld-1.0" then JSON_LD_10_EXPECTED_KEYS
else JSON_LD_EXPECTED_KEYS
end
# Any of these keys cause us to process as json-ld-1.1, unless otherwise set
if processingMode.nil? && value.any? { |key, _| !JSON_LD_11_EXPECTED_KEYS.include?(key) }
processingMode('json-ld-11')
end
if value.any? { |key, _| !expected_keys.include?(key) }
extra_keys = value.keys - expected_keys.to_a
raise JsonLdError::InvalidTermDefinition,
"Term definition for #{term.inspect} has unexpected keys: #{extra_keys.join(', ')}"
end
# Potentially note that the term is protected
definition.protected = value.fetch('@protected', protected)
if value.key?('@type')
type = value['@type']
# SPEC FIXME: @type may be nil
type = case type
when nil
type
when String
begin
expand_iri(type, vocab: true, documentRelative: false, local_context: local_context, defined: defined)
rescue JsonLdError::InvalidIRIMapping
raise JsonLdError::InvalidTypeMapping,
"invalid mapping for '@type': #{type.inspect} on term #{term.inspect}"
end
else
:error
end
if JSON_LD_11_TYPE_VALUES.include?(type) && processingMode('json-ld-1.1')
# This is okay and used in compaction in 1.1
elsif !JSON_LD_10_TYPE_VALUES.include?(type) && !(type.is_a?(RDF::URI) && type.absolute?)
raise JsonLdError::InvalidTypeMapping,
"unknown mapping for '@type': #{type.inspect} on term #{term.inspect}"
end
# log_debug("") {"type_mapping: #{type.inspect}"}
definition.type_mapping = type
end
if value.key?('@reverse')
raise JsonLdError::InvalidReverseProperty, "unexpected key in #{value.inspect} on term #{term.inspect}" if
value.key?('@id') || value.key?('@nest')
unless value['@reverse'].is_a?(String)
raise JsonLdError::InvalidIRIMapping,
"expected value of @reverse to be a string: #{value['@reverse'].inspect} on term #{term.inspect}"
end
if value['@reverse'].to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
warn "Values beginning with '@' are reserved for future use and ignored: #{value['@reverse']}."
return
end
# Otherwise, set the IRI mapping of definition to the result of using the IRI Expansion algorithm, passing active context, the value associated with the @reverse key for value, true for vocab, true for document relative, local context, and defined. If the result is not an absolute IRI, i.e., it contains no colon (:), an invalid IRI mapping error has been detected and processing is aborted.
definition.id = expand_iri(value['@reverse'],
vocab: true,
local_context: local_context,
defined: defined)
unless definition.id.is_a?(RDF::Node) || (definition.id.is_a?(RDF::URI) && definition.id.absolute?)
raise JsonLdError::InvalidIRIMapping,
"non-absolute @reverse IRI: #{definition.id} on term #{term.inspect}"
end
if term[1..].to_s.include?(':') && (term_iri = expand_iri(term)) != definition.id
raise JsonLdError::InvalidIRIMapping, "term #{term} expands to #{definition.id}, not #{term_iri}"
end
if @options[:validate] && processingMode('json-ld-1.1') && definition.id.to_s.start_with?("_:")
warn "[DEPRECATION] Blank Node terms deprecated in JSON-LD 1.1."
end
# If value contains an @container member, set the container mapping of definition to its value; if its value is neither @set, @index, @type, @id, an absolute IRI nor null, an invalid reverse property error has been detected (reverse properties only support set- and index-containers) and processing is aborted.
if value.key?('@container')
container = value['@container']
unless container.is_a?(String) && ['@set', '@index'].include?(container)
raise JsonLdError::InvalidReverseProperty,
"unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}"
end
definition.container_mapping = check_container(container, local_context, defined, term)
end
definition.reverse_property = true
elsif value.key?('@id') && value['@id'].nil?
# Allowed to reserve a null term, which may be protected
elsif value.key?('@id') && value['@id'] != term
unless value['@id'].is_a?(String)
raise JsonLdError::InvalidIRIMapping,
"expected value of @id to be a string: #{value['@id'].inspect} on term #{term.inspect}"
end
if !KEYWORDS.include?(value['@id'].to_s) && value['@id'].to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
warn "Values beginning with '@' are reserved for future use and ignored: #{value['@id']}."
return
end
definition.id = expand_iri(value['@id'],
vocab: true,
local_context: local_context,
defined: defined)
raise JsonLdError::InvalidKeywordAlias, "expected value of @id to not be @context on term #{term.inspect}" if
definition.id == '@context'
if term.match?(%r{(?::[^:])|/})
term_iri = expand_iri(term,
vocab: true,
local_context: local_context,
defined: defined.merge(term => true))
if term_iri != definition.id
raise JsonLdError::InvalidIRIMapping, "term #{term} expands to #{definition.id}, not #{term_iri}"
end
end
if @options[:validate] && processingMode('json-ld-1.1') && definition.id.to_s.start_with?("_:")
warn "[DEPRECATION] Blank Node terms deprecated in JSON-LD 1.1."
end
# If id ends with a gen-delim, it may be used as a prefix for simple terms
definition.prefix = true if !term.include?(':') &&
simple_term &&
(definition.id.to_s.end_with?(':', '/', '?', '#', '[', ']',
'@') || definition.id.to_s.start_with?('_:'))
elsif term[1..].include?(':')
# If term is a compact IRI with a prefix that is a key in local context then a dependency has been found. Use this algorithm recursively passing active context, local context, the prefix as term, and defined.
prefix, suffix = term.split(':', 2)
create_term_definition(local_context, prefix, defined, protected: protected) if local_context.key?(prefix)
definition.id = if (td = term_definitions[prefix])
# If term's prefix has a term definition in active context, set the IRI mapping for definition to the result of concatenating the value associated with the prefix's IRI mapping and the term's suffix.
td.id + suffix
else
# Otherwise, term is an absolute IRI. Set the IRI mapping for definition to term
term
end
# log_debug("") {"=> #{definition.id}"}
elsif term.include?('/')
# If term is a relative IRI
definition.id = expand_iri(term, vocab: true)
raise JsonLdError::InvalidKeywordAlias, "expected term to expand to an absolute IRI #{term.inspect}" unless
definition.id.absolute?
elsif KEYWORDS.include?(term)
# This should only happen for @type when @container is @set
definition.id = term
else
# Otherwise, active context must have a vocabulary mapping, otherwise an invalid value has been detected, which is an error. Set the IRI mapping for definition to the result of concatenating the value associated with the vocabulary mapping and term.
unless vocab
raise JsonLdError::InvalidIRIMapping,
"relative term definition without vocab: #{term} on term #{term.inspect}"
end
definition.id = vocab + term
# log_debug("") {"=> #{definition.id}"}
end
@iri_to_term[definition.id] = term if simple_term && definition.id
if value.key?('@container')
# log_debug("") {"container_mapping: #{value['@container'].inspect}"}
definition.container_mapping = check_container(value['@container'], local_context, defined, term)
# If @container includes @type
if definition.container_mapping.include?('@type')
# If definition does not have @type, set @type to @id
definition.type_mapping ||= '@id'
# If definition includes @type with a value other than @id or @vocab, an illegal type mapping error has been detected
unless CONTEXT_TYPE_ID_VOCAB.include?(definition.type_mapping)
raise JsonLdError::InvalidTypeMapping, "@container: @type requires @type to be @id or @vocab"
end
end
end
if value.key?('@index')
# property-based indexing
unless definition.container_mapping.include?('@index')
raise JsonLdError::InvalidTermDefinition,
"@index without @index in @container: #{value['@index']} on term #{term.inspect}"
end
unless value['@index'].is_a?(String) && !value['@index'].start_with?('@')
raise JsonLdError::InvalidTermDefinition,
"@index must expand to an IRI: #{value['@index']} on term #{term.inspect}"
end
definition.index = value['@index'].to_s
end
if value.key?('@context')
begin
new_ctx = parse(value['@context'],
base: base,
override_protected: true,
remote_contexts: remote_contexts,
validate_scoped: false)
# Record null context in array form
definition.context = case value['@context']
when String then new_ctx.context_base
when nil then [nil]
else value['@context']
end
# log_debug("") {"context: #{definition.context.inspect}"}
rescue JsonLdError => e
raise JsonLdError::InvalidScopedContext,
"Term definition for #{term.inspect} contains illegal value for @context: #{e.message}"
end
end
if value.key?('@language')
language = value['@language']
language = case value['@language']
when String
# Warn on an invalid language tag, unless :validate is true, in which case it's an error
unless /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/.match?(value['@language'])
warn "@language must be valid BCP47: #{value['@language'].inspect}"
end
options[:lowercaseLanguage] ? value['@language'].downcase : value['@language']
when nil
nil
else
raise JsonLdError::InvalidLanguageMapping,
"language must be null or a string, was #{value['@language'].inspect}} on term #{term.inspect}"
end
# log_debug("") {"language_mapping: #{language.inspect}"}
definition.language_mapping = language || false
end
if value.key?('@direction')
direction = value['@direction']
unless direction.nil? || %w[
ltr rtl
].include?(direction)
raise JsonLdError::InvalidBaseDirection,
"direction must be null, 'ltr', or 'rtl', was #{language.inspect}} on term #{term.inspect}"
end
# log_debug("") {"direction_mapping: #{direction.inspect}"}
definition.direction_mapping = direction || false
end
if value.key?('@nest')
nest = value['@nest']
unless nest.is_a?(String)
raise JsonLdError::InvalidNestValue,
"nest must be a string, was #{nest.inspect}} on term #{term.inspect}"
end
if nest.match?(/^@[a-zA-Z]+$/) && nest != '@nest'
raise JsonLdError::InvalidNestValue,
"nest must not be a keyword other than @nest, was #{nest.inspect}} on term #{term.inspect}"
end
# log_debug("") {"nest: #{nest.inspect}"}
definition.nest = nest
end
if value.key?('@prefix')
if term.match?(%r{:|/})
raise JsonLdError::InvalidTermDefinition,
"@prefix used on compact or relative IRI term #{term.inspect}"
end
case pfx = value['@prefix']
when TrueClass, FalseClass
definition.prefix = pfx
else
raise JsonLdError::InvalidPrefixValue, "unknown value for '@prefix': #{pfx.inspect} on term #{term.inspect}"
end
if pfx && KEYWORDS.include?(definition.id.to_s)
raise JsonLdError::InvalidTermDefinition,
"keywords may not be used as prefixes"
end
end
if previous_definition&.protected? && definition != previous_definition && !override_protected
definition = previous_definition
raise JSON::LD::JsonLdError::ProtectedTermRedefinition, "Attempt to redefine protected term #{term}"
end
term_definitions[term] = definition
defined[term] = true
end
##
# Initial context, without mappings, vocab or default language
#
# @return [Boolean]
def empty?
@term_definitions.empty? && vocab.nil? && default_language.nil?
end
# @param [String] value must be an absolute IRI
def base=(value, **_options)
if value
unless value.is_a?(String) || value.is_a?(RDF::URI)
raise JsonLdError::InvalidBaseIRI,
"@base must be a string: #{value.inspect}"
end
value = RDF::URI(value)
value = @base.join(value) if @base && value.relative?
# still might be relative to document
@base = value
else
@base = false
end
end
# @param [String] value
def default_language=(value, **options)
@default_language = case value
when String
# Warn on an invalid language tag, unless :validate is true, in which case it's an error
unless /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/.match?(value)
warn "@language must be valid BCP47: #{value.inspect}"
end
options[:lowercaseLanguage] ? value.downcase : value
when nil
nil
else
raise JsonLdError::InvalidDefaultLanguage, "@language must be a string: #{value.inspect}"
end
end
# @param [String] value
def default_direction=(value, **_options)
@default_direction = if value
unless %w[
ltr rtl
].include?(value)
raise JsonLdError::InvalidBaseDirection,
"@direction must be one or 'ltr', or 'rtl': #{value.inspect}"
end
value
end
end
##
# Retrieve, or check processing mode.
#
# * With no arguments, retrieves the current set processingMode.
# * With an argument, verifies that the processingMode is at least that provided, either as an integer, or a string of the form "json-ld-1.x"
# * If expecting 1.1, and not set, it has the side-effect of setting mode to json-ld-1.1.
#
# @param [String, Number] expected (nil)
# @return [String]
def processingMode(expected = nil)
case expected
when 1.0, 'json-ld-1.0'
@processingMode == 'json-ld-1.0'
when 1.1, 'json-ld-1.1'
@processingMode.nil? || @processingMode == 'json-ld-1.1'
when nil
@processingMode || 'json-ld-1.1'
else
false
end
end
##
# Set processing mode.
#
# * With an argument, verifies that the processingMode is at least that provided, either as an integer, or a string of the form "json-ld-1.x"
#
# If contex has a @version member, it's value MUST be 1.1, otherwise an "invalid @version value" has been detected, and processing is aborted.
# If processingMode has been set, and it is not "json-ld-1.1", a "processing mode conflict" has been detecting, and processing is aborted.
#
# @param [String, Number] value
# @return [String]
# @raise [JsonLdError::ProcessingModeConflict]
def processingMode=(value = nil, **_options)
value = "json-ld-1.1" if value == 1.1
case value
when "json-ld-1.0", "json-ld-1.1"
if @processingMode && @processingMode != value
raise JsonLdError::ProcessingModeConflict, "#{value} not compatible with #{@processingMode}"
end
@processingMode = value
else
raise JsonLdError::InvalidVersionValue, value.inspect
end
end
# If context has a @vocab member: if its value is not a valid absolute IRI or null trigger an INVALID_VOCAB_MAPPING error; otherwise set the active context's vocabulary mapping to its value and remove the @vocab member from context.
# @param [String] value must be an absolute IRI
def vocab=(value, **_options)
@vocab = case value
when /_:/
# BNode vocab is deprecated
if @options[:validate] && processingMode("json-ld-1.1")
warn "[DEPRECATION] Blank Node vocabularies deprecated in JSON-LD 1.1."
end
value
when String, RDF::URI
if RDF::URI(value.to_s).relative? && processingMode("json-ld-1.0")
raise JsonLdError::InvalidVocabMapping, "@vocab must be an absolute IRI in 1.0 mode: #{value.inspect}"
end
expand_iri(value.to_s, vocab: true, documentRelative: true)
when nil
nil
else
raise JsonLdError::InvalidVocabMapping, "@vocab must be an IRI: #{value.inspect}"
end
end
# Set propagation
# @note: by the time this is called, the work has already been done.
#
# @param [Boolean] value
def propagate=(value, **_options)
if processingMode("json-ld-1.0")
raise JsonLdError::InvalidContextEntry,
"@propagate may only be set in 1.1 mode"
end
unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
raise JsonLdError::InvalidPropagateValue,
"@propagate must be boolean valued: #{value.inspect}"
end
value
end
##
# Generate @context
#