json-ld 3.1.0 → 3.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -46,7 +46,7 @@ module JSON::LD
46
46
  #
47
47
  # @param [Hash{String => String}] env
48
48
  # @return [Array(Integer, Hash, #each)] Status, Headers and Body
49
- # @see http://rack.rubyforge.org/doc/SPEC.html
49
+ # @see https://rubydoc.info/github/rack/rack/file/SPEC
50
50
  def call(env)
51
51
  response = app.call(env)
52
52
  body = response[2].respond_to?(:body) ? response[2].body : response[2]
@@ -3,6 +3,14 @@
3
3
  require 'json'
4
4
  require 'bigdecimal'
5
5
  require 'set'
6
+ require 'rdf/util/cache'
7
+
8
+ begin
9
+ # Attempt to load this to avoid unnecessary context fetches
10
+ require 'json-ld-preloaded'
11
+ rescue LoadError
12
+ # Silently allow this to fail
13
+ end
6
14
 
7
15
  module JSON::LD
8
16
  class Context
@@ -15,6 +23,14 @@ module JSON::LD
15
23
  # @return [Hash{Symbol => Context}]
16
24
  PRELOADED = {}
17
25
 
26
+ # Initial contexts, defined on first access
27
+ INITIAL_CONTEXTS = {}
28
+
29
+ ##
30
+ # Defines the maximum number of interned URI references that can be held
31
+ # cached in memory at any one time.
32
+ CACHE_SIZE = 100 # unlimited by default
33
+
18
34
  class << self
19
35
  ##
20
36
  # Add preloaded context. In the block form, the context is lazy evaulated on first use.
@@ -34,234 +50,11 @@ module JSON::LD
34
50
  end
35
51
  end
36
52
 
