json-ld 3.1.3 → 3.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,8 @@
3
3
  require 'json'
4
4
  require 'bigdecimal'
5
5
  require 'set'
6
+ require 'rdf/util/cache'
7
+
6
8
  begin
7
9
  # Attempt to load this to avoid unnecessary context fetches
8
10
  require 'json-ld-preloaded'
@@ -21,10 +23,13 @@ module JSON::LD
21
23
  # @return [Hash{Symbol => Context}]
22
24
  PRELOADED = {}
23
25
 
26
+ # Initial contexts, defined on first access
27
+ INITIAL_CONTEXTS = {}
28
+
24
29
  ##
25
30
  # Defines the maximum number of interned URI references that can be held
26
31
  # cached in memory at any one time.
27
- CACHE_SIZE = -1 # unlimited by default
32
+ CACHE_SIZE = 100 # unlimited by default
28
33
 
29
34
  class << self
30
35
  ##
@@ -45,242 +50,11 @@ module JSON::LD
45
50
  end
46
51
  end
47
52
 
48
- # Term Definitions specify how properties and values have to be interpreted as well as the current vocabulary mapping and the default language
49
- class TermDefinition
50
- # @return [RDF::URI] IRI map
51
- attr_accessor :id
52
-
53
- # @return [String] term name
54
- attr_accessor :term
55
-
56
- # @return [String] Type mapping
57
- attr_accessor :type_mapping
58
-
59
- # Base container mapping, without @set
60
- # @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] Container mapping
61
- attr_reader :container_mapping
62
-
63
- # @return [String] Term used for nest properties
64
- attr_accessor :nest
65
-
66
- # Language mapping of term, `false` is used if there is an explicit language mapping for this term.
67
- # @return [String] Language mapping
68
- attr_accessor :language_mapping
69
-
70
- # Direction of term, `false` is used if there is explicit direction mapping mapping for this term.
71
- # @return ["ltr", "rtl"] direction_mapping
72
- attr_accessor :direction_mapping
73
-
74
- # @return [Boolean] Reverse Property
75
- attr_accessor :reverse_property
76
-
77
- # This is a simple term definition, not an expanded term definition
78
- # @return [Boolean]
79
- attr_accessor :simple
80
-
81
- # Property used for data indexing; defaults to @index
82
- # @return [Boolean]
83
- attr_accessor :index
84
-
85
- # Indicate that term may be used as a prefix
86
- attr_writer :prefix
87
-
88
- # Term-specific context
89
- # @return [Hash{String => Object}]
90
- attr_accessor :context
91
-
92
- # Term is protected.
93
- # @return [Boolean]
94
- attr_writer :protected
95
-
96
- # This is a simple term definition, not an expanded term definition
97
- # @return [Boolean] simple
98
- def simple?; simple; end
99
-
100
- # This is an appropriate term to use as the prefix of a compact IRI
101
- # @return [Boolean] simple
102
- def prefix?; @prefix; end
103
-
104
- # Create a new Term Mapping with an ID
105
- # @param [String] term
106
- # @param [String] id
107
- # @param [String] type_mapping Type mapping
108
- # @param [Set<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>] container_mapping
109
- # @param [String] language_mapping
110
- # Language mapping of term, `false` is used if there is an explicit language mapping for this term
111
- # @param ["ltr", "rtl"] direction_mapping
112
- # Direction mapping of term, `false` is used if there is an explicit direction mapping for this term
113
- # @param [Boolean] reverse_property
114
- # @param [Boolean] protected
115
- # @param [String] nest term used for nest properties
116
- # @param [Boolean] simple
117
- # This is a simple term definition, not an expanded term definition
118
- # @param [Boolean] prefix
119
- # Term may be used as a prefix
120
- def initialize(term,
121
- id: nil,
122
- index: nil,
123
- type_mapping: nil,
124
- container_mapping: nil,
125
- language_mapping: nil,
126
- direction_mapping: nil,
127
- reverse_property: false,
128
- nest: nil,
129
- protected: nil,
130
- simple: false,
131
- prefix: nil,
132
- context: nil)
133
- @term = term
134
- @id = id.to_s unless id.nil?
135
- @index = index.to_s unless index.nil?
136
- @type_mapping = type_mapping.to_s unless type_mapping.nil?
137
- self.container_mapping = container_mapping
138
- @language_mapping = language_mapping unless language_mapping.nil?
139
- @direction_mapping = direction_mapping unless direction_mapping.nil?
140
- @reverse_property = reverse_property
141
- @protected = protected
142
- @nest = nest unless nest.nil?
143
- @simple = simple
144
- @prefix = prefix unless prefix.nil?
145
- @context = context unless context.nil?
146
- end
147
-
148
- # Term is protected.
149
- # @return [Boolean]
150
- def protected?; !!@protected; end
151
-
152
- # Set container mapping, from an array which may include @set
153
- def container_mapping=(mapping)
154
- mapping = case mapping
155
- when Set then mapping
156
- when Array then Set.new(mapping)
157
- when String then Set[mapping]
158
- when nil then Set.new
159
- else
160
- raise "Shouldn't happen with #{mapping.inspect}"
161
- end
162
- if @as_set = mapping.include?('@set')
163
- mapping = mapping.dup
164
- mapping.delete('@set')
165
- end
166
- @container_mapping = mapping
167
- @index ||= '@index' if mapping.include?('@index')
168
- end
169
-
170
- ##
171
- # Output Hash or String definition for this definition considering @language and @vocab
172
- #
173
- # @param [Context] context
174
- # @return [String, Hash{String => Array[String], String}]
175
- def to_context_definition(context)
176
- cid = if context.vocab && id.start_with?(context.vocab)
177
- # Nothing to return unless it's the same as the vocab
178
- id == context.vocab ? context.vocab : id.to_s[context.vocab.length..-1]
179
- else
180
- # Find a term to act as a prefix
181
- iri, prefix = context.iri_to_term.detect {|i,p| id.to_s.start_with?(i.to_s)}
182
- iri && iri != id ? "#{prefix}:#{id.to_s[iri.length..-1]}" : id
183
- end
184
-
185
- if simple?
186
- cid.to_s unless cid == term && context.vocab
187
- else
188
- defn = {}
189
- defn[reverse_property ? '@reverse' : '@id'] = cid.to_s unless cid == term && !reverse_property
190
- if type_mapping
191
- defn['@type'] = if KEYWORDS.include?(type_mapping)
192
- type_mapping
193
- else
194
- context.compact_iri(type_mapping, vocab: true)
195
- end
196
- end
197
-
198
- cm = Array(container_mapping)
199
- cm << "@set" if as_set? && !cm.include?("@set")
200
- cm = cm.first if cm.length == 1
201
- defn['@container'] = cm unless cm.empty?
202
- # Language set as false to be output as null
203
- defn['@language'] = (@language_mapping ? @language_mapping : nil) unless @language_mapping.nil?
204
- defn['@context'] = @context if @context
205
- defn['@nest'] = @nest if @nest
206
- defn['@index'] = @index if @index
207
- defn['@prefix'] = @prefix unless @prefix.nil?
208
- defn
209
- end
210
- end
211
-
212
- ##
213
- # Turn this into a source for a new instantiation
214
- # FIXME: context serialization
215
- # @return [String]
216
- def to_rb
217
- defn = [%(TermDefinition.new\(#{term.inspect})]
218
- %w(id index type_mapping container_mapping language_mapping direction_mapping reverse_property nest simple prefix context protected).each do |acc|
219
- v = instance_variable_get("@#{acc}".to_sym)
220
- v = v.to_s if v.is_a?(RDF::Term)
221
- if acc == 'container_mapping'
222
- v = v.to_a
223
- v << '@set' if as_set?
224
- v = v.first if v.length <= 1
225
- end
226
- defn << "#{acc}: #{v.inspect}" if v
227
- end
228
- defn.join(', ') + ")"
229
- end
230
-
231
- # If container mapping was defined along with @set
232
- # @return [Boolean]
233
- def as_set?; @as_set || false; end
234
-
235
- # Check if term definitions are identical, modulo @protected
236
- # @return [Boolean]
237
- def ==(other)
238
- other.is_a?(TermDefinition) &&
239
- id == other.id &&
240
- term == other.term &&
241
- type_mapping == other.type_mapping &&
242
- container_mapping == other.container_mapping &&
243
- nest == other.nest &&
244
- language_mapping == other.language_mapping &&
245
- direction_mapping == other.direction_mapping &&
246
- reverse_property == other.reverse_property &&
247
- simple == other.simple &&
248
- index == other.index &&
249
- context == other.context &&
250
- prefix? == other.prefix? &&
251
- as_set? == other.as_set?
252
- end
253
-
254
- def inspect
255
- v = %w([TD)
256
- v << "id=#{@id}"
257
- v << "index=#{index.inspect}" unless index.nil?
258
- v << "term=#{@term}"
259
- v << "rev" if reverse_property
260
- v << "container=#{container_mapping}" if container_mapping
261
- v << "as_set=#{as_set?.inspect}"
262
- v << "lang=#{language_mapping.inspect}" unless language_mapping.nil?
263
- v << "dir=#{direction_mapping.inspect}" unless direction_mapping.nil?
264
- v << "type=#{type_mapping}" unless type_mapping.nil?
265
- v << "nest=#{nest.inspect}" unless nest.nil?
266
- v << "simple=true" if @simple
267
- v << "protected=true" if @protected
268
- v << "prefix=#{@prefix.inspect}" unless @prefix.nil?
269
- v << "has-context" unless context.nil?
270
- v.join(" ") + "]"
271
- end
272
- end
273
-
274
53
  # The base.
275
54
  #
276
55
  # @return [RDF::URI] Current base IRI, used for expanding relative IRIs.
277
56
  attr_reader :base
278
57
 
279
- # The base.
280
- #
281
- # @return [RDF::URI] Document base IRI, to initialize `base`.
282
- attr_reader :doc_base
283
-
284
58
  # @return [RDF::URI] base IRI of the context, if loaded remotely.
285
59
  attr_accessor :context_base
286
60
 
@@ -321,9 +95,6 @@ module JSON::LD
321
95
  # @return [Hash{Symbol => Object}] Global options used in generating IRIs
322
96
  attr_accessor :options
323
97
 
324
- # @return [Context] A context provided to us that we can use without re-serializing XXX
325
- attr_accessor :provided_context
326
-
327
98
  # @return [BlankNodeNamer]
328
99
  attr_accessor :namer
329
100
 
@@ -333,11 +104,29 @@ module JSON::LD
333
104
  # @see #initialize
334
105
  # @see #parse
335
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.
336
113
  # @raise [JsonLdError]
337
114
  # on a remote context load error, syntax error, or a reference to a term which is not defined.
338
115
  # @return [Context]
339
- def self.parse(local_context, override_protected: false, propagate: true, **options)
340
- self.new(**options).parse(local_context, 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
341
130
  end
342
131
 
343
132
  ##
@@ -346,17 +135,50 @@ module JSON::LD
346
135
  # @return [RDF::Util::Cache]
347
136
  # @private
348
137
  def self.cache
349
- require 'rdf/util/cache' unless defined?(::RDF::Util::Cache)
350
138
  @cache ||= RDF::Util::Cache.new(CACHE_SIZE)
351
139
  end
352
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
177
+ end
178
+
353
179
  ##
354
180
  # Create new evaluation context
355
181
  # @param [Hash] options
356
- # @option options [String, #to_s] :base
357
- # 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.
358
- # @option options [Proc] :documentLoader
359
- # 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.
360
182
  # @option options [Hash{Symbol => String}] :prefixes
361
183
  # See `RDF::Reader#initialize`
362
184
  # @option options [String, #to_s] :vocab
@@ -367,11 +189,9 @@ module JSON::LD
367
189
  # @yieldparam [Context]
368
190
  # @return [Context]
369
191
  def initialize(**options)
370
- if options[:base]
371
- @base = @doc_base = RDF::URI(options[:base]).dup
372
- @doc_base.canonicalize! if options[:canonicalize]
192
+ if options[:processingMode] == 'json-ld-1.0'
193
+ @processingMode = 'json-ld-1.0'
373
194
  end
374
- self.processingMode = options[:processingMode] if options.has_key?(:processingMode)
375
195
  @term_definitions = {}
376
196
  @iri_to_term = {
377
197
  RDF.to_uri.to_s => "rdf",
@@ -397,135 +217,6 @@ module JSON::LD
397
217
  yield(self) if block_given?
398
218
  end
399
219
 
400
- ##
401
- # Initial context, without mappings, vocab or default language
402
- #
403
- # @return [Boolean]
404
- def empty?
405
- @term_definitions.empty? && self.vocab.nil? && self.default_language.nil?
406
- end
407
-
408
- # @param [String] value must be an absolute IRI
409
- def base=(value, **options)
410
- if value
411
- raise JsonLdError::InvalidBaseIRI, "@base must be a string: #{value.inspect}" unless value.is_a?(String) || value.is_a?(RDF::URI)
412
- value = RDF::URI(value).dup
413
- value = @base.join(value) if @base && value.relative?
414
- @base = value
415
- @base.canonicalize! if @options[:canonicalize]
416
- raise JsonLdError::InvalidBaseIRI, "@base must be an absolute IRI: #{value.inspect}" unless @base.absolute? || !@options[:validate]
417
- @base
418
- else
419
- @base = nil
420
- end
421
-
422
- end
423
-
424
- # @param [String] value
425
- def default_language=(value, **options)
426
- @default_language = case value
427
- when String
428
- # Warn on an invalid language tag, unless :validate is true, in which case it's an error
429
- if value !~ /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/
430
- warn "@language must be valid BCP47: #{value.inspect}"
431
- end
432
- options[:lowercaseLanguage] ? value.downcase : value
433
- when nil
434
- nil
435
- else
436
- raise JsonLdError::InvalidDefaultLanguage, "@language must be a string: #{value.inspect}"
437
- end
438
- end
439
-
440
- # @param [String] value
441
- def default_direction=(value, **options)
442
- @default_direction = if value
443
- raise JsonLdError::InvalidBaseDirection, "@direction must be one or 'ltr', or 'rtl': #{value.inspect}" unless %w(ltr rtl).include?(value)
444
- value
445
- else
446
- nil
447
- end
448
- end
449
-
450
- ##
451
- # Retrieve, or check processing mode.
452
- #
453
- # * With no arguments, retrieves the current set processingMode.
454
- # * 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"
455
- # * If expecting 1.1, and not set, it has the side-effect of setting mode to json-ld-1.1.
456
- #
457
- # @param [String, Number] expected (nil)
458
- # @return [String]
459
- def processingMode(expected = nil)
460
- case expected
461
- when 1.0, 'json-ld-1.0'
462
- @processingMode == 'json-ld-1.0'
463
- when 1.1, 'json-ld-1.1'
464
- @processingMode ||= 'json-ld-1.1'
465
- @processingMode == 'json-ld-1.1'
466
- when nil
467
- @processingMode
468
- else
469
- false
470
- end
471
- end
472
-
473
- ##
474
- # Set processing mode.
475
- #
476
- # * 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"
477
- #
478
- # 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.
479
- # If processingMode has been set, and it is not "json-ld-1.1", a "processing mode conflict" has been detecting, and processing is aborted.
480
- #
481
- # @param [String, Number] expected
482
- # @return [String]
483
- # @raise [JsonLdError::ProcessingModeConflict]
484
- def processingMode=(value = nil, **options)
485
- value = "json-ld-1.1" if value == 1.1
486
- case value
487
- when "json-ld-1.0", "json-ld-1.1"
488
- if @processingMode && @processingMode != value
489
- raise JsonLdError::ProcessingModeConflict, "#{value} not compatible with #{@processingMode}"
490
- end
491
- @processingMode = value
492
- else
493
- raise JsonLdError::InvalidVersionValue, value.inspect
494
- end
495
- end
496
-
497
- # 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.
498
- # @param [String] value must be an absolute IRI
499
- def vocab=(value, **options)
500
- @vocab = case value
501
- when /_:/
502
- # BNode vocab is deprecated
503
- warn "[DEPRECATION] Blank Node vocabularies deprecated in JSON-LD 1.1." if @options[:validate] && processingMode("json-ld-1.1")
504
- value
505
- when String, RDF::URI
506
- if (RDF::URI(value.to_s).relative? && processingMode("json-ld-1.0"))
507
- raise JsonLdError::InvalidVocabMapping, "@vocab must be an absolute IRI in 1.0 mode: #{value.inspect}"
508
- end
509
- v = expand_iri(value.to_s, vocab: true, documentRelative: true)
510
- raise JsonLdError::InvalidVocabMapping, "@vocab must be an IRI: #{value.inspect}" if !v.valid? && @options[:validate]
511
- v
512
- when nil
513
- nil
514
- else
515
- raise JsonLdError::InvalidVocabMapping, "@vocab must be an IRI: #{value.inspect}"
516
- end
517
- end
518
-
519
- # Set propagation
520
- # @note: by the time this is called, the work has already been done.
521
- #
522
- # @param [Boolean] value
523
- def propagate=(value, **options)
524
- raise JsonLdError::InvalidContextEntry, "@propagate may only be set in 1.1 mode" if processingMode("json-ld-1.0")
525
- raise JsonLdError::InvalidPropagateValue, "@propagate must be boolean valued: #{value.inspect}" unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
526
- value
527
- end
528
-
529
220
  # Create an Evaluation Context
530
221
  #
531
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.
@@ -536,10 +227,12 @@ module JSON::LD
536
227
  #
537
228
  #
538
229
  # @param [String, #read, Array, Hash, Context] local_context
539
- # @param [Array<String>] remote_contexts
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.
540
232
  # @param [Boolean] override_protected Protected terms may be cleared.
541
- # @param [Boolean] propagate
233
+ # @param [Boolean] propagate (true)
542
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 ([])
543
236
  # @param [Boolean] validate_scoped (true).
544
237
  # Validate scoped context, loading if necessary.
545
238
  # If false, do not load scoped contexts.
@@ -548,12 +241,12 @@ module JSON::LD
548
241
  # @return [Context]
549
242
  # @see https://www.w3.org/TR/json-ld11-api/index.html#context-processing-algorithm
550
243
  def parse(local_context,
551
- remote_contexts: [],
244
+ base: nil,
552
245
  override_protected: false,
553
246
  propagate: true,
247
+ remote_contexts: [],
554
248
  validate_scoped: true)
555
249
  result = self.dup
556
- result.provided_context = local_context if self.empty?
557
250
  # Early check for @propagate, which can only appear in a local context
558
251
  propagate = local_context.is_a?(Hash) ? local_context.fetch('@propagate', propagate) : propagate
559
252
  result.previous_context ||= result.dup unless propagate
@@ -562,7 +255,7 @@ module JSON::LD
562
255
 
563
256
  local_context.each do |context|
564
257
  case context
565
- when nil
258
+ when nil,false
566
259
  # 3.1 If the `override_protected` is false, and the active context contains protected terms, an error is raised.
567
260
  if override_protected || result.term_definitions.values.none?(&:protected?)
568
261
  null_context = Context.new(**options)
@@ -581,18 +274,17 @@ module JSON::LD
581
274
  begin
582
275
  ctx = JSON.load(context)
583
276
  raise JSON::LD::JsonLdError::InvalidRemoteContext, "Context missing @context key" if @options[:validate] && ctx['@context'].nil?
584
- result = result.dup.parse(ctx["@context"] ? ctx["@context"].dup : {})
585
- result.provided_context = ctx["@context"] if [context] == local_context
277
+ result = result.parse(ctx["@context"] ? ctx["@context"] : {})
586
278
  rescue JSON::ParserError => e
587
279
  #log_debug("parse") {"Failed to parse @context from remote document at #{context}: #{e.message}"}
588
280
  raise JSON::LD::JsonLdError::InvalidRemoteContext, "Failed to parse remote context at #{context}: #{e.message}" if @options[:validate]
589
- self.dup
281
+ self
590
282
  end
591
283
  when String, RDF::URI
592
284
  #log_debug("parse") {"remote: #{context}, base: #{result.context_base || result.base}"}
593
285
 
594
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].
595
- context = RDF::URI(result.context_base || options[:base]).join(context)
287
+ context = RDF::URI(result.context_base || base).join(context)
596
288
  context_canon = context.canonicalize
597
289
  context_canon.scheme = 'http' if context_canon.scheme == 'https'
598
290
 
@@ -622,13 +314,13 @@ module JSON::LD
622
314
  #context_opts.delete(:headers)
623
315
  JSON::LD::API.loadRemoteDocument(context.to_s, **context_opts) do |remote_doc|
624
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.
625
- raise JsonLdError::InvalidRemoteContext, "#{context}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.has_key?('@context')
317
+ raise JsonLdError::InvalidRemoteContext, "#{context}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.key?('@context')
626
318
 
627
319
  # Parse stand-alone
628
- ctx = Context.new(**options)
320
+ ctx = Context.new(unfrozen: true, **options).dup
629
321
  ctx.context_base = context.to_s
630
322
  ctx = ctx.parse(remote_doc.document['@context'], remote_contexts: remote_contexts.dup)
631
- ctx.base = nil
323
+ ctx.instance_variable_set(:@base, nil)
632
324
  ctx
633
325
  end
634
326
  rescue JsonLdError::LoadingDocumentFailed => e
@@ -647,7 +339,6 @@ module JSON::LD
647
339
 
648
340
  context.previous_context = self unless propagate
649
341
  result = context
650
- #log_debug("parse") {"=> provided_context: #{context.inspect}"}
651
342
  when Hash
652
343
  context = context.dup # keep from modifying a hash passed as a param
653
344
 
@@ -661,12 +352,12 @@ module JSON::LD
661
352
  '@propagate' => :propagate=,
662
353
  '@vocab' => :vocab=,
663
354
  }.each do |key, setter|
664
- next unless context.has_key?(key)
355
+ next unless context.key?(key)
665
356
  if key == '@import'
666
357
  # Retrieve remote context and merge the remaining context object into the result.
667
358
  raise JsonLdError::InvalidContextEntry, "@import may only be used in 1.1 mode}" if result.processingMode("json-ld-1.0")
668
359
  raise JsonLdError::InvalidImportValue, "@import must be a string: #{context['@import'].inspect}" unless context['@import'].is_a?(String)
669
- source = RDF::URI(result.context_base || result.base).join(context['@import'])
360
+ import_loc = RDF::URI(result.context_base || base).join(context['@import'])
670
361
  begin
671
362
  context_opts = @options.merge(
672
363
  profile: 'http://www.w3.org/ns/json-ld#context',
@@ -674,22 +365,22 @@ module JSON::LD
674
365
  base: nil)
675
366
  context_opts.delete(:headers)
676
367
  # FIXME: should cache this, but ContextCache is for parsed contexts
677
- JSON::LD::API.loadRemoteDocument(source, **context_opts) do |remote_doc|
678
- # 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.
679
- raise JsonLdError::InvalidRemoteContext, "#{source}" unless remote_doc.document.is_a?(Hash) && remote_doc.document.has_key?('@context')
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.key?('@context')
680
371
  import_context = remote_doc.document['@context']
681
372
  import_context.delete('@base')
682
373
  raise JsonLdError::InvalidRemoteContext, "#{import_context.to_json} must be an object" unless import_context.is_a?(Hash)
683
- raise JsonLdError::InvalidContextEntry, "#{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.key?('@import')
684
375
  context.delete(key)
685
376
  context = import_context.merge(context)
686
377
  end
687
378
  rescue JsonLdError::LoadingDocumentFailed => e
688
- raise JsonLdError::LoadingRemoteContextFailed, "#{source}: #{e.message}", e.backtrace
379
+ raise JsonLdError::LoadingRemoteContextFailed, "#{import_loc}: #{e.message}", e.backtrace
689
380
  rescue JsonLdError
690
381
  raise
691
382
  rescue StandardError => e
692
- raise JsonLdError::LoadingRemoteContextFailed, "#{source}: #{e.message}", e.backtrace
383
+ raise JsonLdError::LoadingRemoteContextFailed, "#{import_loc}: #{e.message}", e.backtrace
693
384
  end
694
385
  else
695
386
  result.send(setter, context[key], remote_contexts: remote_contexts)
@@ -703,6 +394,7 @@ module JSON::LD
703
394
  context.each_key do |key|
704
395
  # ... where key is not @base, @vocab, @language, or @version
705
396
  result.create_term_definition(context, key, defined,
397
+ base: base,
706
398
  override_protected: override_protected,
707
399
  protected: context['@protected'],
708
400
  remote_contexts: remote_contexts.dup,
@@ -721,18 +413,15 @@ module JSON::LD
721
413
  # Merge in a context, creating a new context with updates from `context`
722
414
  #
723
415
  # @param [Context] context
724
- # @param [Boolean] protected mark resulting context as protected
725
416
  # @param [Boolean] override_protected Allow or disallow protected terms to be changed
726
- # @param [Boolean]
727
417
  # @return [Context]
728
418
  def merge(context, override_protected: false)
729
- ctx = Context.new(term_definitions: self.term_definitions.dup(), standard_prefixes: options[:standard_prefixes])
419
+ ctx = Context.new(term_definitions: self.term_definitions, standard_prefixes: options[:standard_prefixes])
730
420
  ctx.context_base = context.context_base || self.context_base
731
421
  ctx.default_language = context.default_language || self.default_language
732
422
  ctx.default_direction = context.default_direction || self.default_direction
733
423
  ctx.vocab = context.vocab || self.vocab
734
- ctx.base = context.base || self.base
735
- ctx.provided_context = self.provided_context
424
+ ctx.base = self.base unless self.base.nil?
736
425
  if !override_protected
737
426
  ctx.term_definitions.each do |term, definition|
738
427
  next unless definition.protected? && (other = context.term_definitions[term])
@@ -769,10 +458,9 @@ module JSON::LD
769
458
  # @param [Hash] local_context
770
459
  # @param [String] term
771
460
  # @param [Hash] defined
461
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
772
462
  # @param [Boolean] protected if true, causes all terms to be marked protected
773
463
  # @param [Boolean] override_protected Protected terms may be cleared.
774
- # @param [Boolean] propagate
775
- # Context is propagated across node objects.
776
464
  # @param [Array<String>] remote_contexts
777
465
  # @param [Boolean] validate_scoped (true).
778
466
  # Validate scoped context, loading if necessary.
@@ -781,6 +469,7 @@ module JSON::LD
781
469
  # Represents a cyclical term dependency
782
470
  # @see https://www.w3.org/TR/json-ld11-api/index.html#create-term-definition
783
471
  def create_term_definition(local_context, term, defined,
472
+ base: nil,
784
473
  override_protected: false,
785
474
  protected: nil,
786
475
  remote_contexts: [],
@@ -853,7 +542,7 @@ module JSON::LD
853
542
  # Potentially note that the term is protected
854
543
  definition.protected = value.fetch('@protected', protected)
855
544
 
856
- if value.has_key?('@type')
545
+ if value.key?('@type')
857
546
  type = value['@type']
858
547
  # SPEC FIXME: @type may be nil
859
548
  type = case type
@@ -877,7 +566,7 @@ module JSON::LD
877
566
  definition.type_mapping = type
878
567
  end
879
568
 
880
- if value.has_key?('@reverse')
569
+ if value.key?('@reverse')
881
570
  raise JsonLdError::InvalidReverseProperty, "unexpected key in #{value.inspect} on term #{term.inspect}" if
882
571
  value.key?('@id') || value.key?('@nest')
883
572
  raise JsonLdError::InvalidIRIMapping, "expected value of @reverse to be a string: #{value['@reverse'].inspect} on term #{term.inspect}" unless
@@ -903,7 +592,7 @@ module JSON::LD
903
592
  warn "[DEPRECATION] Blank Node terms deprecated in JSON-LD 1.1." if @options[:validate] && processingMode('json-ld-1.1') && definition.id.to_s.start_with?("_:")
904
593
 
905
594
  # 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.
906
- if value.has_key?('@container')
595
+ if value.key?('@container')
907
596
  container = value['@container']
908
597
  raise JsonLdError::InvalidReverseProperty,
909
598
  "unknown mapping for '@container' to #{container.inspect} on term #{term.inspect}" unless
@@ -911,9 +600,9 @@ module JSON::LD
911
600
  definition.container_mapping = check_container(container, local_context, defined, term)
912
601
  end
913
602
  definition.reverse_property = true
914
- elsif value.has_key?('@id') && value['@id'].nil?
603
+ elsif value.key?('@id') && value['@id'].nil?
915
604
  # Allowed to reserve a null term, which may be protected
916
- elsif value.has_key?('@id') && value['@id'] != term
605
+ elsif value.key?('@id') && value['@id'] != term
917
606
  raise JsonLdError::InvalidIRIMapping, "expected value of @id to be a string: #{value['@id'].inspect} on term #{term.inspect}" unless
918
607
  value['@id'].is_a?(String)
919
608
 
@@ -948,7 +637,7 @@ module JSON::LD
948
637
  elsif term[1..-1].include?(':')
949
638
  # 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.
950
639
  prefix, suffix = term.split(':', 2)
951
- create_term_definition(local_context, prefix, defined, protected: protected) if local_context.has_key?(prefix)
640
+ create_term_definition(local_context, prefix, defined, protected: protected) if local_context.key?(prefix)
952
641
 
953
642
  definition.id = if td = term_definitions[prefix]
954
643
  # 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.
@@ -975,7 +664,7 @@ module JSON::LD
975
664
 
976
665
  @iri_to_term[definition.id] = term if simple_term && definition.id
977
666
 
978
- if value.has_key?('@container')
667
+ if value.key?('@container')
979
668
  #log_debug("") {"container_mapping: #{value['@container'].inspect}"}
980
669
  definition.container_mapping = check_container(value['@container'], local_context, defined, term)
981
670
 
@@ -990,16 +679,17 @@ module JSON::LD
990
679
  end
991
680
  end
992
681
 
993
- if value.has_key?('@index')
682
+ if value.key?('@index')
994
683
  # property-based indexing
995
684
  raise JsonLdError::InvalidTermDefinition, "@index without @index in @container: #{value['@index']} on term #{term.inspect}" unless definition.container_mapping.include?('@index')
996
685
  raise JsonLdError::InvalidTermDefinition, "@index must expand to an IRI: #{value['@index']} on term #{term.inspect}" unless value['@index'].is_a?(String) && !value['@index'].start_with?('@')
997
686
  definition.index = value['@index'].to_s
998
687
  end
999
688
 
1000
- if value.has_key?('@context')
689
+ if value.key?('@context')
1001
690
  begin
1002
691
  new_ctx = self.parse(value['@context'],
692
+ base: base,
1003
693
  override_protected: true,
1004
694
  remote_contexts: remote_contexts,
1005
695
  validate_scoped: false)
@@ -1014,7 +704,7 @@ module JSON::LD
1014
704
  end
1015
705
  end
1016
706
 
1017
- if value.has_key?('@language')
707
+ if value.key?('@language')
1018
708
  language = value['@language']
1019
709
  language = case value['@language']
1020
710
  when String
@@ -1032,14 +722,14 @@ module JSON::LD
1032
722
  definition.language_mapping = language || false
1033
723
  end
1034
724
 
1035
- if value.has_key?('@direction')
725
+ if value.key?('@direction')
1036
726
  direction = value['@direction']
1037
727
  raise JsonLdError::InvalidBaseDirection, "direction must be null, 'ltr', or 'rtl', was #{language.inspect}} on term #{term.inspect}" unless direction.nil? || %w(ltr rtl).include?(direction)
1038
728
  #log_debug("") {"direction_mapping: #{direction.inspect}"}
1039
729
  definition.direction_mapping = direction || false
1040
730
  end
1041
731
 
1042
- if value.has_key?('@nest')
732
+ if value.key?('@nest')
1043
733
  nest = value['@nest']
1044
734
  raise JsonLdError::InvalidNestValue, "nest must be a string, was #{nest.inspect}} on term #{term.inspect}" unless nest.is_a?(String)
1045
735
  raise JsonLdError::InvalidNestValue, "nest must not be a keyword other than @nest, was #{nest.inspect}} on term #{term.inspect}" if nest.match?(/^@[a-zA-Z]+$/) && nest != '@nest'
@@ -1047,7 +737,7 @@ module JSON::LD
1047
737
  definition.nest = nest
1048
738
  end
1049
739
 
1050
- if value.has_key?('@prefix')
740
+ if value.key?('@prefix')
1051
741
  raise JsonLdError::InvalidTermDefinition, "@prefix used on compact or relative IRI term #{term.inspect}" if term.match?(%r{:|/})
1052
742
  case pfx = value['@prefix']
1053
743
  when TrueClass, FalseClass
@@ -1066,9 +756,130 @@ module JSON::LD
1066
756
 
1067
757
  term_definitions[term] = definition
1068
758
  defined[term] = true
1069
- ensure
1070
- # Re-build after term definitions set
1071
- @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
1072
883
  end
1073
884
 
1074
885
  ##
@@ -1077,40 +888,44 @@ module JSON::LD
1077
888
  # If a context was supplied in global options, use that, otherwise, generate one
1078
889
  # from this representation.
1079
890
  #
891
+ # @param [Array, Hash, Context, IO, StringIO] provided_context (nil)
892
+ # Original context to use, if available
1080
893
  # @param [Hash{Symbol => Object}] options ({})
1081
894
  # @return [Hash]
1082
- def serialize(**options)
1083
- # FIXME: not setting provided_context now
895
+ def serialize(provided_context: nil, **options)
896
+ #log_debug("serlialize: generate context")
897
+ #log_debug("") {"=> context: #{inspect}"}
1084
898
  use_context = case provided_context
1085
899
  when String, RDF::URI
1086
900
  #log_debug "serlialize: reuse context: #{provided_context.inspect}"
1087
901
  provided_context.to_s
1088
- 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
1089
907
  #log_debug "serlialize: reuse context: #{provided_context.inspect}"
1090
908
  provided_context
909
+ when IO, StringIO
910
+ provided_context.rewind
911
+ JSON.load(provided_context).fetch('@context', {})
1091
912
  else
1092
- #log_debug("serlialize: generate context")
1093
- #log_debug("") {"=> context: #{inspect}"}
1094
913
  ctx = {}
1095
- 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
1096
916
  ctx['@direction'] = default_direction.to_s if default_direction
1097
917
  ctx['@language'] = default_language.to_s if default_language
1098
918
  ctx['@vocab'] = vocab.to_s if vocab
1099
919
 
1100
920
  # Term Definitions
1101
- term_definitions.keys.each do |term|
1102
- defn = term_definitions[term].to_context_definition(self)
1103
- ctx[term] = defn if defn
921
+ term_definitions.each do |term, defn|
922
+ ctx[term] = defn.to_context_definition(self)
1104
923
  end
1105
-
1106
- #log_debug("") {"start_doc: context=#{ctx.inspect}"}
1107
924
  ctx
1108
925
  end
1109
926
 
1110
927
  # Return hash with @context, or empty
1111
- r = {}
1112
- r['@context'] = use_context unless use_context.nil? || use_context.empty?
1113
- r
928
+ use_context.nil? || use_context.empty? ? {} : {'@context' => use_context}
1114
929
  end
1115
930
 
1116
931
  ##
@@ -1203,7 +1018,7 @@ module JSON::LD
1203
1018
 
1204
1019
  term_sym = term.empty? ? "" : term.to_sym
1205
1020
  iri_to_term.delete(term_definitions[term].id.to_s) if term_definitions[term].id.is_a?(String)
1206
- @options[:prefixes][term_sym] = value if @options.has_key?(:prefixes)
1021
+ @options[:prefixes][term_sym] = value if @options.key?(:prefixes)
1207
1022
  iri_to_term[value.to_s] = term
1208
1023
  term_definitions[term]
1209
1024
  end
@@ -1223,7 +1038,7 @@ module JSON::LD
1223
1038
  # @param [Term, #to_s] term in unexpanded form
1224
1039
  # @return [Array<'@index', '@language', '@index', '@set', '@type', '@id', '@graph'>]
1225
1040
  def container(term)
1226
- return [term] if term == '@list'
1041
+ return Set[term] if term == '@list'
1227
1042
  term = find_definition(term)
1228
1043
  term ? term.container_mapping : Set.new
1229
1044
  end
@@ -1319,7 +1134,7 @@ module JSON::LD
1319
1134
  # @return [Term] related term definition
1320
1135
  def reverse_term(term)
1321
1136
  # Direct lookup of term
1322
- term = term_definitions[term.to_s] if term_definitions.has_key?(term.to_s) && !term.is_a?(TermDefinition)
1137
+ term = term_definitions[term.to_s] if term_definitions.key?(term.to_s) && !term.is_a?(TermDefinition)
1323
1138
 
1324
1139
  # Lookup term, assuming term is an IRI
1325
1140
  unless term.is_a?(TermDefinition)
@@ -1338,26 +1153,27 @@ module JSON::LD
1338
1153
  #
1339
1154
  # @param [String] value
1340
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.
1341
1160
  # @param [Boolean] documentRelative (false)
1342
- # @param [Boolean] vocab (false)
1343
1161
  # @param [Hash] local_context
1344
1162
  # Used during Context Processing.
1345
- # @param [Hash] defined
1346
- # Used during Context Processing.
1347
- # @param [Boolean] as_string (false) transform RDF::Resource values to string
1163
+ # @param [Boolean] vocab (false)
1348
1164
  # @param [Hash{Symbol => Object}] options
1349
1165
  # @return [RDF::Resource, String]
1350
1166
  # IRI or String, if it's a keyword
1351
1167
  # @raise [JSON::LD::JsonLdError::InvalidIRIMapping] if the value cannot be expanded
1352
1168
  # @see https://www.w3.org/TR/json-ld11-api/#iri-expansion
1353
1169
  def expand_iri(value,
1170
+ as_string: false,
1171
+ base: nil,
1172
+ defined: nil,
1354
1173
  documentRelative: false,
1355
- vocab: false,
1356
1174
  local_context: nil,
1357
- defined: nil,
1358
- as_string: false,
1359
- **options
1360
- )
1175
+ vocab: false,
1176
+ **options)
1361
1177
  return (value && as_string ? value.to_s : value) unless value.is_a?(String)
1362
1178
 
1363
1179
  return value if KEYWORDS.include?(value)
@@ -1366,7 +1182,7 @@ module JSON::LD
1366
1182
  defined = defined || {} # if we initialized in the keyword arg we would allocate {} at each invokation, even in the 2 (common) early returns above.
1367
1183
 
1368
1184
  # If local context is not null, it contains a key that equals value, and the value associated with the key that equals value in defined is not true, then invoke the Create Term Definition subalgorithm, passing active context, local context, value as term, and defined. This will ensure that a term definition is created for value in active context during Context Processing.
1369
- if local_context && local_context.has_key?(value) && !defined[value]
1185
+ if local_context && local_context.key?(value) && !defined[value]
1370
1186
  create_term_definition(local_context, value, defined)
1371
1187
  end
1372
1188
 
@@ -1377,7 +1193,8 @@ module JSON::LD
1377
1193
  # If active context has a term definition for value, and the associated mapping is a keyword, return that keyword.
1378
1194
  # If vocab is true and the active context has a term definition for value, return the associated IRI mapping.
1379
1195
  if (v_td = term_definitions[value]) && (vocab || KEYWORDS.include?(v_td.id))
1380
- return (as_string ? v_td.id.to_s : 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)
1381
1198
  end
1382
1199
 
1383
1200
  # If value contains a colon (:), it is either an absolute IRI or a compact IRI:
@@ -1395,7 +1212,7 @@ module JSON::LD
1395
1212
  end
1396
1213
 
1397
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.
1398
- if local_context && local_context.has_key?(prefix) && !defined[prefix]
1215
+ if local_context && local_context.key?(prefix) && !defined[prefix]
1399
1216
  create_term_definition(local_context, prefix, defined)
1400
1217
  end
1401
1218
 
@@ -1410,18 +1227,32 @@ module JSON::LD
1410
1227
  end
1411
1228
  end
1412
1229
 
1230
+ iri = value.is_a?(RDF::URI) ? value : RDF::URI(value)
1413
1231
  result = if vocab && self.vocab
1414
1232
  # If vocab is true, and active context has a vocabulary mapping, return the result of concatenating the vocabulary mapping with value.
1415
- self.vocab + value
1416
- elsif documentRelative && (base ||= self.base)
1417
- # 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].
1418
- value = RDF::URI(value)
1419
- value.absolute? ? value : RDF::URI(base).join(value)
1420
- 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?
1421
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.
1422
1253
  raise JSON::LD::JsonLdError::InvalidIRIMapping, "not an absolute IRI: #{value}"
1423
1254
  else
1424
- RDF::URI(value)
1255
+ iri
1425
1256
  end
1426
1257
  result && as_string ? result.to_s : result
1427
1258
  end
@@ -1442,21 +1273,21 @@ module JSON::LD
1442
1273
  # Compacts an absolute IRI to the shortest matching term or compact IRI
1443
1274
  #
1444
1275
  # @param [RDF::URI] iri
1276
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1445
1277
  # @param [Object] value
1446
1278
  # Value, used to select among various maps for the same IRI
1447
- # @param [Boolean] vocab
1448
- # specifies whether the passed iri should be compacted using the active context's vocabulary mapping
1449
1279
  # @param [Boolean] reverse
1450
1280
  # specifies whether a reverse property is being compacted
1451
- # @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
1452
1283
  #
1453
1284
  # @return [String] compacted form of IRI
1454
1285
  # @see https://www.w3.org/TR/json-ld11-api/#iri-compaction
1455
- def compact_iri(iri, value: nil, vocab: nil, reverse: false, **options)
1286
+ def compact_iri(iri, base: nil, reverse: false, value: nil, vocab: nil)
1456
1287
  return if iri.nil?
1457
1288
  iri = iri.to_s
1458
1289
 
1459
- if vocab && inverse_context.has_key?(iri)
1290
+ if vocab && inverse_context.key?(iri)
1460
1291
  default_language = if self.default_direction
1461
1292
  "#{self.default_language}_#{self.default_direction}".downcase
1462
1293
  else
@@ -1467,7 +1298,7 @@ module JSON::LD
1467
1298
  containers.concat(CONTAINERS_INDEX_SET) if index?(value) && !graph?(value)
1468
1299
 
1469
1300
  # If the value is a JSON Object with the key @preserve, use the value of @preserve.
1470
- value = value['@preserve'].first if value.is_a?(Hash) && value.has_key?('@preserve')
1301
+ value = value['@preserve'].first if value.is_a?(Hash) && value.key?('@preserve')
1471
1302
 
1472
1303
  if reverse
1473
1304
  tl, tl_value = "@type", "@reverse"
@@ -1481,11 +1312,11 @@ module JSON::LD
1481
1312
  list.each do |item|
1482
1313
  item_language, item_type = "@none", "@none"
1483
1314
  if value?(item)
1484
- if item.has_key?('@direction')
1315
+ if item.key?('@direction')
1485
1316
  item_language = "#{item['@language']}_#{item['@direction']}".downcase
1486
- elsif item.has_key?('@language')
1317
+ elsif item.key?('@language')
1487
1318
  item_language = item['@language'].downcase
1488
- elsif item.has_key?('@type')
1319
+ elsif item.key?('@type')
1489
1320
  item_type = item['@type']
1490
1321
  else
1491
1322
  item_language = "@null"
@@ -1513,14 +1344,14 @@ module JSON::LD
1513
1344
  elsif graph?(value)
1514
1345
  # Prefer @index and @id containers, then @graph, then @index
1515
1346
  containers.concat(CONTAINERS_GRAPH_INDEX_INDEX) if index?(value)
1516
- containers.concat(CONTAINERS_GRAPH) if value.has_key?('@id')
1347
+ containers.concat(CONTAINERS_GRAPH) if value.key?('@id')
1517
1348
 
1518
1349
  # Prefer an @graph container next
1519
1350
  containers.concat(CONTAINERS_GRAPH_SET)
1520
1351
 
1521
1352
  # Lastly, in 1.1, any graph can be indexed on @index or @id, so add if we haven't already
1522
1353
  containers.concat(CONTAINERS_GRAPH_INDEX) unless index?(value)
1523
- containers.concat(CONTAINERS_GRAPH) unless value.has_key?('@id')
1354
+ containers.concat(CONTAINERS_GRAPH) unless value.key?('@id')
1524
1355
  containers.concat(CONTAINERS_INDEX_SET) unless index?(value)
1525
1356
  containers << '@set'
1526
1357
 
@@ -1528,13 +1359,13 @@ module JSON::LD
1528
1359
  else
1529
1360
  if value?(value)
1530
1361
  # In 1.1, an language map can be used to index values using @none
1531
- if value.has_key?('@language') && !index?(value)
1362
+ if value.key?('@language') && !index?(value)
1532
1363
  tl_value = value['@language'].downcase
1533
1364
  tl_value += "_#{value['@direction']}" if value['@direction']
1534
1365
  containers.concat(CONTAINERS_LANGUAGE)
1535
- elsif value.has_key?('@direction') && !index?(value)
1366
+ elsif value.key?('@direction') && !index?(value)
1536
1367
  tl_value = "_#{value['@direction']}"
1537
- elsif value.has_key?('@type')
1368
+ elsif value.key?('@type')
1538
1369
  tl_value = value['@type']
1539
1370
  tl = '@type'
1540
1371
  end
@@ -1556,8 +1387,8 @@ module JSON::LD
1556
1387
  tl_value ||= '@null'
1557
1388
  preferred_values = []
1558
1389
  preferred_values << '@reverse' if tl_value == '@reverse'
1559
- if (tl_value == '@id' || tl_value == '@reverse') && value.is_a?(Hash) && value.has_key?('@id')
1560
- t_iri = compact_iri(value['@id'], vocab: true, document_relative: true)
1390
+ if (tl_value == '@id' || tl_value == '@reverse') && value.is_a?(Hash) && value.key?('@id')
1391
+ t_iri = compact_iri(value['@id'], vocab: true, base: base)
1561
1392
  if (r_td = term_definitions[t_iri]) && r_td.id == value['@id']
1562
1393
  preferred_values.concat(CONTAINERS_VOCAB_ID)
1563
1394
  else
@@ -1582,7 +1413,7 @@ module JSON::LD
1582
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:
1583
1414
  if vocab && self.vocab && iri.start_with?(self.vocab) && iri.length > self.vocab.length
1584
1415
  suffix = iri[self.vocab.length..-1]
1585
- return suffix unless term_definitions.has_key?(suffix)
1416
+ return suffix unless term_definitions.key?(suffix)
1586
1417
  end
1587
1418
 
1588
1419
  # The iri could not be compacted using the active context's vocabulary mapping. Try to create a compact IRI, starting by initializing compact IRI to null. This variable will be used to tore the created compact IRI, if any.
@@ -1596,7 +1427,7 @@ module JSON::LD
1596
1427
 
1597
1428
  suffix = iri[td.id.length..-1]
1598
1429
  ciri = "#{term}:#{suffix}"
1599
- candidates << ciri unless value && term_definitions.has_key?(ciri)
1430
+ candidates << ciri unless value && term_definitions.key?(ciri)
1600
1431
  end
1601
1432
 
1602
1433
  return candidates.sort.first if !candidates.empty?
@@ -1623,7 +1454,7 @@ module JSON::LD
1623
1454
 
1624
1455
  if !vocab
1625
1456
  # transform iri to a relative IRI using the document's base IRI
1626
- iri = remove_base(iri)
1457
+ iri = remove_base(self.base || base, iri)
1627
1458
  return iri
1628
1459
  else
1629
1460
  return iri
@@ -1643,26 +1474,24 @@ module JSON::LD
1643
1474
  # Value (literal or IRI) to be expanded
1644
1475
  # @param [Boolean] useNativeTypes (false) use native representations
1645
1476
  # @param [Boolean] rdfDirection (nil) decode i18n datatype if i18n-datatype
1477
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1646
1478
  # @param [Hash{Symbol => Object}] options
1647
1479
  #
1648
1480
  # @return [Hash] Object representation of value
1649
1481
  # @raise [RDF::ReaderError] if the iri cannot be expanded
1650
1482
  # @see https://www.w3.org/TR/json-ld11-api/#value-expansion
1651
- def expand_value(property, value, useNativeTypes: false, rdfDirection: nil, **options)
1652
- #log_debug("expand_value") {"property: #{property.inspect}, value: #{value.inspect}"}
1653
-
1483
+ def expand_value(property, value, useNativeTypes: false, rdfDirection: nil, base: nil, **options)
1654
1484
  td = term_definitions.fetch(property, TermDefinition.new(property))
1655
1485
 
1656
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.
1657
1487
  if value.is_a?(String) && td.type_mapping == '@id'
1658
1488
  #log_debug("") {"as relative IRI: #{value.inspect}"}
1659
- return {'@id' => expand_iri(value, documentRelative: true).to_s}
1489
+ return {'@id' => expand_iri(value, documentRelative: true, base: base).to_s}
1660
1490
  end
1661
1491
 
1662
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.
1663
1493
  if value.is_a?(String) && td.type_mapping == '@vocab'
1664
- #log_debug("") {"as vocab IRI: #{value.inspect}"}
1665
- 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}
1666
1495
  end
1667
1496
 
1668
1497
  value = RDF::Literal(value) if
@@ -1672,16 +1501,14 @@ module JSON::LD
1672
1501
 
1673
1502
  result = case value
1674
1503
  when RDF::URI, RDF::Node
1675
- #log_debug("URI | BNode") { value.to_s }
1676
1504
  {'@id' => value.to_s}
1677
1505
  when RDF::Literal
1678
- #log_debug("Literal") {"datatype: #{value.datatype.inspect}"}
1679
1506
  res = {}
1680
1507
  if value.datatype == RDF::URI(RDF.to_uri + "JSON") && processingMode('json-ld-1.1')
1681
1508
  # Value parsed as JSON
1682
1509
  # FIXME: MultiJson
1683
- res['@value'] = ::JSON.parse(value.object)
1684
1510
  res['@type'] = '@json'
1511
+ res['@value'] = ::JSON.parse(value.object)
1685
1512
  elsif value.datatype.start_with?("https://www.w3.org/ns/i18n#") && rdfDirection == 'i18n-datatype' && processingMode('json-ld-1.1')
1686
1513
  lang, dir = value.datatype.fragment.split('_')
1687
1514
  res['@value'] = value.to_s
@@ -1696,25 +1523,24 @@ module JSON::LD
1696
1523
  res['@language'] = lang
1697
1524
  end
1698
1525
  res['@direction'] = dir
1699
- elsif useNativeTypes && RDF_LITERAL_NATIVE_TYPES.include?(value.datatype)
1700
- res['@value'] = value.object
1526
+ elsif useNativeTypes && RDF_LITERAL_NATIVE_TYPES.include?(value.datatype) && value.valid?
1701
1527
  res['@type'] = uri(coerce(property)) if coerce(property)
1528
+ res['@value'] = value.object
1702
1529
  else
1703
- value.canonicalize! if value.datatype == RDF::XSD.double
1704
- res['@value'] = value.to_s
1530
+ value.canonicalize! if value.valid? && value.datatype == RDF::XSD.double
1705
1531
  if coerce(property)
1706
1532
  res['@type'] = uri(coerce(property)).to_s
1707
- elsif value.has_datatype?
1533
+ elsif value.datatype?
1708
1534
  res['@type'] = uri(value.datatype).to_s
1709
- elsif value.has_language? || language(property)
1535
+ elsif value.language? || language(property)
1710
1536
  res['@language'] = (value.language || language(property)).to_s
1711
- # FIXME: direction
1712
1537
  end
1538
+ res['@value'] = value.to_s
1713
1539
  end
1714
1540
  res
1715
1541
  else
1716
1542
  # Otherwise, initialize result to a JSON object with an @value member whose value is set to value.
1717
- res = {'@value' => value}
1543
+ res = {}
1718
1544
 
1719
1545
  if td.type_mapping && !CONTAINERS_ID_VOCAB.include?(td.type_mapping.to_s)
1720
1546
  res['@type'] = td.type_mapping.to_s
@@ -1725,10 +1551,9 @@ module JSON::LD
1725
1551
  res['@direction'] = direction if direction
1726
1552
  end
1727
1553
 
1728
- res
1554
+ res.merge('@value' => value)
1729
1555
  end
1730
1556
 
1731
- #log_debug("") {"=> #{result.inspect}"}
1732
1557
  result
1733
1558
  rescue ::JSON::ParserError => e
1734
1559
  raise JSON::LD::JsonLdError::InvalidJsonLiteral, e.message
@@ -1741,13 +1566,13 @@ module JSON::LD
1741
1566
  # Associated property used to find coercion rules
1742
1567
  # @param [Hash] value
1743
1568
  # Value (literal or IRI), in full object representation, to be compacted
1744
- # @param [Hash{Symbol => Object}] options
1569
+ # @param [String, RDF::URI] base for resolving document-relative IRIs
1745
1570
  #
1746
1571
  # @return [Hash] Object representation of value
1747
1572
  # @raise [JsonLdError] if the iri cannot be expanded
1748
1573
  # @see https://www.w3.org/TR/json-ld11-api/#value-compaction
1749
1574
  # FIXME: revisit the specification version of this.
1750
- def compact_value(property, value, **options)
1575
+ def compact_value(property, value, base: nil)
1751
1576
  #log_debug("compact_value") {"property: #{property.inspect}, value: #{value.inspect}"}
1752
1577
 
1753
1578
  indexing = index?(value) && container(property).include?('@index')
@@ -1755,15 +1580,15 @@ module JSON::LD
1755
1580
  direction = direction(property)
1756
1581
 
1757
1582
  result = case
1758
- when coerce(property) == '@id' && value.has_key?('@id') && (value.keys - %w(@id @index)).empty?
1583
+ when coerce(property) == '@id' && value.key?('@id') && (value.keys - %w(@id @index)).empty?
1759
1584
  # Compact an @id coercion
1760
1585
  #log_debug("") {" (@id & coerce)"}
1761
- compact_iri(value['@id'])
1762
- when coerce(property) == '@vocab' && value.has_key?('@id') && (value.keys - %w(@id @index)).empty?
1586
+ compact_iri(value['@id'], base: base)
1587
+ when coerce(property) == '@vocab' && value.key?('@id') && (value.keys - %w(@id @index)).empty?
1763
1588
  # Compact an @id coercion
1764
1589
  #log_debug("") {" (@id & coerce & vocab)"}
1765
1590
  compact_iri(value['@id'], vocab: true)
1766
- when value.has_key?('@id')
1591
+ when value.key?('@id')
1767
1592
  #log_debug("") {" (@id)"}
1768
1593
  # return value as is
1769
1594
  value
@@ -1784,7 +1609,7 @@ module JSON::LD
1784
1609
  value
1785
1610
  end
1786
1611
 
1787
- if result.is_a?(Hash) && result.has_key?('@type') && value['@type'] != '@json'
1612
+ if result.is_a?(Hash) && result.key?('@type') && value['@type'] != '@json'
1788
1613
  # Compact values of @type
1789
1614
  c_type = if result['@type'].is_a?(Array)
1790
1615
  result['@type'].map {|t| compact_iri(t, vocab: true)}
@@ -1848,16 +1673,21 @@ module JSON::LD
1848
1673
  v.join(" ") + "]"
1849
1674
  end
1850
1675
 
1676
+ # Duplicate an active context, allowing it to be modified.
1851
1677
  def dup
1852
- # Also duplicate mappings, coerce and list
1853
1678
  that = self
1854
- 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
+
1855
1688
  ec.instance_eval do
1856
- @term_definitions = that.term_definitions.inject({}) do |memo, (term, defn)|
1857
- memo.merge(term => defn.dup())
1858
- end
1859
- @iri_to_term = that.iri_to_term.dup
1860
- @inverse_context = nil
1689
+ @term_definitions = that.term_definitions.dup
1690
+ @iri_to_term = that.iri_to_term
1861
1691
  end
1862
1692
  ec
1863
1693
  end
@@ -1903,20 +1733,11 @@ module JSON::LD
1903
1733
  bnode(namer.get_sym($1))
1904
1734
  else
1905
1735
  value = RDF::URI(value)
1906
- value.validate! if @options[:validate]
1907
- value.canonicalize! if @options[:canonicalize]
1908
- value = RDF::URI.intern(value, {}) if @options[:intern]
1736
+ #value.validate! if options[:validate]
1909
1737
  value
1910
1738
  end
1911
1739
  end
1912
1740
 
1913
- # Clear the provided context, used for testing
1914
- # @return [Context] self
1915
- def clear_provided_context
1916
- @provided_context = nil
1917
- self
1918
- end
1919
-
1920
1741
  # Keep track of allocated BNodes
1921
1742
  #
1922
1743
  # Don't actually use the name provided, to prevent name alias issues.
@@ -1957,7 +1778,7 @@ module JSON::LD
1957
1778
  # @return [Hash{String => Hash{String => String}}]
1958
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
1959
1780
  def inverse_context
1960
- @inverse_context ||= begin
1781
+ Context.inverse_cache[self.hash] ||= begin
1961
1782
  result = {}
1962
1783
  default_language = (self.default_language || '@none').downcase
1963
1784
  term_definitions.keys.sort do |a, b|
@@ -2036,11 +1857,11 @@ module JSON::LD
2036
1857
  container_map = inverse_context[iri]
2037
1858
  #log_debug(" ") {"container_map: #{container_map.inspect}"}
2038
1859
  containers.each do |container|
2039
- next unless container_map.has_key?(container)
1860
+ next unless container_map.key?(container)
2040
1861
  tl_map = container_map[container]
2041
1862
  value_map = tl_map[type_language]
2042
1863
  preferred_values.each do |item|
2043
- next unless value_map.has_key?(item)
1864
+ next unless value_map.key?(item)
2044
1865
  #log_debug("=>") {value_map[item].inspect}
2045
1866
  return value_map[item]
2046
1867
  end
@@ -2052,10 +1873,11 @@ module JSON::LD
2052
1873
  ##
2053
1874
  # Removes a base IRI from the given absolute IRI.
2054
1875
  #
1876
+ # @param [String] base the base used for making `iri` relative
2055
1877
  # @param [String] iri the absolute IRI
2056
1878
  # @return [String]
2057
1879
  # the relative IRI if relative to base, otherwise the absolute IRI.
2058
- def remove_base(iri)
1880
+ def remove_base(base, iri)
2059
1881
  return iri unless base
2060
1882
  @base_and_parents ||= begin
2061
1883
  u = base
@@ -2165,5 +1987,232 @@ module JSON::LD
2165
1987
  end
2166
1988
  Array(container)
2167
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
2168
2217
  end
2169
2218
  end