37
- # Term Definitions specify how properties and values have to be interpreted as well as the current vocabulary mapping and the default language
38
- class TermDefinition
39
- # @return [RDF::URI] IRI map
40
- attr_accessor :id
41
-
42
- # @return [String] term name
43
- attr_accessor :term
44
-
45
- # @return [String] Type mapping
46
- attr_accessor :type_mapping
47
-
48
- # Base container mapping, without @set
49
- # @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] Container mapping
50
- attr_reader :container_mapping
51
-
52
- # @return [String] Term used for nest properties
53
- attr_accessor :nest
54
-
55
- # Language mapping of term, `false` is used if there is an explicit language mapping for this term.
56
- # @return [String] Language mapping
57
- attr_accessor :language_mapping
58
-
59
- # Direction of term, `false` is used if there is explicit direction mapping mapping for this term.
60
- # @return ["ltr", "rtl"] direction_mapping
61
- attr_accessor :direction_mapping
62
-
63
- # @return [Boolean] Reverse Property
64
- attr_accessor :reverse_property
65
-
66
- # This is a simple term definition, not an expanded term definition
67
- # @return [Boolean]
68
- attr_accessor :simple
69
-
70
- # Property used for data indexing; defaults to @index
71
- # @return [Boolean]
72
- attr_accessor :index
73
-
74
- # Indicate that term may be used as a prefix
75
- attr_writer :prefix
76
-
77
- # Term-specific context
78
- # @return [Hash{String => Object}]
79
- attr_accessor :context
80
-
81
- # Term is protected.
82
- # @return [Boolean]
83
- attr_writer :protected
84
-
85
- # This is a simple term definition, not an expanded term definition
86
- # @return [Boolean] simple
87
- def simple?; simple; end
88
-
89
- # This is an appropriate term to use as the prefix of a compact IRI
90
- # @return [Boolean] simple
91
- def prefix?; @prefix; end
92
-
93
- # Create a new Term Mapping with an ID
94
- # @param [String] term
95
- # @param [String] id
96
- # @param [String] type_mapping Type mapping
97
- # @param [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] container_mapping
98
- # @param [String] language_mapping
99
- # Language mapping of term, `false` is used if there is an explicit language mapping for this term
100
- # @param ["ltr", "rtl"] direction_mapping
101
- # Direction mapping of term, `false` is used if there is an explicit direction mapping for this term
102
- # @param [Boolean] reverse_property
103
- # @param [Boolean] protected
104
- # @param [String] nest term used for nest properties
105
- # @param [Boolean] simple
106
- # This is a simple term definition, not an expanded term definition
107
- # @param [Boolean] prefix
108
- # Term may be used as a prefix
109
- def initialize(term,
110
- id: nil,
111
- index: nil,
112
- type_mapping: nil,
113
- container_mapping: nil,
114
- language_mapping: nil,
115
- direction_mapping: nil,
116
- reverse_property: false,
117
- nest: nil,
118
- protected: false,
119
- simple: false,
120
- prefix: nil,
121
- context: nil)
122
- @term = term
123
- @id = id.to_s unless id.nil?
124
- @index = index.to_s unless index.nil?
125
- @type_mapping = type_mapping.to_s unless type_mapping.nil?
126
- self.container_mapping = container_mapping
127
- @language_mapping = language_mapping unless language_mapping.nil?
128
- @direction_mapping = direction_mapping unless direction_mapping.nil?
129
- @reverse_property = reverse_property
130
- @protected = protected
131
- @nest = nest unless nest.nil?
132
- @simple = simple
133
- @prefix = prefix unless prefix.nil?
134
- @context = context unless context.nil?
135
- end
136
-
137
- # Term is protected.
138
- # @return [Boolean]
139
- def protected?; !!@protected; end
140
-
141
- # Set container mapping, from an array which may include @set
142
- def container_mapping=(mapping)
143
- mapping = Array(mapping)
144
- if @as_set = mapping.include?('@set')
145
- mapping = mapping.dup
146
- mapping.delete('@set')
147
- end
148
- @container_mapping = mapping.sort
149
- @index ||= '@index' if mapping.include?('@index')
150
- end
151
-
152
- ##
153
- # Output Hash or String definition for this definition considering @language and @vocab
154
- #
155
- # @param [Context] context
156
- # @return [String, Hash{String => Array[String], String}]
157
- def to_context_definition(context)
158
- cid = if context.vocab && id.start_with?(context.vocab)
159
- # Nothing to return unless it's the same as the vocab
160
- id == context.vocab ? context.vocab : id.to_s[context.vocab.length..-1]
161
- else
162
- # Find a term to act as a prefix
163
- iri, prefix = context.iri_to_term.detect {|i,p| id.to_s.start_with?(i.to_s)}
164
- iri && iri != id ? "#{prefix}:#{id.to_s[iri.length..-1]}" : id
165
- end
166
-
167
- if simple?
168
- cid.to_s unless cid == term && context.vocab
169
- else
170
- defn = {}
171
- defn[reverse_property ? '@reverse' : '@id'] = cid.to_s unless cid == term && !reverse_property
172
- if type_mapping
173
- defn['@type'] = if KEYWORDS.include?(type_mapping)
174
- type_mapping
175
- else
176
- context.compact_iri(type_mapping, vocab: true)
177
- end
178
- end
179
-
180
- cm = Array(container_mapping)
181
- cm << "@set" if as_set? && !cm.include?("@set")
182
- cm = cm.first if cm.length == 1
183
- defn['@container'] = cm unless cm.empty?
184
- # Language set as false to be output as null
185
- defn['@language'] = (@language_mapping ? @language_mapping : nil) unless @language_mapping.nil?
186
- defn['@context'] = @context if @context
187
- defn['@nest'] = @nest if @nest
188
- defn['@index'] = @index if @index
189
- defn['@prefix'] = @prefix unless @prefix.nil?
190
- defn
191
- end
192
- end
193
-
194
- ##
195
- # Turn this into a source for a new instantiation
196
- # FIXME: context serialization
197
- # @return [String]
198
- def to_rb
199
- defn = [%(TermDefinition.new\(#{term.inspect})]
200
- %w(id index type_mapping container_mapping language_mapping direction_mapping reverse_property nest simple prefix context protected).each do |acc|
201
- v = instance_variable_get("@#{acc}".to_sym)
202
- v = v.to_s if v.is_a?(RDF::Term)
203
- if acc == 'container_mapping'
204
- v.concat(%w(@set)) if as_set?
205
- v = v.first if v.length <= 1
206
- end
207
- defn << "#{acc}: #{v.inspect}" if v
208
- end
209
- defn.join(', ') + ")"
210
- end
211
-
212
- # If container mapping was defined along with @set
213
- # @return [Boolean]
214
- def as_set?; @as_set || false; end
215
-
216
- # Check if term definitions are identical, modulo @protected
217
- # @return [Boolean]
218
- def ==(other)
219
- other.is_a?(TermDefinition) &&
220
- id == other.id &&
221
- term == other.term &&
222
- type_mapping == other.type_mapping &&
223
- container_mapping == other.container_mapping &&
224
- nest == other.nest &&
225
- language_mapping == other.language_mapping &&
226
- direction_mapping == other.direction_mapping &&
227
- reverse_property == other.reverse_property &&
228
- simple == other.simple &&
229
- index == other.index &&
230
- context == other.context &&
231
- prefix? == other.prefix? &&
232
- as_set? == other.as_set?
233
- end
234
-
235
- def inspect
236
- v = %w([TD)
237
- v << "id=#{@id}"
238
- v << "index=#{index.inspect}" unless index.nil?
239
- v << "term=#{@term}"
240
- v << "rev" if reverse_property
241
- v << "container=#{container_mapping}" if container_mapping
242
- v << "as_set=#{as_set?.inspect}"
243
- v << "lang=#{language_mapping.inspect}" unless language_mapping.nil?
244
- v << "dir=#{direction_mapping.inspect}" unless direction_mapping.nil?
245
- v << "type=#{type_mapping}" unless type_mapping.nil?
246
- v << "nest=#{nest.inspect}" unless nest.nil?
247
- v << "simple=true" if @simple
248
- v << "protected=true" if @protected
249
- v << "prefix=#{@prefix.inspect}" unless @prefix.nil?
250
- v << "has-context" unless context.nil?
251
- v.join(" ") + "]"
252
- end
253
- end
254
-
255
53
  # The base.
256
54
  #
257
55
  # @return [RDF::URI] Current base IRI, used for expanding relative IRIs.
258
56
  attr_reader :base
259
57
 
260
- # The base.
261
- #
262
- # @return [RDF::URI] Document base IRI, to initialize `base`.
263
- attr_reader :doc_base
264
-
265
58
  # @return [RDF::URI] base IRI of the context, if loaded remotely.
266
59
  attr_accessor :context_base
267
60
 
@@ -302,9 +95,6 @@ module JSON::LD
302
95
  # @return [Hash{Symbol => Object}] Global options used in generating IRIs
303
96
  attr_accessor :options
304
97
 
305
- # @return [Context] A context provided to us that we can use without re-serializing XXX
306
- attr_accessor :provided_context
307
-
308
98
  # @return [BlankNodeNamer]
309
99
  attr_accessor :namer
310
100
 
@@ -314,20 +104,81 @@ module JSON::LD
314
104
  # @see #initialize
315
105
  # @see #parse
316
106
  # @param [String, #read, Array, Hash, Context] local_context
107
+ # @param [String, #to_s] base (nil)
108
+ # 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.
109
+ # @param [Boolean] override_protected (false)
110
+ # Protected terms may be cleared.
111
+ # @param [Boolean] propagate (true)
112
+ # If false, retains any previously defined term, which can be rolled back when the descending into a new node object changes.
317
113
  # @raise [JsonLdError]
318
114
  # on a remote context load error, syntax error, or a reference to a term which is not defined.
319
115
  # @return [Context]
320
- def self.parse(local_context, protected: false, override_protected: false, propagate: true, **options)
321
- self.new(**options).parse(local_context, protected: false, override_protected: override_protected, propagate: propagate)
116
+ def self.parse(local_context,
117
+ base: nil,
118
+ override_protected: false,
119
+ propagate: true,
120
+ **options)
121
+ c = self.new(**options)
122
+ if local_context.respond_to?(:empty?) && local_context.empty?
123
+ c
124
+ else
125
+ c.parse(local_context,
126
+ base: base,
127
+ override_protected: override_protected,
128
+ propagate: propagate)
129
+ end
130
+ end
131
+
132
+ ##
133
+ # Class-level cache used for retaining parsed remote contexts.
134
+ #
135
+ # @return [RDF::Util::Cache]
136
+ # @private
137
+ def self.cache
138
+ @cache ||= RDF::Util::Cache.new(CACHE_SIZE)
139
+ end
140
+
141
+ ##
142
+ # Class-level cache inverse contexts.
143
+ #
144
+ # @return [RDF::Util::Cache]
145
+ # @private
146
+ def self.inverse_cache
147
+ @inverse_cache ||= RDF::Util::Cache.new(CACHE_SIZE)
148
+ end
149
+
150
+ ##
151
+ # @private
152
+ # Allow caching of well-known contexts
153
+ def self.new(**options)
154
+ if (options.keys - [
155
+ :compactArrays,
156
+ :documentLoader,
157
+ :extractAllScripts,
158
+ :ordered,
159
+ :processingMode,
160
+ :validate
161
+ ]).empty?
162
+ # allow caching
163
+ key = options.hash
164
+ INITIAL_CONTEXTS[key] ||= begin
165
+ context = JSON::LD::Context.allocate
166
+ context.send(:initialize, **options)
167
+ context.freeze
168
+ context.term_definitions.freeze
169
+ context
170
+ end
171
+ else
172
+ # Don't try to cache
173
+ context = JSON::LD::Context.allocate
174
+ context.send(:initialize, **options)
175
+ context
176
+ end
322
177
  end
323
178
 
324
179
  ##
325
180
  # Create new evaluation context
326
181
  # @param [Hash] options
327
- # @option options [String, #to_s] :base
328
- # 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.
329
- # @option options [Proc] :documentLoader
330
- # The callback of the loader to be used to retrieve remote documents and contexts. If specified, it must be used to retrieve remote documents and contexts; otherwise, if not specified, the processor's built-in loader must be used. See {API.documentLoader} for the method signature.
331
182
  # @option options [Hash{Symbol => String}] :prefixes
332
183
  # See `RDF::Reader#initialize`
333
184
  # @option options [String, #to_s] :vocab
@@ -338,11 +189,9 @@ module JSON::LD
338
189
  # @yieldparam [Context]
339
190
  # @return [Context]
340
191
  def initialize(**options)
341
- if options[:base]
342
- @base = @doc_base = RDF::URI(options[:base]).dup
343
- @doc_base.canonicalize! if options[:canonicalize]
192
+ if options[:processingMode] == 'json-ld-1.0'
193
+ @processingMode = 'json-ld-1.0'
344
194
  end
345
- self.processingMode = options[:processingMode] if options.has_key?(:processingMode)
346
195
  @term_definitions = {}
347
196
  @iri_to_term = {
348
197
  RDF.to_uri.to_s => "rdf",
@@ -368,135 +217,6 @@ module JSON::LD
368
217
  yield(self) if block_given?
369
218
  end
370
219
 
371
- ##
372
- # Initial context, without mappings, vocab or default language
373
- #
374
- # @return [Boolean]
375
- def empty?
376
- @term_definitions.empty? && self.vocab.nil? && self.default_language.nil?
377
- end
378
-
379
- # @param [String] value must be an absolute IRI
380
- def base=(value, **options)
381
- if value
382
- raise JsonLdError::InvalidBaseIRI, "@base must be a string: #{value.inspect}" unless value.is_a?(String) || value.is_a?(RDF::URI)
383
- value = RDF::URI(value).dup
384
- value = @base.join(value) if @base && value.relative?
385
- @base = value
386
- @base.canonicalize! if @options[:canonicalize]
387
- raise JsonLdError::InvalidBaseIRI, "@base must be an absolute IRI: #{value.inspect}" unless @base.absolute? || !@options[:validate]
388
- @base
389
- else
390
- @base = nil
391
- end
392
-
393
- end
394
-
395
- # @param [String] value
396
- def default_language=(value, **options)
397
- @default_language = case value
398
- when String
399
- # Warn on an invalid language tag, unless :validate is true, in which case it's an error
400
- if value !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
401
- warn "@language must be valid BCP47: #{value.inspect}"
402
- end
403
- options[:lowercaseLanguage] ? value.downcase : value
404
- when nil
405
- nil
406
- else
407
- raise JsonLdError::InvalidDefaultLanguage, "@language must be a string: #{value.inspect}"
408
- end
409
- end
410
-
411
- # @param [String] value
412
- def default_direction=(value, **options)
413
- @default_direction = if value
414
- raise JsonLdError::InvalidBaseDirection, "@direction must be one or 'ltr', or 'rtl': #{value.inspect}" unless %w(ltr rtl).include?(value)
415
- value
416
- else
417
- nil
418
- end
419
- end
420
-
421
- ##
422
- # Retrieve, or check processing mode.
423
- #
424
- # * With no arguments, retrieves the current set processingMode.
425
- # * 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"
426
- # * If expecting 1.1, and not set, it has the side-effect of setting mode to json-ld-1.1.
427
- #
428
- # @param [String, Number] expected (nil)
429
- # @return [String]
430
- def processingMode(expected = nil)
431
- case expected
432
- when 1.0, 'json-ld-1.0'
433
- @processingMode == 'json-ld-1.0'
434
- when 1.1, 'json-ld-1.1'
435
- @processingMode ||= 'json-ld-1.1'
436
- @processingMode == 'json-ld-1.1'
437
- when nil
438
- @processingMode
439
- else
440
- false
441
- end
442
- end
443
-
444
- ##
445
- # Set processing mode.
446
- #
447
- # * 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"
448
- #
449
- # 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.
450
- # If processingMode has been set, and it is not "json-ld-1.1", a "processing mode conflict" has been detecting, and processing is aborted.
451
- #
452
- # @param [String, Number] expected
453
- # @return [String]
454
- # @raise [JsonLdError::ProcessingModeConflict]
455
- def processingMode=(value = nil, **options)
456
- value = "json-ld-1.1" if value == 1.1
457
- case value
458
- when "json-ld-1.0", "json-ld-1.1"
459
- if @processingMode && @processingMode != value
460
- raise JsonLdError::ProcessingModeConflict, "#{value} not compatible with #{@processingMode}"
461
- end
462
- @processingMode = value
463
- else
464
- raise JsonLdError::InvalidVersionValue, value.inspect
465
- end
466
- end
467
-
468
- # 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.
469
- # @param [String] value must be an absolute IRI
470
- def vocab=(value, **options)
471
- @vocab = case value
472
- when /_:/
473
- # BNode vocab is deprecated
474
- warn "[DEPRECATION] Blank Node vocabularies deprecated in JSON-LD 1.1." if @options[:validate] && processingMode("json-ld-1.1")
475
- value
476
- when String, RDF::URI
477
- if (RDF::URI(value.to_s).relative? && processingMode("json-ld-1.0"))
478
- raise JsonLdError::InvalidVocabMapping, "@vocab must be an absolute IRI in 1.0 mode: #{value.inspect}"
479
- end
480
- v = expand_iri(value.to_s, vocab: true, documentRelative: true)
481
- raise JsonLdError::InvalidVocabMapping, "@vocab must be an IRI: #{value.inspect}" if !v.valid? && @options[:validate]
482
- v
483
- when nil
484
- nil
485
- else
486
- raise JsonLdError::InvalidVocabMapping, "@vocab must be an IRI: #{value.inspect}"
487
- end
488
- end
489
-
490
- # Set propagation
491
- # @note: by the time this is called, the work has already been done.
492
- #
493
- # @param [Boolean] value
494
- def propagate=(value, **options)
495
- raise JsonLdError::InvalidContextMember, "@propagate may only be set in 1.1 mode" if processingMode("json-ld-1.0")
496
- raise JsonLdError::InvalidPropagateValue, "@propagate must be boolean valued: #{value.inspect}" unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
497
- value
498
- end
499
-
500
220
  # Create an Evaluation Context
501
221
  #
502
222
  # 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.
@@ -507,18 +227,26 @@ module JSON::LD
507
227
  #
508
228
  #
509
229
  # @param [String, #read, Array, Hash, Context] local_context
510
- # @param [Array<String>] remote_contexts
511
- # @param [Boolean] protected Make defined terms protected (as if `@protected` were used).
230
+ # @param [String, #to_s] base
231
+ # 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.
512
232
  # @param [Boolean] override_protected Protected terms may be cleared.
513
- # @param [Boolean] propagate
233
+ # @param [Boolean] propagate (true)
514
234
  # If false, retains any previously defined term, which can be rolled back when the descending into a new node object changes.
235
+ # @param [Array<String>] remote_contexts ([])
236
+ # @param [Boolean] validate_scoped (true).
237
+ # Validate scoped context, loading if necessary.
238
+ # If false, do not load scoped contexts.
515
239
  # @raise [JsonLdError]
516
240
  # on a remote context load error, syntax error, or a reference to a term which is not defined.
517
241
  # @return [Context]
518
242
  # @see https://www.w3.org/TR/json-ld11-api/index.html#context-processing-algorithm
519
- def parse(local_context, remote_contexts: [], protected: false, override_protected: false, propagate: true)
243
+ def parse(local_context,
244
+ base: nil,
245
+ override_protected: false,
246
+ propagate: true,
247
+ remote_contexts: [],
248
+ validate_scoped: true)
520
249
  result = self.dup
521
- result.provided_context = local_context if self.empty?
522
250
  # Early check for @propagate, which can only appear in a local context
523
251
  propagate = local_context.is_a?(Hash) ? local_context.fetch('@propagate', propagate) : propagate
524
252
  result.previous_context ||= result.dup unless propagate
@@ -527,7 +255,7 @@ module JSON::LD
527
255
 
528
256
  local_context.each do |context|
529
257
  case context
530
- when nil
258
+ when nil,false
531
259
  # 3.1 If the `override_protected` is false, and the active context contains protected terms, an error is raised.
532
260
  if override_protected || result.term_definitions.values.none?(&:protected?)
533
261
  null_context = Context.new(**options)
@@ -539,37 +267,34 @@ module JSON::LD
539
267
  end
540
268
  when Context
541
269
  #log_debug("parse") {"context: #{context.inspect}"}
542
- result = context.dup
270
+ result = result.merge(context)
543
271
  when IO, StringIO
544
272
  #log_debug("parse") {"io: #{context}"}
545
273
  # Load context document, if it is an open file
546
274
  begin
547
275
  ctx = JSON.load(context)
548
276
  raise JSON::LD::JsonLdError::InvalidRemoteContext, "Context missing @context key" if @options[:validate] && ctx['@context'].nil?
549
- result = result.dup.parse(ctx["@context"] ? ctx["@context"].dup : {})
550
- result.provided_context = ctx["@context"] if [context] == local_context
551
- result
277
+ result = result.parse(ctx["@context"] ? ctx["@context"] : {})
552
278
  rescue JSON::ParserError => e
553
279
  #log_debug("parse") {"Failed to parse @context from remote document at #{context}: #{e.message}"}
554
280
  raise JSON::LD::JsonLdError::InvalidRemoteContext, "Failed to parse remote context at #{context}: #{e.message}" if @options[:validate]
555
- self.dup
281
+ self
556
282
  end
557
283
  when String, RDF::URI
558
284
  #log_debug("parse") {"remote: #{context}, base: #{result.context_base || result.base}"}
559
285
 
560
286
  # 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].
561
- context = RDF::URI(result.context_base || options[:base]).join(context)
287
+ context = RDF::URI(result.context_base || base).join(context)
562
288
  context_canon = context.canonicalize
563
- context_canon.scheme == 'http' if context_canon.scheme == 'https'
289
+ context_canon.scheme = 'http' if context_canon.scheme == 'https'
290
+
291
+ # If validating a scoped context which has already been loaded, skip to the next one
292
+ next if !validate_scoped && remote_contexts.include?(context.to_s)
564
293
 
565
294
  remote_contexts << context.to_s
566
295
  raise JsonLdError::ContextOverflow, "#{context}" if remote_contexts.length >= MAX_CONTEXTS_LOADED
567
296
 
568
- context_no_base = result.dup
569
- context_no_base.base = nil
570
- context_no_base.context_base = context.to_s
571
-
572
- if PRELOADED[context_canon.to_s]
297
+ cached_context = if PRELOADED[context_canon.to_s]
573
298
  # If we have a cached context, merge it into the current context (result) and use as the new context
574
299
  #log_debug("parse") {"=> cached_context: #{context_canon.to_s.inspect}"}
575
300
 
@@ -578,10 +303,10 @@ module JSON::LD
578
303
  #log_debug("parse") {"=> (call)"}
579
304
  PRELOADED[context_canon.to_s] = PRELOADED[context_canon.to_s].call
580
305
  end
581
- context = context_no_base.merge!(PRELOADED[context_canon.to_s])
306
+ PRELOADED[context_canon.to_s]
582
307
  else
583
308
  # Load context document, if it is a string
584
- begin
309
+ Context.cache[context_canon.to_s] ||= begin
585
310
  context_opts = @options.merge(
586
311
  profile: 'http://www.w3.org/ns/json-ld#context',
587
312
  requestProfile: 'http://www.w3.org/ns/json-ld#context',
@@ -590,26 +315,30 @@ module JSON::LD
590
315
  JSON::LD::API.loadRemoteDocument(context.to_s, **context_opts) do |remote_doc|
591
316
  # 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.
592
317
  raise JsonLdError::InvalidRemoteContext, "#{context}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.has_key?('@context')
593
- context = remote_doc.document['@context']
318
+
319
+ # Parse stand-alone
320
+ ctx = Context.new(unfrozen: true, **options).dup
321
+ ctx.context_base = context.to_s
322
+ ctx = ctx.parse(remote_doc.document['@context'], remote_contexts: remote_contexts.dup)
323
+ ctx.instance_variable_set(:@base, nil)
324
+ ctx
594
325
  end
595
326
  rescue JsonLdError::LoadingDocumentFailed => e
596
327
  #log_debug("parse") {"Failed to retrieve @context from remote document at #{context_no_base.context_base.inspect}: #{e.message}"}
597
- raise JsonLdError::LoadingRemoteContextFailed, "#{context_no_base.context_base}: #{e.message}", e.backtrace
328
+ raise JsonLdError::LoadingRemoteContextFailed, "#{context}: #{e.message}", e.backtrace
598
329
  rescue JsonLdError
599
330
  raise
600
331
  rescue StandardError => e
601
332
  #log_debug("parse") {"Failed to retrieve @context from remote document at #{context_no_base.context_base.inspect}: #{e.message}"}
602
- raise JsonLdError::LoadingRemoteContextFailed, "#{context_no_base.context_base}: #{e.message}", e.backtrace
333
+ raise JsonLdError::LoadingRemoteContextFailed, "#{context}: #{e.message}", e.backtrace
603
334
  end
604
-
605
- # 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.
606
- context = context_no_base.parse(context, remote_contexts: remote_contexts.dup, protected: protected, override_protected: override_protected, propagate: propagate)
607
- PRELOADED[context_canon.to_s] = context.dup
608
- context.provided_context = result.provided_context
609
335
  end
610
- context.base ||= result.base
336
+
337
+ # Merge loaded context noting protected term overriding
338
+ context = result.merge(cached_context, override_protected: override_protected)
339
+
340
+ context.previous_context = self unless propagate
611
341
  result = context
612
- #log_debug("parse") {"=> provided_context: #{context.inspect}"}
613
342
  when Hash
614
343
  context = context.dup # keep from modifying a hash passed as a param
615
344
 
@@ -626,33 +355,35 @@ module JSON::LD
626
355
  next unless context.has_key?(key)
627
356
  if key == '@import'
628
357
  # Retrieve remote context and merge the remaining context object into the result.
629
- raise JsonLdError::InvalidContextMember, "@import may only be used in 1.1 mode}" if result.processingMode("json-ld-1.0")
358
+ raise JsonLdError::InvalidContextEntry, "@import may only be used in 1.1 mode}" if result.processingMode("json-ld-1.0")
630
359
  raise JsonLdError::InvalidImportValue, "@import must be a string: #{context['@import'].inspect}" unless context['@import'].is_a?(String)
631
- source = RDF::URI(result.context_base || result.base).join(context['@import'])
360
+ import_loc = RDF::URI(result.context_base || base).join(context['@import'])
632
361
  begin
633
362
  context_opts = @options.merge(
634
363
  profile: 'http://www.w3.org/ns/json-ld#context',
635
364
  requestProfile: 'http://www.w3.org/ns/json-ld#context',
636
365
  base: nil)
637
366
  context_opts.delete(:headers)
638
- JSON::LD::API.loadRemoteDocument(source, **context_opts) do |remote_doc|
639
- # Dereference source. 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.
640
- raise JsonLdError::InvalidRemoteContext, "#{source}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.has_key?('@context')
367
+ # FIXME: should cache this, but ContextCache is for parsed contexts
368
+ JSON::LD::API.loadRemoteDocument(import_loc, **context_opts) do |remote_doc|
369
+ # 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.
370
+ raise JsonLdError::InvalidRemoteContext, "#{import_loc}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.has_key?('@context')
641
371
  import_context = remote_doc.document['@context']
372
+ import_context.delete('@base')
642
373
  raise JsonLdError::InvalidRemoteContext, "#{import_context.to_json} must be an object" unless import_context.is_a?(Hash)
643
- raise JsonLdError::InvalidContextMember, "#{import_context.to_json} must not include @import entry" if import_context.has_key?('@import')
374
+ raise JsonLdError::InvalidContextEntry, "#{import_context.to_json} must not include @import entry" if import_context.has_key?('@import')
644
375
  context.delete(key)
645
376
  context = import_context.merge(context)
646
377
  end
647
378
  rescue JsonLdError::LoadingDocumentFailed => e
648
- raise JsonLdError::LoadingRemoteContextFailed, "#{source}: #{e.message}", e.backtrace
379
+ raise JsonLdError::LoadingRemoteContextFailed, "#{import_loc}: #{e.message}", e.backtrace
649
380
  rescue JsonLdError
650
381
  raise
651
382
  rescue StandardError => e
652
- raise JsonLdError::LoadingRemoteContextFailed, "#{source}: #{e.message}", e.backtrace
383
+ raise JsonLdError::LoadingRemoteContextFailed, "#{import_loc}: #{e.message}", e.backtrace
653
384
  end
654
385
  else
655
- result.send(setter, context[key], remote_contexts: remote_contexts, protected: context.fetch('@protected', protected))
386
+ result.send(setter, context[key], remote_contexts: remote_contexts)
656
387
  end
657
388
  context.delete(key)
658
389
  end
@@ -663,8 +394,12 @@ module JSON::LD
663
394
  context.each_key do |key|
664
395
  # ... where key is not @base, @vocab, @language, or @version
665
396
  result.create_term_definition(context, key, defined,
397
+ base: base,
666
398
  override_protected: override_protected,
667
- protected: context.fetch('@protected', protected)) unless NON_TERMDEF_KEYS.include?(key)
399
+ protected: context['@protected'],
400
+ remote_contexts: remote_contexts.dup,
401
+ validate_scoped: validate_scoped
402
+ ) unless NON_TERMDEF_KEYS.include?(key)
668
403
  end
669
404
  else
670
405
  # 3.3) If context is not a JSON object, an invalid local context error has been detected and processing is aborted.
@@ -678,28 +413,29 @@ module JSON::LD
678
413
  # Merge in a context, creating a new context with updates from `context`
679
414
  #
680
415
  # @param [Context] context
416
+ # @param [Boolean] override_protected Allow or disallow protected terms to be changed
681
417
  # @return [Context]
682
- def merge(context)
683
- c = self.dup.merge!(context)
684
- c.instance_variable_set(:@term_definitions, context.term_definitions.dup)
685
- c
686
- end
418
+ def merge(context, override_protected: false)
419
+ ctx = Context.new(term_definitions: self.term_definitions, standard_prefixes: options[:standard_prefixes])
420
+ ctx.context_base = context.context_base || self.context_base
421
+ ctx.default_language = context.default_language || self.default_language
422
+ ctx.default_direction = context.default_direction || self.default_direction
423
+ ctx.vocab = context.vocab || self.vocab
424
+ ctx.base = self.base unless self.base.nil?
425
+ if !override_protected
426
+ ctx.term_definitions.each do |term, definition|
427
+ next unless definition.protected? && (other = context.term_definitions[term])
428
+ unless definition == other
429
+ raise JSON::LD::JsonLdError::ProtectedTermRedefinition, "Attempt to redefine protected term #{term}"
430
+ end
431
+ end
432
+ end
687
433
 
688
- ##
689
- # Update context with definitions from `context`
690
- #
691
- # @param [Context] context
692
- # @return [self]
693
- def merge!(context)
694
- # FIXME: if new context removes the default language, this won't do anything
695
- self.default_language = context.default_language if context.default_language
696
- self.vocab = context.vocab if context.vocab
697
- self.base = context.base if context.base
698
-
699
- # Merge in Term Definitions
700
- term_definitions.merge!(context.term_definitions)
701
- @inverse_context = nil # Re-build after term definitions set
702
- self
434
+ # Add term definitions
435
+ context.term_definitions.each do |term, definition|
436
+ ctx.term_definitions[term] = definition
437
+ end
438
+ ctx
703
439
  end
704
440
 
705
441
  # The following constants are used to reduce object allocations in #create_term_definition below
@@ -722,14 +458,22 @@ module JSON::LD
722
458
  # @param [Hash] local_context
723
459
  # @param [String] term
724
460
  # @param [Hash] defined
461
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
725
462
  # @param [Boolean] protected if true, causes all terms to be marked protected
726
463
  # @param [Boolean] override_protected Protected terms may be cleared.
727
- # @param [Boolean] propagate
728
- # Context is propagated across node objects.
464
+ # @param [Array<String>] remote_contexts
465
+ # @param [Boolean] validate_scoped (true).
466
+ # Validate scoped context, loading if necessary.
467
+ # If false, do not load scoped contexts.
729
468
  # @raise [JsonLdError]
730
469
  # Represents a cyclical term dependency
731
470
  # @see https://www.w3.org/TR/json-ld11-api/index.html#create-term-definition
732
- def create_term_definition(local_context, term, defined, override_protected: false, protected: false)
471
+ def create_term_definition(local_context, term, defined,
472
+ base: nil,
473
+ override_protected: false,
474
+ protected: nil,
475
+ remote_contexts: [],
476
+ validate_scoped: true)
733
477
  # Expand a string value, unless it matches a keyword
734
478
  #log_debug("create_term_definition") {"term = #{term.inspect}"}
735
479
 
@@ -749,6 +493,7 @@ module JSON::LD
749
493
  # Since keywords cannot be overridden, term must not be a keyword. Otherwise, an invalid value has been detected, which is an error.
750
494
  if term == '@type' &&
751
495
  value.is_a?(Hash) &&
496
+ !value.empty? &&
752
497
  processingMode("json-ld-1.1") &&
753
498
  (value.keys - %w(@container @protected)).empty? &&
754
499
  value.fetch('@container', '@set') == '@set'
@@ -827,6 +572,11 @@ module JSON::LD
827
572
  raise JsonLdError::InvalidIRIMapping, "expected value of @reverse to be a string: #{value['@reverse'].inspect} on term #{term.inspect}" unless
828
573
  value['@reverse'].is_a?(String)
829
574
 
575
+ if value['@reverse'].to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
576
+ warn "Values beginning with '@' are reserved for future use and ignored: #{value['@reverse']}."
577
+ return
578
+ end
579
+
830
580
  # 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.
831
581
  definition.id = expand_iri(value['@reverse'],
832
582
  vocab: true,
@@ -835,11 +585,6 @@ module JSON::LD
835
585
  raise JsonLdError::InvalidIRIMapping, "non-absolute @reverse IRI: #{definition.id} on term #{term.inspect}" unless
836
586
  definition.id.is_a?(RDF::Node) || definition.id.is_a?(RDF::URI) && definition.id.absolute?
837
587
 
838
- if value['@reverse'].to_s.match?(/^@[a-zA-Z]+$/) && @options[:validate]
839
- warn "Values beginning with '@' are reserved for future use and ignored: #{value['@reverse']}."
840
- return
841
- end
842
-
843
588
  if term[1..-1].to_s.include?(':') && (term_iri = expand_iri(term)) != definition.id
844
589
  raise JsonLdError::InvalidIRIMapping, "term #{term} expands to #{definition.id}, not #{term_iri}"
845
590
  end
@@ -943,9 +688,17 @@ module JSON::LD
943
688
 
944
689
  if value.has_key?('@context')
945
690
  begin
946
- self.parse(value['@context'], override_protected: true)
691
+ new_ctx = self.parse(value['@context'],
692
+ base: base,
693
+ override_protected: true,
694
+ remote_contexts: remote_contexts,
695
+ validate_scoped: false)
947
696
  # Record null context in array form
948
- definition.context = value['@context'] ? value['@context'] : [nil]
697
+ definition.context = case value['@context']
698
+ when String then new_ctx.context_base
699
+ when nil then [nil]
700
+ else value['@context']
701
+ end
949
702
  rescue JsonLdError => e
950
703
  raise JsonLdError::InvalidScopedContext, "Term definition for #{term.inspect} contains illegal value for @context: #{e.message}"
951
704
  end
@@ -1003,9 +756,130 @@ module JSON::LD
1003
756
 
1004
757
  term_definitions[term] = definition
1005
758
  defined[term] = true
1006
- ensure
1007
- # Re-build after term definitions set
1008
- @inverse_context = nil
759
+ end
760
+
761
+ ##
762
+ # Initial context, without mappings, vocab or default language
763
+ #
764
+ # @return [Boolean]
765
+ def empty?
766
+ @term_definitions.empty? && self.vocab.nil? && self.default_language.nil?
767
+ end
768
+
769
+ # @param [String] value must be an absolute IRI
770
+ def base=(value, **options)
771
+ if value
772
+ raise JsonLdError::InvalidBaseIRI, "@base must be a string: #{value.inspect}" unless value.is_a?(String) || value.is_a?(RDF::URI)
773
+ value = RDF::URI(value)
774
+ value = @base.join(value) if @base && value.relative?
775
+ # still might be relative to document
776
+ @base = value
777
+ else
778
+ @base = false
779
+ end
780
+
781
+ end
782
+
783
+ # @param [String] value
784
+ def default_language=(value, **options)
785
+ @default_language = case value
786
+ when String
787
+ # Warn on an invalid language tag, unless :validate is true, in which case it's an error
788
+ if value !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
789
+ warn "@language must be valid BCP47: #{value.inspect}"
790
+ end
791
+ options[:lowercaseLanguage] ? value.downcase : value
792
+ when nil
793
+ nil
794
+ else
795
+ raise JsonLdError::InvalidDefaultLanguage, "@language must be a string: #{value.inspect}"
796
+ end
797
+ end
798
+
799
+ # @param [String] value
800
+ def default_direction=(value, **options)
801
+ @default_direction = if value
802
+ raise JsonLdError::InvalidBaseDirection, "@direction must be one or 'ltr', or 'rtl': #{value.inspect}" unless %w(ltr rtl).include?(value)
803
+ value
804
+ else
805
+ nil
806
+ end
807
+ end
808
+
809
+ ##
810
+ # Retrieve, or check processing mode.
811
+ #
812
+ # * With no arguments, retrieves the current set processingMode.
813
+ # * 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"
814
+ # * If expecting 1.1, and not set, it has the side-effect of setting mode to json-ld-1.1.
815
+ #
816
+ # @param [String, Number] expected (nil)
817
+ # @return [String]
818
+ def processingMode(expected = nil)
819
+ case expected
820
+ when 1.0, 'json-ld-1.0'
821
+ @processingMode == 'json-ld-1.0'
822
+ when 1.1, 'json-ld-1.1'
823
+ @processingMode.nil? || @processingMode == 'json-ld-1.1'
824
+ when nil
825
+ @processingMode || 'json-ld-1.1'
826
+ else
827
+ false
828
+ end
829
+ end
830
+
831
+ ##
832
+ # Set processing mode.
833
+ #
834
+ # * 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"
835
+ #
836
+ # 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.
837
+ # If processingMode has been set, and it is not "json-ld-1.1", a "processing mode conflict" has been detecting, and processing is aborted.
838
+ #
839
+ # @param [String, Number] value
840
+ # @return [String]
841
+ # @raise [JsonLdError::ProcessingModeConflict]
842
+ def processingMode=(value = nil, **options)
843
+ value = "json-ld-1.1" if value == 1.1
844
+ case value
845
+ when "json-ld-1.0", "json-ld-1.1"
846
+ if @processingMode && @processingMode != value
847
+ raise JsonLdError::ProcessingModeConflict, "#{value} not compatible with #{@processingMode}"
848
+ end
849
+ @processingMode = value
850
+ else
851
+ raise JsonLdError::InvalidVersionValue, value.inspect
852
+ end
853
+ end
854
+
855
+ # 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.
856
+ # @param [String] value must be an absolute IRI
857
+ def vocab=(value, **options)
858
+ @vocab = case value
859
+ when /_:/
860
+ # BNode vocab is deprecated
861
+ warn "[DEPRECATION] Blank Node vocabularies deprecated in JSON-LD 1.1." if @options[:validate] && processingMode("json-ld-1.1")
862
+ value
863
+ when String, RDF::URI
864
+ if (RDF::URI(value.to_s).relative? && processingMode("json-ld-1.0"))
865
+ raise JsonLdError::InvalidVocabMapping, "@vocab must be an absolute IRI in 1.0 mode: #{value.inspect}"
866
+ end
867
+ expand_iri(value.to_s, vocab: true, documentRelative: true)
868
+ when nil
869
+ nil
870
+ else
871
+ raise JsonLdError::InvalidVocabMapping, "@vocab must be an IRI: #{value.inspect}"
872
+ end
873
+ end
874
+
875
+ # Set propagation
876
+ # @note: by the time this is called, the work has already been done.
877
+ #
878
+ # @param [Boolean] value
879
+ def propagate=(value, **options)
880
+ raise JsonLdError::InvalidContextEntry, "@propagate may only be set in 1.1 mode" if processingMode("json-ld-1.0")
881
+ raise JsonLdError::InvalidPropagateValue, "@propagate must be boolean valued: #{value.inspect}" unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
882
+ value
1009
883
  end
1010
884
 
1011
885
  ##
@@ -1014,40 +888,44 @@ module JSON::LD
1014
888
  # If a context was supplied in global options, use that, otherwise, generate one
1015
889
  # from this representation.
1016
890
  #
891
+ # @param [Array, Hash, Context, IO, StringIO] provided_context (nil)
892
+ # Original context to use, if available
1017
893
  # @param [Hash{Symbol => Object}] options ({})
1018
894
  # @return [Hash]
1019
- def serialize(**options)
1020
- # FIXME: not setting provided_context now
895
+ def serialize(provided_context: nil, **options)
896
+ #log_debug("serlialize: generate context")
897
+ #log_debug("") {"=> context: #{inspect}"}
1021
898
  use_context = case provided_context
1022
899
  when String, RDF::URI
1023
900
  #log_debug "serlialize: reuse context: #{provided_context.inspect}"
1024
901
  provided_context.to_s
1025
- when Hash, Array
902
+ when Hash
903
+ #log_debug "serlialize: reuse context: #{provided_context.inspect}"
904
+ # If it has an @context entry use it, otherwise it is assumed to be the body of a context
905
+ provided_context.fetch('@context', provided_context)
906
+ when Array
1026
907
  #log_debug "serlialize: reuse context: #{provided_context.inspect}"
1027
908
  provided_context
909
+ when IO, StringIO
910
+ provided_context.rewind
911
+ JSON.load(provided_context).fetch('@context', {})
1028
912
  else
1029
- #log_debug("serlialize: generate context")
1030
- #log_debug("") {"=> context: #{inspect}"}
1031
913
  ctx = {}
1032
- ctx['@base'] = base.to_s if base && base != doc_base
914
+ ctx['@version'] = 1.1 if @processingMode == 'json-ld-1.1'
915
+ ctx['@base'] = base.to_s if base
1033
916
  ctx['@direction'] = default_direction.to_s if default_direction
1034
917
  ctx['@language'] = default_language.to_s if default_language
1035
918
  ctx['@vocab'] = vocab.to_s if vocab
1036
919
 
1037
920
  # Term Definitions
1038
- term_definitions.keys.each do |term|
1039
- defn = term_definitions[term].to_context_definition(self)
1040
- ctx[term] = defn if defn
921
+ term_definitions.each do |term, defn|
922
+ ctx[term] = defn.to_context_definition(self)
1041
923
  end
1042
-
1043
- #log_debug("") {"start_doc: context=#{ctx.inspect}"}
1044
924
  ctx
1045
925
  end
1046
926
 
1047
927
  # Return hash with @context, or empty
1048
- r = {}
1049
- r['@context'] = use_context unless use_context.nil? || use_context.empty?
1050
- r
928
+ use_context.nil? || use_context.empty? ? {} : {'@context' => use_context}
1051
929
  end
1052
930
 
1053
931
  ##
@@ -1162,7 +1040,7 @@ module JSON::LD
1162
1040
  def container(term)
1163
1041
  return [term] if term == '@list'
1164
1042
  term = find_definition(term)
1165
- term ? term.container_mapping : []
1043
+ term ? term.container_mapping : Set.new
1166
1044
  end
1167
1045
 
1168
1046
  ##
@@ -1214,7 +1092,7 @@ module JSON::LD
1214
1092
  term.nest
1215
1093
  else
1216
1094
  nest_term = find_definition(term.nest)
1217
- raise JsonLdError::InvalidNestValue, "nest must a term resolving to @nest, was #{nest_term.inspect}" unless nest_term && nest_term.simple? && nest_term.id == '@nest'
1095
+ raise JsonLdError::InvalidNestValue, "nest must a term resolving to @nest, was #{nest_term.inspect}" unless nest_term && nest_term.id == '@nest'
1218
1096
  term.nest
1219
1097
  end
1220
1098
  end
@@ -1275,24 +1153,31 @@ module JSON::LD
1275
1153
  #
1276
1154
  # @param [String] value
1277
1155
  # A keyword, term, prefix:suffix or possibly relative IRI
1156
+ # @param [Boolean] as_string (false) transform RDF::Resource values to string
1157
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1158
+ # @param [Hash] defined
1159
+ # Used during Context Processing.
1278
1160
  # @param [Boolean] documentRelative (false)
1279
- # @param [Boolean] vocab (false)
1280
1161
  # @param [Hash] local_context
1281
1162
  # Used during Context Processing.
1282
- # @param [Hash] defined
1283
- # Used during Context Processing.
1284
- # @param [Boolean] quiet (false)
1163
+ # @param [Boolean] vocab (false)
1285
1164
  # @param [Hash{Symbol => Object}] options
1286
- # @return [RDF::URI, String]
1165
+ # @return [RDF::Resource, String]
1287
1166
  # IRI or String, if it's a keyword
1288
1167
  # @raise [JSON::LD::JsonLdError::InvalidIRIMapping] if the value cannot be expanded
1289
1168
  # @see https://www.w3.org/TR/json-ld11-api/#iri-expansion
1290
- def expand_iri(value, documentRelative: false, vocab: false, local_context: nil, defined: nil, quiet: false, **options)
1291
- return value unless value.is_a?(String)
1169
+ def expand_iri(value,
1170
+ as_string: false,
1171
+ base: nil,
1172
+ defined: nil,
1173
+ documentRelative: false,
1174
+ local_context: nil,
1175
+ vocab: false,
1176
+ **options)
1177
+ return (value && as_string ? value.to_s : value) unless value.is_a?(String)
1292
1178
 
1293
1179
  return value if KEYWORDS.include?(value)
1294
1180
  return nil if value.match?(/^@[a-zA-Z]+$/)
1295
- #log_debug("expand_iri") {"value: #{value.inspect}"} unless quiet
1296
1181
 
1297
1182
  defined = defined || {} # if we initialized in the keyword arg we would allocate {} at each invokation, even in the 2 (common) early returns above.
1298
1183
 
@@ -1302,25 +1187,29 @@ module JSON::LD
1302
1187
  end
1303
1188
 
1304
1189
  if (v_td = term_definitions[value]) && KEYWORDS.include?(v_td.id)
1305
- #log_debug("") {"match with #{v_td.id}"} unless quiet
1306
- return v_td.id
1190
+ return (as_string ? v_td.id.to_s : v_td.id)
1307
1191
  end
1308
1192
 
1309
1193
  # If active context has a term definition for value, and the associated mapping is a keyword, return that keyword.
1310
1194
  # If vocab is true and the active context has a term definition for value, return the associated IRI mapping.
1311
1195
  if (v_td = term_definitions[value]) && (vocab || KEYWORDS.include?(v_td.id))
1312
- #log_debug("") {"match with #{v_td.id}"} unless quiet
1313
- return v_td.id
1196
+ iri = base && v_td.id ? base.join(v_td.id) : v_td.id # vocab might be doc relative
1197
+ return (as_string ? iri.to_s : iri)
1314
1198
  end
1315
1199
 
1316
1200
  # If value contains a colon (:), it is either an absolute IRI or a compact IRI:
1317
1201
  if value[1..-1].to_s.include?(':')
1318
1202
  prefix, suffix = value.split(':', 2)
1319
- #log_debug("") {"prefix: #{prefix.inspect}, suffix: #{suffix.inspect}, vocab: #{self.vocab.inspect}"} unless quiet
1320
1203
 
1321
1204
  # If prefix is underscore (_) or suffix begins with double-forward-slash (//), return value as it is already an absolute IRI or a blank node identifier.
1322
- return RDF::Node.new(namer.get_sym(suffix)) if prefix == '_'
1323
- return RDF::URI(value) if suffix.start_with?('//')
1205
+ if prefix == '_'
1206
+ v = RDF::Node.new(namer.get_sym(suffix))
1207
+ return (as_string ? v.to_s : v)
1208
+ end
1209
+ if suffix.start_with?('//')
1210
+ v = RDF::URI(value)
1211
+ return (as_string ? v.to_s : v)
1212
+ end
1324
1213
 
1325
1214
  # If local context is not null, it contains a key that equals prefix, and the value associated with the key that equals prefix in defined is not true, invoke the Create Term Definition algorithm, passing active context, local context, prefix as term, and defined. This will ensure that a term definition is created for prefix in active context during Context Processing.
1326
1215
  if local_context && local_context.has_key?(prefix) && !defined[prefix]
@@ -1329,31 +1218,43 @@ module JSON::LD
1329
1218
 
1330
1219
  # If active context contains a term definition for prefix, return the result of concatenating the IRI mapping associated with prefix and suffix.
1331
1220
  if (td = term_definitions[prefix]) && !td.id.nil? && td.prefix?
1332
- return td.id + suffix
1221
+ return (as_string ? td.id.to_s : td.id) + suffix
1333
1222
  elsif RDF::URI(value).absolute?
1334
1223
  # Otherwise, if the value has the form of an absolute IRI, return it
1335
- return RDF::URI(value)
1224
+ return (as_string ? value.to_s : RDF::URI(value))
1336
1225
  else
1337
1226
  # Otherwise, it is a relative IRI
1338
1227
  end
1339
1228
  end
1340
- #log_debug("") {"=> #{result.inspect}"} unless quiet
1341
1229
 
1230
+ iri = value.is_a?(RDF::URI) ? value : RDF::URI(value)
1342
1231
  result = if vocab && self.vocab
1343
1232
  # If vocab is true, and active context has a vocabulary mapping, return the result of concatenating the vocabulary mapping with value.
1344
- self.vocab + value
1345
- elsif documentRelative && (base ||= self.base)
1346
- # Otherwise, if document relative is true, set value to the result of resolving value against the base IRI. 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].
1347
- value = RDF::URI(value)
1348
- value.absolute? ? value : RDF::URI(base).join(value)
1349
- elsif local_context && RDF::URI(value).relative?
1233
+ # Note that @vocab could still be relative to a document base
1234
+ (base && self.vocab.is_a?(RDF::URI) && self.vocab.relative? ? base.join(self.vocab) : self.vocab) + value
1235
+ elsif documentRelative
1236
+ if iri.absolute?
1237
+ iri
1238
+ elsif self.base.is_a?(RDF::URI) && self.base.absolute?
1239
+ self.base.join(iri)
1240
+ elsif self.base == false
1241
+ # No resollution of `@base: null`
1242
+ iri
1243
+ elsif base && self.base
1244
+ base.join(self.base).join(iri)
1245
+ elsif base
1246
+ base.join(iri)
1247
+ else
1248
+ # Returns a relative IRI in an odd case.
1249
+ iri
1250
+ end
1251
+ elsif local_context && iri.relative?
1350
1252
  # If local context is not null and value is not an absolute IRI, an invalid IRI mapping error has been detected and processing is aborted.
1351
1253
  raise JSON::LD::JsonLdError::InvalidIRIMapping, "not an absolute IRI: #{value}"
1352
1254
  else
1353
- RDF::URI(value)
1255
+ iri
1354
1256
  end
1355
- #log_debug("") {"=> #{result}"} unless quiet
1356
- result
1257
+ result && as_string ? result.to_s : result
1357
1258
  end
1358
1259
 
1359
1260
  # The following constants are used to reduce object allocations in #compact_iri below
@@ -1372,24 +1273,21 @@ module JSON::LD
1372
1273
  # Compacts an absolute IRI to the shortest matching term or compact IRI
1373
1274
  #
1374
1275
  # @param [RDF::URI] iri
1276
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1375
1277
  # @param [Object] value
1376
1278
  # Value, used to select among various maps for the same IRI
1377
- # @param [Boolean] vocab
1378
- # specifies whether the passed iri should be compacted using the active context's vocabulary mapping
1379
1279
  # @param [Boolean] reverse
1380
1280
  # specifies whether a reverse property is being compacted
1381
- # @param [Boolean] quiet (false)
1382
- # @param [Hash{Symbol => Object}] options ({})
1281
+ # @param [Boolean] vocab
1282
+ # specifies whether the passed iri should be compacted using the active context's vocabulary mapping
1383
1283
  #
1384
1284
  # @return [String] compacted form of IRI
1385
1285
  # @see https://www.w3.org/TR/json-ld11-api/#iri-compaction
1386
- def compact_iri(iri, value: nil, vocab: nil, reverse: false, quiet: false, **options)
1286
+ def compact_iri(iri, base: nil, reverse: false, value: nil, vocab: nil)
1387
1287
  return if iri.nil?
1388
1288
  iri = iri.to_s
1389
- #log_debug("compact_iri(#{iri.inspect}", options) {[value, vocab, reverse].inspect} unless quiet
1390
1289
 
1391
1290
  if vocab && inverse_context.has_key?(iri)
1392
- #log_debug("") {"vocab and key in inverse context"} unless quiet
1393
1291
  default_language = if self.default_direction
1394
1292
  "#{self.default_language}_#{self.default_direction}".downcase
1395
1293
  else
@@ -1406,7 +1304,6 @@ module JSON::LD
1406
1304
  tl, tl_value = "@type", "@reverse"
1407
1305
  containers << '@set'
1408
1306
  elsif list?(value)
1409
- #log_debug("") {"list(#{value.inspect})"} unless quiet
1410
1307
  # if value is a list object, then set type/language and type/language value to the most specific values that work for all items in the list as follows:
1411
1308
  containers << "@list" unless index?(value)
1412
1309
  list = value['@list']
@@ -1429,25 +1326,21 @@ module JSON::LD
1429
1326
  end
1430
1327
  common_language ||= item_language
1431
1328
  if item_language != common_language && value?(item)
1432
- #log_debug("") {"-- #{item_language} conflicts with #{common_language}, use @none"} unless quiet
1433
1329
  common_language = '@none'
1434
1330
  end
1435
1331
  common_type ||= item_type
1436
1332
  if item_type != common_type
1437
1333
  common_type = '@none'
1438
- #log_debug("") {"#{item_type} conflicts with #{common_type}, use @none"} unless quiet
1439
1334
  end
1440
1335
  end
1441
1336
 
1442
1337
  common_language ||= '@none'
1443
1338
  common_type ||= '@none'
1444
- #log_debug("") {"common type: #{common_type}, common language: #{common_language}"} unless quiet
1445
1339
  if common_type != '@none'
1446
1340
  tl, tl_value = '@type', common_type
1447
1341
  else
1448
1342
  tl_value = common_language
1449
1343
  end
1450
- #log_debug("") {"list: containers: #{containers.inspect}, type/language: #{tl.inspect}, type/language value: #{tl_value.inspect}"} unless quiet
1451
1344
  elsif graph?(value)
1452
1345
  # Prefer @index and @id containers, then @graph, then @index
1453
1346
  containers.concat(CONTAINERS_GRAPH_INDEX_INDEX) if index?(value)
@@ -1482,7 +1375,6 @@ module JSON::LD
1482
1375
  tl, tl_value = '@type', '@id'
1483
1376
  end
1484
1377
  containers << '@set'
1485
- #log_debug("") {"value: containers: #{containers.inspect}, type/language: #{tl.inspect}, type/language value: #{tl_value.inspect}"} unless quiet
1486
1378
  end
1487
1379
 
1488
1380
  containers << '@none'
@@ -1496,7 +1388,7 @@ module JSON::LD
1496
1388
  preferred_values = []
1497
1389
  preferred_values << '@reverse' if tl_value == '@reverse'
1498
1390
  if (tl_value == '@id' || tl_value == '@reverse') && value.is_a?(Hash) && value.has_key?('@id')
1499
- t_iri = compact_iri(value['@id'], vocab: true, document_relative: true)
1391
+ t_iri = compact_iri(value['@id'], vocab: true, base: base)
1500
1392
  if (r_td = term_definitions[t_iri]) && r_td.id == value['@id']
1501
1393
  preferred_values.concat(CONTAINERS_VOCAB_ID)
1502
1394
  else
@@ -1506,7 +1398,6 @@ module JSON::LD
1506
1398
  tl = '@any' if list?(value) && value['@list'].empty?
1507
1399
  preferred_values.concat([tl_value, '@none'].compact)
1508
1400
  end
1509
- #log_debug("") {"preferred_values: #{preferred_values.inspect}"} unless quiet
1510
1401
  preferred_values << '@any'
1511
1402
 
1512
1403
  # if containers included `@language` and preferred_values includes something of the form language-tag_direction, add just the _direction part, to select terms that have that direction.
@@ -1515,7 +1406,6 @@ module JSON::LD
1515
1406
  end
1516
1407
 
1517
1408
  if p_term = select_term(iri, containers, tl, preferred_values)
1518
- #log_debug("") {"=> term: #{p_term.inspect}"} unless quiet
1519
1409
  return p_term
1520
1410
  end
1521
1411
  end
@@ -1523,7 +1413,6 @@ module JSON::LD
1523
1413
  # At this point, there is no simple term that iri can be compacted to. If vocab is true and active context has a vocabulary mapping:
1524
1414
  if vocab && self.vocab && iri.start_with?(self.vocab) && iri.length > self.vocab.length
1525
1415
  suffix = iri[self.vocab.length..-1]
1526
- #log_debug("") {"=> vocab suffix: #{suffix.inspect}"} unless quiet
1527
1416
  return suffix unless term_definitions.has_key?(suffix)
1528
1417
  end
1529
1418
 
@@ -1565,11 +1454,9 @@ module JSON::LD
1565
1454
 
1566
1455
  if !vocab
1567
1456
  # transform iri to a relative IRI using the document's base IRI
1568
- iri = remove_base(iri)
1569
- #log_debug("") {"=> relative iri: #{iri.inspect}"} unless quiet
1457
+ iri = remove_base(self.base || base, iri)
1570
1458
  return iri
1571
1459
  else
1572
- #log_debug("") {"=> absolute iri: #{iri.inspect}"} unless quiet
1573
1460
  return iri
1574
1461
  end
1575
1462
  end
@@ -1587,26 +1474,24 @@ module JSON::LD
1587
1474
  # Value (literal or IRI) to be expanded
1588
1475
  # @param [Boolean] useNativeTypes (false) use native representations
1589
1476
  # @param [Boolean] rdfDirection (nil) decode i18n datatype if i18n-datatype
1477
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1590
1478
  # @param [Hash{Symbol => Object}] options
1591
1479
  #
1592
1480
  # @return [Hash] Object representation of value
1593
1481
  # @raise [RDF::ReaderError] if the iri cannot be expanded
1594
1482
  # @see https://www.w3.org/TR/json-ld11-api/#value-expansion
1595
- def expand_value(property, value, useNativeTypes: false, rdfDirection: nil, **options)
1596
- #log_debug("expand_value") {"property: #{property.inspect}, value: #{value.inspect}"}
1597
-
1483
+ def expand_value(property, value, useNativeTypes: false, rdfDirection: nil, base: nil, **options)
1598
1484
  td = term_definitions.fetch(property, TermDefinition.new(property))
1599
1485
 
1600
1486
  # If the active property has a type mapping in active context that is @id, return a new JSON object containing a single key-value pair where the key is @id and the value is the result of using the IRI Expansion algorithm, passing active context, value, and true for document relative.
1601
1487
  if value.is_a?(String) && td.type_mapping == '@id'
1602
1488
  #log_debug("") {"as relative IRI: #{value.inspect}"}
1603
- return {'@id' => expand_iri(value, documentRelative: true).to_s}
1489
+ return {'@id' => expand_iri(value, documentRelative: true, base: base).to_s}
1604
1490
  end
1605
1491
 
1606
1492
  # If active property has a type mapping in active context that is @vocab, return a new JSON object containing a single key-value pair where the key is @id and the value is the result of using the IRI Expansion algorithm, passing active context, value, true for vocab, and true for document relative.
1607
1493
  if value.is_a?(String) && td.type_mapping == '@vocab'
1608
- #log_debug("") {"as vocab IRI: #{value.inspect}"}
1609
- return {'@id' => expand_iri(value, vocab: true, documentRelative: true).to_s}
1494
+ return {'@id' => expand_iri(value, vocab: true, documentRelative: true, base: base).to_s}
1610
1495
  end
1611
1496
 
1612
1497
  value = RDF::Literal(value) if
@@ -1616,16 +1501,14 @@ module JSON::LD
1616
1501
 
1617
1502
  result = case value
1618
1503
  when RDF::URI, RDF::Node
1619
- #log_debug("URI | BNode") { value.to_s }
1620
1504
  {'@id' => value.to_s}
1621
1505
  when RDF::Literal
1622
- #log_debug("Literal") {"datatype: #{value.datatype.inspect}"}
1623
1506
  res = {}
1624
1507
  if value.datatype == RDF::URI(RDF.to_uri + "JSON") && processingMode('json-ld-1.1')
1625
1508
  # Value parsed as JSON
1626
1509
  # FIXME: MultiJson
1627
- res['@value'] = ::JSON.parse(value.object)
1628
1510
  res['@type'] = '@json'
1511
+ res['@value'] = ::JSON.parse(value.object)
1629
1512
  elsif value.datatype.start_with?("https://www.w3.org/ns/i18n#") && rdfDirection == 'i18n-datatype' && processingMode('json-ld-1.1')
1630
1513
  lang, dir = value.datatype.fragment.split('_')
1631
1514
  res['@value'] = value.to_s
@@ -1641,24 +1524,23 @@ module JSON::LD
1641
1524
  end
1642
1525
  res['@direction'] = dir
1643
1526
  elsif useNativeTypes && RDF_LITERAL_NATIVE_TYPES.include?(value.datatype)
1644
- res['@value'] = value.object
1645
1527
  res['@type'] = uri(coerce(property)) if coerce(property)
1528
+ res['@value'] = value.object
1646
1529
  else
1647
1530
  value.canonicalize! if value.datatype == RDF::XSD.double
1648
- res['@value'] = value.to_s
1649
1531
  if coerce(property)
1650
1532
  res['@type'] = uri(coerce(property)).to_s
1651
1533
  elsif value.has_datatype?
1652
1534
  res['@type'] = uri(value.datatype).to_s
1653
1535
  elsif value.has_language? || language(property)
1654
1536
  res['@language'] = (value.language || language(property)).to_s
1655
- # FIXME: direction
1656
1537
  end
1538
+ res['@value'] = value.to_s
1657
1539
  end
1658
1540
  res
1659
1541
  else
1660
1542
  # Otherwise, initialize result to a JSON object with an @value member whose value is set to value.
1661
- res = {'@value' => value}
1543
+ res = {}
1662
1544
 
1663
1545
  if td.type_mapping && !CONTAINERS_ID_VOCAB.include?(td.type_mapping.to_s)
1664
1546
  res['@type'] = td.type_mapping.to_s
@@ -1669,10 +1551,9 @@ module JSON::LD
1669
1551
  res['@direction'] = direction if direction
1670
1552
  end
1671
1553
 
1672
- res
1554
+ res.merge('@value' => value)
1673
1555
  end
1674
1556
 
1675
- #log_debug("") {"=> #{result.inspect}"}
1676
1557
  result
1677
1558
  rescue ::JSON::ParserError => e
1678
1559
  raise JSON::LD::JsonLdError::InvalidJsonLiteral, e.message
@@ -1685,13 +1566,13 @@ module JSON::LD
1685
1566
  # Associated property used to find coercion rules
1686
1567
  # @param [Hash] value
1687
1568
  # Value (literal or IRI), in full object representation, to be compacted
1688
- # @param [Hash{Symbol => Object}] options
1569
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1689
1570
  #
1690
1571
  # @return [Hash] Object representation of value
1691
1572
  # @raise [JsonLdError] if the iri cannot be expanded
1692
1573
  # @see https://www.w3.org/TR/json-ld11-api/#value-compaction
1693
1574
  # FIXME: revisit the specification version of this.
1694
- def compact_value(property, value, **options)
1575
+ def compact_value(property, value, base: nil)
1695
1576
  #log_debug("compact_value") {"property: #{property.inspect}, value: #{value.inspect}"}
1696
1577
 
1697
1578
  indexing = index?(value) && container(property).include?('@index')
@@ -1702,7 +1583,7 @@ module JSON::LD
1702
1583
  when coerce(property) == '@id' && value.has_key?('@id') && (value.keys - %w(@id @index)).empty?
1703
1584
  # Compact an @id coercion
1704
1585
  #log_debug("") {" (@id & coerce)"}
1705
- compact_iri(value['@id'])
1586
+ compact_iri(value['@id'], base: base)
1706
1587
  when coerce(property) == '@vocab' && value.has_key?('@id') && (value.keys - %w(@id @index)).empty?
1707
1588
  # Compact an @id coercion
1708
1589
  #log_debug("") {" (@id & coerce & vocab)"}
@@ -1792,13 +1673,21 @@ module JSON::LD
1792
1673
  v.join(" ") + "]"
1793
1674
  end
1794
1675
 
1676
+ # Duplicate an active context, allowing it to be modified.
1795
1677
  def dup
1796
- # Also duplicate mappings, coerce and list
1797
1678
  that = self
1798
- ec = super
1679
+ ec = Context.new(unfrozen: true, **@options)
1680
+ ec.context_base = that.context_base
1681
+ ec.base = that.base unless that.base.nil?
1682
+ ec.default_direction = that.default_direction
1683
+ ec.default_language = that.default_language
1684
+ ec.previous_context = that.previous_context
1685
+ ec.processingMode = that.processingMode if that.instance_variable_get(:@processingModee)
1686
+ ec.vocab = that.vocab if that.vocab
1687
+
1799
1688
  ec.instance_eval do
1800
1689
  @term_definitions = that.term_definitions.dup
1801
- @iri_to_term = that.iri_to_term.dup
1690
+ @iri_to_term = that.iri_to_term
1802
1691
  end
1803
1692
  ec
1804
1693
  end
@@ -1812,7 +1701,7 @@ module JSON::LD
1812
1701
  # @param [String] term
1813
1702
  # @return [Boolean]
1814
1703
  def term_valid?(term)
1815
- term.is_a?(String)
1704
+ term.is_a?(String) && !term.empty?
1816
1705
  end
1817
1706
 
1818
1707
  # Reverse term mapping, typically used for finding aliases for keys.
@@ -1830,9 +1719,9 @@ module JSON::LD
1830
1719
 
1831
1720
  private
1832
1721
 
1833
- CONTEXT_CONTAINER_ARRAY_TERMS = %w(@set @list @graph).freeze
1834
- CONTEXT_CONTAINER_ID_GRAPH = %w(@id @graph).freeze
1835
- CONTEXT_CONTAINER_INDEX_GRAPH = %w(@index @graph).freeze
1722
+ CONTEXT_CONTAINER_ARRAY_TERMS = Set.new(%w(@set @list @graph)).freeze
1723
+ CONTEXT_CONTAINER_ID_GRAPH = Set.new(%w(@id @graph)).freeze
1724
+ CONTEXT_CONTAINER_INDEX_GRAPH = Set.new(%w(@index @graph)).freeze
1836
1725
  CONTEXT_BASE_FRAG_OR_QUERY = %w(? #).freeze
1837
1726
  CONTEXT_TYPE_ID_VOCAB = %w(@id @vocab).freeze
1838
1727
 
@@ -1844,20 +1733,11 @@ module JSON::LD
1844
1733
  bnode(namer.get_sym($1))
1845
1734
  else
1846
1735
  value = RDF::URI(value)
1847
- value.validate! if @options[:validate]
1848
- value.canonicalize! if @options[:canonicalize]
1849
- value = RDF::URI.intern(value, {}) if @options[:intern]
1736
+ #value.validate! if options[:validate]
1850
1737
  value
1851
1738
  end
1852
1739
  end
1853
1740
 
1854
- # Clear the provided context, used for testing
1855
- # @return [Context] self
1856
- def clear_provided_context
1857
- @provided_context = nil
1858
- self
1859
- end
1860
-
1861
1741
  # Keep track of allocated BNodes
1862
1742
  #
1863
1743
  # Don't actually use the name provided, to prevent name alias issues.
@@ -1898,7 +1778,7 @@ module JSON::LD
1898
1778
  # @return [Hash{String => Hash{String => String}}]
1899
1779
  # @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
1900
1780
  def inverse_context
1901
- @inverse_context ||= begin
1781
+ Context.inverse_cache[self.hash] ||= begin
1902
1782
  result = {}
1903
1783
  default_language = (self.default_language || '@none').downcase
1904
1784
  term_definitions.keys.sort do |a, b|
@@ -1906,7 +1786,7 @@ module JSON::LD
1906
1786
  end.each do |term|
1907
1787
  next unless td = term_definitions[term]
1908
1788
 
1909
- container = td.container_mapping.join('')
1789
+ container = td.container_mapping.to_a.join('')
1910
1790
  if container.empty?
1911
1791
  container = td.as_set? ? %(@set) : %(@none)
1912
1792
  end
@@ -1943,7 +1823,7 @@ module JSON::LD
1943
1823
  lang_dir = td.direction_mapping ? "_#{td.direction_mapping}" : '@none'
1944
1824
  language_map[lang_dir] ||= term
1945
1825
  elsif default_direction
1946
- language_map[("#{td.language_mapping}_#{default_direction}").downcase] ||= term
1826
+ language_map["_#{default_direction}"] ||= term
1947
1827
  language_map['@none'] ||= term
1948
1828
  type_map['@none'] ||= term
1949
1829
  else
@@ -1993,10 +1873,11 @@ module JSON::LD
1993
1873
  ##
1994
1874
  # Removes a base IRI from the given absolute IRI.
1995
1875
  #
1876
+ # @param [String] base the base used for making `iri` relative
1996
1877
  # @param [String] iri the absolute IRI
1997
1878
  # @return [String]
1998
1879
  # the relative IRI if relative to base, otherwise the absolute IRI.
1999
- def remove_base(iri)
1880
+ def remove_base(base, iri)
2000
1881
  return iri unless base
2001
1882
  @base_and_parents ||= begin
2002
1883
  u = base
@@ -2058,7 +1939,7 @@ module JSON::LD
2058
1939
  "'@container' on term #{term.inspect} must be a string: #{container.inspect}"
2059
1940
  end
2060
1941
 
2061
- val = Array(container).dup
1942
+ val = Set.new(Array(container))
2062
1943
  val.delete('@set') if has_set = val.include?('@set')
2063
1944
 
2064
1945
  if val.include?('@list')
@@ -2088,7 +1969,7 @@ module JSON::LD
2088
1969
  processingMode('json-ld-1.0')
2089
1970
  raise JsonLdError::InvalidContainerMapping,
2090
1971
  "'@container' on term #{term.inspect} using @id cannot have any values other than @set and/or @graph, found #{container.inspect}" unless
2091
- (val - CONTEXT_CONTAINER_ID_GRAPH).empty?
1972
+ val.subset?(CONTEXT_CONTAINER_ID_GRAPH)
2092
1973
  # Okay
2093
1974
  elsif val.include?('@type') || val.include?('@graph')
2094
1975
  raise JsonLdError::InvalidContainerMapping,
@@ -2106,5 +1987,232 @@ module JSON::LD
2106
1987
  end
2107
1988
  Array(container)
2108
1989
  end
1990
+
1991
+ # Term Definitions specify how properties and values have to be interpreted as well as the current vocabulary mapping and the default language
1992
+ class TermDefinition
1993
+ # @return [RDF::URI] IRI map
1994
+ attr_accessor :id
1995
+
1996
+ # @return [String] term name
1997
+ attr_accessor :term
1998
+
1999
+ # @return [String] Type mapping
2000
+ attr_accessor :type_mapping
2001
+
2002
+ # Base container mapping, without @set
2003
+ # @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] Container mapping
2004
+ attr_reader :container_mapping
2005
+
2006
+ # @return [String] Term used for nest properties
2007
+ attr_accessor :nest
2008
+
2009
+ # Language mapping of term, `false` is used if there is an explicit language mapping for this term.
2010
+ # @return [String] Language mapping
2011
+ attr_accessor :language_mapping
2012
+
2013
+ # Direction of term, `false` is used if there is explicit direction mapping mapping for this term.
2014
+ # @return ["ltr", "rtl"] direction_mapping
2015
+ attr_accessor :direction_mapping
2016
+
2017
+ # @return [Boolean] Reverse Property
2018
+ attr_accessor :reverse_property
2019
+
2020
+ # This is a simple term definition, not an expanded term definition
2021
+ # @return [Boolean]
2022
+ attr_accessor :simple
2023
+
2024
+ # Property used for data indexing; defaults to @index
2025
+ # @return [Boolean]
2026
+ attr_accessor :index
2027
+
2028
+ # Indicate that term may be used as a prefix
2029
+ attr_writer :prefix
2030
+
2031
+ # Term-specific context
2032
+ # @return [Hash{String => Object}]
2033
+ attr_accessor :context
2034
+
2035
+ # Term is protected.
2036
+ # @return [Boolean]
2037
+ attr_writer :protected
2038
+
2039
+ # This is a simple term definition, not an expanded term definition
2040
+ # @return [Boolean] simple
2041
+ def simple?; simple; end
2042
+
2043
+ # This is an appropriate term to use as the prefix of a compact IRI
2044
+ # @return [Boolean] simple
2045
+ def prefix?; @prefix; end
2046
+
2047
+ # Create a new Term Mapping with an ID
2048
+ # @param [String] term
2049
+ # @param [String] id
2050
+ # @param [String] type_mapping Type mapping
2051
+ # @param [Set<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] container_mapping
2052
+ # @param [String] language_mapping
2053
+ # Language mapping of term, `false` is used if there is an explicit language mapping for this term
2054
+ # @param ["ltr", "rtl"] direction_mapping
2055
+ # Direction mapping of term, `false` is used if there is an explicit direction mapping for this term
2056
+ # @param [Boolean] reverse_property
2057
+ # @param [Boolean] protected mark resulting context as protected
2058
+ # @param [String] nest term used for nest properties
2059
+ # @param [Boolean] simple
2060
+ # This is a simple term definition, not an expanded term definition
2061
+ # @param [Boolean] prefix
2062
+ # Term may be used as a prefix
2063
+ def initialize(term,
2064
+ id: nil,
2065
+ index: nil,
2066
+ type_mapping: nil,
2067
+ container_mapping: nil,
2068
+ language_mapping: nil,
2069
+ direction_mapping: nil,
2070
+ reverse_property: false,
2071
+ nest: nil,
2072
+ protected: nil,
2073
+ simple: false,
2074
+ prefix: nil,
2075
+ context: nil)
2076
+ @term = term
2077
+ @id = id.to_s unless id.nil?
2078
+ @index = index.to_s unless index.nil?
2079
+ @type_mapping = type_mapping.to_s unless type_mapping.nil?
2080
+ self.container_mapping = container_mapping
2081
+ @language_mapping = language_mapping unless language_mapping.nil?
2082
+ @direction_mapping = direction_mapping unless direction_mapping.nil?
2083
+ @reverse_property = reverse_property
2084
+ @protected = protected
2085
+ @nest = nest unless nest.nil?
2086
+ @simple = simple
2087
+ @prefix = prefix unless prefix.nil?
2088
+ @context = context unless context.nil?
2089
+ end
2090
+
2091
+ # Term is protected.
2092
+ # @return [Boolean]
2093
+ def protected?; !!@protected; end
2094
+
2095
+ # Set container mapping, from an array which may include @set
2096
+ def container_mapping=(mapping)
2097
+ mapping = case mapping
2098
+ when Set then mapping
2099
+ when Array then Set.new(mapping)
2100
+ when String then Set[mapping]
2101
+ when nil then Set.new
2102
+ else
2103
+ raise "Shouldn't happen with #{mapping.inspect}"
2104
+ end
2105
+ if @as_set = mapping.include?('@set')
2106
+ mapping = mapping.dup
2107
+ mapping.delete('@set')
2108
+ end
2109
+ @container_mapping = mapping
2110
+ @index ||= '@index' if mapping.include?('@index')
2111
+ end
2112
+
2113
+ ##
2114
+ # Output Hash or String definition for this definition considering @language and @vocab
2115
+ #
2116
+ # @param [Context] context
2117
+ # @return [String, Hash{String => Array[String], String}]
2118
+ def to_context_definition(context)
2119
+ cid = if context.vocab && id.start_with?(context.vocab)
2120
+ # Nothing to return unless it's the same as the vocab
2121
+ id == context.vocab ? context.vocab : id.to_s[context.vocab.length..-1]
2122
+ else
2123
+ # Find a term to act as a prefix
2124
+ iri, prefix = context.iri_to_term.detect {|i,p| id.to_s.start_with?(i.to_s)}
2125
+ iri && iri != id ? "#{prefix}:#{id.to_s[iri.length..-1]}" : id
2126
+ end
2127
+
2128
+ if simple?
2129
+ cid.to_s unless cid == term && context.vocab
2130
+ else
2131
+ defn = {}
2132
+ defn[reverse_property ? '@reverse' : '@id'] = cid.to_s unless cid == term && !reverse_property
2133
+ if type_mapping
2134
+ defn['@type'] = if KEYWORDS.include?(type_mapping)
2135
+ type_mapping
2136
+ else
2137
+ context.compact_iri(type_mapping, vocab: true)
2138
+ end
2139
+ end
2140
+
2141
+ cm = Array(container_mapping)
2142
+ cm << "@set" if as_set? && !cm.include?("@set")
2143
+ cm = cm.first if cm.length == 1
2144
+ defn['@container'] = cm unless cm.empty?
2145
+ # Language set as false to be output as null
2146
+ defn['@language'] = (@language_mapping ? @language_mapping : nil) unless @language_mapping.nil?
2147
+ defn['@direction'] = (@direction_mapping ? @direction_mapping : nil) unless @direction_mapping.nil?
2148
+ defn['@context'] = @context if @context
2149
+ defn['@nest'] = @nest if @nest
2150
+ defn['@index'] = @index if @index
2151
+ defn['@prefix'] = @prefix unless @prefix.nil?
2152
+ defn
2153
+ end
2154
+ end
2155
+
2156
+ ##
2157
+ # Turn this into a source for a new instantiation
2158
+ # FIXME: context serialization
2159
+ # @return [String]
2160
+ def to_rb
2161
+ defn = [%(TermDefinition.new\(#{term.inspect})]
2162
+ %w(id index type_mapping container_mapping language_mapping direction_mapping reverse_property nest simple prefix context protected).each do |acc|
2163
+ v = instance_variable_get("@#{acc}".to_sym)
2164
+ v = v.to_s if v.is_a?(RDF::Term)
2165
+ if acc == 'container_mapping'
2166
+ v = v.to_a
2167
+ v << '@set' if as_set?
2168
+ v = v.first if v.length <= 1
2169
+ end
2170
+ defn << "#{acc}: #{v.inspect}" if v
2171
+ end
2172
+ defn.join(', ') + ")"
2173
+ end
2174
+
2175
+ # If container mapping was defined along with @set
2176
+ # @return [Boolean]
2177
+ def as_set?; @as_set || false; end
2178
+
2179
+ # Check if term definitions are identical, modulo @protected
2180
+ # @return [Boolean]
2181
+ def ==(other)
2182
+ other.is_a?(TermDefinition) &&
2183
+ id == other.id &&
2184
+ term == other.term &&
2185
+ type_mapping == other.type_mapping &&
2186
+ container_mapping == other.container_mapping &&
2187
+ nest == other.nest &&
2188
+ language_mapping == other.language_mapping &&
2189
+ direction_mapping == other.direction_mapping &&
2190
+ reverse_property == other.reverse_property &&
2191
+ simple == other.simple &&
2192
+ index == other.index &&
2193
+ context == other.context &&
2194
+ prefix? == other.prefix? &&
2195
+ as_set? == other.as_set?
2196
+ end
2197
+
2198
+ def inspect
2199
+ v = %w([TD)
2200
+ v << "id=#{@id}"
2201
+ v << "index=#{index.inspect}" unless index.nil?
2202
+ v << "term=#{@term}"
2203
+ v << "rev" if reverse_property
2204
+ v << "container=#{container_mapping}" if container_mapping
2205
+ v << "as_set=#{as_set?.inspect}"
2206
+ v << "lang=#{language_mapping.inspect}" unless language_mapping.nil?
2207
+ v << "dir=#{direction_mapping.inspect}" unless direction_mapping.nil?
2208
+ v << "type=#{type_mapping}" unless type_mapping.nil?
2209
+ v << "nest=#{nest.inspect}" unless nest.nil?
2210
+ v << "simple=true" if @simple
2211
+ v << "protected=true" if @protected
2212
+ v << "prefix=#{@prefix.inspect}" unless @prefix.nil?
2213
+ v << "has-context" unless context.nil?
2214
+ v.join(" ") + "]"
2215
+ end
2216
+ end
2109
2217
  end
2110
2218
  end